diff --git a/packages/examples/accessibility-test/src/app/App.tsx b/packages/examples/accessibility-test/src/app/App.tsx index ffb25281e..05e862eb4 100644 --- a/packages/examples/accessibility-test/src/app/App.tsx +++ b/packages/examples/accessibility-test/src/app/App.tsx @@ -4,6 +4,7 @@ import { Badge, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Button, + ButtonGroup, ButtonGroupItem, Card, Checkbox, CheckboxControl, CheckboxLabel, Clipboard, ClipboardControl, ClipboardTrigger, @@ -249,6 +250,19 @@ function App(): ReactElement { +
+

Button Group

+ + + Hourly + Daily + Monthly + + Custom + + +
+

Card

diff --git a/packages/ods-react/src/components/button-group/.storybook/main.ts b/packages/ods-react/src/components/button-group/.storybook/main.ts new file mode 100644 index 000000000..c8ecf218d --- /dev/null +++ b/packages/ods-react/src/components/button-group/.storybook/main.ts @@ -0,0 +1,26 @@ +import { StorybookConfig } from '@storybook/react-vite'; + +const config: StorybookConfig = { + core: { + disableTelemetry: true, + disableWhatsNewNotifications: true, + }, + docs: { + autodocs: false, + }, + framework: '@storybook/react-vite', + previewHead: (head) => ` + ${head} + + `, + stories: [ + '../src/dev.stories.tsx', + '../tests/**/*.stories.tsx', + ], +}; + +export default config; diff --git a/packages/ods-react/src/components/button-group/.storybook/manager.ts b/packages/ods-react/src/components/button-group/.storybook/manager.ts new file mode 100644 index 000000000..7007ab574 --- /dev/null +++ b/packages/ods-react/src/components/button-group/.storybook/manager.ts @@ -0,0 +1,10 @@ +import { addons } from '@storybook/manager-api'; + +addons.register('custom-panel', (api) => { + api.togglePanel(false); +}); + +addons.setConfig({ + enableShortcuts: false, + showToolbar: true, +}); diff --git a/packages/ods-react/src/components/button-group/.storybook/preview.ts b/packages/ods-react/src/components/button-group/.storybook/preview.ts new file mode 100644 index 000000000..604373b1c --- /dev/null +++ b/packages/ods-react/src/components/button-group/.storybook/preview.ts @@ -0,0 +1,9 @@ +import { type Preview } from '@storybook/react'; +import '@ovhcloud/ods-themes/default/css'; +import '@ovhcloud/ods-themes/default/fonts'; + +const preview: Preview = { + parameters: {}, +}; + +export default preview; diff --git a/packages/ods-react/src/components/button-group/jest-puppeteer.config.ts b/packages/ods-react/src/components/button-group/jest-puppeteer.config.ts new file mode 100644 index 000000000..73ec1b96e --- /dev/null +++ b/packages/ods-react/src/components/button-group/jest-puppeteer.config.ts @@ -0,0 +1,17 @@ +const isCI = !!process.env.CI; + +export default { + launch: { + headless: isCI, + slowMo: isCI ? 0 : 300, + product: 'chrome', + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + "--disable-dev-shm-usage", + "--disable-accelerated-2d-canvas", + "--disable-gpu", + '--font-render-hinting=none', + ], + }, +}; diff --git a/packages/ods-react/src/components/button-group/jest.config.ts b/packages/ods-react/src/components/button-group/jest.config.ts new file mode 100644 index 000000000..0b172199c --- /dev/null +++ b/packages/ods-react/src/components/button-group/jest.config.ts @@ -0,0 +1,26 @@ +const baseOption = { + collectCoverage: false, + testPathIgnorePatterns: [ + 'node_modules/', + 'dist/', + ], + testRegex: 'tests\\/.*\\.spec\\.(ts|tsx)$', + transform: { + '\\.(ts|tsx)$': 'ts-jest', + }, + verbose: true, +}; + +export default !!process.env.E2E ? + { + ...baseOption, + preset: 'jest-puppeteer', + testRegex: 'tests\\/.*\\.e2e\\.ts$', + testTimeout: 60000, + } : { + ...baseOption, + transform: { + ...baseOption.transform, + '\\.scss$': 'jest-transform-stub', + } + }; diff --git a/packages/ods-react/src/components/button-group/modules.d.ts b/packages/ods-react/src/components/button-group/modules.d.ts new file mode 100644 index 000000000..875203d56 --- /dev/null +++ b/packages/ods-react/src/components/button-group/modules.d.ts @@ -0,0 +1,2 @@ +declare module '*.css'; +declare module '*.scss'; diff --git a/packages/ods-react/src/components/button-group/package.json b/packages/ods-react/src/components/button-group/package.json new file mode 100644 index 000000000..72a0e461d --- /dev/null +++ b/packages/ods-react/src/components/button-group/package.json @@ -0,0 +1,23 @@ +{ + "name": "@ovhcloud/ods-react-button-group", + "version": "19.3.0", + "private": true, + "description": "ODS React ButtonGroup component", + "type": "module", + "main": "dist/index.js", + "scripts": { + "clean": "rimraf documentation node_modules", + "doc": "npm run clean && npm run doc:ts && npm run doc:css", + "doc:css": "sass src/components:documentation --no-source-map --pkg-importer=node && node ../../../scripts/generate-component-token-list.js", + "doc:ts": "typedoc", + "lint:a11y": "eslint --config ../../../../../.eslintrc-a11y 'src/**/*.{js,ts,tsx}' --ignore-pattern '*.stories.tsx'", + "lint:scss": "stylelint --aei 'src/components/**/*.scss'", + "lint:ts": "eslint '{src,tests}/**/*.{js,ts,tsx}' --ignore-pattern '*.stories.tsx'", + "start": "npm run start:storybook", + "start:storybook": "storybook dev -p 3000 --no-open", + "test:e2e": "E2E=true start-server-and-test 'npm run start:storybook' 3000 'jest -i --detectOpenHandles'", + "test:e2e:ci": "CI=true npm run test:e2e", + "test:spec": "jest --passWithNoTests", + "test:spec:ci": "npm run test:spec" + } +} diff --git a/packages/ods-react/src/components/button-group/src/components/button-group-item/ButtonGroupItem.tsx b/packages/ods-react/src/components/button-group/src/components/button-group-item/ButtonGroupItem.tsx new file mode 100644 index 000000000..42bf1852f --- /dev/null +++ b/packages/ods-react/src/components/button-group/src/components/button-group-item/ButtonGroupItem.tsx @@ -0,0 +1,53 @@ +import { ToggleGroup, useToggleGroupContext } from '@ark-ui/react/toggle-group'; +import classNames from 'classnames'; +import { type ComponentPropsWithRef, type FC, type JSX, forwardRef } from 'react'; +import { BUTTON_COLOR, BUTTON_VARIANT, Button } from '../../../../button/src'; +import { useButtonGroup } from '../../contexts/useButtonGroup'; +import style from './buttonGroupItem.module.scss'; + +interface ButtonGroupItemProp extends ComponentPropsWithRef<'button'> { + /** + * Whether the component is disabled. + */ + disabled?: boolean; + /** + * The value of the item. + */ + value: string, +} + +const ButtonGroupItem: FC = forwardRef(({ + children, + className, + disabled, + value, + ...props +}, ref): JSX.Element => { + const { value: selection } = useToggleGroupContext(); + const { size } = useButtonGroup(); + + return ( + + + + ); +}); + +ButtonGroupItem.displayName = 'ButtonGroupItem'; + +export { + ButtonGroupItem, + type ButtonGroupItemProp, +}; diff --git a/packages/ods-react/src/components/button-group/src/components/button-group-item/buttonGroupItem.module.scss b/packages/ods-react/src/components/button-group/src/components/button-group-item/buttonGroupItem.module.scss new file mode 100644 index 000000000..a1f7ec739 --- /dev/null +++ b/packages/ods-react/src/components/button-group/src/components/button-group-item/buttonGroupItem.module.scss @@ -0,0 +1,51 @@ +$ods-button-group-item-z-index: 1; + +@layer ods-molecules { + .button-group-item { + --ods-button-group-item-background-color-checked-disabled: var(--ods-color-neutral-500); + + z-index: $ods-button-group-item-z-index; + + &:first-child { + margin-inline-start: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + &:not(:first-child) { + margin-inline-start: -1px; + + &:not(:last-child) { + border-radius: 0; + } + } + + &:last-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + &:hover, + &[data-focus], + &[data-state="on"] { + z-index: $ods-button-group-item-z-index + 1; + } + + &[data-state="on"] { + &:not([data-disabled]) { + border-color: var(--ods-theme-background-color-selected); + background-color: var(--ods-theme-background-color-selected); + } + + &[data-disabled] { + border-color: var(--ods-button-group-item-background-color-checked-disabled); + background-color: var(--ods-button-group-item-background-color-checked-disabled); + color: var(--ods-button-text-color-primary); + } + } + + &[data-disabled] { + z-index: $ods-button-group-item-z-index - 1; + } + } +} diff --git a/packages/ods-react/src/components/button-group/src/components/button-group/ButtonGroup.tsx b/packages/ods-react/src/components/button-group/src/components/button-group/ButtonGroup.tsx new file mode 100644 index 000000000..6b9097192 --- /dev/null +++ b/packages/ods-react/src/components/button-group/src/components/button-group/ButtonGroup.tsx @@ -0,0 +1,43 @@ +import { ToggleGroup } from '@ark-ui/react/toggle-group'; +import classNames from 'classnames'; +import { type FC, type JSX, forwardRef } from 'react'; +import { ButtonGroupProvider, type ButtonGroupRootProp } from '../../contexts/useButtonGroup'; +import style from './buttonGroup.module.scss'; + +interface ButtonGroupProp extends ButtonGroupRootProp {} + +const ButtonGroup: FC = forwardRef(({ + children, + className, + defaultValue, + disabled, + multiple, + onValueChange, + size, + value, + ...props +}, ref): JSX.Element => { + return ( + + + { children } + + + ); +}); + +ButtonGroup.displayName = 'ButtonGroup'; + +export { + ButtonGroup, + type ButtonGroupProp, +}; diff --git a/packages/ods-react/src/components/button-group/src/components/button-group/buttonGroup.module.scss b/packages/ods-react/src/components/button-group/src/components/button-group/buttonGroup.module.scss new file mode 100644 index 000000000..c78db1dba --- /dev/null +++ b/packages/ods-react/src/components/button-group/src/components/button-group/buttonGroup.module.scss @@ -0,0 +1,6 @@ +@layer ods-molecules { + .button-group { + display: flex; + flex-direction: row; + } +} diff --git a/packages/ods-react/src/components/button-group/src/constants/button-group-size.ts b/packages/ods-react/src/components/button-group/src/constants/button-group-size.ts new file mode 100644 index 000000000..98ffff2f6 --- /dev/null +++ b/packages/ods-react/src/components/button-group/src/constants/button-group-size.ts @@ -0,0 +1,7 @@ +import { BUTTON_SIZE, BUTTON_SIZES, type ButtonSize } from '../../../button/src'; + +export { + BUTTON_SIZE as BUTTON_GROUP_SIZE, + BUTTON_SIZES as BUTTON_GROUP_SIZES, + type ButtonSize as ButtonGroupSize, +}; diff --git a/packages/ods-react/src/components/button-group/src/contexts/useButtonGroup.tsx b/packages/ods-react/src/components/button-group/src/contexts/useButtonGroup.tsx new file mode 100644 index 000000000..482ab64d1 --- /dev/null +++ b/packages/ods-react/src/components/button-group/src/contexts/useButtonGroup.tsx @@ -0,0 +1,71 @@ +import { type ComponentPropsWithRef, type JSX, type ReactNode, createContext, useContext } from 'react'; +import { type ButtonGroupSize } from '../constants/button-group-size'; + +interface ButtonGroupValueChangeDetail { + value: string[], +} + +type ButtonGroupRootProp = ComponentPropsWithRef<'div'> & { + /** + * The initial value of the selected items. Use when you don't need to control the value of the component. + */ + defaultValue?: string[]; + /** + * Whether the component is disabled. + */ + disabled?: boolean; + /** + * Whether multiple items can be selected at the same time. + */ + multiple?: boolean; + /** + * Callback fired when the selection changes. + */ + onValueChange?: (detail: ButtonGroupValueChangeDetail) => void; + /** + * The size preset to use. + */ + size?: ButtonGroupSize, + /** + * The controlled value of the selected items. + */ + value?: string[]; +}; + +interface ButtonGroupProviderProp extends ButtonGroupRootProp { + children: ReactNode; +} + +type ButtonGroupContextType = Omit; + +const ButtonGroupContext = createContext(undefined); + +const ButtonGroupProvider = ({ + children, + size, +}: ButtonGroupProviderProp): JSX.Element => { + return ( + + { children } + + ); +}; + +function useButtonGroup(): ButtonGroupContextType { + const context = useContext(ButtonGroupContext); + + if (!context) { + throw new Error('useButtonGroup must be used within a ButtonGroupProvider'); + } + + return context; +} + +export { + ButtonGroupProvider, + type ButtonGroupRootProp, + type ButtonGroupValueChangeDetail, + useButtonGroup, +}; diff --git a/packages/ods-react/src/components/button-group/src/dev.module.css b/packages/ods-react/src/components/button-group/src/dev.module.css new file mode 100644 index 000000000..db5f5fab8 --- /dev/null +++ b/packages/ods-react/src/components/button-group/src/dev.module.css @@ -0,0 +1,3 @@ +.custom-button-group { + +} diff --git a/packages/ods-react/src/components/button-group/src/dev.stories.tsx b/packages/ods-react/src/components/button-group/src/dev.stories.tsx new file mode 100644 index 000000000..faa12ff73 --- /dev/null +++ b/packages/ods-react/src/components/button-group/src/dev.stories.tsx @@ -0,0 +1,104 @@ +import { useState } from 'react'; +import { BUTTON_GROUP_SIZE, ButtonGroup, ButtonGroupItem } from '.'; + +export default { + component: ButtonGroup, + title: 'ButtonGroup dev', +}; + +export const Controlled = () => { + const [values, setValues] = useState([]); + + return ( + <> + setValues(value) } + value={ values }> + Button 1 + Button 2 + Button 3 + + + + + ); +}; + +export const Default = () => ( + + Button 1 + Button 2 + Button 3 + +); + +export const DefaultValue = () => ( + + Button 1 + Button 2 + Button 3 + +); + +export const Disabled = () => ( + <> +

Whole component disabled

+ + Button 1 + Button 2 + Button 3 + + +

Specific item disabled

+ + Button 1 + Button 2 + Button 3 + Button 4 + + +

Selected item disabled

+ + Button 1 + Button 2 + Button 3 + Button 4 + + +); + +export const Multiple = () => ( + + Button 1 + Button 2 + Button 3 + Button 4 + +); + +export const Sizes = () => ( + <> +

MD

+ + Button 1 + Button 2 + Button 3 + + +

SM

+ + Button 1 + Button 2 + Button 3 + + +

XS

+ + Button 1 + Button 2 + Button 3 + + +); diff --git a/packages/ods-react/src/components/button-group/src/index.ts b/packages/ods-react/src/components/button-group/src/index.ts new file mode 100644 index 000000000..1b59778ec --- /dev/null +++ b/packages/ods-react/src/components/button-group/src/index.ts @@ -0,0 +1,4 @@ +export { ButtonGroup, type ButtonGroupProp } from './components/button-group/ButtonGroup'; +export { ButtonGroupItem, type ButtonGroupItemProp } from './components/button-group-item/ButtonGroupItem'; +export { BUTTON_GROUP_SIZE, BUTTON_GROUP_SIZES, type ButtonGroupSize } from './constants/button-group-size'; +export { type ButtonGroupValueChangeDetail } from './contexts/useButtonGroup'; diff --git a/packages/ods-react/src/components/button-group/tests/rendering/button-group.e2e.ts b/packages/ods-react/src/components/button-group/tests/rendering/button-group.e2e.ts new file mode 100644 index 000000000..a11ad8b8a --- /dev/null +++ b/packages/ods-react/src/components/button-group/tests/rendering/button-group.e2e.ts @@ -0,0 +1,14 @@ +import 'jest-puppeteer'; +import { gotoStory } from '../../../../helpers/test'; + +describe('ButtonGroup rendering', () => { + it('should render the component', async() => { + await gotoStory(page, 'rendering/render'); + + expect(await page.waitForSelector('[data-ods="button-group"]')).not.toBeNull(); + + const items = await page.$$('[data-ods="button-group-item"]'); + + expect(items.length).toBe(3); + }); +}); diff --git a/packages/ods-react/src/components/button-group/tests/rendering/button-group.stories.tsx b/packages/ods-react/src/components/button-group/tests/rendering/button-group.stories.tsx new file mode 100644 index 000000000..0dbc694c0 --- /dev/null +++ b/packages/ods-react/src/components/button-group/tests/rendering/button-group.stories.tsx @@ -0,0 +1,14 @@ +import { ButtonGroup, ButtonGroupItem } from '../../src'; + +export default { + component: ButtonGroup, + title: 'Tests rendering', +}; + +export const render = () => ( + + Button 1 + Button 2 + Button 3 + +); diff --git a/packages/ods-react/src/components/button-group/tsconfig.json b/packages/ods-react/src/components/button-group/tsconfig.json new file mode 100644 index 000000000..e201f26bd --- /dev/null +++ b/packages/ods-react/src/components/button-group/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["modules.d.ts", "src", "tests"], + "exclude": [".storybook", "node_modules"] +} diff --git a/packages/ods-react/src/components/button-group/typedoc.json b/packages/ods-react/src/components/button-group/typedoc.json new file mode 100644 index 000000000..d7e0a9c0c --- /dev/null +++ b/packages/ods-react/src/components/button-group/typedoc.json @@ -0,0 +1,17 @@ +{ + "disableGit": true, + "disableSources": true, + "entryPoints": ["src/index.ts"], + "excludeExternals": true, + "excludeInternal": true, + "excludePrivate": true, + "excludeProtected": true, + "outputs": [ + { + "name": "json", + "path": "./documentation/button-group.json" + } + ], + "sort": ["source-order"], + "tsconfig":"tsconfig.json" +} diff --git a/packages/ods-react/src/components/index.ts b/packages/ods-react/src/components/index.ts index 07c81106d..681f7041b 100644 --- a/packages/ods-react/src/components/index.ts +++ b/packages/ods-react/src/components/index.ts @@ -42,3 +42,5 @@ export * from './tree-view/src'; export * from './meter/src'; export * from './toaster/src'; export * from './kbd/src'; + +export * from './button-group/src'; \ No newline at end of file diff --git a/packages/storybook/assets/components/button-group/anatomy-tech.png b/packages/storybook/assets/components/button-group/anatomy-tech.png new file mode 100644 index 000000000..2cd80a448 Binary files /dev/null and b/packages/storybook/assets/components/button-group/anatomy-tech.png differ diff --git a/packages/storybook/assets/components/button-group/anatomy.png b/packages/storybook/assets/components/button-group/anatomy.png new file mode 100644 index 000000000..c98bc16f9 Binary files /dev/null and b/packages/storybook/assets/components/button-group/anatomy.png differ diff --git a/packages/storybook/src/components/themeGenerator/themeGeneratorPreview/ThemeGeneratorPreview.tsx b/packages/storybook/src/components/themeGenerator/themeGeneratorPreview/ThemeGeneratorPreview.tsx index 96250098d..b20720c4c 100644 --- a/packages/storybook/src/components/themeGenerator/themeGeneratorPreview/ThemeGeneratorPreview.tsx +++ b/packages/storybook/src/components/themeGenerator/themeGeneratorPreview/ThemeGeneratorPreview.tsx @@ -4,6 +4,7 @@ import * as AccordionStories from '../../../../stories/components/accordion/acco import * as BadgeStories from '../../../../stories/components/badge/badge.stories'; import * as BreadcrumbStories from '../../../../stories/components/breadcrumb/breadcrumb.stories'; import * as ButtonStories from '../../../../stories/components/button/button.stories'; +import * as ButtonGroupStories from '../../../../stories/components/button-group/button-group.stories'; import * as CardStories from '../../../../stories/components/card/card.stories'; import * as CheckboxStories from '../../../../stories/components/checkbox/checkbox.stories'; import * as ClipboardStories from '../../../../stories/components/clipboard/clipboard.stories'; @@ -52,6 +53,7 @@ const THEME_STORY_MODULES = { Badge: BadgeStories, Breadcrumb: BreadcrumbStories, Button: ButtonStories, + ButtonGroup: ButtonGroupStories, Card: CardStories, Checkbox: CheckboxStories, Clipboard: ClipboardStories, @@ -110,6 +112,7 @@ const THEME_PREVIEW_COMPONENTS: ThemePreviewItem[] = [ { key: 'Badge', kind: REACT_COMPONENTS_TITLE.badge, label: 'Badge' }, { key: 'Breadcrumb', kind: REACT_COMPONENTS_TITLE.breadcrumb, label: 'Breadcrumb' }, { key: 'Button', kind: REACT_COMPONENTS_TITLE.button, label: 'Button' }, + { key: 'ButtonGroup', kind: REACT_COMPONENTS_TITLE.buttonGroup, label: 'Button Group' }, { key: 'Card', kind: REACT_COMPONENTS_TITLE.card, label: 'Card' }, { key: 'Checkbox', kind: REACT_COMPONENTS_TITLE.checkbox, label: 'Checkbox' }, { key: 'Clipboard', kind: REACT_COMPONENTS_TITLE.clipboard, label: 'Clipboard' }, diff --git a/packages/storybook/src/constants/meta.ts b/packages/storybook/src/constants/meta.ts index e2b097354..f957c48ee 100644 --- a/packages/storybook/src/constants/meta.ts +++ b/packages/storybook/src/constants/meta.ts @@ -34,6 +34,7 @@ enum REACT_COMPONENTS_TITLE { badge = `${SECTION.reactComponents}/Badge`, breadcrumb = `${SECTION.reactComponents}/Breadcrumb`, button = `${SECTION.reactComponents}/Button`, + buttonGroup = `${SECTION.reactComponents}/Button Group`, card = `${SECTION.reactComponents}/Card`, cart = `${SECTION.reactComponents}/Cart`, checkbox = `${SECTION.reactComponents}/Checkbox`, diff --git a/packages/storybook/stories/components/button-group/button-group.stories.tsx b/packages/storybook/stories/components/button-group/button-group.stories.tsx new file mode 100644 index 000000000..ff6e30e90 --- /dev/null +++ b/packages/storybook/stories/components/button-group/button-group.stories.tsx @@ -0,0 +1,212 @@ +import { type Meta, type StoryObj } from '@storybook/react'; +import React, { useState } from 'react'; +import { BUTTON_GROUP_SIZE, BUTTON_GROUP_SIZES, ButtonGroup, ButtonGroupItem, type ButtonGroupProp } from '../../../../ods-react/src/components/button-group/src'; +import { ICON_NAME, Icon } from '../../../../ods-react/src/components/icon/src'; +import { CONTROL_CATEGORY } from '../../../src/constants/controls'; +import { excludeFromDemoControls, orderControls } from '../../../src/helpers/controls'; +import { staticSourceRenderConfig } from '../../../src/helpers/source'; + +type Story = StoryObj; + +const meta: Meta = { + argTypes: excludeFromDemoControls(['defaultValue', 'onValueChange', 'value']), + component: ButtonGroup, + subcomponents: { ButtonGroupItem }, + tags: ['new'], + title: 'React Components/Button Group', +}; + +export default meta; + +export const Demo: Story = { + render: (arg) => ( + + Option 1 + Option 2 + Option 3 + + ), + argTypes: orderControls({ + disabled: { + table: { + category: CONTROL_CATEGORY.general, + }, + control: 'boolean', + }, + multiple: { + table: { + category: CONTROL_CATEGORY.general, + }, + control: 'boolean', + }, + size: { + table: { + category: CONTROL_CATEGORY.design, + type: { summary: 'BUTTON_GROUP_SIZE' } + }, + control: { type: 'select' }, + options: BUTTON_GROUP_SIZES, + }, + }), +}; + +export const Controlled: Story = { + globals: { + imports: `import { ICON_NAME, ButtonGroup, ButtonGroupItem, Icon } from '@ovhcloud/ods-react'; +import { useState } from 'react';`, + }, + parameters: { + docs: { + source: { ...staticSourceRenderConfig() }, + }, + }, + tags: ['!dev'], + render: ({}) => { + const [values, setValues] = useState(['hourly']); + + return ( + setValues(value) } + value={ values }> + Hourly + Daily + Monthly + + Custom + + + ); + }, +}; + +export const Default: Story = { + globals: { + imports: `import { ICON_NAME, ButtonGroup, ButtonGroupItem, Icon } from '@ovhcloud/ods-react';`, + }, + tags: ['!dev'], + render: ({}) => ( + + Hourly + Daily + Monthly + + Custom + + + ), +}; + +export const Disabled: Story = { + globals: { + imports: `import { ICON_NAME, ButtonGroup, ButtonGroupItem, Icon } from '@ovhcloud/ods-react';`, + }, + tags: ['!dev'], + render: ({}) => ( + + Hourly + Daily + Monthly + + Custom + + + ), +}; + +export const DisabledItem: Story = { + globals: { + imports: `import { ICON_NAME, ButtonGroup, ButtonGroupItem, Icon } from '@ovhcloud/ods-react';`, + }, + tags: ['!dev'], + render: ({}) => ( + + Hourly + Daily + Monthly + + Custom + + + ), +}; + +export const Multiple: Story = { + globals: { + imports: `import { ButtonGroup, ButtonGroupItem } from '@ovhcloud/ods-react';`, + }, + tags: ['!dev'], + render: ({}) => ( + + Option 1 + Option 2 + Option 3 + Option 4 + + ), +}; + +export const Overview: Story = { + tags: ['!dev'], + parameters: { + layout: 'centered', + }, + render: ({}) => ( + + Hourly + Daily + Monthly + + Custom + + + ), +}; + +export const Size: Story = { + globals: { + imports: `import { BUTTON_GROUP_SIZE, ButtonGroup, ButtonGroupItem } from '@ovhcloud/ods-react';`, + }, + tags: ['!dev'], + render: ({}) => ( + <> +

