From 07fe0df1eb35f31228097b25137aa61c649bc49c Mon Sep 17 00:00:00 2001 From: Adam Kudrna Date: Mon, 16 Oct 2023 12:11:21 +0200 Subject: [PATCH] Feat(web-react): Use the new Placement dictionary in `Tooltip` #DS-923 --- .../src/components/Tooltip/README.md | 38 ++-- .../__tests__/useTooltipStyleProps.test.ts | 14 +- .../Tooltip/demo/TooltipOnHover.tsx | 46 +++++ .../Tooltip/demo/TooltipPlacements.tsx | 177 ++++++++++++++---- .../src/components/Tooltip/demo/index.tsx | 8 +- .../Tooltip/stories/Tooltip.stories.tsx | 4 +- .../stories/UncontrolledTooltip.stories.tsx | 4 +- .../Tooltip/useTooltipStyleProps.ts | 7 +- .../web-react/src/constants/dictionaries.ts | 16 ++ .../src/types/shared/dictionaries.ts | 5 + packages/web-react/src/types/tooltip.ts | 4 +- .../src/utils/__tests__/string.test.ts | 17 ++ packages/web-react/src/utils/index.ts | 1 + packages/web-react/src/utils/string.ts | 7 + 14 files changed, 279 insertions(+), 69 deletions(-) create mode 100644 packages/web-react/src/components/Tooltip/demo/TooltipOnHover.tsx create mode 100644 packages/web-react/src/utils/__tests__/string.test.ts create mode 100644 packages/web-react/src/utils/string.ts diff --git a/packages/web-react/src/components/Tooltip/README.md b/packages/web-react/src/components/Tooltip/README.md index 99a008081f..571b3d4d26 100644 --- a/packages/web-react/src/components/Tooltip/README.md +++ b/packages/web-react/src/components/Tooltip/README.md @@ -68,27 +68,27 @@ const toggleHandler = () => setOpen(!open); ## Tooltip Props -| Name | Type | Default | Required | Description | -| ------------------ | ------------------------------------------------- | -------- | -------- | ----------------------------------------- | -| `children` | `ReactNode` | — | ✔ | Tooltip children's nodes | -| `closeLabel` | `string` | `Close` | ✕ | Tooltip label on close button | -| `isDismissible` | `bool` | — | ✕ | When it should appear with a close button | -| `onClose` | `(event: ClickEvent) => void` | — | ✕ | Close button callback | -| `open` | `bool` | — | ✕ | Tooltip open state control | -| `placement` | [`top` \| `right` \| `bottom` \| `left` \| `off`] | `bottom` | ✕ | Tooltip placement | -| `UNSAFE_className` | `string` | — | ✕ | Tooltip custom class name | -| `UNSAFE_style` | `CSSProperties` | — | ✕ | Tooltip custom style | +| Name | Type | Default | Required | Description | +| ------------------ | --------------------------------------------------- | -------- | -------- | ----------------------------------------- | +| `children` | `ReactNode` | — | ✔ | Tooltip children's nodes | +| `closeLabel` | `string` | `Close` | ✕ | Tooltip label on close button | +| `isDismissible` | `bool` | — | ✕ | When it should appear with a close button | +| `onClose` | `(event: ClickEvent) => void` | — | ✕ | Close button callback | +| `open` | `bool` | — | ✕ | Tooltip open state control | +| `placement` | [Placement dictionary][dictionary-placement], 'off' | `bottom` | ✕ | Tooltip placement | +| `UNSAFE_className` | `string` | — | ✕ | Tooltip custom class name | +| `UNSAFE_style` | `CSSProperties` | — | ✕ | Tooltip custom style | ## UncontrolledTooltip Props -| Name | Type | Default | Required | Description | -| ------------------ | ------------------------------------------------- | -------- | -------- | ----------------------------------------- | -| `children` | `ReactNode` | — | ✔ | Tooltip children's nodes | -| `closeLabel` | `string` | `Close` | ✕ | Tooltip label on close button | -| `isDismissible` | `bool` | — | ✕ | When it should appear with a close button | -| `placement` | [`top` \| `right` \| `bottom` \| `left` \| `off`] | `bottom` | ✕ | Tooltip placement | -| `UNSAFE_className` | `string` | — | ✕ | Tooltip custom class name | -| `UNSAFE_style` | `CSSProperties` | — | ✕ | Tooltip custom style | +| Name | Type | Default | Required | Description | +| ------------------ | --------------------------------------------------- | -------- | -------- | ----------------------------------------- | +| `children` | `ReactNode` | — | ✔ | Tooltip children's nodes | +| `closeLabel` | `string` | `Close` | ✕ | Tooltip label on close button | +| `isDismissible` | `bool` | — | ✕ | When it should appear with a close button | +| `placement` | [Placement dictionary][dictionary-placement], 'off' | `bottom` | ✕ | Tooltip placement | +| `UNSAFE_className` | `string` | — | ✕ | Tooltip custom class name | +| `UNSAFE_style` | `CSSProperties` | — | ✕ | Tooltip custom style | ## TooltipWrapper Props @@ -219,3 +219,5 @@ const { getReferenceProps, getFloatingProps } = useInteractions([hover, focus, d ; ``` + +[dictionary-placement]: https://github.com/lmc-eu/spirit-design-system/tree/main/docs/DICTIONARIES.md#placement diff --git a/packages/web-react/src/components/Tooltip/__tests__/useTooltipStyleProps.test.ts b/packages/web-react/src/components/Tooltip/__tests__/useTooltipStyleProps.test.ts index 3ebe192035..58fd22022e 100644 --- a/packages/web-react/src/components/Tooltip/__tests__/useTooltipStyleProps.test.ts +++ b/packages/web-react/src/components/Tooltip/__tests__/useTooltipStyleProps.test.ts @@ -1,21 +1,29 @@ import { renderHook } from '@testing-library/react-hooks'; -import { useTooltipStyleProps } from '../useTooltipStyleProps'; +import { PlacementDictionaryType } from '../../../types'; +import { useTooltipStyleProps, UseTooltipStyleProps } from '../useTooltipStyleProps'; describe('useTooltipStyleProps', () => { it('should return defaults', () => { const { result } = renderHook(() => useTooltipStyleProps({})); - expect(result.current.classProps).toBeDefined(); expect(result.current.classProps.rootClassName).toBe('Tooltip Tooltip--bottom'); expect(result.current.classProps.wrapperClassName).toBe('TooltipWrapper'); expect(result.current.classProps.arrowClassName).toBe('Tooltip__arrow'); expect(result.current.classProps.closeButtonClassName).toBe('Tooltip__close'); }); + it('should change placement', () => { + const props = { + placement: 'top-right' as PlacementDictionaryType, + } as UseTooltipStyleProps; + const { result } = renderHook(() => useTooltipStyleProps(props)); + + expect(result.current.classProps.rootClassName).toBe('Tooltip Tooltip--topRight'); + }); + it('should return dismissible class', () => { const { result } = renderHook(() => useTooltipStyleProps({ isDismissible: true })); - expect(result.current.classProps).toBeDefined(); expect(result.current.classProps.rootClassName).toBe('Tooltip Tooltip--bottom Tooltip--dismissible'); }); }); diff --git a/packages/web-react/src/components/Tooltip/demo/TooltipOnHover.tsx b/packages/web-react/src/components/Tooltip/demo/TooltipOnHover.tsx new file mode 100644 index 0000000000..6bfe2ee0a9 --- /dev/null +++ b/packages/web-react/src/components/Tooltip/demo/TooltipOnHover.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { ButtonLink } from '../../Button'; +import TooltipWrapper from '../TooltipWrapper'; +import Tooltip from '../Tooltip'; + +const TooltipOnHover = () => ( + <> + + + Tooltip on top + + + Hello there! + + + + + + Tooltip on right + + + Hello there! + + + + + + Tooltip on bottom + + + Hello there! + + + + + + Tooltip on left + + + Hello there! + + + +); + +export default TooltipOnHover; diff --git a/packages/web-react/src/components/Tooltip/demo/TooltipPlacements.tsx b/packages/web-react/src/components/Tooltip/demo/TooltipPlacements.tsx index 835d2e898c..90803452c6 100644 --- a/packages/web-react/src/components/Tooltip/demo/TooltipPlacements.tsx +++ b/packages/web-react/src/components/Tooltip/demo/TooltipPlacements.tsx @@ -1,46 +1,145 @@ -import React from 'react'; -import { ButtonLink } from '../../Button'; +import React, { ChangeEvent, useState } from 'react'; +import DocsBox from '../../../../docs/DocsBox'; +import { PlacementDictionaryType } from '../../../types'; +import { Radio } from '../../Radio'; import TooltipWrapper from '../TooltipWrapper'; import Tooltip from '../Tooltip'; -const TooltipPlacements = () => ( - <> - - - Tooltip on top - - - Hello there! - - +const TooltipPlacements = () => { + const [placement, setPlacement] = useState('bottom'); - - - Tooltip on right - - - Hello there! - - + const handlePlacementChange = (e: ChangeEvent) => { + setPlacement(e.target.value as PlacementDictionaryType); + }; - - - Tooltip on bottom - - - Hello there! - - - - - - Tooltip on left - - - Hello there! - - - -); + return ( +
+
+
+ {' '} + {' '} + +
+
+ {' '} + {' '} + +
+
+ + + +
+
+ + + +
+
+ + Click the dots! + Hello! + +
+
+
+ ); +}; export default TooltipPlacements; diff --git a/packages/web-react/src/components/Tooltip/demo/index.tsx b/packages/web-react/src/components/Tooltip/demo/index.tsx index ec350a05a5..02d97bf247 100644 --- a/packages/web-react/src/components/Tooltip/demo/index.tsx +++ b/packages/web-react/src/components/Tooltip/demo/index.tsx @@ -7,8 +7,9 @@ import ReactDOM from 'react-dom/client'; import icons from '@lmc-eu/spirit-icons/dist/icons'; import DocsSection from '../../../../docs/DocsSections'; import { IconsProvider } from '../../../context'; -import TooltipDefault from './TooltipDefault'; import TooltipPlacements from './TooltipPlacements'; +import TooltipDefault from './TooltipDefault'; +import TooltipOnHover from './TooltipOnHover'; import TooltipClickable from './TooltipClickable'; import TooltipDismissible from './TooltipDismissible'; import TooltipFloatingUI from './TooltipFloatingUi'; @@ -17,11 +18,14 @@ import TooltipDismissibleViaJS from './TooltipDismissibleViaJS'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + - + diff --git a/packages/web-react/src/components/Tooltip/stories/Tooltip.stories.tsx b/packages/web-react/src/components/Tooltip/stories/Tooltip.stories.tsx index e8d46cb6d6..fe7b3ad2fa 100644 --- a/packages/web-react/src/components/Tooltip/stories/Tooltip.stories.tsx +++ b/packages/web-react/src/components/Tooltip/stories/Tooltip.stories.tsx @@ -2,6 +2,8 @@ import React from 'react'; import { Markdown } from '@storybook/blocks'; import type { Meta, StoryObj } from '@storybook/react'; +import { Placements } from '../../../constants'; + import { Button } from '../..'; import ReadMe from '../README.md'; import { Tooltip, TooltipWrapper } from '..'; @@ -33,7 +35,7 @@ const meta: Meta = { }, placement: { control: 'select', - options: ['top', 'right', 'bottom', 'left', 'off'], + options: [...Object.values(Placements), 'off'], table: { defaultValue: { summary: 'bottom' }, }, diff --git a/packages/web-react/src/components/Tooltip/stories/UncontrolledTooltip.stories.tsx b/packages/web-react/src/components/Tooltip/stories/UncontrolledTooltip.stories.tsx index 5936c0679a..87920749c1 100644 --- a/packages/web-react/src/components/Tooltip/stories/UncontrolledTooltip.stories.tsx +++ b/packages/web-react/src/components/Tooltip/stories/UncontrolledTooltip.stories.tsx @@ -1,6 +1,8 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; +import { Placements } from '../../../constants'; + import { Button } from '../..'; import { TooltipWrapper, UncontrolledTooltip } from '..'; @@ -25,7 +27,7 @@ const meta: Meta = { }, placement: { control: 'select', - options: ['top', 'right', 'bottom', 'left', 'off'], + options: [...Object.values(Placements), 'off'], table: { defaultValue: { summary: 'bottom' }, }, diff --git a/packages/web-react/src/components/Tooltip/useTooltipStyleProps.ts b/packages/web-react/src/components/Tooltip/useTooltipStyleProps.ts index 98598ee225..968ad1c47d 100644 --- a/packages/web-react/src/components/Tooltip/useTooltipStyleProps.ts +++ b/packages/web-react/src/components/Tooltip/useTooltipStyleProps.ts @@ -2,8 +2,9 @@ import { useMemo } from 'react'; import classNames from 'classnames'; import { SpiritTooltipProps, TooltipProps } from '../../types'; import { useClassNamePrefix } from '../../hooks'; +import { kebabCaseToCamelCase } from '../../utils'; -export interface UseTooltipStylePropsProps extends SpiritTooltipProps {} +export interface UseTooltipStyleProps extends SpiritTooltipProps {} export interface UseTooltipStylePropsReturn { classProps: { @@ -15,7 +16,7 @@ export interface UseTooltipStylePropsReturn { props: TooltipProps; } -export const useTooltipStyleProps = (props: UseTooltipStylePropsProps): UseTooltipStylePropsReturn => { +export const useTooltipStyleProps = (props: UseTooltipStyleProps): UseTooltipStylePropsReturn => { const { placement = 'bottom', isDismissible, open, ...modifiedProps } = props; const tooltipClass = useClassNamePrefix('Tooltip'); @@ -23,7 +24,7 @@ export const useTooltipStyleProps = (props: UseTooltipStylePropsProps): UseToolt const arrowClass = `${tooltipClass}__arrow`; const closeButtonClass = `${tooltipClass}__close`; const rootDismissibleClass = `${tooltipClass}--dismissible`; - const rootPlacementClass = placement !== 'off' ? `${tooltipClass}--${placement}` : null; + const rootPlacementClass = placement !== 'off' ? `${tooltipClass}--${kebabCaseToCamelCase(placement)}` : null; const rootHiddenClass = 'is-hidden'; const isHiddenClass = useMemo(() => open === false, [open]); diff --git a/packages/web-react/src/constants/dictionaries.ts b/packages/web-react/src/constants/dictionaries.ts index 18f44a244c..0f69fdff58 100644 --- a/packages/web-react/src/constants/dictionaries.ts +++ b/packages/web-react/src/constants/dictionaries.ts @@ -46,6 +46,22 @@ export const SizesExtended = { XLARGE: 'xlarge', } as const; +/* Placement */ +export const Placements = { + TOP: 'top', + TOP_RIGHT: 'top-right', + TOP_LEFT: 'top-left', + BOTTOM: 'bottom', + BOTTOM_RIGHT: 'bottom-right', + BOTTOM_LEFT: 'bottom-left', + LEFT: 'left', + LEFT_TOP: 'left-top', + LEFT_BOTTOM: 'left-bottom', + RIGHT: 'right', + RIGHT_TOP: 'right-top', + RIGHT_BOTTOM: 'right-bottom', +} as const; + /* Validation */ export const ValidationStates = { SUCCESS: 'success', diff --git a/packages/web-react/src/types/shared/dictionaries.ts b/packages/web-react/src/types/shared/dictionaries.ts index a9f21eb8a5..3b9a96d5a2 100644 --- a/packages/web-react/src/types/shared/dictionaries.ts +++ b/packages/web-react/src/types/shared/dictionaries.ts @@ -4,6 +4,7 @@ import { EmotionColors, ActionLinkColors, TextColors, + Placements, Sizes, SizesExtended, ValidationStates, @@ -27,6 +28,10 @@ export type EmotionColorsDictionaryType = (typeof EmotionColors)[ export type TextColorsDictionaryKeys = keyof typeof TextColors; export type TextColorsDictionaryType = (typeof TextColors)[TextColorsDictionaryKeys] | C; +/* Placement */ +export type PlacementDictionaryKeys = keyof typeof Placements; +export type PlacementDictionaryType = (typeof Placements)[PlacementDictionaryKeys] | T; + /* Size */ export type SizesDictionaryKeys = keyof typeof Sizes; export type SizesDictionaryType = (typeof Sizes)[SizesDictionaryKeys] | T; diff --git a/packages/web-react/src/types/tooltip.ts b/packages/web-react/src/types/tooltip.ts index c47fbc97e5..1a0814581b 100644 --- a/packages/web-react/src/types/tooltip.ts +++ b/packages/web-react/src/types/tooltip.ts @@ -1,4 +1,4 @@ -import { ChildrenProps, StyleProps, ClickEvent } from './shared'; +import { ChildrenProps, PlacementDictionaryType, StyleProps, ClickEvent } from './shared'; export interface TooltipHandlingProps { open?: boolean | undefined; @@ -9,7 +9,7 @@ export interface BaseTooltipProps extends ChildrenProps, StyleProps { closeLabel?: string; id?: string; isDismissible?: boolean; - placement?: 'top' | 'right' | 'bottom' | 'left' | 'off'; + placement?: PlacementDictionaryType | 'off'; } export interface TooltipWrapperProps extends ChildrenProps, StyleProps {} diff --git a/packages/web-react/src/utils/__tests__/string.test.ts b/packages/web-react/src/utils/__tests__/string.test.ts new file mode 100644 index 0000000000..a63af20cd5 --- /dev/null +++ b/packages/web-react/src/utils/__tests__/string.test.ts @@ -0,0 +1,17 @@ +import { kebabCaseToCamelCase } from '../string'; + +describe('string', () => { + describe('#kebabCaseToCamelCase', () => { + it.each([ + ['foo-bar', 'fooBar'], + ['test-case', 'testCase'], + ['some-words-here', 'someWordsHere'], + ['single', 'single'], + ['', ''], + ['kebab-case-test', 'kebabCaseTest'], + ])('should convert kebab-case string "%s" to camelCase string "%s"', (input, expected) => { + const result = kebabCaseToCamelCase(input); + expect(result).toBe(expected); + }); + }); +}); diff --git a/packages/web-react/src/utils/index.ts b/packages/web-react/src/utils/index.ts index 235dc52ee3..ff56365084 100644 --- a/packages/web-react/src/utils/index.ts +++ b/packages/web-react/src/utils/index.ts @@ -1,3 +1,4 @@ export * from './classname'; export * from './compose'; export * from './debounce'; +export * from './string'; diff --git a/packages/web-react/src/utils/string.ts b/packages/web-react/src/utils/string.ts new file mode 100644 index 0000000000..faa1898c3d --- /dev/null +++ b/packages/web-react/src/utils/string.ts @@ -0,0 +1,7 @@ +/** + * Converts a kebab-case string to camelCase + * @param str + */ +export const kebabCaseToCamelCase = (str: string): string => { + return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); +};