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 {
Warning
+
+ 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 (
+
+ -1 ? BUTTON_VARIANT.default : BUTTON_VARIANT.outline }>
+ { children }
+
+
+ );
+});
+
+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
+
+
+ setValues(['1']) }>
+ Force value 1
+
+ >
+ );
+};
+
+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"