+++ /dev/null
-/*
- * 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();
- }
-}
+++ /dev/null
-/*
- * 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()}
- </>
- );
- }
-}
+++ /dev/null
-/*
- * 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} />);
- };
-}
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';
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 (
</li>
)}
</ul>
-
- {canComment && (
- <IssueCommentAction
- commentPlaceholder={commentPlaceholder}
- currentPopup={currentPopup === 'comment'}
- issueKey={issue.key}
- onChange={onChange}
- toggleComment={toggleComment}
- />
- )}
</div>
);
}
+++ /dev/null
-/*
- * 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>
- );
- }
-}
+++ /dev/null
-/*
- * 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>
- </>
- );
-}
+++ /dev/null
-/*
- * 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>
- );
- }
-}