Browse Source

SONAR-21656 Remove legacy Dropdown component

tags/10.5.0.89998
Jeremy Davis 2 months ago
parent
commit
645082de0e

+ 0
- 68
server/sonar-web/src/main/js/components/controls/Dropdown.tsx View File

@@ -1,68 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { Popup, PopupPlacement } from '../ui/popups';
import ScreenPositionFixer from './ScreenPositionFixer';

interface OverlayProps {
className?: string;
children: React.ReactNode;
noPadding?: boolean;
placement?: PopupPlacement;
useEventBoundary?: boolean;
}

export class DropdownOverlay extends React.Component<OverlayProps> {
get placement() {
return this.props.placement || PopupPlacement.Bottom;
}

renderPopup = (leftFix?: number, topFix?: number) => (
<Popup
arrowStyle={
leftFix !== undefined && topFix !== undefined
? { transform: `translate(${-leftFix}px, ${-topFix}px)` }
: undefined
}
className={this.props.className}
noPadding={this.props.noPadding}
placement={this.placement}
style={
leftFix !== undefined && topFix !== undefined
? { marginLeft: `calc(50% + ${leftFix}px)` }
: undefined
}
useEventBoundary={this.props.useEventBoundary}
>
{this.props.children}
</Popup>
);

render() {
if (this.placement === PopupPlacement.Bottom) {
return (
<ScreenPositionFixer>
{({ leftFix, topFix }) => this.renderPopup(leftFix, topFix)}
</ScreenPositionFixer>
);
}
return this.renderPopup();
}
}

+ 0
- 89
server/sonar-web/src/main/js/components/controls/Toggler.tsx View File

@@ -1,89 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import DocumentClickHandler from './DocumentClickHandler';
import EscKeydownHandler from './EscKeydownHandler';
import FocusOutHandler from './FocusOutHandler';
import OutsideClickHandler from './OutsideClickHandler';
import UpDownKeyboardHanlder from './UpDownKeyboardHandler';

interface Props {
children?: React.ReactNode;
closeOnClick?: boolean;
closeOnClickOutside?: boolean;
closeOnEscape?: boolean;
closeOnFocusOut?: boolean;
navigateWithKeyboard?: boolean;
onRequestClose: () => void;
open: boolean;
overlay: React.ReactNode;
}

export default class Toggler extends React.Component<Props> {
renderOverlay() {
const {
closeOnClick = false,
closeOnClickOutside = true,
closeOnEscape = true,
closeOnFocusOut = true,
navigateWithKeyboard = true,
onRequestClose,
overlay,
} = this.props;

let renderedOverlay = overlay;

if (navigateWithKeyboard) {
renderedOverlay = <UpDownKeyboardHanlder>{renderedOverlay}</UpDownKeyboardHanlder>;
}

if (closeOnFocusOut) {
renderedOverlay = (
<FocusOutHandler onFocusOut={onRequestClose}>{renderedOverlay}</FocusOutHandler>
);
}

if (closeOnEscape) {
renderedOverlay = (
<EscKeydownHandler onKeydown={onRequestClose}>{renderedOverlay}</EscKeydownHandler>
);
}

if (closeOnClick) {
return (
<DocumentClickHandler onClick={onRequestClose}>{renderedOverlay}</DocumentClickHandler>
);
} else if (closeOnClickOutside) {
return (
<OutsideClickHandler onClickOutside={onRequestClose}>{renderedOverlay}</OutsideClickHandler>
);
}
return renderedOverlay;
}

render() {
return (
<>
{this.props.children}
{this.props.open && this.renderOverlay()}
</>
);
}
}

+ 0
- 237
server/sonar-web/src/main/js/components/controls/__tests__/Toggler-test.tsx View File

@@ -1,237 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
import * as React from 'react';
import { byRole } from '../../../helpers/testSelector';
import Toggler from '../Toggler';

