diff --git a/packages/web-react/src/components/SplitButton/README.md b/packages/web-react/src/components/SplitButton/README.md index 5a2d9978bc..903f0a5a4a 100644 --- a/packages/web-react/src/components/SplitButton/README.md +++ b/packages/web-react/src/components/SplitButton/README.md @@ -104,7 +104,7 @@ const onDropdownToggle = () => setIsOpen(!isOpen); ; ``` -## API +### API | Name | Type | Default | Required | Description | | ------- | ------------------------------------------ | --------- | -------- | ------------- | @@ -115,7 +115,62 @@ On top of the API options, the components accept [additional attributes][readme- If you need more control over the styling of a component, you can use [style props][readme-style-props] and [escape hatches][readme-escape-hatches]. +## Uncontrolled Split Button + +Uncontrolled Split Button is combination of Button component and Dropdown component. +It is used when you want to have a button with additional actions in a dropdown menu. + +Simple variant: + +```jsx + alert('Button clicked')} +> + {/* Dropdown content */} + +``` + +Full example: + +```jsx + alert('Button clicked')} + color="secondary" + dropdownIconName="more" + dropdownLabel="More" + dropdownPlacement="bottom-start" + id="uncontrolled-split-button" + isDisabled={false} + size="large" +> + {/* Dropdown content */} + +``` + +### API + +| Name | Type | Default | Required | Description | +| ------------------- | -------------------------------------------- | -------------- | -------- | -------------------------------------------------------- | +| `buttonIconName` | `string` | - | ✕ \* | Name of the icon to be displayed in the Button | +| `buttonLabel` | `string` | - | ✕ \* | Label of the Button | +| `buttonOnClick` | `function` | - | ✓ | Function to be called when the Button is clicked | +| `children` | `ReactNode` | - | ✓ | Dropdown content | +| `color` | \[`primary` \| `secondary` \| `tertiary` ] | `primary` | ✕ | Color variant | +| `dropdownIconName` | `string` | `chevron-down` | ✕ | Name of the icon to be displayed in the Dropdown Trigger | +| `dropdownLabel` | `string` | - | ✕ | Label of the Dropdown Trigger | +| `dropdownPlacement` | [Placement dictionary][dictionary-placement] | `bottom-end` | ✕ | Placement of the Dropdown | +| `id` | `string` | - | ✓ | Id of the Split Button and part of Dropdown id | +| `isDisabled` | `boolean` | `false` | ✕ | Disables the Split Button | +| `size` | [Size dictionary][dictionary-size] | `medium` | ✕ | Size variant | + +(\*) Conditionally required: either `buttonIconName` or `buttonLabel` must be provided. + [dictionary-size]: https://github.com/lmc-eu/spirit-design-system/tree/main/docs/DICTIONARIES.md#size +[dictionary-placement]: https://github.com/lmc-eu/spirit-design-system/tree/main/docs/DICTIONARIES.md#placement [readme-additional-attributes]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#additional-attributes [readme-button]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/src/components/Button/README.md [readme-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/SplitButton/UncontrolledSplitButton.tsx b/packages/web-react/src/components/SplitButton/UncontrolledSplitButton.tsx new file mode 100644 index 0000000000..3fb2a0eb93 --- /dev/null +++ b/packages/web-react/src/components/SplitButton/UncontrolledSplitButton.tsx @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; +import { UncontrolledSplitButtonProps } from '../../types'; +import { Button } from '../Button'; +import { Dropdown, DropdownPopover, DropdownTrigger } from '../Dropdown'; +import { Icon } from '../Icon'; +import { VisuallyHidden } from '../VisuallyHidden'; +import SplitButton from './SplitButton'; + +const defaultProps: Partial = { + dropdownPlacement: 'bottom-end', + dropdownIconName: 'chevron-down', +}; + +const UncontrolledSplitButton = (props: UncontrolledSplitButtonProps) => { + const propsWithDefaults = { ...defaultProps, ...props }; + const { + children, + dropdownIconName, + dropdownLabel, + dropdownPlacement, + id, + isDisabled, + buttonLabel, + buttonOnClick, + buttonIconName, + ...restProps + } = propsWithDefaults; + const [openDropdownStates, setOpenDropdownStates] = useState(false); + + return ( + + + setOpenDropdownStates(!openDropdownStates)} + placement={dropdownPlacement} + > + + {!dropdownLabel && {dropdownIconName}} + {dropdownLabel && dropdownLabel} + + + {children} + + + ); +}; + +export default UncontrolledSplitButton; diff --git a/packages/web-react/src/components/SplitButton/__tests__/UncontrolledSplitButton.test.tsx b/packages/web-react/src/components/SplitButton/__tests__/UncontrolledSplitButton.test.tsx new file mode 100644 index 0000000000..70c39d3ce8 --- /dev/null +++ b/packages/web-react/src/components/SplitButton/__tests__/UncontrolledSplitButton.test.tsx @@ -0,0 +1,92 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { classNamePrefixProviderTest, restPropsTest, stylePropsTest } from '@local/tests'; +import { ComponentButtonColors, Sizes } from '../../../constants'; +import UncontrolledSplitButton from '../UncontrolledSplitButton'; + +describe('SplitButton', () => { + const splitButtonColors = Object.values(ComponentButtonColors).filter( + (color) => color !== ComponentButtonColors.PLAIN, + ); + + const onClick = jest.fn(); + + classNamePrefixProviderTest(UncontrolledSplitButton, 'SplitButton'); + + stylePropsTest(UncontrolledSplitButton); + + restPropsTest(UncontrolledSplitButton, 'div'); + + it('should have default classname', () => { + render( + + Content + , + ); + + expect(screen.getByTestId('test')).toHaveClass('SplitButton'); + }); + + it('should render dropdown content', () => { + render( + + Content + , + ); + + expect(screen.getByText('Content')).toHaveClass('DropdownPopover'); + }); + + it.each(Object.values(Sizes))('should render size %s on buttons', (size) => { + render( + + Content + , + ); + + expect(screen.getByText('Button')).toHaveClass(`Button--${size}`); + }); + + it.each(splitButtonColors)('should render color %s on buttons', (color) => { + render( + + Content + , + ); + + expect(screen.getByText('Button')).toHaveClass(`Button--${color}`); + }); + + it('should render color and size on buttons', () => { + render( + + Content + , + ); + + expect(screen.getByText('Button')).toHaveClass('Button--secondary'); + expect(screen.getByText('Button')).toHaveClass('Button--small'); + }); +}); diff --git a/packages/web-react/src/components/SplitButton/demo/UncontrolledSplitButton.tsx b/packages/web-react/src/components/SplitButton/demo/UncontrolledSplitButton.tsx new file mode 100644 index 0000000000..aa01398884 --- /dev/null +++ b/packages/web-react/src/components/SplitButton/demo/UncontrolledSplitButton.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Item } from '../../Item'; +import UncontrolledSplitButton from '../UncontrolledSplitButton'; + +const UncontrolledSplitButtonDemo = () => { + return ( + alert('Button clicked')} + color="secondary" + dropdownIconName="more" + dropdownLabel="More" + dropdownPlacement="top-end" + id="uncontrolled-split-button" + isDisabled={false} + size="large" + > + + + + ); +}; + +export default UncontrolledSplitButtonDemo; diff --git a/packages/web-react/src/components/SplitButton/index.ts b/packages/web-react/src/components/SplitButton/index.ts index 63df5c084f..cbcd1daba4 100644 --- a/packages/web-react/src/components/SplitButton/index.ts +++ b/packages/web-react/src/components/SplitButton/index.ts @@ -1,4 +1,5 @@ 'use client'; export { default as SplitButton } from './SplitButton'; +export { default as UncontrolledSplitButton } from './UncontrolledSplitButton'; export * from './useSplitButtonStyleProps'; diff --git a/packages/web-react/src/components/SplitButton/stories/SplitButtonParts.tsx b/packages/web-react/src/components/SplitButton/stories/SplitButtonParts.tsx index 5b7d60c7c9..2569afff97 100644 --- a/packages/web-react/src/components/SplitButton/stories/SplitButtonParts.tsx +++ b/packages/web-react/src/components/SplitButton/stories/SplitButtonParts.tsx @@ -7,7 +7,7 @@ import { Tooltip, TooltipPopover, TooltipTrigger } from '../../Tooltip'; import { VisuallyHidden } from '../../VisuallyHidden'; import { dropdownContent } from '../demo/constants'; -const DropdownContent = () => { +export const DropdownContent = () => { return ( <> {dropdownContent.map(({ icon, text }) => ( diff --git a/packages/web-react/src/components/SplitButton/stories/UncontrolledSplitButton.stories.tsx b/packages/web-react/src/components/SplitButton/stories/UncontrolledSplitButton.stories.tsx new file mode 100644 index 0000000000..7a285992da --- /dev/null +++ b/packages/web-react/src/components/SplitButton/stories/UncontrolledSplitButton.stories.tsx @@ -0,0 +1,126 @@ +import { Markdown } from '@storybook/blocks'; +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { ComponentButtonColors, Placements, Sizes } from '../../../constants'; +import ReadMe from '../README.md'; +import { UncontrolledSplitButton } from '..'; +import { DropdownContent } from './SplitButtonParts'; + +const meta: Meta = { + title: 'Components/SplitButton', + component: UncontrolledSplitButton, + parameters: { + docs: { + page: () => {ReadMe}, + }, + controls: { exclude: ['ElementTag', 'UNSAFE_className', 'UNSAFE_style', 'transferClassName'] }, + }, + argTypes: { + id: { + control: 'text', + description: 'The ID of the Uncontrolled Split Button.', + table: { + type: { summary: 'string' }, + }, + }, + size: { + control: 'select', + options: [...Object.values(Sizes)], + description: 'Size of the button.', + table: { + defaultValue: { summary: Sizes.MEDIUM }, + type: { summary: 'ButtonSize' }, + }, + }, + color: { + control: 'select', + options: [...Object.values(ComponentButtonColors).filter((color) => color !== ComponentButtonColors.PLAIN)], + description: 'Color of the button.', + table: { + defaultValue: { summary: ComponentButtonColors.PRIMARY }, + type: { summary: 'ButtonColor' }, + }, + }, + buttonLabel: { + control: 'text', + description: 'The label for the button.', + table: { + type: { summary: 'string' }, + }, + }, + buttonIconName: { + control: 'text', + description: 'The name of the icon to display on the button.', + table: { + type: { summary: 'string' }, + }, + }, + dropdownLabel: { + control: 'text', + description: 'The label for the dropdown button.', + table: { + type: { summary: 'string' }, + }, + }, + children: { + control: 'text', + description: 'The content of the dropdown.', + table: { + type: { summary: 'ReactNode' }, + }, + }, + dropdownPlacement: { + control: 'select', + options: Object.values(Placements), + table: { + defaultValue: { summary: Placements.BOTTOM_END }, + }, + description: 'The placement of the dropdown.', + }, + isDisabled: { + control: 'boolean', + description: 'Whether the button is disabled.', + table: { + type: { summary: 'boolean' }, + }, + }, + dropdownIconName: { + control: 'text', + description: 'The name of the icon to display on the dropdown button.', + table: { + type: { summary: 'string' }, + }, + }, + buttonOnClick: { + description: 'Function to call when the button is clicked.', + table: { + type: { summary: '() => void' }, + }, + }, + }, + args: { + buttonIconName: undefined, + buttonLabel: 'Button', + buttonOnClick: () => {}, + children: , + color: ComponentButtonColors.PRIMARY, + dropdownIconName: 'chevron-down', + dropdownLabel: undefined, + dropdownPlacement: Placements.BOTTOM_END, + id: 'uncontrolled-split-button', + isDisabled: false, + size: Sizes.MEDIUM, + }, +}; + +export default meta; +type Story = StoryObj; + +export const UncontrolledSplitButtonPlayground: Story = { + name: 'UncontrolledSplitButton', + render: (args) => { + const { children, ...rest } = args; + + return {children}; + }, +}; diff --git a/packages/web-react/src/types/splitButton.ts b/packages/web-react/src/types/splitButton.ts index 810ca25db6..c8fc314281 100644 --- a/packages/web-react/src/types/splitButton.ts +++ b/packages/web-react/src/types/splitButton.ts @@ -1,5 +1,6 @@ +import { ReactNode } from 'react'; import { ButtonColor, ButtonSize } from './button'; -import { ChildrenProps, StyleProps, TransferProps } from './shared'; +import { ChildrenProps, PlacementDictionaryType, StyleProps, TransferProps } from './shared'; export interface SplitButtonProps extends TransferProps, StyleProps, ChildrenProps {} @@ -7,3 +8,23 @@ export interface SpiritSplitButtonProps extends SplitButtonP color?: ButtonColor; size?: ButtonSize; } + +export type UncontrolledSplitButtonProps = { + buttonOnClick: () => void; + children: ReactNode; + dropdownIconName?: string; + dropdownLabel?: string; + dropdownPlacement?: PlacementDictionaryType; + id: string; + isDisabled?: boolean; +} & ( + | { + buttonLabel?: string; + buttonIconName: string; + } + | { + buttonLabel: string; + buttonIconName?: string; + } +) & + SpiritSplitButtonProps;