From f4aa004a9bb365313a8688367e94bd3fc6693a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kry=C5=A1p=C3=ADn?= Date: Thu, 8 Feb 2024 16:25:38 +0100 Subject: [PATCH] Feat(web-react): Deprecate non-flow-relative placements in `Dropdown` #DS-1132 Use flow-relative placement values instead, e.g. `top-left` is now `top-start`. --- .../src/components/Dropdown/Dropdown.tsx | 9 ++- .../components/Dropdown/DropdownContext.ts | 2 +- .../components/Dropdown/DropdownPopover.tsx | 4 +- .../src/components/Dropdown/README.md | 57 ++++++++------ .../__tests__/useDropdownAriaProps.test.ts | 5 ++ .../__tests__/useDropdownStyleProps.test.ts | 16 +--- .../Dropdown/demo/DropdownEnhancedShadow.tsx | 2 +- .../Dropdown/demo/DropdownFullwidthAll.tsx | 2 +- .../demo/DropdownFullwidthMobileOnly.tsx | 2 +- .../Dropdown/demo/DropdownPlacements.tsx | 74 +++++++++---------- .../Dropdown/stories/Dropdown.stories.tsx | 4 +- .../Dropdown/useDropdownAriaProps.ts | 41 ++++++++-- .../Dropdown/useDropdownStyleProps.ts | 7 +- .../Resources/components/Dropdown/README.md | 2 +- 14 files changed, 130 insertions(+), 97 deletions(-) diff --git a/packages/web-react/src/components/Dropdown/Dropdown.tsx b/packages/web-react/src/components/Dropdown/Dropdown.tsx index 400b6358b4..b0e7a2a6ab 100644 --- a/packages/web-react/src/components/Dropdown/Dropdown.tsx +++ b/packages/web-react/src/components/Dropdown/Dropdown.tsx @@ -13,17 +13,18 @@ import { useDropdownStyleProps } from './useDropdownStyleProps'; const defaultProps = { enableAutoClose: true, - placement: Placements.BOTTOM_LEFT, + placement: Placements.BOTTOM_START, }; export const Dropdown = (props: SpiritDropdownProps) => { const { - id = Math.random().toString(36).slice(2, 7), children, - renderTrigger, enableAutoClose, fullWidthMode, + id = Math.random().toString(36).slice(2, 7), onAutoClose, + placement = defaultProps.placement, + renderTrigger, ...rest } = props; @@ -32,7 +33,7 @@ export const Dropdown = (props: SpiritDropdownProps) => { const { isOpen, toggleHandler } = useDropdown({ dropdownRef, triggerRef, enableAutoClose, onAutoClose }); const { classProps, props: modifiedProps } = useDropdownStyleProps({ isOpen, ...rest }); - const { triggerProps, contentProps } = useDropdownAriaProps({ id, isOpen, toggleHandler, fullWidthMode }); + const { triggerProps, contentProps } = useDropdownAriaProps({ id, isOpen, fullWidthMode, placement, toggleHandler }); const { styleProps: contentStyleProps, props: contentOtherProps } = useStyleProps({ ...modifiedProps }); const { styleProps: triggerStyleProps } = useStyleProps({ diff --git a/packages/web-react/src/components/Dropdown/DropdownContext.ts b/packages/web-react/src/components/Dropdown/DropdownContext.ts index 10086003e8..663fa427b9 100644 --- a/packages/web-react/src/components/Dropdown/DropdownContext.ts +++ b/packages/web-react/src/components/Dropdown/DropdownContext.ts @@ -19,7 +19,7 @@ const defaultContext: DropdownContextType = { id: '', isOpen: false, onToggle: () => {}, - placement: Placements.BOTTOM_LEFT, + placement: Placements.BOTTOM_START, triggerRef: { current: undefined }, }; diff --git a/packages/web-react/src/components/Dropdown/DropdownPopover.tsx b/packages/web-react/src/components/Dropdown/DropdownPopover.tsx index a64b3a6587..b09efd8f22 100644 --- a/packages/web-react/src/components/Dropdown/DropdownPopover.tsx +++ b/packages/web-react/src/components/Dropdown/DropdownPopover.tsx @@ -11,9 +11,9 @@ interface DropdownPopoverProps extends ChildrenProps, StyleProps {} export const DropdownPopover = (props: DropdownPopoverProps) => { const { children, ...rest } = props; const { id, isOpen, onToggle, fullWidthMode, placement } = useDropdownContext(); - const { classProps, props: modifiedProps } = useDropdownStyleProps({ isOpen, placement, ...rest }); + const { classProps, props: modifiedProps } = useDropdownStyleProps({ isOpen, ...rest }); const { styleProps: contentStyleProps, props: contentOtherProps } = useStyleProps({ ...modifiedProps }); - const { contentProps } = useDropdownAriaProps({ id, isOpen, toggleHandler: onToggle, fullWidthMode }); + const { contentProps } = useDropdownAriaProps({ id, isOpen, toggleHandler: onToggle, placement, fullWidthMode }); return (
` | ✕ | Component id | -| `onAutoClose` . | `(event: Event) => void` | — | ✕ | Callback on close on click outside of Dropdown | -| `placement` | [Placement dictionary][dictionary-placement] | `bottom-left` | ✕ | Alignment of the component | -| `renderTrigger` | `(render: DropdownRenderProps) => ReactNode` | — | ✕ | Properties for trigger render | +| Name | Type | Default | Required | Description | +| ----------------- | ------------------------------------------------ | -------------- | -------- | ---------------------------------------------- | +| `enableAutoClose` | `bool` | `true` | ✕ | Enables close on click outside of Dropdown | +| `fullWidthMode` | [`DropdownFullwidthMode`][dropdownfullwidthmode] | `off` | ✕ | Full-width mode | +| `id` | `string` | `` | ✕ | Component id | +| `onAutoClose` . | `(event: Event) => void` | — | ✕ | Callback on close on click outside of Dropdown | +| `placement` | [Placement dictionary][dictionary-placement] | `bottom-start` | ✕ | Alignment of the component | +| `renderTrigger` | `(render: DropdownRenderProps) => ReactNode` | — | ✕ | Properties for trigger render | On top of the API options, the components accept [additional attributes][readme-additional-attributes]. If you need more control over the styling of a component, you can use [style props][readme-style-props] @@ -77,6 +77,14 @@ const onToggle = () => setIsOpen(!isOpen); ; ``` +### ⚠️ DEPRECATION NOTICE + +Both cross-axis placements have been renamed from `top-left`, `top-right`, `right-top`, `right-bottom`, +etc. to `top-start`, `top-end`, `right-start`, `right-end`, etc. The old names are deprecated and will be +removed in the next major release. + +[What are deprecations?][readme-deprecations] + ### Dropdown with Item Enhance your DropdownPopover by incorporating the versatile [Item][item] component. @@ -111,15 +119,15 @@ import { UncontrolledDropdown, DropdownTrigger, DropdownPopover } from '@lmc-eu/ ### DropdownModern -| Name | Type | Default | Required | Description | -| ----------------- | ------------------------------------------------ | ------------- | -------- | ---------------------------------------------- | -| `enableAutoClose` | `bool` | `true` | ✕ | Enables close on click outside of Dropdown | -| `fullWidthMode` | [`DropdownFullwidthMode`][dropdownfullwidthmode] | `off` | ✕ | Full-width mode | -| `id` | `string` | - | ✔ | Component id | -| `isOpen` | `bool` | `false` | ✔ | Open state | -| `onAutoClose` | `(event: Event) => void` | — | ✕ | Callback on close on click outside of Dropdown | -| `onToggle` | `() => void` | — | ✔ | Function for toggle open state of dropdown | -| `placement` | [Placement dictionary][dictionary-placement] | `bottom-left` | ✕ | Alignment of the component | +| Name | Type | Default | Required | Description | +| ----------------- | ------------------------------------------------ | -------------- | -------- | ---------------------------------------------- | +| `enableAutoClose` | `bool` | `true` | ✕ | Enables close on click outside of Dropdown | +| `fullWidthMode` | [`DropdownFullwidthMode`][dropdownfullwidthmode] | `off` | ✕ | Full-width mode | +| `id` | `string` | - | ✔ | Component id | +| `isOpen` | `bool` | `false` | ✔ | Open state | +| `onAutoClose` | `(event: Event) => void` | — | ✕ | Callback on close on click outside of Dropdown | +| `onToggle` | `() => void` | — | ✔ | Function for toggle open state of dropdown | +| `placement` | [Placement dictionary][dictionary-placement] | `bottom-start` | ✕ | Alignment of the component | On top of the API options, the components accept [additional attributes][readme-additional-attributes]. If you need more control over the styling of a component, you can use [style props][readme-style-props] @@ -148,13 +156,13 @@ and [escape hatches][readme-escape-hatches]. ### UncontrolledDropdown -| Name | Type | Default | Required | Description | -| ----------------- | ------------------------------------------------ | ------------- | -------- | ---------------------------------------------- | -| `enableAutoClose` | `bool` | `true` | ✕ | Enables close on click outside of Dropdown | -| `fullWidthMode` | [`DropdownFullwidthMode`][dropdownfullwidthmode] | `off` | ✕ | Full-width mode | -| `id` | `string` | `` | ✕ | Component id | -| `onAutoClose` | `(event: Event) => void` | — | ✕ | Callback on close on click outside of Dropdown | -| `placement` | [Placement dictionary][dictionary-placement] | `bottom-left` | ✕ | Alignment of the component | +| Name | Type | Default | Required | Description | +| ----------------- | ------------------------------------------------ | -------------- | -------- | ---------------------------------------------- | +| `enableAutoClose` | `bool` | `true` | ✕ | Enables close on click outside of Dropdown | +| `fullWidthMode` | [`DropdownFullwidthMode`][dropdownfullwidthmode] | `off` | ✕ | Full-width mode | +| `id` | `string` | `` | ✕ | Component id | +| `onAutoClose` | `(event: Event) => void` | — | ✕ | Callback on close on click outside of Dropdown | +| `placement` | [Placement dictionary][dictionary-placement] | `bottom-start` | ✕ | Alignment of the component | On top of the API options, the components accept [additional attributes][readme-additional-attributes]. If you need more control over the styling of a component, you can use [style props][readme-style-props] @@ -167,5 +175,6 @@ and [escape hatches][readme-escape-hatches]. [dropdownfullwidthmode]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/src/types/dropdown.ts#L19 [item]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/src/components/Item/README.md [readme-additional-attributes]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#additional-attributes +[readme-deprecations]: https://github.com/lmc-eu/spirit-design-system/tree/main/packages/web-react/README.md#deprecations [readme-escape-hatches]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#escape-hatches [readme-style-props]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#style-props diff --git a/packages/web-react/src/components/Dropdown/__tests__/useDropdownAriaProps.test.ts b/packages/web-react/src/components/Dropdown/__tests__/useDropdownAriaProps.test.ts index 0579374ef7..0ed8e8870e 100644 --- a/packages/web-react/src/components/Dropdown/__tests__/useDropdownAriaProps.test.ts +++ b/packages/web-react/src/components/Dropdown/__tests__/useDropdownAriaProps.test.ts @@ -1,4 +1,5 @@ import { renderHook } from '@testing-library/react-hooks'; +import { PlacementDictionaryType } from '../../../types'; import { useDropdownAriaProps } from '../useDropdownAriaProps'; describe('useDropdownAriaProps', () => { @@ -7,6 +8,7 @@ describe('useDropdownAriaProps', () => { fullWidthMode: undefined, id: 'test-dropdown-id', isOpen: true, + placement: 'bottom-start' as PlacementDictionaryType, toggleHandler: () => null, }; const { result } = renderHook(() => useDropdownAriaProps(props)); @@ -15,5 +17,8 @@ describe('useDropdownAriaProps', () => { expect(result.current.triggerProps['aria-expanded']).toBeDefined(); expect(result.current.triggerProps['aria-controls']).toBeDefined(); expect(result.current.triggerProps.onClick).toBeDefined(); + expect(result.current.contentProps).toBeDefined(); + expect(result.current.contentProps['data-spirit-placement']).toBeDefined(); + expect(result.current.contentProps['data-spirit-placement']).toBe('bottom-start'); }); }); diff --git a/packages/web-react/src/components/Dropdown/__tests__/useDropdownStyleProps.test.ts b/packages/web-react/src/components/Dropdown/__tests__/useDropdownStyleProps.test.ts index 4e906db94f..24d05a3b2b 100644 --- a/packages/web-react/src/components/Dropdown/__tests__/useDropdownStyleProps.test.ts +++ b/packages/web-react/src/components/Dropdown/__tests__/useDropdownStyleProps.test.ts @@ -1,12 +1,11 @@ import { renderHook } from '@testing-library/react-hooks'; -import { PlacementDictionaryType } from '../../../types'; import { useDropdownStyleProps, UseDropdownStyleProps } from '../useDropdownStyleProps'; describe('useDropdownStyleProps', () => { it('should return defaults', () => { const { result } = renderHook(() => useDropdownStyleProps()); - expect(result.current.classProps.contentClassName).toBe('Dropdown Dropdown--bottomLeft'); + expect(result.current.classProps.contentClassName).toBe('Dropdown'); expect(result.current.classProps.wrapperClassName).toBe('DropdownWrapper'); }); @@ -16,19 +15,10 @@ describe('useDropdownStyleProps', () => { } as UseDropdownStyleProps; const { result } = renderHook(() => useDropdownStyleProps(props)); - expect(result.current.classProps.contentClassName).toBe('Dropdown Dropdown--bottomLeft is-open'); + expect(result.current.classProps.contentClassName).toBe('Dropdown is-open'); expect(result.current.classProps.triggerClassName).toBe('is-expanded'); }); - it('should change placement', () => { - const props = { - placement: 'top-right' as PlacementDictionaryType, - } as UseDropdownStyleProps; - const { result } = renderHook(() => useDropdownStyleProps(props)); - - expect(result.current.classProps.contentClassName).toBe('Dropdown Dropdown--topRight'); - }); - it('should transfer additional props', () => { const props = { isOpen: false, @@ -36,7 +26,7 @@ describe('useDropdownStyleProps', () => { } as UseDropdownStyleProps; const { result } = renderHook(() => useDropdownStyleProps(props)); - expect(result.current.classProps.contentClassName).toBe('Dropdown Dropdown--bottomLeft'); + expect(result.current.classProps.contentClassName).toBe('Dropdown'); expect(result.current.props).toEqual({ transferProp: 'test' }); }); }); diff --git a/packages/web-react/src/components/Dropdown/demo/DropdownEnhancedShadow.tsx b/packages/web-react/src/components/Dropdown/demo/DropdownEnhancedShadow.tsx index 52e7a87982..0530bac3a8 100644 --- a/packages/web-react/src/components/Dropdown/demo/DropdownEnhancedShadow.tsx +++ b/packages/web-react/src/components/Dropdown/demo/DropdownEnhancedShadow.tsx @@ -14,7 +14,7 @@ const DropdownEnhancedShadow = () => { return (
- +
diff --git a/packages/web-react/src/components/Dropdown/demo/DropdownFullwidthAll.tsx b/packages/web-react/src/components/Dropdown/demo/DropdownFullwidthAll.tsx index 40e0380d16..f6fc7a806d 100644 --- a/packages/web-react/src/components/Dropdown/demo/DropdownFullwidthAll.tsx +++ b/packages/web-react/src/components/Dropdown/demo/DropdownFullwidthAll.tsx @@ -13,7 +13,7 @@ const DropdownFullwidthAll = () => { ); return ( - + ); diff --git a/packages/web-react/src/components/Dropdown/demo/DropdownFullwidthMobileOnly.tsx b/packages/web-react/src/components/Dropdown/demo/DropdownFullwidthMobileOnly.tsx index 9f1818422c..5dacadc91b 100644 --- a/packages/web-react/src/components/Dropdown/demo/DropdownFullwidthMobileOnly.tsx +++ b/packages/web-react/src/components/Dropdown/demo/DropdownFullwidthMobileOnly.tsx @@ -13,7 +13,7 @@ const DropdownFullwidthMobileOnly = () => { ); return ( - + ); diff --git a/packages/web-react/src/components/Dropdown/demo/DropdownPlacements.tsx b/packages/web-react/src/components/Dropdown/demo/DropdownPlacements.tsx index e073e0d142..046786de7e 100644 --- a/packages/web-react/src/components/Dropdown/demo/DropdownPlacements.tsx +++ b/packages/web-react/src/components/Dropdown/demo/DropdownPlacements.tsx @@ -7,7 +7,7 @@ import { Radio } from '../../Radio'; import { Dropdown } from '..'; const DropdownPlacements = () => { - const [placement, setPlacement] = useState('bottom-left'); + const [placement, setPlacement] = useState('bottom-start'); const handlePlacementChange = (e: ChangeEvent) => { setPlacement(e.target.value as PlacementDictionaryType); @@ -25,59 +25,59 @@ const DropdownPlacements = () => { {' '} {' '} {' '} {' '} { > { > diff --git a/packages/web-react/src/components/Dropdown/stories/Dropdown.stories.tsx b/packages/web-react/src/components/Dropdown/stories/Dropdown.stories.tsx index a56b3d6f15..99471a45e2 100644 --- a/packages/web-react/src/components/Dropdown/stories/Dropdown.stories.tsx +++ b/packages/web-react/src/components/Dropdown/stories/Dropdown.stories.tsx @@ -40,7 +40,7 @@ const meta: Meta = { control: 'select', options: Object.values(Placements), table: { - defaultValue: { summary: Placements.BOTTOM_LEFT }, + defaultValue: { summary: Placements.BOTTOM_START }, }, }, }, @@ -66,7 +66,7 @@ const meta: Meta = { ), id: 'DropdownExample', - placement: Placements.BOTTOM_LEFT, + placement: Placements.BOTTOM_START, }, }; diff --git a/packages/web-react/src/components/Dropdown/useDropdownAriaProps.ts b/packages/web-react/src/components/Dropdown/useDropdownAriaProps.ts index 06249cec88..7919c0aced 100644 --- a/packages/web-react/src/components/Dropdown/useDropdownAriaProps.ts +++ b/packages/web-react/src/components/Dropdown/useDropdownAriaProps.ts @@ -1,8 +1,11 @@ -import { Booleanish, ClickEvent, DropdownFullWidthMode } from '../../types'; +import { Placements } from '../../constants'; +import { useDeprecationMessage } from '../../hooks'; +import { Booleanish, ClickEvent, DropdownFullWidthMode, PlacementDictionaryType } from '../../types'; const NAME_ARIA_EXPANDED = 'aria-expanded'; const NAME_ARIA_CONTROLS = 'aria-controls'; const NAME_DATA_FULLWIDTHMODE = 'data-spirit-fullwidthmode'; +const NAME_DATA_PLACEMENT = 'data-spirit-placement'; export enum fullWidthModeKeys { 'off' = 'off', @@ -14,10 +17,12 @@ export interface UseDropdownAriaPropsProps { id: string; /** open state */ isOpen: boolean; - /** toggle callback */ - toggleHandler: (event: ClickEvent) => void; /** fullWidthMode */ fullWidthMode: DropdownFullWidthMode | undefined; + /** placement */ + placement?: PlacementDictionaryType; + /** toggle callback */ + toggleHandler: (event: ClickEvent) => void; } export interface UseDropdownAriaPropsReturn { @@ -25,6 +30,7 @@ export interface UseDropdownAriaPropsReturn { contentProps: { id: string; [NAME_DATA_FULLWIDTHMODE]?: keyof typeof fullWidthModeKeys | undefined; + [NAME_DATA_PLACEMENT]?: PlacementDictionaryType; }; /** trigger returned props */ triggerProps: { @@ -34,8 +40,29 @@ export interface UseDropdownAriaPropsReturn { }; } +const deprecatedPlacements = { + 'top-left': 'top-start', + 'top-right': 'top-end', + 'right-top': 'right-start', + 'right-bottom': 'right-end', + 'bottom-left': 'bottom-start', + 'bottom-right': 'bottom-end', + 'left-top': 'left-start', + 'left-bottom': 'left-end', +}; + export const useDropdownAriaProps = (props: UseDropdownAriaPropsProps): UseDropdownAriaPropsReturn => { - const { fullWidthMode, id, isOpen, toggleHandler } = props; + const { fullWidthMode, id, isOpen, placement = Placements.BOTTOM_START, toggleHandler } = props; + useDeprecationMessage({ + method: 'property', + trigger: !!deprecatedPlacements[placement as keyof typeof deprecatedPlacements], + componentName: 'Tooltip', + propertyProps: { + deprecatedValue: placement, + newValue: deprecatedPlacements[placement as keyof typeof deprecatedPlacements], + propertyName: 'placement', + }, + }); const triggerProps = { id, @@ -43,7 +70,11 @@ export const useDropdownAriaProps = (props: UseDropdownAriaPropsProps): UseDropd [NAME_ARIA_CONTROLS]: String(id), onClick: toggleHandler, }; - const contentProps = { id, [NAME_DATA_FULLWIDTHMODE]: fullWidthMode }; + const contentProps = { + id, + [NAME_DATA_FULLWIDTHMODE]: fullWidthMode, + [NAME_DATA_PLACEMENT]: placement, + }; return { contentProps, diff --git a/packages/web-react/src/components/Dropdown/useDropdownStyleProps.ts b/packages/web-react/src/components/Dropdown/useDropdownStyleProps.ts index 7535cfceb2..4483a874a9 100644 --- a/packages/web-react/src/components/Dropdown/useDropdownStyleProps.ts +++ b/packages/web-react/src/components/Dropdown/useDropdownStyleProps.ts @@ -1,8 +1,6 @@ import classNames from 'classnames'; -import { Placements } from '../../constants'; import { useClassNamePrefix } from '../../hooks'; import { DropdownProps, SpiritDropdownProps } from '../../types'; -import { kebabCaseToCamelCase } from '../../utils'; export interface UseDropdownStyleProps extends SpiritDropdownProps { /** open state */ @@ -21,15 +19,14 @@ export interface UseDropdownStylePropsReturn { export const useDropdownStyleProps = ( props: UseDropdownStyleProps = { isOpen: false }, ): UseDropdownStylePropsReturn => { - const { isOpen, placement = Placements.BOTTOM_LEFT, ...modifiedProps } = props; + const { isOpen, ...modifiedProps } = props; const dropdownClass = useClassNamePrefix('Dropdown'); const dropdownWrapperClass = `${dropdownClass}Wrapper`; - const dropdownPlacementClass = `${dropdownClass}--${kebabCaseToCamelCase(placement)}`; const expandedClass = isOpen ? 'is-expanded' : ''; const openClass = isOpen ? 'is-open' : ''; - const dropdownClassName = classNames(dropdownClass, dropdownPlacementClass, openClass); + const dropdownClassName = classNames(dropdownClass, openClass); const triggerClassName = classNames(expandedClass); return { diff --git a/packages/web-twig/src/Resources/components/Dropdown/README.md b/packages/web-twig/src/Resources/components/Dropdown/README.md index 1f12be973e..b935c558ad 100644 --- a/packages/web-twig/src/Resources/components/Dropdown/README.md +++ b/packages/web-twig/src/Resources/components/Dropdown/README.md @@ -60,7 +60,7 @@ and [escape hatches][readme-escape-hatches]. #### ⚠️ DEPRECATION NOTICE -Both cross-axis placements are renamed from `top-left`, `top-right`, `right-top`, `right-bottom`, +Both cross-axis placements have been renamed from `top-left`, `top-right`, `right-top`, `right-bottom`, etc. to `top-start`, `top-end`, `right-start`, `right-end`, etc. The old names are deprecated and will be removed in the next major release.