@@ -58,6 +58,7 @@ export interface SearchSelectDropdownProps< | |||
controlLabel?: React.ReactNode | string; | |||
controlSize?: InputSizeKeys; | |||
isDiscreet?: boolean; | |||
zLevel?: PopupZLevel; | |||
} | |||
export function SearchSelectDropdown< | |||
@@ -77,6 +78,7 @@ export function SearchSelectDropdown< | |||
minLength, | |||
controlAriaLabel, | |||
menuIsOpen, | |||
zLevel = PopupZLevel.Global, | |||
...rest | |||
} = props; | |||
const [open, setOpen] = React.useState(false); | |||
@@ -161,7 +163,7 @@ export function SearchSelectDropdown< | |||
</SearchHighlighterContext.Provider> | |||
} | |||
placement={PopupPlacement.BottomLeft} | |||
zLevel={PopupZLevel.Global} | |||
zLevel={zLevel} | |||
> | |||
<SearchSelectDropdownControl | |||
ariaLabel={controlAriaLabel} |
@@ -24,7 +24,7 @@ import React, { AriaRole } from 'react'; | |||
import { createPortal, findDOMNode } from 'react-dom'; | |||
import tw from 'twin.macro'; | |||
import { THROTTLE_SCROLL_DELAY } from '../helpers/constants'; | |||
import { PopupPlacement, popupPositioning, PopupZLevel } from '../helpers/positioning'; | |||
import { PopupPlacement, PopupZLevel, popupPositioning } from '../helpers/positioning'; | |||
import { themeBorder, themeColor, themeContrast, themeShadow } from '../helpers/theme'; | |||
import ClickEventBoundary from './ClickEventBoundary'; | |||
@@ -129,7 +129,7 @@ export class Popup extends React.PureComponent<PopupProps, State> { | |||
}; | |||
positionPopup = () => { | |||
if (this.mounted) { | |||
if (this.mounted && this.props.zLevel !== PopupZLevel.Absolute) { | |||
// `findDOMNode(this)` will search for the DOM node for the current component | |||
// first it will find a React.Fragment (see `render`), | |||
// so it will get the DOM node of the first child, i.e. DOM node of `this.props.children` | |||
@@ -216,6 +216,7 @@ const PopupWrapper = styled.div<{ zLevel: PopupZLevel }>` | |||
[PopupZLevel.Default]: tw`sw-z-popup`, | |||
[PopupZLevel.Global]: tw`sw-z-global-popup`, | |||
[PopupZLevel.Content]: tw`sw-z-content-popup`, | |||
[PopupZLevel.Absolute]: tw`sw-z-global-popup`, | |||
}[zLevel])}; | |||
&.is-bottom, |
@@ -52,6 +52,7 @@ export enum PopupZLevel { | |||
Content = 'content', | |||
Default = 'popup', | |||
Global = 'global', | |||
Absolute = 'absolute', | |||
} | |||
export type BasePlacement = Extract< |
@@ -69,19 +69,14 @@ export default class IssueHeader extends React.PureComponent<Props, State> { | |||
} | |||
handleIssuePopupToggle = (popupName: string, open?: boolean) => { | |||
const openPopupState = { issuePopupName: popupName }; | |||
const closePopupState = { issuePopupName: undefined }; | |||
this.setState(({ issuePopupName }) => { | |||
if (open) { | |||
return openPopupState; | |||
} else if (open === false) { | |||
return closePopupState; | |||
const samePopup = popupName && issuePopupName === popupName; | |||
if (open !== false && !samePopup) { | |||
return { issuePopupName: popupName }; | |||
} else if (open !== true && samePopup) { | |||
return { issuePopupName: undefined }; | |||
} | |||
// toggle popup | |||
return issuePopupName === popupName ? closePopupState : openPopupState; | |||
return { issuePopupName }; | |||
}); | |||
}; | |||
@@ -430,6 +430,14 @@ export class App extends React.PureComponent<Props, State> { | |||
} | |||
}; | |||
selectIssue = (issueKey: string) => { | |||
this.setState({ | |||
selected: issueKey, | |||
selectedFlowIndex: undefined, | |||
selectedLocationIndex: undefined, | |||
}); | |||
}; | |||
closeIssue = () => { | |||
if (this.state.query) { | |||
this.props.router.push({ | |||
@@ -1124,6 +1132,7 @@ export class App extends React.PureComponent<Props, State> { | |||
onIssueChange={this.handleIssueChange} | |||
onIssueCheck={currentUser.isLoggedIn ? this.handleIssueCheck : undefined} | |||
onIssueClick={this.openIssue} | |||
onIssueSelect={this.selectIssue} | |||
onPopupToggle={this.handlePopupToggle} | |||
openPopup={this.state.openPopup} | |||
selectedIssue={selectedIssue} |
@@ -34,6 +34,7 @@ interface Props { | |||
onIssueChange: (issue: Issue) => void; | |||
onIssueCheck: ((issueKey: string) => void) | undefined; | |||
onIssueClick: (issueKey: string) => void; | |||
onIssueSelect: (issueKey: string) => void; | |||
onPopupToggle: (issue: string, popupName: string, open?: boolean) => void; | |||
openPopup: { issue: string; name: string } | undefined; | |||
selectedIssue: Issue | undefined; | |||
@@ -77,6 +78,7 @@ export default class IssuesList extends React.PureComponent<Props, State> { | |||
onChange={this.props.onIssueChange} | |||
onCheck={this.props.onIssueCheck} | |||
onClick={this.props.onIssueClick} | |||
onSelect={this.props.onIssueSelect} | |||
onFilterChange={this.props.onFilterChange} | |||
onPopupToggle={this.props.onPopupToggle} | |||
openPopup={openPopup && openPopup.issue === issue.key ? openPopup.name : undefined} |
@@ -30,6 +30,7 @@ interface Props { | |||
onChange: (issue: TypeIssue) => void; | |||
onCheck: ((issueKey: string) => void) | undefined; | |||
onClick: (issueKey: string) => void; | |||
onSelect: (issueKey: string) => void; | |||
onFilterChange: (changes: Partial<Query>) => void; | |||
onPopupToggle: (issue: string, popupName: string, open?: boolean) => void; | |||
openPopup: string | undefined; | |||
@@ -93,6 +94,7 @@ export default class ListItem extends React.PureComponent<Props> { | |||
onChange={this.props.onChange} | |||
onCheck={this.props.onCheck} | |||
onClick={this.props.onClick} | |||
onSelect={this.props.onSelect} | |||
onPopupToggle={this.props.onPopupToggle} | |||
openPopup={this.props.openPopup} | |||
selected={this.props.selected} |
@@ -70,7 +70,7 @@ export default function LineIssuesList(props: LineIssuesListProps) { | |||
issue={issue} | |||
key={issue.key} | |||
onChange={props.onIssueChange} | |||
onClick={props.onIssueClick} | |||
onSelect={props.onIssueClick} | |||
onPopupToggle={props.onIssuePopupToggle} | |||
openPopup={issuePopup && issuePopup.issue === issue.key ? issuePopup.name : undefined} | |||
selected={props.selectedIssue === issue.key} |
@@ -49,8 +49,8 @@ exports[`should render issues 1`] = ` | |||
} | |||
key="issue" | |||
onChange={[MockFunction]} | |||
onClick={[MockFunction]} | |||
onPopupToggle={[MockFunction]} | |||
onSelect={[MockFunction]} | |||
selected={true} | |||
/> | |||
</div> |
@@ -35,6 +35,7 @@ interface Props { | |||
onChange: (issue: TypeIssue) => void; | |||
onCheck?: (issue: string) => void; | |||
onClick?: (issueKey: string) => void; | |||
onSelect: (issueKey: string) => void; | |||
onPopupToggle: (issue: string, popupName: string, open?: boolean) => void; | |||
openPopup?: string; | |||
selected: boolean; | |||
@@ -119,6 +120,7 @@ export default class Issue extends React.PureComponent<Props> { | |||
onChange={this.props.onChange} | |||
onCheck={this.props.onCheck} | |||
onClick={this.props.onClick} | |||
onSelect={this.props.onSelect} | |||
selected={this.props.selected} | |||
togglePopup={this.togglePopup} | |||
/> |
@@ -52,7 +52,7 @@ describe('rendering', () => { | |||
const { ui } = getPageObject(); | |||
const issue = mockIssue(true, { effort: '2 days', message: 'This is an issue' }); | |||
const onClick = jest.fn(); | |||
renderIssue({ issue, onClick }); | |||
renderIssue({ issue, onSelect: onClick }); | |||
expect(ui.effort('2 days').get()).toBeInTheDocument(); | |||
await ui.clickIssueMessage(); | |||
@@ -432,7 +432,11 @@ function renderIssue(props: Partial<Omit<Issue['props'], 'onChange' | 'onPopupTo | |||
); | |||
} | |||
return renderApp('/', <Wrapper issue={mockIssue()} selected={false} {...props} />, { | |||
currentUser: mockLoggedInUser({ login: 'leia', name: 'Organa' }), | |||
}); | |||
return renderApp( | |||
'/', | |||
<Wrapper onSelect={jest.fn()} issue={mockIssue()} selected={false} {...props} />, | |||
{ | |||
currentUser: mockLoggedInUser({ login: 'leia', name: 'Organa' }), | |||
} | |||
); | |||
} |
@@ -162,7 +162,7 @@ export default function IssueActionsBar(props: Props) { | |||
<IssueCommentAction | |||
commentAutoTriggered={commentState.commentAutoTriggered} | |||
commentPlaceholder={commentState.commentPlaceholder} | |||
currentPopup={currentPopup} | |||
currentPopup={currentPopup === 'comment'} | |||
issueKey={issue.key} | |||
onChange={onChange} | |||
toggleComment={toggleComment} |
@@ -17,7 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { LabelValueSelectOption, SearchSelectDropdown } from 'design-system'; | |||
import { LabelValueSelectOption, PopupZLevel, SearchSelectDropdown } from 'design-system'; | |||
import * as React from 'react'; | |||
import { Options, SingleValue } from 'react-select'; | |||
import { searchUsers } from '../../../api/users'; | |||
@@ -132,26 +132,29 @@ export default function IssueAssignee(props: Props) { | |||
} | |||
return ( | |||
<SearchSelectDropdown | |||
size="medium" | |||
className="it__issue-assign" | |||
controlAriaLabel={ | |||
assinedUser | |||
? translateWithParameters('issue.assign.assigned_to_x_click_to_change', assinedUser) | |||
: translate('issue.assign.unassigned_click_to_assign') | |||
} | |||
defaultOptions={defaultOptions} | |||
onChange={handleAssign} | |||
loadOptions={handleSearchAssignees} | |||
menuIsOpen={props.isOpen} | |||
minLength={minSearchLength} | |||
onMenuOpen={() => toggleAssign(true)} | |||
onMenuClose={handleClose} | |||
isDiscreet | |||
controlLabel={controlLabel} | |||
tooShortText={translateWithParameters('search.tooShort', String(minSearchLength))} | |||
placeholder={translate('search.search_for_users')} | |||
aria-label={translate('search.search_for_users')} | |||
/> | |||
<div className="sw-relative"> | |||
<SearchSelectDropdown | |||
size="medium" | |||
className="it__issue-assign" | |||
controlAriaLabel={ | |||
assinedUser | |||
? translateWithParameters('issue.assign.assigned_to_x_click_to_change', assinedUser) | |||
: translate('issue.assign.unassigned_click_to_assign') | |||
} | |||
defaultOptions={defaultOptions} | |||
onChange={handleAssign} | |||
loadOptions={handleSearchAssignees} | |||
menuIsOpen={props.isOpen} | |||
minLength={minSearchLength} | |||
onMenuOpen={() => toggleAssign(true)} | |||
onMenuClose={handleClose} | |||
isDiscreet | |||
controlLabel={controlLabel} | |||
tooShortText={translateWithParameters('search.tooShort', String(minSearchLength))} | |||
placeholder={translate('search.search_for_users')} | |||
aria-label={translate('search.search_for_users')} | |||
zLevel={PopupZLevel.Absolute} | |||
/> | |||
</div> | |||
); | |||
} |
@@ -29,7 +29,7 @@ interface Props { | |||
canComment: boolean; | |||
commentAutoTriggered?: boolean; | |||
commentPlaceholder: string; | |||
currentPopup?: string; | |||
currentPopup?: boolean; | |||
issueKey: string; | |||
onChange: (issue: Issue) => void; | |||
toggleComment: (open?: boolean, placeholder?: string, autoTriggered?: boolean) => void; | |||
@@ -69,7 +69,7 @@ export default class IssueCommentAction extends React.PureComponent<Props> { | |||
<Toggler | |||
closeOnClickOutside={false} | |||
onRequestClose={this.handleClose} | |||
open={this.props.currentPopup === 'comment'} | |||
open={!!this.props.currentPopup} | |||
overlay={ | |||
showCommentsInPopup ? ( | |||
<CommentListPopup |
@@ -23,11 +23,11 @@ import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getComponentIssuesUrl } from '../../../helpers/urls'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { IssueType } from '../../../types/issues'; | |||
import { Issue } from '../../../types/types'; | |||
import { IssueMessageHighlighting } from '../IssueMessageHighlighting'; | |||
export interface IssueMessageProps { | |||
onClick?: () => void; | |||
issue: Issue; | |||
branchLike?: BranchLike; | |||
displayWhyIsThisAnIssue?: boolean; | |||
@@ -46,10 +46,15 @@ export default function IssueMessage(props: IssueMessageProps) { | |||
why: '1', | |||
}); | |||
const issueUrl = getComponentIssuesUrl(issue.project, { | |||
...getBranchLikeQuery(branchLike), | |||
open: issue.key, | |||
types: issue.type === IssueType.SecurityHotspot ? issue.type : undefined, | |||
}); | |||
return ( | |||
<> | |||
{props.onClick ? ( | |||
<StandoutLink onClick={props.onClick} className="it__issue-message" preventDefault to={{}}> | |||
{issueUrl?.pathname ? ( | |||
<StandoutLink className="it__issue-message" to={issueUrl}> | |||
<IssueMessageHighlighting message={message} messageFormattings={messageFormattings} /> | |||
</StandoutLink> | |||
) : ( |
@@ -28,7 +28,6 @@ import IssueTags from './IssueTags'; | |||
export interface IssueTitleBarProps { | |||
currentPopup?: string; | |||
branchLike?: BranchLike; | |||
onClick?: () => void; | |||
displayWhyIsThisAnIssue?: boolean; | |||
issue: Issue; | |||
onChange: (issue: Issue) => void; | |||
@@ -46,7 +45,6 @@ export default function IssueTitleBar(props: IssueTitleBarProps) { | |||
issue={issue} | |||
branchLike={props.branchLike} | |||
displayWhyIsThisAnIssue={displayWhyIsThisAnIssue} | |||
onClick={props.onClick} | |||
/> | |||
</div> | |||
<div className="js-issue-tags sw-body-sm sw-grow-0 sw-whitespace-nowrap"> |
@@ -38,6 +38,7 @@ interface Props { | |||
onAssign: (login: string) => void; | |||
onChange: (issue: Issue) => void; | |||
onCheck?: (issue: string) => void; | |||
onSelect: (issueKey: string) => void; | |||
onClick?: (issueKey: string) => void; | |||
selected: boolean; | |||
togglePopup: (popup: string, show: boolean | void) => void; | |||
@@ -49,14 +50,14 @@ export default class IssueView extends React.PureComponent<Props> { | |||
componentDidMount() { | |||
const { selected } = this.props; | |||
if (this.nodeRef && selected) { | |||
this.nodeRef.scrollIntoView({ block: 'center', inline: 'center' }); | |||
this.nodeRef.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); | |||
} | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
const { selected } = this.props; | |||
if (!prevProps.selected && selected && this.nodeRef) { | |||
this.nodeRef.scrollIntoView({ block: 'center', inline: 'center' }); | |||
this.nodeRef.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); | |||
} | |||
} | |||
@@ -66,19 +67,6 @@ export default class IssueView extends React.PureComponent<Props> { | |||
} | |||
}; | |||
handleBoxClick = (event: React.MouseEvent<HTMLDivElement>) => { | |||
if (!isClickable(event.target as HTMLElement) && this.props.onClick) { | |||
event.preventDefault(); | |||
this.handleDetailClick(); | |||
} | |||
}; | |||
handleDetailClick = () => { | |||
if (this.props.onClick) { | |||
this.props.onClick(this.props.issue.key); | |||
} | |||
}; | |||
editComment = (comment: string, text: string) => { | |||
updateIssue(this.props.onChange, editIssueComment({ comment, text })); | |||
}; | |||
@@ -102,6 +90,7 @@ export default class IssueView extends React.PureComponent<Props> { | |||
return ( | |||
<IssueItem | |||
onClick={() => this.props.onSelect(issue.key)} | |||
className={issueClass} | |||
role="region" | |||
aria-label={issue.message} | |||
@@ -120,7 +109,6 @@ export default class IssueView extends React.PureComponent<Props> { | |||
<IssueTitleBar | |||
currentPopup={currentPopup} | |||
branchLike={branchLike} | |||
onClick={this.handleDetailClick} | |||
displayWhyIsThisAnIssue={displayWhyIsThisAnIssue} | |||
issue={issue} | |||
onChange={this.props.onChange} | |||
@@ -141,15 +129,6 @@ export default class IssueView extends React.PureComponent<Props> { | |||
} | |||
} | |||
function isClickable(node: HTMLElement | undefined | null): boolean { | |||
if (!node) { | |||
return false; | |||
} | |||
const clickableTags = ['A', 'BUTTON', 'INPUT', 'TEXTAREA']; | |||
const tagName = (node.tagName || '').toUpperCase(); | |||
return clickableTags.includes(tagName) || isClickable(node.parentElement); | |||
} | |||
const IssueItem = styled.li` | |||
box-sizing: border-box; | |||
border: ${themeBorder('default', 'transparent')}; |