const ui = {
toggleButton: byRole('button', { name: 'toggle' }),
outButton: byRole('button', { name: 'out' }),
overlayButton: byRole('button', { name: 'overlay' }),
nextOverlayButton: byRole('button', { name: 'next overlay' }),
overlayTextarea: byRole('textbox'),
overlayLatButton: byRole('button', { name: 'last' }),
};

async function openToggler(user: UserEvent) {
await user.click(ui.toggleButton.get());
expect(ui.overlayButton.get()).toBeInTheDocument();
}

async function focusOut() {
await userEvent.click(ui.outButton.get());
}

it('should handle key up/down', async () => {
const user = userEvent.setup({ delay: null });
const rerender = renderToggler(
{},
<>
<textarea name="test-area" />
<button type="button">last</button>
</>,
);

await openToggler(user);
await user.keyboard('{ArrowUp}');
expect(ui.overlayLatButton.get()).toHaveFocus();

await user.keyboard('{ArrowUp}');
expect(ui.overlayTextarea.get()).toHaveFocus();

// Focus does not escape multiline input
await user.keyboard('{ArrowDown}');
expect(ui.overlayTextarea.get()).toHaveFocus();
await user.keyboard('{ArrowUp}');
expect(ui.overlayTextarea.get()).toHaveFocus();

// Escapt textarea
await user.keyboard('{Tab}');

// No focus change when using shortcut
await user.keyboard('{Control>}{ArrowUp}{/Control}');
expect(ui.overlayLatButton.get()).toHaveFocus();

await user.keyboard('{ArrowDown}');
expect(ui.overlayButton.get()).toHaveFocus();

await user.keyboard('{ArrowDown}');
expect(ui.nextOverlayButton.get()).toHaveFocus();

rerender();
await openToggler(user);
await user.keyboard('{ArrowDown}');
expect(ui.overlayButton.get()).toHaveFocus();
});

it('should handle escape correclty', async () => {
const user = userEvent.setup({ delay: null });
const rerender = renderToggler({
closeOnEscape: true,
closeOnClick: false,
closeOnClickOutside: false,
closeOnFocusOut: false,
});

await openToggler(user);

await user.keyboard('{Escape}');
expect(ui.overlayButton.query()).not.toBeInTheDocument();

rerender({ closeOnEscape: false });
await openToggler(user);

await user.keyboard('{Escape}');
expect(ui.overlayButton.get()).toBeInTheDocument();
});

it('should handle focus correctly', async () => {
const user = userEvent.setup({ delay: null });
const rerender = renderToggler({
closeOnEscape: false,
closeOnClick: false,
closeOnClickOutside: false,
closeOnFocusOut: true,
});

await openToggler(user);

await focusOut();
expect(ui.overlayButton.query()).not.toBeInTheDocument();

rerender({ closeOnFocusOut: false });
await openToggler(user);

await focusOut();
expect(ui.overlayButton.get()).toBeInTheDocument();
});

it('should handle click correctly', async () => {
const user = userEvent.setup({ delay: null });
const rerender = renderToggler({
closeOnEscape: false,
closeOnClick: true,
closeOnClickOutside: false,
closeOnFocusOut: false,
});

await openToggler(user);

await user.click(ui.outButton.get());
expect(ui.overlayButton.query()).not.toBeInTheDocument();

await openToggler(user);

await user.click(ui.overlayButton.get());
expect(ui.overlayButton.query()).not.toBeInTheDocument();

rerender({ closeOnClick: false });
await openToggler(user);

await user.click(ui.outButton.get());
expect(ui.overlayButton.get()).toBeInTheDocument();
});

it('should handle click outside correctly', async () => {
const user = userEvent.setup({ delay: null });
const rerender = renderToggler({
closeOnEscape: false,
closeOnClick: false,
closeOnClickOutside: true,
closeOnFocusOut: false,
});

await openToggler(user);

await user.click(ui.overlayButton.get());
expect(await ui.overlayButton.find()).toBeInTheDocument();

await user.click(ui.outButton.get());
expect(ui.overlayButton.query()).not.toBeInTheDocument();

rerender({ closeOnClickOutside: false });
await openToggler(user);

await user.click(ui.outButton.get());
expect(ui.overlayButton.get()).toBeInTheDocument();
});

