diff --git a/packages/web-react/src/components/Dropdown/Dropdown.tsx b/packages/web-react/src/components/Dropdown/Dropdown.tsx index e46e35bbf6..ed8ce40266 100644 --- a/packages/web-react/src/components/Dropdown/Dropdown.tsx +++ b/packages/web-react/src/components/Dropdown/Dropdown.tsx @@ -1,9 +1,9 @@ import classNames from 'classnames'; import React, { LegacyRef, createElement, useRef } from 'react'; import { Placements } from '../../constants'; -import { useStyleProps } from '../../hooks'; -import { SpiritDropdownProps } from '../../types'; import DropdownWrapper from './DropdownWrapper'; +import { useDeprecationMessage, useStyleProps } from '../../hooks'; +import { SpiritDropdownProps } from '../../types'; import { useDropdown } from './useDropdown'; import { useDropdownAriaProps } from './useDropdownAriaProps'; import { useDropdownStyleProps } from './useDropdownStyleProps'; @@ -13,7 +13,10 @@ const defaultProps = { placement: Placements.BOTTOM_LEFT, }; -const Dropdown = (props: SpiritDropdownProps) => { +/** + * @deprecated Dropdown component is deprecated and will be removed in the next major version. Please use "DropdownModern" component instead. + */ +export const Dropdown = (props: SpiritDropdownProps) => { const { id = Math.random().toString(36).slice(2, 7), children, @@ -36,6 +39,14 @@ const Dropdown = (props: SpiritDropdownProps) => { UNSAFE_className: classProps.triggerClassName, }); + useDeprecationMessage({ + method: 'custom', + trigger: true, + componentName: 'Dropdown', + customText: + 'Dropdown component is deprecated and will be removed in the next major version. Please use "DropdownModern" component instead.', + }); + const triggerRenderHandler = () => { if (renderTrigger) { return renderTrigger({ diff --git a/packages/web-react/src/components/Dropdown/DropdownContext.ts b/packages/web-react/src/components/Dropdown/DropdownContext.ts new file mode 100644 index 0000000000..10086003e8 --- /dev/null +++ b/packages/web-react/src/components/Dropdown/DropdownContext.ts @@ -0,0 +1,33 @@ +import { MutableRefObject, createContext, useContext } from 'react'; +import { Placements } from '../../constants'; +import { ClickEvent, PlacementDictionaryType } from '../../types'; +import { fullWidthModeKeys } from './useDropdownAriaProps'; + +type DropdownContextType = { + dropdownRef: MutableRefObject; + fullWidthMode?: keyof typeof fullWidthModeKeys; + id: string; + isOpen: boolean; + onToggle: (event: ClickEvent) => void; + placement?: PlacementDictionaryType; + triggerRef: MutableRefObject; +}; + +const defaultContext: DropdownContextType = { + dropdownRef: { current: null }, + fullWidthMode: fullWidthModeKeys.off, + id: '', + isOpen: false, + onToggle: () => {}, + placement: Placements.BOTTOM_LEFT, + triggerRef: { current: undefined }, +}; + +const DropdownContext = createContext(defaultContext); +const DropdownProvider = DropdownContext.Provider; +const DropdownConsumer = DropdownContext.Consumer; +const useDropdownContext = (): DropdownContextType => useContext(DropdownContext); + +export default DropdownContext; +export { DropdownConsumer, DropdownProvider, useDropdownContext }; +export type { DropdownContextType }; diff --git a/packages/web-react/src/components/Dropdown/DropdownModern.tsx b/packages/web-react/src/components/Dropdown/DropdownModern.tsx new file mode 100644 index 0000000000..680da7faba --- /dev/null +++ b/packages/web-react/src/components/Dropdown/DropdownModern.tsx @@ -0,0 +1,67 @@ +import React, { useRef } from 'react'; +import classNames from 'classnames'; +import { useClickOutside, useDeprecationMessage, useStyleProps } from '../../hooks'; +import { ChildrenProps, SpiritDropdownModernProps } from '../../types'; +import { DropdownProvider } from './DropdownContext'; +import { useDropdownStyleProps } from './useDropdownStyleProps'; + +interface DropdownProps extends ChildrenProps, SpiritDropdownModernProps {} + +export const DropdownModern = (props: DropdownProps) => { + const { + children, + enableAutoClose = true, + fullWidthMode, + id, + isOpen = false, + onAutoClose, + onToggle, + placement, + ...rest + } = props; + const { classProps } = useDropdownStyleProps(); + const { styleProps, props: otherProps } = useStyleProps({ ...rest }); + + const dropdownRef = useRef(null); + const triggerRef = useRef(); + + const closeHandler = (event: Event) => { + if (!enableAutoClose) { + return; + } + + if (!triggerRef?.current?.contains(event?.target as Node)) { + if (onAutoClose) { + onAutoClose(event); + } + + onToggle && isOpen && onToggle(); + } + }; + + useClickOutside({ ref: dropdownRef, callback: closeHandler }); + + useDeprecationMessage({ + method: 'component', + trigger: true, + componentName: 'DropdownModern', + componentProps: { + newName: 'Dropdown', + }, + }); + + return ( + +
+ {children} +
+
+ ); +}; + +export default DropdownModern; diff --git a/packages/web-react/src/components/Dropdown/DropdownPopover.tsx b/packages/web-react/src/components/Dropdown/DropdownPopover.tsx new file mode 100644 index 0000000000..a64b3a6587 --- /dev/null +++ b/packages/web-react/src/components/Dropdown/DropdownPopover.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import classNames from 'classnames'; +import { useStyleProps } from '../../hooks'; +import { ChildrenProps, StyleProps } from '../../types'; +import { useDropdownContext } from './DropdownContext'; +import { useDropdownAriaProps } from './useDropdownAriaProps'; +import { useDropdownStyleProps } from './useDropdownStyleProps'; + +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 { styleProps: contentStyleProps, props: contentOtherProps } = useStyleProps({ ...modifiedProps }); + const { contentProps } = useDropdownAriaProps({ id, isOpen, toggleHandler: onToggle, fullWidthMode }); + + return ( +
+ {children} +
+ ); +}; + +export default DropdownPopover; diff --git a/packages/web-react/src/components/Dropdown/DropdownTrigger.tsx b/packages/web-react/src/components/Dropdown/DropdownTrigger.tsx new file mode 100644 index 0000000000..73986b2972 --- /dev/null +++ b/packages/web-react/src/components/Dropdown/DropdownTrigger.tsx @@ -0,0 +1,36 @@ +import React, { ElementType } from 'react'; +import { useStyleProps } from '../../hooks'; +import { StyleProps } from '../../types'; +import { useDropdownContext } from './DropdownContext'; +import { useDropdownAriaProps } from './useDropdownAriaProps'; +import { useDropdownStyleProps } from './useDropdownStyleProps'; + +interface DropdownTriggerProps extends StyleProps { + elementType?: ElementType | string; + children: string | React.ReactNode | ((props: { isOpen: boolean }) => React.ReactNode); +} + +export const DropdownTrigger = (props: DropdownTriggerProps) => { + const { elementType = 'button', children, ...rest } = props; + const { id, isOpen, onToggle, fullWidthMode, triggerRef } = useDropdownContext(); + + const Component = elementType; + + const { classProps } = useDropdownStyleProps({ isOpen, ...rest }); + const { styleProps: triggerStyleProps } = useStyleProps({ + UNSAFE_className: classProps.triggerClassName, + }); + const { triggerProps } = useDropdownAriaProps({ id, isOpen, toggleHandler: onToggle, fullWidthMode }); + + return ( + + {typeof children === 'function' ? children({ isOpen }) : children} + + ); +}; + +DropdownTrigger.defaultProps = { + elementType: 'button', +}; + +export default DropdownTrigger; diff --git a/packages/web-react/src/components/Dropdown/DropdownWrapper.tsx b/packages/web-react/src/components/Dropdown/DropdownWrapper.tsx index 2e68eb3881..5683acb4ee 100644 --- a/packages/web-react/src/components/Dropdown/DropdownWrapper.tsx +++ b/packages/web-react/src/components/Dropdown/DropdownWrapper.tsx @@ -4,7 +4,7 @@ import { useDropdownStyleProps } from './useDropdownStyleProps'; interface DropdownWrapperProps extends ChildrenProps {} -const DropdownWrapper = ({ children }: DropdownWrapperProps) => { +export const DropdownWrapper = ({ children }: DropdownWrapperProps) => { const { classProps } = useDropdownStyleProps(); return
{children}
; diff --git a/packages/web-react/src/components/Dropdown/README.md b/packages/web-react/src/components/Dropdown/README.md index 9463f667b7..beb17a1e3b 100644 --- a/packages/web-react/src/components/Dropdown/README.md +++ b/packages/web-react/src/components/Dropdown/README.md @@ -1,5 +1,6 @@ # Dropdown +⚠️ Dropdown component is [deprecated][deprecated] and will be removed in the next major version. Please use "DropdownModern" component instead. This is the React implementation of the [Dropdown] component. ## Usage @@ -9,8 +10,8 @@ import { Dropdown } from '@lmc-eu/spirit-web-react/components'; ``` ```jsx - }> - ... + }> + … ``` @@ -39,7 +40,88 @@ import { Dropdown } from '@lmc-eu/spirit-web-react/components'; | `trigger['aria-controls']` | `string` | Trigger aria controls | | `trigger.ref` | `LegacyRef` | Trigger reference | +--- + +# DropdownModern + +⚠️ `DropdownModern` component is [deprecated] and will be renamed to `Dropdown` in the next major version. + +## Usage + +```jsx +import { DropdownModern, DropdownTrigger, DropdownPopover } from '@lmc-eu/spirit-web-react/components'; +``` + +```jsx +const [isOpen, setIsOpen] = React.useState(false); +const onToggle = () => setIsOpen(!isOpen); + + + Trigger button + +; +``` + +### Uncontrolled dropdown + +```jsx +import { UncontrolledDropdown, DropdownTrigger, DropdownPopover } from '@lmc-eu/spirit-web-react/components'; +``` + +```jsx + + Trigger button + + +``` + +## API + +### 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 | +| `UNSAFE_className` | `string` | — | ✕ | Wrapper custom classname | +| `UNSAFE_style` | `CSSProperties` | — | ✕ | Wrapper custom style | + +### DropdownTrigger + +| Name | Type | Default | Required | Description | +| ------------------ | ------------------------- | -------- | -------- | -------------------------------- | +| `children` | [`string` \| `ReactNode`] | — | ✔ | Content of trigger element | +| `elementType` | [`string` \| `ReactNode`] | `button` | ✕ | Element type of dropdown trigger | +| `UNSAFE_className` | `string` | — | ✕ | DropdownTrigger custom classname | +| `UNSAFE_style` | `CSSProperties` | — | ✕ | DropdownTrigger custom style | + +### DropdownPopover + +| Name | Type | Default | Required | Description | +| ------------------ | ------------------------- | ------- | -------- | -------------------------------- | +| `children` | [`string` \| `ReactNode`] | — | ✔ | Content of trigger element | +| `UNSAFE_className` | `string` | — | ✕ | DropdownPopover custom classname | +| `UNSAFE_style` | `CSSProperties` | — | ✕ | DropdownPopover custom style | + +### 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 | +| `UNSAFE_className` | `string` | — | ✕ | Wrapper custom classname | +| `UNSAFE_style` | `CSSProperties` | — | ✕ | Wrapper custom style | + +[deprecated]: https://github.com/lmc-eu/spirit-design-system/tree/main/packages/web-react/README.md#deprecations +[dictionary-placement]: https://github.com/lmc-eu/spirit-design-system/tree/main/docs/DICTIONARIES.md#placement [dropdown]: https://github.com/lmc-eu/spirit-design-system/tree/main/packages/web/src/scss/components/Dropdown [dropdownbreakpoint]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/src/types/dropdown.ts#L11 [dropdownfullwidthmode]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/src/types/dropdown.ts#L19 -[dictionary-placement]: https://github.com/lmc-eu/spirit-design-system/tree/main/docs/DICTIONARIES.md#placement diff --git a/packages/web-react/src/components/Dropdown/UncontrolledDropdown.tsx b/packages/web-react/src/components/Dropdown/UncontrolledDropdown.tsx new file mode 100644 index 0000000000..1cf57ca0e9 --- /dev/null +++ b/packages/web-react/src/components/Dropdown/UncontrolledDropdown.tsx @@ -0,0 +1,33 @@ +import React, { useRef } from 'react'; +import classNames from 'classnames'; +import { useStyleProps } from '../../hooks'; +import { UncontrolledDropdownProps } from '../../types'; +import { DropdownProvider } from './DropdownContext'; +import { useDropdownStyleProps } from './useDropdownStyleProps'; +import { useDropdown } from './useDropdown'; + +export const UncontrolledDropdown = (props: UncontrolledDropdownProps) => { + const { children, enableAutoClose = true, fullWidthMode, id, onAutoClose, placement, ...rest } = props; + const { classProps } = useDropdownStyleProps(); + const { styleProps, props: otherProps } = useStyleProps({ ...rest }); + + const dropdownRef = useRef(null); + const triggerRef = useRef(); + + const { isOpen, toggleHandler: onToggle } = useDropdown({ dropdownRef, triggerRef, enableAutoClose, onAutoClose }); + + return ( + +
+ {children} +
+
+ ); +}; + +export default UncontrolledDropdown; diff --git a/packages/web-react/src/components/Dropdown/__tests__/DropdownModern.test.tsx b/packages/web-react/src/components/Dropdown/__tests__/DropdownModern.test.tsx new file mode 100644 index 0000000000..367c0ca85f --- /dev/null +++ b/packages/web-react/src/components/Dropdown/__tests__/DropdownModern.test.tsx @@ -0,0 +1,68 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest'; +import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; +import { restPropsTest } from '../../../../tests/providerTests/restPropsTest'; +import DropdownModern from '../DropdownModern'; +import DropdownPopover from '../DropdownPopover'; +import DropdownTrigger from '../DropdownTrigger'; + +describe('Dropdown', () => { + classNamePrefixProviderTest(DropdownModern, 'DropdownWrapper'); + + stylePropsTest( + (props: Record) => ( + {}} {...props} data-testid="test-dropdown" /> + ), + 'test-dropdown', + ); + + restPropsTest(DropdownModern, '.DropdownWrapper'); + + it('should render text children', () => { + const dom = render( + {}}> + Trigger + Hello World + , + ); + const trigger = dom.container.querySelector('button') as HTMLElement; + const element = dom.container.querySelector('.Dropdown') as HTMLElement; + + expect(trigger.textContent).toBe('Trigger'); + expect(element.textContent).toBe('Hello World'); + }); + + it('should be opened', () => { + const onToggle = jest.fn(); + + const dom = render( + + trigger + Hello World + , + ); + const element = dom.container.querySelector('.Dropdown') as HTMLElement; + const trigger = dom.container.querySelector('#dropdown') as HTMLElement; + + expect(element).toHaveClass('is-open'); + expect(trigger).toHaveClass('is-expanded'); + }); + + it('should call toggle function', () => { + const onToggle = jest.fn(); + + const dom = render( + + trigger + Hello World + , + ); + const trigger = dom.container.querySelector('#dropdown') as HTMLElement; + + fireEvent.click(trigger); + + expect(onToggle).toHaveBeenCalled(); + }); +}); diff --git a/packages/web-react/src/components/Dropdown/index.html b/packages/web-react/src/components/Dropdown/index.html index 0c405c45e7..c697014256 100644 --- a/packages/web-react/src/components/Dropdown/index.html +++ b/packages/web-react/src/components/Dropdown/index.html @@ -1 +1 @@ -{{> demo}} \ No newline at end of file +{{> demo}} diff --git a/packages/web-react/src/components/Dropdown/index.ts b/packages/web-react/src/components/Dropdown/index.ts index b03fd4ea7c..62d23b4b1d 100644 --- a/packages/web-react/src/components/Dropdown/index.ts +++ b/packages/web-react/src/components/Dropdown/index.ts @@ -1,5 +1,10 @@ export { default as Dropdown } from './Dropdown'; export { default as DropdownWrapper } from './DropdownWrapper'; +export { default as DropdownModern } from './DropdownModern'; +export { default as DropdownTrigger } from './DropdownTrigger'; +export { default as DropdownPopover } from './DropdownPopover'; +export { default as DropdownContext } from './DropdownContext'; +export { default as UncontrolledDropdown } from './UncontrolledDropdown'; export * from './DropdownWrapper'; export * from './useDropdown'; export * from './useDropdownAriaProps'; diff --git a/packages/web-react/src/components/Dropdown/useDropdown.ts b/packages/web-react/src/components/Dropdown/useDropdown.ts index 4a781ea522..b8cad186a3 100644 --- a/packages/web-react/src/components/Dropdown/useDropdown.ts +++ b/packages/web-react/src/components/Dropdown/useDropdown.ts @@ -5,12 +5,12 @@ import { useClickOutside } from '../../hooks'; export interface UseDropdownProps { /** dropdown element reference */ dropdownRef: MutableRefObject; - /** trigger element reference */ - triggerRef: MutableRefObject; /** enabled click outside event */ enableAutoClose?: boolean; /** on close callback */ onAutoClose?: (event: Event) => void; + /** trigger element reference */ + triggerRef: MutableRefObject; } export interface UseDropdownReturn { diff --git a/packages/web-react/src/components/DropdownModern/README.md b/packages/web-react/src/components/DropdownModern/README.md new file mode 100644 index 0000000000..29b82d488b --- /dev/null +++ b/packages/web-react/src/components/DropdownModern/README.md @@ -0,0 +1,9 @@ +# DropdownModern + +⚠️ `DropdownModern` component is [deprecated] and will be renamed to `Dropdown` in the next major version. + +For more information and implementation examples, please visit the [`DropdownModern` section][dropdown-modern-section] section in the [readme][dropdown] for the `Dropdown` component. + +[deprecated]: https://github.com/lmc-eu/spirit-design-system/tree/main/packages/web-react/README.md#deprecations +[dropdown-modern-section]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/src/components/Dropdown/README.md#dropdownmodern +[dropdown]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/src/components/Dropdown/README.md diff --git a/packages/web-react/src/components/DropdownModern/demo/DropdownContentFactory.tsx b/packages/web-react/src/components/DropdownModern/demo/DropdownContentFactory.tsx new file mode 100644 index 0000000000..19604b2807 --- /dev/null +++ b/packages/web-react/src/components/DropdownModern/demo/DropdownContentFactory.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Icon } from '../../Icon'; + +type Props = { + content: Content[]; +}; + +type Content = { + icon: string; + text: string; +}; + +const DropdownContentFactory = ({ content }: Props) => { + const lastRow = content.length - 1; + + return ( + <> + {content.map(({ icon, text }, index) => ( + + + {text} + + ))} + + ); +}; + +export default DropdownContentFactory; diff --git a/packages/web-react/src/components/DropdownModern/demo/DropdownModernDefault.tsx b/packages/web-react/src/components/DropdownModern/demo/DropdownModernDefault.tsx new file mode 100644 index 0000000000..925197f0f1 --- /dev/null +++ b/packages/web-react/src/components/DropdownModern/demo/DropdownModernDefault.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import DropdownModern from '../../Dropdown/DropdownModern'; +import DropdownTrigger from '../../Dropdown/DropdownTrigger'; +import DropdownPopover from '../../Dropdown/DropdownPopover'; +import { Button } from '../../Button'; +import { dropdownContent } from './constants'; +import DropdownContentFactory from './DropdownContentFactory'; + +const DropdownModernDefault = () => { + const [isOpen, setIsOpen] = React.useState(false); + const onToggle = () => setIsOpen(!isOpen); + + return ( + + Button as anchor + + + + + ); +}; + +export default DropdownModernDefault; diff --git a/packages/web-react/src/components/DropdownModern/demo/DropdownModernDisabledAutoclose.tsx b/packages/web-react/src/components/DropdownModern/demo/DropdownModernDisabledAutoclose.tsx new file mode 100644 index 0000000000..473733d6ce --- /dev/null +++ b/packages/web-react/src/components/DropdownModern/demo/DropdownModernDisabledAutoclose.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import DropdownModern from '../../Dropdown/DropdownModern'; +import DropdownTrigger from '../../Dropdown/DropdownTrigger'; +import DropdownPopover from '../../Dropdown/DropdownPopover'; +import { Button } from '../../Button'; +import { dropdownContent } from './constants'; +import DropdownContentFactory from './DropdownContentFactory'; + +const DropdownModernDisabledAutoclose = () => { + const [isOpen, setIsOpen] = React.useState(false); + const onToggle = () => setIsOpen(!isOpen); + + return ( + + Button as anchor + + + + + ); +}; + +export default DropdownModernDisabledAutoclose; diff --git a/packages/web-react/src/components/DropdownModern/demo/DropdownModernFullwidthAll.tsx b/packages/web-react/src/components/DropdownModern/demo/DropdownModernFullwidthAll.tsx new file mode 100644 index 0000000000..ec95172d5c --- /dev/null +++ b/packages/web-react/src/components/DropdownModern/demo/DropdownModernFullwidthAll.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import DropdownModern from '../../Dropdown/DropdownModern'; +import DropdownTrigger from '../../Dropdown/DropdownTrigger'; +import DropdownPopover from '../../Dropdown/DropdownPopover'; +import { Button } from '../../Button'; +import { dropdownContent } from './constants'; +import DropdownContentFactory from './DropdownContentFactory'; + +const DropdownModernFullwidthAll = () => { + const [isOpen, setIsOpen] = React.useState(false); + const onToggle = () => setIsOpen(!isOpen); + + return ( + + Finibus quis imperdiet, semper imperdiet aliquam + + + + + ); +}; + +export default DropdownModernFullwidthAll; diff --git a/packages/web-react/src/components/DropdownModern/demo/DropdownModernFullwidthMobileOnly.tsx b/packages/web-react/src/components/DropdownModern/demo/DropdownModernFullwidthMobileOnly.tsx new file mode 100644 index 0000000000..3f19b36c65 --- /dev/null +++ b/packages/web-react/src/components/DropdownModern/demo/DropdownModernFullwidthMobileOnly.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import DropdownModern from '../../Dropdown/DropdownModern'; +import DropdownTrigger from '../../Dropdown/DropdownTrigger'; +import DropdownPopover from '../../Dropdown/DropdownPopover'; +import { Button } from '../../Button'; +import { dropdownContent } from './constants'; +import DropdownContentFactory from './DropdownContentFactory'; + +const DropdownModernFullwidthMobileOnly = () => { + const [isOpen, setIsOpen] = React.useState(false); + const onToggle = () => setIsOpen(!isOpen); + + return ( + + Finibus quis imperdiet, semper imperdiet aliquam + + + + + ); +}; + +export default DropdownModernFullwidthMobileOnly; diff --git a/packages/web-react/src/components/DropdownModern/demo/DropdownModernLongerContent.tsx b/packages/web-react/src/components/DropdownModern/demo/DropdownModernLongerContent.tsx new file mode 100644 index 0000000000..0161def6c7 --- /dev/null +++ b/packages/web-react/src/components/DropdownModern/demo/DropdownModernLongerContent.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import DropdownModern from '../../Dropdown/DropdownModern'; +import DropdownTrigger from '../../Dropdown/DropdownTrigger'; +import DropdownPopover from '../../Dropdown/DropdownPopover'; +import { Button } from '../../Button'; +import { dropdownContentLonger } from './constants'; +import DropdownContentFactory from './DropdownContentFactory'; + +const DropdownModernLongerContent = () => { + const [isOpen, setIsOpen] = React.useState(false); + const onToggle = () => setIsOpen(!isOpen); + + return ( + + Button as anchor + + + + + ); +}; + +export default DropdownModernLongerContent; diff --git a/packages/web-react/src/components/DropdownModern/demo/DropdownModernTopRight.tsx b/packages/web-react/src/components/DropdownModern/demo/DropdownModernTopRight.tsx new file mode 100644 index 0000000000..97eb566112 --- /dev/null +++ b/packages/web-react/src/components/DropdownModern/demo/DropdownModernTopRight.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import DropdownModern from '../../Dropdown/DropdownModern'; +import DropdownTrigger from '../../Dropdown/DropdownTrigger'; +import DropdownPopover from '../../Dropdown/DropdownPopover'; +import { Button } from '../../Button'; +import { dropdownContent } from './constants'; +import DropdownContentFactory from './DropdownContentFactory'; + +const DropdownModernTopRight = () => { + const [isOpen, setIsOpen] = React.useState(false); + const onToggle = () => setIsOpen(!isOpen); + + return ( + + Button as anchor + + + + + ); +}; + +export default DropdownModernTopRight; diff --git a/packages/web-react/src/components/DropdownModern/demo/constants.ts b/packages/web-react/src/components/DropdownModern/demo/constants.ts new file mode 100644 index 0000000000..7cb77b025b --- /dev/null +++ b/packages/web-react/src/components/DropdownModern/demo/constants.ts @@ -0,0 +1,13 @@ +export const dropdownContent = [ + { icon: 'info', text: 'Information' }, + { icon: 'link', text: 'Bibendum aliquam, fusce integer sit amet congue non nulla aliquet enim' }, + { icon: 'profile', text: 'Profile' }, + { icon: 'help', text: 'Help' }, +]; + +export const dropdownContentLonger = [ + { icon: 'info', text: 'Bibendum aliquam, fusce integer sit amet congue non nulla aliquet enim' }, + { icon: 'link', text: 'Nulla condimentum, purus commodo cursus non nulla rhoncus' }, + { icon: 'profile', text: 'Mauris nunc, elementum enim in lacinia vitae quam placerat sem, euismod accumsan' }, + { icon: 'help', text: 'Donec dui, nunc dui vel varius libero molestie nibh nunc' }, +]; diff --git a/packages/web-react/src/components/DropdownModern/demo/index.tsx b/packages/web-react/src/components/DropdownModern/demo/index.tsx new file mode 100644 index 0000000000..a877f25255 --- /dev/null +++ b/packages/web-react/src/components/DropdownModern/demo/index.tsx @@ -0,0 +1,40 @@ +// Because there is no `dist` directory during the CI run +/* eslint-disable import/no-extraneous-dependencies, import/extensions, import/no-unresolved */ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment, import/extensions, import/no-unresolved +// @ts-ignore: No declaration file +import icons from '@lmc-eu/spirit-icons/dist/icons'; +import DocsSection from '../../../../docs/DocsSections'; +import { IconsProvider } from '../../../context'; +import DropdownModernDefault from './DropdownModernDefault'; +import DropdownModernTopRight from './DropdownModernTopRight'; +import DropdownModernDisabledAutoclose from './DropdownModernDisabledAutoclose'; +import DropdownModernLongerContent from './DropdownModernLongerContent'; +import DropdownModernFullwidthAll from './DropdownModernFullwidthAll'; +import DropdownModernFullwidthMobileOnly from './DropdownModernFullwidthMobileOnly'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + + + + + + + + + + + + + + + + + + , +); diff --git a/packages/web-react/src/components/DropdownModern/index.html b/packages/web-react/src/components/DropdownModern/index.html new file mode 100644 index 0000000000..c697014256 --- /dev/null +++ b/packages/web-react/src/components/DropdownModern/index.html @@ -0,0 +1 @@ +{{> demo}} diff --git a/packages/web-react/src/components/DropdownModern/index.ts b/packages/web-react/src/components/DropdownModern/index.ts new file mode 100644 index 0000000000..3724ebb9b5 --- /dev/null +++ b/packages/web-react/src/components/DropdownModern/index.ts @@ -0,0 +1 @@ +export * from '../Dropdown'; diff --git a/packages/web-react/src/components/DropdownModern/stories/DropdownModern.stories.tsx b/packages/web-react/src/components/DropdownModern/stories/DropdownModern.stories.tsx new file mode 100644 index 0000000000..6e12a5b12c --- /dev/null +++ b/packages/web-react/src/components/DropdownModern/stories/DropdownModern.stories.tsx @@ -0,0 +1,92 @@ +import React, { useState } from 'react'; +import { Markdown } from '@storybook/blocks'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { Placements } from '../../../constants'; +import { DropdownFullWidthModes, SpiritDropdownModernProps } from '../../../types'; +import ReadMe from '../../Dropdown/README.md'; +import { Button, Icon, Text } from '../..'; +import { DropdownModern, DropdownTrigger, DropdownPopover } from '../../Dropdown'; + +const meta: Meta = { + title: 'Components/DropdownModern', + component: DropdownModern, + parameters: { + docs: { + page: () => {ReadMe}, + }, + layout: 'centered', + }, + argTypes: { + children: { + control: 'object', + }, + enableAutoClose: { + control: 'boolean', + table: { + defaultValue: { summary: true }, + }, + }, + fullWidthMode: { + control: 'select', + options: [...Object.values(DropdownFullWidthModes), undefined], + table: { + defaultValue: { summary: undefined }, + }, + }, + id: { + control: 'text', + }, + placement: { + control: 'select', + options: Object.values(Placements), + table: { + defaultValue: { summary: Placements.BOTTOM_LEFT }, + }, + }, + }, + args: { + children: ( + <> + + + Information + + + + More links + + + + Profile + + + + Help + + + ), + id: 'DropdownModernExample', + }, +}; + +export default meta; +type Story = StoryObj; + +const DropdownModernWithHooks = (args: SpiritDropdownModernProps) => { + const { children, isOpen } = args; + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const onDropdownToggle = () => setIsDropdownOpen(!isDropdownOpen); + + return ( + + Button as anchor + {children} + + ); +}; + +export const DropdownModernPlayground: Story = { + name: 'DropdownModern', + render: (args) => , +}; diff --git a/packages/web-react/src/components/DropdownModern/stories/DropdownPopover.stories.tsx b/packages/web-react/src/components/DropdownModern/stories/DropdownPopover.stories.tsx new file mode 100644 index 0000000000..2413c1a994 --- /dev/null +++ b/packages/web-react/src/components/DropdownModern/stories/DropdownPopover.stories.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { Markdown } from '@storybook/blocks'; +import type { Meta, StoryObj } from '@storybook/react'; + +import ReadMe from '../../Dropdown/README.md'; +import { Button, Icon, Text } from '../..'; +import { DropdownModern, DropdownPopover, DropdownTrigger } from '../../Dropdown'; + +const meta: Meta = { + title: 'Components/DropdownModern', + component: DropdownPopover, + parameters: { + docs: { + page: () => {ReadMe}, + }, + layout: 'centered', + }, + argTypes: { + children: { + control: 'object', + }, + }, + args: { + children: ( + <> + + + Information + + + + More links + + + + Profile + + + + Help + + + ), + }, +}; + +export default meta; +type Story = StoryObj; + +export const DropdownPopoverPlayground: Story = { + name: 'DropdownPopover', + render: (args) => ( + {}}> + Button as anchor + {args.children} + + ), +}; diff --git a/packages/web-react/src/components/DropdownModern/stories/DropdownTrigger.stories.tsx b/packages/web-react/src/components/DropdownModern/stories/DropdownTrigger.stories.tsx new file mode 100644 index 0000000000..58815bc49b --- /dev/null +++ b/packages/web-react/src/components/DropdownModern/stories/DropdownTrigger.stories.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Markdown } from '@storybook/blocks'; +import type { Meta, StoryObj } from '@storybook/react'; + +import ReadMe from '../../Dropdown/README.md'; +import { DropdownTrigger } from '../../Dropdown'; +import { Button } from '../../Button'; + +const meta: Meta = { + title: 'Components/DropdownModern', + component: DropdownTrigger, + parameters: { + docs: { + page: () => {ReadMe}, + }, + layout: 'centered', + }, + argTypes: { + children: { + control: 'object', + description: 'The content to display in the component.', + }, + elementType: { + control: 'object', + table: { + defaultValue: { summary: Button }, + }, + }, + }, + args: { + children: 'Hello World', + elementType: Button, + }, +}; + +export default meta; +type Story = StoryObj; + +export const DropdownTriggerPlayground: Story = { + name: 'DropdownTrigger', + render: (args) => {args.children}, +}; diff --git a/packages/web-react/src/components/DropdownModern/stories/UncontolledDropdown.stories.tsx b/packages/web-react/src/components/DropdownModern/stories/UncontolledDropdown.stories.tsx new file mode 100644 index 0000000000..7392261d29 --- /dev/null +++ b/packages/web-react/src/components/DropdownModern/stories/UncontolledDropdown.stories.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { Markdown } from '@storybook/blocks'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { Placements } from '../../../constants'; +import { DropdownFullWidthModes } from '../../../types'; +import ReadMe from '../../Dropdown/README.md'; +import { Button, Icon, Text } from '../..'; +import { DropdownTrigger, DropdownPopover, UncontrolledDropdown } from '../../Dropdown'; + +const meta: Meta = { + title: 'Components/DropdownModern', + component: UncontrolledDropdown, + parameters: { + docs: { + page: () => {ReadMe}, + }, + layout: 'centered', + }, + argTypes: { + children: { + control: 'object', + }, + enableAutoClose: { + control: 'boolean', + table: { + defaultValue: { summary: true }, + }, + }, + fullWidthMode: { + control: 'select', + options: [...Object.values(DropdownFullWidthModes), undefined], + table: { + defaultValue: { summary: undefined }, + }, + }, + id: { + control: 'text', + }, + placement: { + control: 'select', + options: Object.values(Placements), + table: { + defaultValue: { summary: Placements.BOTTOM_LEFT }, + }, + }, + }, + args: { + children: ( + <> + + + Information + + + + More links + + + + Profile + + + + Help + + + ), + id: 'UncontrolledDropdownExample', + }, +}; + +export default meta; +type Story = StoryObj; + +export const UncontrolledDropdownPlayground: Story = { + name: 'UncontrolledDropdown', + render: (args) => ( + + Button as anchor + {args.children} + + ), +}; diff --git a/packages/web-react/src/hooks/useDeprecationMessage.ts b/packages/web-react/src/hooks/useDeprecationMessage.ts index e6f00d61ea..1e5feedce9 100644 --- a/packages/web-react/src/hooks/useDeprecationMessage.ts +++ b/packages/web-react/src/hooks/useDeprecationMessage.ts @@ -39,7 +39,7 @@ export const useDeprecationMessage = ({ switch (method) { case 'property': if (propertyProps?.delete) { - message = `${messageBase} "${propertyProps?.deprecatedName}" property will be deleted in the next major version..️️`; + message = `${messageBase} "${propertyProps?.deprecatedName}" property will be deleted in the next major version.`; } else if (propertyProps?.deprecatedValue && propertyProps?.newValue && propertyProps?.propertyName) { message = `${messageBase} The "${propertyProps?.deprecatedValue}" value for "${propertyProps?.propertyName}" property will be renamed to "${propertyProps?.newValue}" in the next major version.`; } else { diff --git a/packages/web-react/src/types/dropdown.ts b/packages/web-react/src/types/dropdown.ts index 4a496d41e9..8ad62d1357 100644 --- a/packages/web-react/src/types/dropdown.ts +++ b/packages/web-react/src/types/dropdown.ts @@ -25,12 +25,30 @@ export type DropdownRenderProps = { export interface DropdownProps extends ChildrenProps, StyleProps { id?: string; + /** @deprecated Will be removed in the next major version. Use modern version of dropdown instead: DropdownModern */ renderTrigger?: (render: DropdownRenderProps) => ReactNode; } +export interface DropdownModernProps extends ChildrenProps, StyleProps { + id: string; +} + export interface SpiritDropdownProps extends DropdownProps { enableAutoClose?: boolean; placement?: PlacementDictionaryType; fullWidthMode?: DropdownFullWidthMode; onAutoClose?: (event: Event) => void; } + +export interface SpiritDropdownModernProps extends DropdownModernProps, ChildrenProps { + enableAutoClose?: boolean; + placement?: PlacementDictionaryType; + fullWidthMode?: DropdownFullWidthMode; + onAutoClose?: (event: Event) => void; + isOpen: boolean; + onToggle: () => void; +} + +export interface UncontrolledDropdownProps + extends ChildrenProps, + Omit {}