MD

+ + Option 1 + Option 2 + Option 3 + Option 4 + + +

SM

+ + Option 1 + Option 2 + Option 3 + Option 4 + + +

XS

+ + Option 1 + Option 2 + Option 3 + Option 4 + + + ), +}; + +export const ThemeGenerator: Story = { + parameters: { + layout: 'fullscreen', + }, + tags: ['!dev'], + render: ({}) => ( + + Option 1 + Option 2 + Option 3 + Option 4 + + ), +}; diff --git a/packages/storybook/stories/components/button-group/documentation.mdx b/packages/storybook/stories/components/button-group/documentation.mdx new file mode 100644 index 000000000..ada1ce44e --- /dev/null +++ b/packages/storybook/stories/components/button-group/documentation.mdx @@ -0,0 +1,115 @@ +import { Kbd } from '@ovhcloud/ods-react'; +import { Meta } from '@storybook/blocks'; +import * as ButtonGroupStories from './button-group.stories'; +import { Anatomy } from '../../../src/components/anatomy/Anatomy'; +import { Banner } from '../../../src/components/banner/Banner'; +import { Canvas } from '../../../src/components/canvas/Canvas'; +import { BestPractices } from '../../../src/components/bestPractices/BestPractices'; +import { Heading } from '../../../src/components/heading/Heading'; +import { IdentityCard } from '../../../src/components/identityCard/IdentityCard'; +import { StorybookLink } from '../../../src/components/storybookLink/StorybookLink'; +import { REACT_COMPONENTS_TITLE, STORY } from '../../../src/constants/meta'; + + + + + + + + + + + The **Button Group** component arranges multiple related button elements into a single, cohesive container. + It enables users to make selections from a set of related options, either allowing one or multiple active states depending on configuration. + + + + +Use a **Button Group** when multiple buttons represent a set of related options or actions. + +It is commonly used for: +- Toggle sets for text formatting or styling. +- Filtering or grouping options within a toolbar. +- Segmented controls in compact layouts. + + + + + + + + + +1. **ButtonGroup** +2. **Inactive buttons** +3. **Active buttons** + + + +**Button Group** buttons can be focused and triggered. +**Button Group** or its buttons can be disabled. + + + +Only one button in the group can be selected at a time. + +Selecting a new button automatically deselects the previously selected one. + + + +Several buttons can be selected simultaneously. + +Each button toggles independently between selected and unselected states. + + + +It may be allowed that no button is selected at all, when the user can clear all selections (e.g., "No filter applied"). + + + + + +When the **Button Group** receives focus, it is set on the first button. + +Disabled buttons are skipped in the focus order and cannot be activated. + +Focus remains within the group when navigating between items using arrow keys. + + + +Pressing Tab moves focus to the first button in the group. + +Pressing Shift + Tab moves focus to the previous focusable element outside the **Button Group**. + +Pressing Arrow Right moves focus to the next button in the group. + +Pressing Arrow Left moves focus to the previous item in the group. + +Pressing Home (or fn + Arrow Left) moves focus to the first button. + +Pressing End (or fn + Arrow Right) moves focus to the last button. + +Pressing Space or Enter activates or deactivates the focused button, updating the selection immediately. + + + +The **Button Group** component handles by itself the accessibility requirements regarding the control grouping. + +Though you need to ensure that each of your items follows the +Button Accessibility Best Practices. diff --git a/packages/storybook/stories/components/button-group/technical-information.mdx b/packages/storybook/stories/components/button-group/technical-information.mdx new file mode 100644 index 000000000..875197c3e --- /dev/null +++ b/packages/storybook/stories/components/button-group/technical-information.mdx @@ -0,0 +1,55 @@ +import { Meta } from '@storybook/blocks'; +import cssVariable from '../../../../ods-react/src/components/button-group/documentation/cssVariable.json'; +import specificationsButtonGroup from '../../../../ods-react/src/components/button-group/documentation/button-group.json'; +import { Anatomy } from '../../../src/components/anatomy/Anatomy'; +import { Banner } from '../../../src/components/banner/Banner'; +import { Canvas } from '../../../src/components/canvas/Canvas'; +import { Heading } from '../../../src/components/heading/Heading'; +import { TechnicalSpecification } from '../../../src/components/technicalSpecification/TechnicalSpecification'; +import * as ButtonGroupStories from './button-group.stories'; + + + + + + + + + + + + + +1. **ButtonGroup** +2. **ButtonGroupItem** + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/storybook/stories/components/gallery.mdx b/packages/storybook/stories/components/gallery.mdx index 3c2186dd1..422899717 100644 --- a/packages/storybook/stories/components/gallery.mdx +++ b/packages/storybook/stories/components/gallery.mdx @@ -3,6 +3,7 @@ import { Overview as Accordion } from './accordion/accordion.stories'; import { Overview as Badge } from './badge/badge.stories'; import { Overview as Breadcrumb } from './breadcrumb/breadcrumb.stories'; import { Overview as Button } from './button/button.stories'; +import { Overview as ButtonGroup } from './button-group/button-group.stories'; import { Overview as Card } from './card/card.stories'; import { Overview as Checkbox } from './checkbox/checkbox.stories'; import { Overview as Clipboard } from './clipboard/clipboard.stories'; @@ -61,6 +62,7 @@ import { REACT_COMPONENTS_TITLE } from '../../src/constants/meta'; { kind: REACT_COMPONENTS_TITLE.badge, name: 'Badge', story: Badge }, { kind: REACT_COMPONENTS_TITLE.breadcrumb, name: 'Breadcrumb', story: Breadcrumb }, { kind: REACT_COMPONENTS_TITLE.button, name: 'Button', story: Button }, + { kind: REACT_COMPONENTS_TITLE.buttonGroup, name: 'Button Group', story: ButtonGroup }, { kind: REACT_COMPONENTS_TITLE.card, name: 'Card', story: Card }, { kind: REACT_COMPONENTS_TITLE.checkbox, name: 'Checkbox', story: Checkbox }, { kind: REACT_COMPONENTS_TITLE.clipboard, name: 'Clipboard', story: Clipboard }, diff --git a/scripts/component-generator/templates/storybook/documentation.mdx.hbs b/scripts/component-generator/templates/storybook/documentation.mdx.hbs index d7e87f9b5..9139793a0 100644 --- a/scripts/component-generator/templates/storybook/documentation.mdx.hbs +++ b/scripts/component-generator/templates/storybook/documentation.mdx.hbs @@ -15,7 +15,7 @@ import { REACT_COMPONENTS_TITLE, STORY } from '../../../src/constants/meta'; _**{{> ComponentName }}** TODO some text about the component_ - ComponentName }}Stories.Overview } sourceState='none' /> + ComponentName }}Stories.Overview } sourceState="none" /> diff --git a/scripts/component-generator/templates/storybook/{{ prefix-join prefix name }}.stories.tsx.hbs b/scripts/component-generator/templates/storybook/{{ prefix-join prefix name }}.stories.tsx.hbs index bf67d5577..a89d2b18f 100644 --- a/scripts/component-generator/templates/storybook/{{ prefix-join prefix name }}.stories.tsx.hbs +++ b/scripts/component-generator/templates/storybook/{{ prefix-join prefix name }}.stories.tsx.hbs @@ -9,7 +9,7 @@ type Story = StoryObj<{{> ComponentName }}Prop>; const meta: Meta<{{> ComponentName }}Prop> = { component: {{> ComponentName }}, // subcomponents: { {{> ComponentName }}Xxx }, // Uncomment if sub components, otherwise remove - tags={ ['new'] } + tags: ['new'], title: 'React Components/{{> ComponentName }}', }; diff --git a/yarn.lock b/yarn.lock index a7dd603a6..682c88293 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4723,6 +4723,12 @@ __metadata: languageName: unknown linkType: soft +"@ovhcloud/ods-react-button-group@workspace:packages/ods-react/src/components/button-group": + version: 0.0.0-use.local + resolution: "@ovhcloud/ods-react-button-group@workspace:packages/ods-react/src/components/button-group" + languageName: unknown + linkType: soft + "@ovhcloud/ods-react-button@workspace:packages/ods-react/src/components/button": version: 0.0.0-use.local resolution: "@ovhcloud/ods-react-button@workspace:packages/ods-react/src/components/button"