it('should open/close correctly when default props is applied', async () => {
const user = userEvent.setup({ delay: null });
renderToggler();

await openToggler(user);

// Should not close when on overlay
await user.click(ui.overlayButton.get());
expect(await ui.overlayButton.find()).toBeInTheDocument();

// Focus out should close
await focusOut();
expect(ui.overlayButton.query()).not.toBeInTheDocument();

await openToggler(user);

// Escape should close
await user.keyboard('{Escape}');
expect(ui.overlayButton.query()).not.toBeInTheDocument();

await openToggler(user);

// Click should close (focus out is trigger first)
await user.click(ui.outButton.get());
expect(ui.overlayButton.query()).not.toBeInTheDocument();
});

function renderToggler(override?: Partial<Toggler['props']>, additionalOverlay?: React.ReactNode) {
function App(props: Partial<Toggler['props']>) {
const [open, setOpen] = React.useState(false);

return (
<>
<Toggler
onRequestClose={() => setOpen(false)}
open={open}
overlay={
<div className="popup">
<button type="button">overlay</button>
<button type="button">next overlay</button>
{additionalOverlay}
</div>
}
{...props}
>
<button onClick={() => setOpen(true)} type="button">
toggle
</button>
</Toggler>
<button type="button">out</button>
</>
);
}

const { rerender } = render(<App {...override} />);
return function (reoverride?: Partial<Toggler['props']>) {
return rerender(<App {...override} {...reoverride} />);
};
}

+ 0
- 20
server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx View File

@@ -22,7 +22,6 @@ import * as React from 'react';
import { IssueActions } from '../../../types/issues';
import { Issue } from '../../../types/types';
import IssueAssign from './IssueAssign';
import IssueCommentAction from './IssueCommentAction';
import IssueTags from './IssueTags';
import IssueTransition from './IssueTransition';
import SonarLintBadge from './SonarLintBadge';
@@ -50,16 +49,7 @@ export default function IssueActionsBar(props: Readonly<Props>) {
canSetTags,
} = props;

const [commentPlaceholder, setCommentPlaceholder] = React.useState('');

const toggleComment = (open: boolean, placeholder = '') => {
setCommentPlaceholder(placeholder);

togglePopup('comment', open);
};

const canAssign = issue.actions.includes(IssueActions.Assign);
const canComment = issue.actions.includes(IssueActions.Comment);
const tagsPopupOpen = currentPopup === 'edit-tags' && canSetTags;

return (
@@ -107,16 +97,6 @@ export default function IssueActionsBar(props: Readonly<Props>) {
</li>
)}
</ul>

{canComment && (
<IssueCommentAction
commentPlaceholder={commentPlaceholder}
currentPopup={currentPopup === 'comment'}
issueKey={issue.key}
onChange={onChange}
toggleComment={toggleComment}
/>
)}
</div>
);
}

+ 0
- 71
server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.tsx View File

@@ -1,71 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { addIssueComment, deleteIssueComment, editIssueComment } from '../../../api/issues';
import Toggler from '../../../components/controls/Toggler';
import { Issue } from '../../../types/types';
import { updateIssue } from '../actions';
import CommentPopup from '../popups/CommentPopup';

interface Props {
commentPlaceholder: string;
currentPopup?: boolean;
issueKey: string;
onChange: (issue: Issue) => void;
toggleComment: (open: boolean, placeholder?: string, autoTriggered?: boolean) => void;
}

