diff options
author | stanislavh <stanislav.honcharov@sonarsource.com> | 2023-11-06 11:13:33 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-11-08 20:02:53 +0000 |
commit | 19c131bdfccb98c24bcf432c5e5968bc5d4ffd5c (patch) | |
tree | 459256e361a1f8a308e1d244737e11b979dab683 /server/sonar-web/design-system/src/components | |
parent | c8d1b7eb2494d92f20fb8b498efdbb2e3f8ea12c (diff) | |
download | sonarqube-19c131bdfccb98c24bcf432c5e5968bc5d4ffd5c.tar.gz sonarqube-19c131bdfccb98c24bcf432c5e5968bc5d4ffd5c.zip |
SONAR-20873 Create new education tour for accepting issues
Diffstat (limited to 'server/sonar-web/design-system/src/components')
7 files changed, 148 insertions, 26 deletions
diff --git a/server/sonar-web/design-system/src/components/Dropdown.tsx b/server/sonar-web/design-system/src/components/Dropdown.tsx index 3bd340bcdf9..cce9579c5cb 100644 --- a/server/sonar-web/design-system/src/components/Dropdown.tsx +++ b/server/sonar-web/design-system/src/components/Dropdown.tsx @@ -53,6 +53,8 @@ interface Props { overlay: React.ReactNode; placement?: PopupPlacement; size?: InputSizeKeys; + withClickOutHandler?: boolean; + withFocusOutHandler?: boolean; zLevel?: PopupZLevel; } @@ -124,6 +126,8 @@ export class Dropdown extends React.PureComponent<Readonly<Props>, State> { </DropdownMenu> } placement={this.props.placement} + withClickOutHandler={this.props.withClickOutHandler} + withFocusOutHandler={this.props.withFocusOutHandler} zLevel={zLevel} > {children} diff --git a/server/sonar-web/design-system/src/components/DropdownMenu.tsx b/server/sonar-web/design-system/src/components/DropdownMenu.tsx index bf983364dc3..3076fd7e535 100644 --- a/server/sonar-web/design-system/src/components/DropdownMenu.tsx +++ b/server/sonar-web/design-system/src/components/DropdownMenu.tsx @@ -284,7 +284,7 @@ export const ItemDivider = styled.li` `; ItemDivider.defaultProps = { role: 'separator' }; -const DropdownMenuWrapper = styled.ul` +export const DropdownMenuWrapper = styled.ul` background-color: ${themeColor('dropdownMenu')}; color: ${themeContrast('dropdownMenu')}; width: var(--inputSize); diff --git a/server/sonar-web/design-system/src/components/DropdownToggler.tsx b/server/sonar-web/design-system/src/components/DropdownToggler.tsx index ee0f65e5733..15cabb5337a 100644 --- a/server/sonar-web/design-system/src/components/DropdownToggler.tsx +++ b/server/sonar-web/design-system/src/components/DropdownToggler.tsx @@ -27,24 +27,35 @@ type PopupProps = Popup['props']; interface Props extends PopupProps { onRequestClose: VoidFunction; open: boolean; + withClickOutHandler?: boolean; + withFocusOutHandler?: boolean; } export function DropdownToggler(props: Props) { - const { children, open, onRequestClose, overlay, ...popupProps } = props; + const { + children, + open, + onRequestClose, + withClickOutHandler = true, + withFocusOutHandler = true, + overlay, + ...popupProps + } = props; + + let finalOverlay = <EscKeydownHandler onKeydown={onRequestClose}>{overlay}</EscKeydownHandler>; + + if (withFocusOutHandler) { + finalOverlay = <FocusOutHandler onFocusOut={onRequestClose}>{finalOverlay}</FocusOutHandler>; + } + + if (withClickOutHandler) { + finalOverlay = ( + <OutsideClickHandler onClickOutside={onRequestClose}>{finalOverlay}</OutsideClickHandler> + ); + } return ( - <Popup - overlay={ - open ? ( - <OutsideClickHandler onClickOutside={onRequestClose}> - <FocusOutHandler onFocusOut={onRequestClose}> - <EscKeydownHandler onKeydown={onRequestClose}>{overlay}</EscKeydownHandler> - </FocusOutHandler> - </OutsideClickHandler> - ) : undefined - } - {...popupProps} - > + <Popup overlay={open && finalOverlay} {...popupProps}> {children} </Popup> ); diff --git a/server/sonar-web/design-system/src/components/HighlightRing.tsx b/server/sonar-web/design-system/src/components/HighlightRing.tsx new file mode 100644 index 00000000000..864b5de07b7 --- /dev/null +++ b/server/sonar-web/design-system/src/components/HighlightRing.tsx @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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 styled from '@emotion/styled'; +import tw from 'twin.macro'; +import { themeColor } from '../helpers'; + +export const HighlightRing = styled.div` + &.active { + box-shadow: 0 0 4px 0 ${themeColor('primary')}; + background: ${themeColor('highlightRingBackground')}; + ${tw`sw-rounded-1/2`} + } +`; diff --git a/server/sonar-web/design-system/src/components/SpotlightTour.tsx b/server/sonar-web/design-system/src/components/SpotlightTour.tsx index fcc529ca98b..a6018def24b 100644 --- a/server/sonar-web/design-system/src/components/SpotlightTour.tsx +++ b/server/sonar-web/design-system/src/components/SpotlightTour.tsx @@ -42,6 +42,7 @@ export interface SpotlightTourProps extends Omit<JoyrideProps, 'steps'> { skipLabel?: string; stepXofYLabel?: (x: number, y: number) => string; steps: SpotlightTourStep[]; + width?: number; } export type SpotlightTourStep = Pick<JoyrideStep, 'target' | 'content' | 'title'> & { @@ -55,6 +56,7 @@ export type SpotlightTourStep = Pick<JoyrideStep, 'target' | 'content' | 'title' const PULSE_SIZE = 8; const ARROW_LENGTH = 40; const DEFAULT_PLACEMENT = 'bottom'; +const DEFAULT_WIDTH = 315; function TooltipComponent({ continuous, @@ -68,9 +70,11 @@ function TooltipComponent({ primaryProps, stepXofYLabel, tooltipProps, + width = DEFAULT_WIDTH, }: TooltipRenderProps & { step: SpotlightTourStep; stepXofYLabel: SpotlightTourProps['stepXofYLabel']; + width?: number; }) { const [arrowPosition, setArrowPosition] = React.useState({ left: 0, top: 0, rotate: '0deg' }); const ref = React.useRef<HTMLDivElement | null>(null); @@ -81,6 +85,17 @@ function TooltipComponent({ const intl = useIntl(); React.useEffect(() => { + const target = + typeof step.target === 'string' ? document.querySelector(step.target) : step.target; + // To show the highlight, target has to be HighlightRing from design system + target?.classList.add('active'); + + return () => { + target?.classList.remove('active'); + }; + }, [step]); + + React.useEffect(() => { // We don't compute for "center"; "center" will simply not show any arrow. if (placement !== 'center' && ref.current?.parentNode) { let left = 0; @@ -115,10 +130,20 @@ function TooltipComponent({ } }, [step, ref, setArrowPosition, placement]); + /** + * Preventing click events from bubbling to avoid closing other popups, in cases when the guide + * is shown simultaneously with other popups. + */ + function handleClick(e: React.MouseEvent) { + e.stopPropagation(); + } + return ( <StyledPopupWrapper - className="sw-p-3 sw-body-sm sw-w-[315px] sw-relative sw-border-0" + className="sw-p-3 sw-body-sm sw-relative sw-border-0" + onClick={handleClick} placement={(step.placement as Placement | undefined) ?? DEFAULT_PLACEMENT} + style={{ width }} zLevel={PopupZLevel.Absolute} {...tooltipProps} > @@ -138,7 +163,7 @@ function TooltipComponent({ </WrapperButton> </div> <div>{step.content}</div> - <div className="sw-flex sw-justify-between sw-items-center sw-mt-3"> + <div className="sw-flex sw-justify-between sw-items-center sw-mt-4"> {(stepXofYLabel || size > 1) && ( <strong> {stepXofYLabel @@ -166,14 +191,23 @@ function TooltipComponent({ } export function SpotlightTour(props: SpotlightTourProps) { - const { steps, skipLabel, backLabel, closeLabel, nextLabel, stepXofYLabel, ...otherProps } = - props; + const { + steps, + skipLabel, + backLabel, + closeLabel, + nextLabel, + stepXofYLabel, + disableOverlay = true, + width, + ...otherProps + } = props; const intl = useIntl(); return ( <ReactJoyride - disableOverlay + disableOverlay={disableOverlay} floaterProps={{ styles: { floater: { @@ -202,7 +236,7 @@ export function SpotlightTour(props: SpotlightTourProps) { }))} tooltipComponent={( tooltipProps: React.PropsWithChildren<TooltipRenderProps & { step: SpotlightTourStep }>, - ) => <TooltipComponent stepXofYLabel={stepXofYLabel} {...tooltipProps} />} + ) => <TooltipComponent stepXofYLabel={stepXofYLabel} width={width} {...tooltipProps} />} {...otherProps} /> ); diff --git a/server/sonar-web/design-system/src/components/__tests__/Dropdown-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Dropdown-test.tsx index 34f43149e08..7843646e777 100644 --- a/server/sonar-web/design-system/src/components/__tests__/Dropdown-test.tsx +++ b/server/sonar-web/design-system/src/components/__tests__/Dropdown-test.tsx @@ -24,7 +24,7 @@ import { ButtonSecondary } from '../buttons'; describe('Dropdown', () => { it('renders', async () => { - const { user } = setupWithChildren(); + const { user } = renderDropdown(); expect(screen.getByRole('button')).toBeInTheDocument(); await user.click(screen.getByRole('button')); @@ -32,17 +32,59 @@ describe('Dropdown', () => { }); it('toggles with render prop', async () => { - const { user } = setupWithChildren(({ onToggleClick }) => ( - <ButtonSecondary onClick={onToggleClick} /> - )); + const { user } = renderDropdown({ + children: ({ onToggleClick }) => <ButtonSecondary onClick={onToggleClick} />, + }); await user.click(screen.getByRole('button')); expect(screen.getByRole('menu')).toBeVisible(); }); - function setupWithChildren(children?: Dropdown['props']['children']) { + it('closes when clicking outside of menu', async () => { + const { user } = renderDropdown(); + + await user.click(screen.getByRole('button')); + expect(screen.getByRole('menu')).toBeInTheDocument(); + + await user.click(document.body); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); + }); + + it('does not close when clicking ouside of menu', async () => { + const { user } = renderDropdown({ withClickOutHandler: false }); + + await user.click(screen.getByRole('button')); + expect(screen.getByRole('menu')).toBeInTheDocument(); + + await user.click(document.body); + expect(screen.getByRole('menu')).toBeInTheDocument(); + }); + + it('closes when other target gets focus', async () => { + const { user } = renderDropdown(); + + await user.click(screen.getByRole('button')); + expect(screen.getByRole('menu')).toBeInTheDocument(); + + await user.tab(); + + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); + }); + + it('does not close when other target gets focus', async () => { + const { user } = renderDropdown({ withFocusOutHandler: false }); + + await user.click(screen.getByRole('button')); + expect(screen.getByRole('menu')).toBeInTheDocument(); + + await user.tab(); + expect(screen.getByRole('menu')).toBeInTheDocument(); + }); + + function renderDropdown(props: Partial<Dropdown['props']> = {}) { + const { children, ...rest } = props; return renderWithRouter( - <Dropdown id="test-menu" overlay={<div id="overlay" />}> + <Dropdown id="test-menu" overlay={<div id="overlay" />} {...rest}> {children ?? <ButtonSecondary />} </Dropdown>, ); diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index 180db1f3da7..5ed00fbfce7 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -41,6 +41,7 @@ export { FailedQGConditionLink } from './FailedQGConditionLink'; export * from './FavoriteButton'; export { DismissableFlagMessage, FlagMessage } from './FlagMessage'; export * from './FlowStep'; +export * from './HighlightRing'; export * from './HighlightedSection'; export { Histogram } from './Histogram'; export { HotspotRating } from './HotspotRating'; |