From 19c131bdfccb98c24bcf432c5e5968bc5d4ffd5c Mon Sep 17 00:00:00 2001 From: stanislavh Date: Mon, 6 Nov 2023 11:13:33 +0100 Subject: SONAR-20873 Create new education tour for accepting issues --- .../design-system/src/components/Dropdown.tsx | 4 ++ .../design-system/src/components/DropdownMenu.tsx | 2 +- .../src/components/DropdownToggler.tsx | 37 +++++++++------ .../design-system/src/components/HighlightRing.tsx | 30 ++++++++++++ .../design-system/src/components/SpotlightTour.tsx | 46 +++++++++++++++--- .../src/components/__tests__/Dropdown-test.tsx | 54 +++++++++++++++++++--- .../design-system/src/components/index.ts | 1 + 7 files changed, 148 insertions(+), 26 deletions(-) create mode 100644 server/sonar-web/design-system/src/components/HighlightRing.tsx (limited to 'server/sonar-web/design-system/src/components') 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, State> { } 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 = {overlay}; + + if (withFocusOutHandler) { + finalOverlay = {finalOverlay}; + } + + if (withClickOutHandler) { + finalOverlay = ( + {finalOverlay} + ); + } return ( - - - {overlay} - - - ) : undefined - } - {...popupProps} - > + {children} ); 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 { skipLabel?: string; stepXofYLabel?: (x: number, y: number) => string; steps: SpotlightTourStep[]; + width?: number; } export type SpotlightTourStep = Pick & { @@ -55,6 +56,7 @@ export type SpotlightTourStep = Pick(null); @@ -80,6 +84,17 @@ function TooltipComponent({ const placement = step.placement ?? DEFAULT_PLACEMENT; 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) { @@ -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 ( @@ -138,7 +163,7 @@ function TooltipComponent({
{step.content}
-
+
{(stepXofYLabel || size > 1) && ( {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 ( , - ) => } + ) => } {...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 }) => ( - - )); + const { user } = renderDropdown({ + children: ({ 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 = {}) { + const { children, ...rest } = props; return renderWithRouter( - }> + } {...rest}> {children ?? } , ); 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'; -- cgit v1.2.3