export default class IssueCommentAction extends React.PureComponent<Props> {
addComment = (text: string) => {
updateIssue(this.props.onChange, addIssueComment({ issue: this.props.issueKey, text }));
this.handleClose();
};

handleEditComment = (comment: string, text: string) => {
updateIssue(this.props.onChange, editIssueComment({ comment, text }));
};

handleDeleteComment = (comment: string) => {
updateIssue(this.props.onChange, deleteIssueComment({ comment }));
};

handleClose = () => {
this.props.toggleComment(false);
};

render() {
return (
<div className="issue-meta dropdown">
<Toggler
closeOnClickOutside={false}
onRequestClose={this.handleClose}
open={!!this.props.currentPopup}
overlay={
<CommentPopup
onComment={this.addComment}
placeholder={this.props.commentPlaceholder}
toggleComment={this.props.toggleComment}
/>
}
/>
</div>
);
}
}

+ 0
- 95
server/sonar-web/src/main/js/components/issue/popups/CommentForm.tsx View File

@@ -1,95 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { KeyboardKeys } from '../../../helpers/keycodes';
import { translate } from '../../../helpers/l10n';
import FormattingTips from '../../common/FormattingTips';
import { Button, ResetButtonLink } from '../../controls/buttons';

export interface CommentFormProps {
comment?: string;
onCancel: () => void;
onSaveComment: (comment: string) => void;
placeholder?: string;
showFormatHelp: boolean;
autoTriggered?: boolean;
}

export default function CommentForm(props: CommentFormProps) {
const { comment, placeholder, showFormatHelp, autoTriggered } = props;
const [editComment, setEditComment] = React.useState(comment || '');

return (
<>
<div className="issue-comment-form-text">
<textarea
autoFocus
className="sw-w-full"
style={{ resize: 'vertical' }}
placeholder={placeholder}
aria-label={translate('issue.comment.enter_comment')}
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => {
setEditComment(event.target.value);
}}
onKeyDown={(event: React.KeyboardEvent) => {
if (event.nativeEvent.key === KeyboardKeys.Enter && (event.metaKey || event.ctrlKey)) {
props.onSaveComment(editComment);
setEditComment('');
}
}}
rows={2}
value={editComment}
/>
</div>
<div className="sw-flex sw-justify-between issue-comment-form-footer">
{showFormatHelp && (
<div className="issue-comment-form-tips">
<FormattingTips />
</div>
)}
<div>
<div className="issue-comment-form-actions">
<Button
className="js-issue-comment-submit"
disabled={editComment.trim().length < 1}
onClick={() => {
props.onSaveComment(editComment);
setEditComment('');
}}
>
{comment ? translate('save') : translate('issue.comment.formlink')}
</Button>
<ResetButtonLink
className="js-issue-comment-cancel little-spacer-left"
aria-label={
comment
? translate('issue.comment.edit.cancel')
: translate('issue.comment.add_comment.cancel')
}
onClick={props.onCancel}
>
{autoTriggered ? translate('skip') : translate('cancel')}
</ResetButtonLink>
</div>
</div>
</div>
</>
);
}

+ 0
- 56
server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx View File

@@ -1,56 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { DropdownOverlay } from '../../../components/controls/Dropdown';
import { PopupPlacement } from '../../../components/ui/popups';
import { IssueComment } from '../../../types/types';
import CommentForm from './CommentForm';

export interface CommentPopupProps {
comment?: Pick<IssueComment, 'markdown'>;
onComment: (text: string) => void;
toggleComment: (visible: boolean) => void;
placeholder: string;
placement?: PopupPlacement;
}

export default class CommentPopup extends React.PureComponent<CommentPopupProps> {
handleCancelClick = () => {
this.props.toggleComment(false);
};

render() {
const { comment } = this.props;

return (
<DropdownOverlay placement={this.props.placement}>
<div className="sw-min-w-abs-500 issue-comment-bubble-popup">
<CommentForm
placeholder={this.props.placeholder}
onCancel={this.handleCancelClick}
onSaveComment={this.props.onComment}
showFormatHelp
comment={comment?.markdown}
/>
</div>
</DropdownOverlay>
);
}
}

Loading…
Cancel
Save