From 1fc2c6c591ce8f5cd0cfb51ef6716e4a244529de Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Wed, 24 Dec 2025 16:18:40 +0100 Subject: [PATCH 01/13] chore: wip ToggleButton/ToggleButtonGroup Signed-off-by: Gabriel Dugny --- docs-ui/src/content/toggle-button-group.mdx | 115 +++++++++ .../src/content/toggle-button-group.props.ts | 75 ++++++ docs-ui/src/content/toggle-button.mdx | 94 ++++++++ docs-ui/src/content/toggle-button.props.ts | 59 +++++ docs-ui/src/snippets/stories-snippets.tsx | 6 + docs-ui/src/utils/data.ts | 8 + .../ToggleButton/ToggleButton.module.css | 106 ++++++++ .../ToggleButton/ToggleButton.stories.tsx | 226 ++++++++++++++++++ .../components/ToggleButton/ToggleButton.tsx | 59 +++++ .../src/components/ToggleButton/definition.ts | 32 +++ .../ui/src/components/ToggleButton/index.ts | 19 ++ .../ui/src/components/ToggleButton/types.ts | 35 +++ .../ToggleButtonGroup.module.css | 112 +++++++++ .../ToggleButtonGroup.stories.tsx | 179 ++++++++++++++ .../ToggleButtonGroup/ToggleButtonGroup.tsx | 51 ++++ .../ToggleButtonGroup/definition.ts | 30 +++ .../src/components/ToggleButtonGroup/index.ts | 19 ++ .../src/components/ToggleButtonGroup/types.ts | 27 +++ packages/ui/src/definitions.ts | 2 + packages/ui/src/index.ts | 2 + 20 files changed, 1256 insertions(+) create mode 100644 docs-ui/src/content/toggle-button-group.mdx create mode 100644 docs-ui/src/content/toggle-button-group.props.ts create mode 100644 docs-ui/src/content/toggle-button.mdx create mode 100644 docs-ui/src/content/toggle-button.props.ts create mode 100644 packages/ui/src/components/ToggleButton/ToggleButton.module.css create mode 100644 packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx create mode 100644 packages/ui/src/components/ToggleButton/ToggleButton.tsx create mode 100644 packages/ui/src/components/ToggleButton/definition.ts create mode 100644 packages/ui/src/components/ToggleButton/index.ts create mode 100644 packages/ui/src/components/ToggleButton/types.ts create mode 100644 packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css create mode 100644 packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx create mode 100644 packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx create mode 100644 packages/ui/src/components/ToggleButtonGroup/definition.ts create mode 100644 packages/ui/src/components/ToggleButtonGroup/index.ts create mode 100644 packages/ui/src/components/ToggleButtonGroup/types.ts diff --git a/docs-ui/src/content/toggle-button-group.mdx b/docs-ui/src/content/toggle-button-group.mdx new file mode 100644 index 0000000000..fa979d65fe --- /dev/null +++ b/docs-ui/src/content/toggle-button-group.mdx @@ -0,0 +1,115 @@ +import { PropsTable } from '@/components/PropsTable'; +import { Snippet } from '@/components/Snippet'; +import { CodeBlock } from '@/components/CodeBlock'; +import { + toggleButtonGroupPropDefs, + toggleButtonGroupUsageSnippet, + toggleButtonGroupSingleSnippet, + toggleButtonGroupMultipleSnippet, + toggleButtonGroupVerticalSnippet, + toggleButtonGroupDisabledSnippet, + toggleButtonGroupDisallowEmptySnippet, + toggleButtonGroupIconsSnippet, + toggleButtonGroupIconsOnlySnippet, +} from './toggle-button-group.props'; +import { ToggleButtonGroupSnippet } from '@/snippets/stories-snippets'; +import { PageTitle } from '@/components/PageTitle'; +import { Theming } from '@/components/Theming'; +import { ChangelogComponent } from '@/components/ChangelogComponent'; +import { ToggleButtonGroupDefinition } from '../utils/definitions'; + + + +} + code={toggleButtonGroupSingleSnippet} +/> + +## Usage + + + +## API reference + + + +## Examples + +### Single Selection + +} + code={toggleButtonGroupSingleSnippet} +/> + +### Multiple Selection + +} + code={toggleButtonGroupMultipleSnippet} +/> + +### Icons and Text + +} + code={toggleButtonGroupIconsSnippet} +/> + +### Icons Only + +} + code={toggleButtonGroupIconsOnlySnippet} +/> + +### Disallow Empty Selection + +} + code={toggleButtonGroupDisallowEmptySnippet} +/> + +### Vertical Orientation + +} + code={toggleButtonGroupVerticalSnippet} +/> + +### Disabled + +} + code={toggleButtonGroupDisabledSnippet} +/> + + + + diff --git a/docs-ui/src/content/toggle-button-group.props.ts b/docs-ui/src/content/toggle-button-group.props.ts new file mode 100644 index 0000000000..94299f71a3 --- /dev/null +++ b/docs-ui/src/content/toggle-button-group.props.ts @@ -0,0 +1,75 @@ +import { + classNamePropDefs, + stylePropDefs, + type PropDef, +} from '@/utils/propDefs'; + +export const toggleButtonGroupPropDefs: Record = { + selectionMode: { + type: 'enum', + values: ['single', 'multiple'], + default: 'single', + }, + orientation: { + type: 'enum', + values: ['horizontal', 'vertical'], + default: 'horizontal', + responsive: true, + }, + selectedKeys: { type: 'enum', values: ['Iterable'] }, + defaultSelectedKeys: { type: 'enum', values: ['Iterable'] }, + onSelectionChange: { type: 'enum', values: ['(keys) => void'] }, + isDisabled: { type: 'boolean', default: 'false' }, + disallowEmptySelection: { type: 'boolean', default: 'false' }, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const toggleButtonGroupUsageSnippet = `import { ToggleButtonGroup, ToggleButton } from '@backstage/ui'; + + + Dogs + Cats +`; + +export const toggleButtonGroupSingleSnippet = ` + Apples + Oranges + Bananas +`; + +export const toggleButtonGroupMultipleSnippet = ` + Frontend + Backend + Platform +`; + +export const toggleButtonGroupVerticalSnippet = ` + Low + Medium + High +`; + +export const toggleButtonGroupDisabledSnippet = ` + Cat + Dog + Bird +`; + +export const toggleButtonGroupDisallowEmptySnippet = ` + One + Two + Three +`; + +export const toggleButtonGroupIconsSnippet = ` + Cloud + Starred + Next +`; + +export const toggleButtonGroupIconsOnlySnippet = ` + + + +`; diff --git a/docs-ui/src/content/toggle-button.mdx b/docs-ui/src/content/toggle-button.mdx new file mode 100644 index 0000000000..2ec644cdd5 --- /dev/null +++ b/docs-ui/src/content/toggle-button.mdx @@ -0,0 +1,94 @@ +import { PropsTable } from '@/components/PropsTable'; +import { Snippet } from '@/components/Snippet'; +import { CodeBlock } from '@/components/CodeBlock'; +import { ToggleButtonSnippet } from '@/snippets/stories-snippets'; +import { + toggleButtonPropDefs, + toggleButtonUsageSnippet, + toggleButtonVariantsSnippet, + toggleButtonSizesSnippet, + toggleButtonIconsSnippet, + toggleButtonDisabledSnippet, + toggleButtonControlledSnippet, +} from './toggle-button.props'; +import { PageTitle } from '@/components/PageTitle'; +import { Theming } from '@/components/Theming'; +import { ChangelogComponent } from '@/components/ChangelogComponent'; +import { ToggleButtonDefinition } from '../utils/definitions'; + + + +} + code={toggleButtonVariantsSnippet} +/> + +## Usage + + + +## API reference + + + +## Examples + +### Variants + +} + code={toggleButtonVariantsSnippet} +/> + +### Sizes + +} + code={toggleButtonSizesSnippet} +/> + +### With Icons + +} + code={toggleButtonIconsSnippet} +/> + +### Disabled + +} + code={toggleButtonDisabledSnippet} +/> + +### Controlled + +} + code={toggleButtonControlledSnippet} +/> + + + + + diff --git a/docs-ui/src/content/toggle-button.props.ts b/docs-ui/src/content/toggle-button.props.ts new file mode 100644 index 0000000000..71625e7e6f --- /dev/null +++ b/docs-ui/src/content/toggle-button.props.ts @@ -0,0 +1,59 @@ +import { + classNamePropDefs, + stylePropDefs, + type PropDef, +} from '@/utils/propDefs'; + +export const toggleButtonPropDefs: Record = { + variant: { + type: 'enum', + values: ['primary', 'secondary'], + default: 'primary', + responsive: true, + }, + size: { + type: 'enum', + values: ['small', 'medium'], + default: 'small', + responsive: true, + }, + iconStart: { type: 'enum', values: ['ReactNode'] }, + iconEnd: { type: 'enum', values: ['ReactNode'] }, + isSelected: { type: 'boolean' }, + defaultSelected: { type: 'boolean' }, + onChange: { type: 'enum', values: ['(isSelected: boolean) => void'] }, + isDisabled: { type: 'boolean', default: 'false' }, + children: { type: 'enum', values: ['ReactNode'] }, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const toggleButtonUsageSnippet = `import { ToggleButton } from '@backstage/ui'; + +Toggle`; + +export const toggleButtonVariantsSnippet = ` + Primary + Secondary +`; + +export const toggleButtonSizesSnippet = ` + Small + Medium +`; + +export const toggleButtonIconsSnippet = ` + Favorite + Confirm +`; + +export const toggleButtonDisabledSnippet = ` + Disabled + Selected +`; + +export const toggleButtonControlledSnippet = `const [selected, setSelected] = useState(false); + + + {selected ? 'On' : 'Off'} +`; diff --git a/docs-ui/src/snippets/stories-snippets.tsx b/docs-ui/src/snippets/stories-snippets.tsx index 0507c85c9f..ba722a5edb 100644 --- a/docs-ui/src/snippets/stories-snippets.tsx +++ b/docs-ui/src/snippets/stories-snippets.tsx @@ -30,6 +30,8 @@ import * as TableStories from '../../../packages/ui/src/components/Table/stories import * as TagGroupStories from '../../../packages/ui/src/components/TagGroup/TagGroup.stories'; import * as PasswordFieldStories from '../../../packages/ui/src/components/PasswordField/PasswordField.stories'; import * as VisuallyHiddenStories from '../../../packages/ui/src/components/VisuallyHidden/VisuallyHidden.stories'; +import * as ToggleButtonStories from '../../../packages/ui/src/components/ToggleButton/ToggleButton.stories'; +import * as ToggleButtonGroupStories from '../../../packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories'; // Helper function to create snippet components // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -75,6 +77,10 @@ export const HeaderSnippet = createSnippetComponent(HeaderStories); export const HeaderPageSnippet = createSnippetComponent(HeaderPageStories); export const TableSnippet = createSnippetComponent(TableStories); export const TagGroupSnippet = createSnippetComponent(TagGroupStories); +export const ToggleButtonSnippet = createSnippetComponent(ToggleButtonStories); +export const ToggleButtonGroupSnippet = createSnippetComponent( + ToggleButtonGroupStories, +); export const VisuallyHiddenSnippet = createSnippetComponent( VisuallyHiddenStories, ); diff --git a/docs-ui/src/utils/data.ts b/docs-ui/src/utils/data.ts index de369043d1..b20725142c 100644 --- a/docs-ui/src/utils/data.ts +++ b/docs-ui/src/utils/data.ts @@ -116,6 +116,14 @@ export const components: Page[] = [ title: 'TextField', slug: 'text-field', }, + { + title: 'ToggleButton', + slug: 'toggle-button', + }, + { + title: 'ToggleButtonGroup', + slug: 'toggle-button-group', + }, { title: 'Tooltip', slug: 'tooltip', diff --git a/packages/ui/src/components/ToggleButton/ToggleButton.module.css b/packages/ui/src/components/ToggleButton/ToggleButton.module.css new file mode 100644 index 0000000000..ad3b173f04 --- /dev/null +++ b/packages/ui/src/components/ToggleButton/ToggleButton.module.css @@ -0,0 +1,106 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@layer tokens, base, components, utilities; + +@layer components { + .bui-ToggleButton { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--bui-space-1_5); + border: none; + border-radius: var(--bui-radius-2); + background: var(--bui-bg-surface-1); + color: var(--bui-fg-primary); + font-family: var(--bui-font-regular); + font-weight: var(--bui-font-weight-bold); + padding: 0 var(--bui-space-2); + cursor: pointer; + transition: background-color 150ms ease, box-shadow 150ms ease, + color 150ms ease, transform 100ms ease; + } + + .bui-ToggleButton[data-variant='secondary'] { + background: transparent; + color: var(--bui-fg-primary); + box-shadow: inset 0 0 0 1px var(--bui-border); + } + + .bui-ToggleButton[data-variant='primary'][data-selected], + .bui-ToggleButton[data-variant='primary'][data-pressed] { + background: var(--bui-bg-solid); + color: var(--bui-fg-solid); + } + + .bui-ToggleButton[data-variant='primary']:not([data-selected])[data-hovered] { + background: var(--bui-bg-surface-2); + } + + .bui-ToggleButton[data-variant='secondary'][data-selected], + .bui-ToggleButton[data-variant='secondary'][data-pressed] { + background: var(--bui-bg-surface-2); + box-shadow: inset 0 0 0 1px var(--bui-border-hover); + } + + .bui-ToggleButton[data-focus-visible] { + outline: 2px solid var(--bui-ring); + outline-offset: 2px; + } + + .bui-ToggleButton[data-disabled] { + cursor: not-allowed; + color: var(--bui-fg-disabled); + background: var(--bui-bg-disabled); + box-shadow: inset 0 0 0 1px var(--bui-border-disabled); + } + + .bui-ToggleButton[data-pressed]:not([data-disabled]) { + transform: scale(0.98); + } + + .bui-ToggleButton[data-size='small'] { + height: 2rem; + font-size: var(--bui-font-size-3); + padding: 0 var(--bui-space-2); + + svg { + width: 1rem; + height: 1rem; + } + } + + .bui-ToggleButton[data-size='medium'] { + height: 2.5rem; + font-size: var(--bui-font-size-4); + padding: 0 var(--bui-space-3); + + svg { + width: 1.25rem; + height: 1.25rem; + } + } + + .bui-ToggleButtonContent { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--bui-space-1_5); + height: 100%; + width: 100%; + } +} diff --git a/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx b/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx new file mode 100644 index 0000000000..55395542e3 --- /dev/null +++ b/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx @@ -0,0 +1,226 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import preview from '../../../../../.storybook/preview'; +import { ToggleButton } from './ToggleButton'; +import { ToggleButtonGroup } from '../ToggleButtonGroup/ToggleButtonGroup'; +import { Flex } from '../Flex'; +import { Text } from '../Text'; +import { useState } from 'react'; +import { + RiCheckLine, + RiHeartLine, + RiStarFill, + RiStarLine, + RiCloudLine, + RiArrowRightSLine, +} from '@remixicon/react'; + +const meta = preview.meta({ + title: 'Backstage UI/ToggleButton', + component: ToggleButton, + argTypes: { + size: { + control: 'select', + options: ['small', 'medium'], + }, + variant: { + control: 'select', + options: ['primary', 'secondary'], + }, + }, +}); + +export const Default = meta.story({ + args: { + children: 'Toggle', + }, +}); + +export const Variants = meta.story({ + args: { + children: 'Toggle', + }, + parameters: { + argTypes: { + variant: { + control: false, + }, + }, + }, + render: () => ( + + Primary + Secondary + + ), +}); + +export const Sizes = meta.story({ + args: { + children: 'Toggle', + }, + parameters: { + argTypes: { + size: { + control: false, + }, + }, + }, + render: () => ( + + Small + Medium + + ), +}); + +export const WithIcons = meta.story({ + args: { + children: 'Favorite', + }, + render: args => ( + + } /> + } defaultSelected /> + } /> + } /> + + ), +}); + +export const IconsOnly = meta.story({ + render: () => ( + + } /> + } + /> + } /> + + ), +}); + +export const IconsAndText = meta.story({ + render: () => ( + + }>Cloud + }>Star + }>Next + + ), +}); + +export const MixedIcons = meta.story({ + render: () => ( + + }> + Cloud + + } /> + }> + Next + + + ), +}); + +export const Disabled = meta.story({ + render: () => ( + + + Primary + + + Secondary + + + Selected + + + ), +}); + +export const Controlled = meta.story({ + render: () => { + const [selected, setSelected] = useState(false); + return ( + + : } + > + {selected ? 'Starred' : 'Not starred'} + + State: {selected ? 'selected' : 'unselected'} + + ); + }, +}); + +export const GroupSingle = meta.story({ + render: () => ( + + Apples + Oranges + Bananas + + ), +}); + +export const GroupMultiple = meta.story({ + render: () => ( + + Dogs + Cats + Rabbits + + ), +}); + +export const GroupWithIcons = meta.story({ + render: () => ( + + }> + Chill + + }> + Focus + + }> + Party + + + ), +}); + +export const VerticalGroup = meta.story({ + render: () => ( + + Low + Medium + High + + ), +}); diff --git a/packages/ui/src/components/ToggleButton/ToggleButton.tsx b/packages/ui/src/components/ToggleButton/ToggleButton.tsx new file mode 100644 index 0000000000..e688b29c96 --- /dev/null +++ b/packages/ui/src/components/ToggleButton/ToggleButton.tsx @@ -0,0 +1,59 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import clsx from 'clsx'; +import { forwardRef, Ref } from 'react'; +import { ToggleButton as AriaToggleButton } from 'react-aria-components'; +import type { ToggleButtonProps } from './types'; +import { useStyles } from '../../hooks/useStyles'; +import { ToggleButtonDefinition } from './definition'; +import styles from './ToggleButton.module.css'; + +/** @public */ +export const ToggleButton = forwardRef( + (props: ToggleButtonProps, ref: Ref) => { + const { classNames, dataAttributes, cleanedProps } = useStyles( + ToggleButtonDefinition, + { + size: 'small', + variant: 'primary', + ...props, + }, + ); + + const { children, className, iconStart, iconEnd, ...rest } = cleanedProps; + + return ( + + + {iconStart} + {children} + {iconEnd} + + + ); + }, +); + +ToggleButton.displayName = 'ToggleButton'; diff --git a/packages/ui/src/components/ToggleButton/definition.ts b/packages/ui/src/components/ToggleButton/definition.ts new file mode 100644 index 0000000000..78e026eed8 --- /dev/null +++ b/packages/ui/src/components/ToggleButton/definition.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ComponentDefinition } from '../../types'; + +/** + * Component definition for ToggleButton + * @public + */ +export const ToggleButtonDefinition = { + classNames: { + root: 'bui-ToggleButton', + content: 'bui-ToggleButtonContent', + }, + dataAttributes: { + size: ['small', 'medium'] as const, + variant: ['primary', 'secondary'] as const, + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/ToggleButton/index.ts b/packages/ui/src/components/ToggleButton/index.ts new file mode 100644 index 0000000000..2d50f072e4 --- /dev/null +++ b/packages/ui/src/components/ToggleButton/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './ToggleButton'; +export * from './types'; +export { ToggleButtonDefinition } from './definition'; diff --git a/packages/ui/src/components/ToggleButton/types.ts b/packages/ui/src/components/ToggleButton/types.ts new file mode 100644 index 0000000000..9bffb72c83 --- /dev/null +++ b/packages/ui/src/components/ToggleButton/types.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Breakpoint } from '../..'; +import type { ReactElement, ReactNode } from 'react'; +import type { ToggleButtonProps as AriaToggleButtonProps } from 'react-aria-components'; + +/** + * Properties for {@link ToggleButton} + * + * @public + */ +export interface ToggleButtonProps extends AriaToggleButtonProps { + size?: 'small' | 'medium' | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + iconStart?: ReactElement; + iconEnd?: ReactElement; + children?: ReactNode; +} diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css new file mode 100644 index 0000000000..173679f64b --- /dev/null +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css @@ -0,0 +1,112 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@layer tokens, base, components, utilities; + +@layer components { + .bui-ToggleButtonGroup { + display: inline-flex; + align-items: center; + flex-wrap: nowrap; + border-radius: var(--bui-radius-2); + overflow: hidden; + box-shadow: inset 0 0 0 1px var(--bui-border); + } + + .bui-ToggleButtonGroup[data-orientation='vertical'] { + flex-direction: column; + align-items: stretch; + width: fit-content; + } + + .bui-ToggleButtonGroup > :global(.bui-ToggleButton) { + border-radius: 0; + margin: 0; + box-shadow: none; + border: 0; + } + + .bui-ToggleButtonGroup :global(.bui-ToggleButton)[data-variant='secondary'] { + box-shadow: none; + } + + /* Horizontal radius rules (default orientation) */ + .bui-ToggleButtonGroup:not([data-orientation='vertical']) + > :global(.bui-ToggleButton) { + border-radius: 0; + } + + .bui-ToggleButtonGroup:not([data-orientation='vertical']) + > :global(.bui-ToggleButton):first-child { + border-radius: var(--bui-radius-2) 0 0 var(--bui-radius-2); + } + + .bui-ToggleButtonGroup:not([data-orientation='vertical']) + > :global(.bui-ToggleButton):last-child { + border-radius: 0 var(--bui-radius-2) var(--bui-radius-2) 0; + } + + .bui-ToggleButtonGroup:not([data-orientation='vertical']) + > :global(.bui-ToggleButton):only-child { + border-radius: var(--bui-radius-2); + } + + /* Horizontal dividers */ + .bui-ToggleButtonGroup:not([data-orientation='vertical']) + :global(.bui-ToggleButton) + + :global(.bui-ToggleButton) { + border-left: 1px solid var(--bui-border); + } + + /* Vertical dividers */ + .bui-ToggleButtonGroup[data-orientation='vertical'] + :global(.bui-ToggleButton) { + width: 100%; + } + + .bui-ToggleButtonGroup[data-orientation='vertical'] + :global(.bui-ToggleButton) + + :global(.bui-ToggleButton) { + border-top: 1px solid var(--bui-border); + } + + /* Vertical radius rules */ + .bui-ToggleButtonGroup[data-orientation='vertical'] + > :global(.bui-ToggleButton) { + border-radius: 0; + } + + .bui-ToggleButtonGroup[data-orientation='vertical'] + > :global(.bui-ToggleButton):first-child { + border-radius: var(--bui-radius-2) var(--bui-radius-2) 0 0; + } + + .bui-ToggleButtonGroup[data-orientation='vertical'] + > :global(.bui-ToggleButton):last-child { + border-radius: 0 0 var(--bui-radius-2) var(--bui-radius-2); + } + + .bui-ToggleButtonGroup[data-orientation='vertical'] + > :global(.bui-ToggleButton):only-child { + border-radius: var(--bui-radius-2); + } + + /* Focus ring on group surface */ + .bui-ToggleButtonGroup:focus-visible { + outline: 2px solid var(--bui-ring); + outline-offset: 2px; + } +} diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx new file mode 100644 index 0000000000..2294069f71 --- /dev/null +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx @@ -0,0 +1,179 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import preview from '../../../../../.storybook/preview'; +import { ToggleButtonGroup } from './ToggleButtonGroup'; +import { ToggleButton } from '../ToggleButton/ToggleButton'; +import { Flex } from '../Flex'; +import { Text } from '../Text'; +import { useState } from 'react'; +import { + RiCloudLine, + RiStarLine, + RiStarFill, + RiArrowRightSLine, +} from '@remixicon/react'; + +const meta = preview.meta({ + title: 'Backstage UI/ToggleButtonGroup', + component: ToggleButtonGroup, + argTypes: { + selectionMode: { + control: 'select', + options: ['single', 'multiple'], + }, + orientation: { + control: 'select', + options: ['horizontal', 'vertical'], + }, + }, +}); + +export const SingleSelection = meta.story({ + args: { + selectionMode: 'single', + defaultSelectedKeys: ['dogs'], + }, + render: args => ( + + Dogs + Cats + Birds + + ), +}); + +export const MultipleSelection = meta.story({ + args: { + selectionMode: 'multiple', + defaultSelectedKeys: ['frontend'], + }, + render: args => ( + + Frontend + Backend + Platform + + ), +}); + +export const DisabledGroup = meta.story({ + args: { + selectionMode: 'single', + isDisabled: true, + }, + render: args => ( + + Cat + Dog + Bird + + ), +}); + +export const DisallowEmptySelection = meta.story({ + args: { + selectionMode: 'single', + disallowEmptySelection: true, + defaultSelectedKeys: ['one'], + }, + render: args => ( + + One + Two + Three + + ), +}); + +export const MixedDisabled = meta.story({ + render: () => ( + + One + + Two + + Three + + ), +}); + +export const Orientation = meta.story({ + args: { + orientation: 'vertical', + }, + render: args => ( + + Morning + Afternoon + Evening + + ), +}); + +export const ControlledGroup = meta.story({ + render: () => { + const [selectedKeys, setSelectedKeys] = useState(new Set(['beta'])); + + return ( + + setSelectedKeys(new Set(keys))} + > + Alpha + Beta + Gamma + + Selected: {[...selectedKeys].join(', ') || 'none'} + + ); + }, +}); + +export const WithIcons = meta.story({ + args: { + selectionMode: 'single', + }, + + render: () => ( + + } /> + } + /> + }> + Star + + }> + Next + + + ), +}); + +export const IconsOnly = meta.story({ + render: () => ( + + } /> + } /> + } /> + + ), +}); diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx new file mode 100644 index 0000000000..26b4eb51c8 --- /dev/null +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx @@ -0,0 +1,51 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import clsx from 'clsx'; +import { forwardRef, Ref } from 'react'; +import { ToggleButtonGroup as AriaToggleButtonGroup } from 'react-aria-components'; +import type { ToggleButtonGroupProps } from './types'; +import { useStyles } from '../../hooks/useStyles'; +import { ToggleButtonGroupDefinition } from './definition'; +import styles from './ToggleButtonGroup.module.css'; + +/** @public */ +export const ToggleButtonGroup = forwardRef( + (props: ToggleButtonGroupProps, ref: Ref) => { + const { classNames, dataAttributes, cleanedProps } = useStyles( + ToggleButtonGroupDefinition, + { + orientation: 'horizontal', + ...props, + }, + ); + + const { className, children, ...rest } = cleanedProps; + + return ( + + {children} + + ); + }, +); + +ToggleButtonGroup.displayName = 'ToggleButtonGroup'; diff --git a/packages/ui/src/components/ToggleButtonGroup/definition.ts b/packages/ui/src/components/ToggleButtonGroup/definition.ts new file mode 100644 index 0000000000..7acf6828f0 --- /dev/null +++ b/packages/ui/src/components/ToggleButtonGroup/definition.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ComponentDefinition } from '../../types'; + +/** + * Component definition for ToggleButtonGroup + * @public + */ +export const ToggleButtonGroupDefinition = { + classNames: { + root: 'bui-ToggleButtonGroup', + }, + dataAttributes: { + orientation: ['horizontal', 'vertical'] as const, + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/ToggleButtonGroup/index.ts b/packages/ui/src/components/ToggleButtonGroup/index.ts new file mode 100644 index 0000000000..63893ec3f4 --- /dev/null +++ b/packages/ui/src/components/ToggleButtonGroup/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './ToggleButtonGroup'; +export * from './types'; +export { ToggleButtonGroupDefinition } from './definition'; diff --git a/packages/ui/src/components/ToggleButtonGroup/types.ts b/packages/ui/src/components/ToggleButtonGroup/types.ts new file mode 100644 index 0000000000..d0de983f66 --- /dev/null +++ b/packages/ui/src/components/ToggleButtonGroup/types.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Breakpoint } from '../..'; +import type { ToggleButtonGroupProps as AriaToggleButtonGroupProps } from 'react-aria-components'; + +/** @public */ +export interface ToggleButtonGroupProps + extends AriaToggleButtonGroupProps { + orientation?: + | 'horizontal' + | 'vertical' + | Partial>; +} diff --git a/packages/ui/src/definitions.ts b/packages/ui/src/definitions.ts index a0dc412697..6c470e4d37 100644 --- a/packages/ui/src/definitions.ts +++ b/packages/ui/src/definitions.ts @@ -47,6 +47,8 @@ export { SearchFieldDefinition } from './components/SearchField/definition'; export { SelectDefinition } from './components/Select/definition'; export { SkeletonDefinition } from './components/Skeleton/definition'; export { SwitchDefinition } from './components/Switch/definition'; +export { ToggleButtonDefinition } from './components/ToggleButton/definition'; +export { ToggleButtonGroupDefinition } from './components/ToggleButtonGroup/definition'; export { TableDefinition } from './components/Table/definition'; export { TablePaginationDefinition } from './components/TablePagination/definition'; export { TabsDefinition } from './components/Tabs/definition'; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 03ae3245e3..1c8529f1e6 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -52,6 +52,8 @@ export * from './components/Link'; export * from './components/Select'; export * from './components/Skeleton'; export * from './components/Switch'; +export * from './components/ToggleButton'; +export * from './components/ToggleButtonGroup'; export * from './components/VisuallyHidden'; // Types From ea0c6d809dc71ad9618ecc6bb5b4c113e600007d Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Wed, 31 Dec 2025 14:15:49 +0100 Subject: [PATCH 02/13] chore: changest Signed-off-by: Gabriel Dugny --- .changeset/rude-trains-marry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rude-trains-marry.md diff --git a/.changeset/rude-trains-marry.md b/.changeset/rude-trains-marry.md new file mode 100644 index 0000000000..e4c41351c5 --- /dev/null +++ b/.changeset/rude-trains-marry.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': minor +--- + +feat: Introduce new `ToggleButton` & `ToggleButtonGroup` components From 28616ca57ac6bc58de29a9785187c694c4d09a86 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Wed, 31 Dec 2025 14:38:57 +0100 Subject: [PATCH 03/13] chore: prettier Signed-off-by: Gabriel Dugny --- docs-ui/src/content/toggle-button.mdx | 1 - 1 file changed, 1 deletion(-) diff --git a/docs-ui/src/content/toggle-button.mdx b/docs-ui/src/content/toggle-button.mdx index 2ec644cdd5..2a7e8f314f 100644 --- a/docs-ui/src/content/toggle-button.mdx +++ b/docs-ui/src/content/toggle-button.mdx @@ -91,4 +91,3 @@ import { ToggleButtonDefinition } from '../utils/definitions'; - From 7e2649e2dffc82fcdd86b1ce7d6aca006e45b77c Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Wed, 31 Dec 2025 16:46:35 +0100 Subject: [PATCH 04/13] fix: Support Responsive Signed-off-by: Gabriel Dugny --- .../ToggleButtonGroup.stories.tsx | 32 +++++++++++++++++-- .../ToggleButtonGroup/ToggleButtonGroup.tsx | 9 ++++-- .../src/components/ToggleButtonGroup/types.ts | 11 +++---- packages/ui/src/hooks/useStyles.ts | 9 ++++-- 4 files changed, 47 insertions(+), 14 deletions(-) diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx index 2294069f71..69e48d5789 100644 --- a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx @@ -20,6 +20,8 @@ import { ToggleButton } from '../ToggleButton/ToggleButton'; import { Flex } from '../Flex'; import { Text } from '../Text'; import { useState } from 'react'; +import type { Selection } from 'react-aria-components'; +import { useBreakpoint } from '../../hooks/useBreakpoint'; import { RiCloudLine, RiStarLine, @@ -124,16 +126,42 @@ export const Orientation = meta.story({ ), }); +export const ResponsiveOrientation = meta.story({ + render: () => { + const { breakpoint } = useBreakpoint(); // For testing purposes only (display breakpoint) + + return ( + + + Breakpoint: {breakpoint} (orientation: vertical on initial/sm, + horizontal from md+) + + + Morning + Afternoon + Evening + + + ); + }, +}); + export const ControlledGroup = meta.story({ render: () => { - const [selectedKeys, setSelectedKeys] = useState(new Set(['beta'])); + const [selectedKeys, setSelectedKeys] = useState( + new Set(['beta']), + ); return ( setSelectedKeys(new Set(keys))} + onSelectionChange={setSelectedKeys} > Alpha Beta diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx index 26b4eb51c8..cec9bd90db 100644 --- a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx @@ -18,27 +18,30 @@ import clsx from 'clsx'; import { forwardRef, Ref } from 'react'; import { ToggleButtonGroup as AriaToggleButtonGroup } from 'react-aria-components'; import type { ToggleButtonGroupProps } from './types'; -import { useStyles } from '../../hooks/useStyles'; +import { resolveResponsiveValue, useStyles } from '../../hooks/useStyles'; import { ToggleButtonGroupDefinition } from './definition'; import styles from './ToggleButtonGroup.module.css'; +import { useBreakpoint } from '../../hooks/useBreakpoint'; /** @public */ export const ToggleButtonGroup = forwardRef( (props: ToggleButtonGroupProps, ref: Ref) => { + const { breakpoint } = useBreakpoint(); const { classNames, dataAttributes, cleanedProps } = useStyles( ToggleButtonGroupDefinition, { - orientation: 'horizontal', + orientation: 'horizontal' as const, ...props, }, ); - const { className, children, ...rest } = cleanedProps; + const { className, children, orientation, ...rest } = cleanedProps; return ( diff --git a/packages/ui/src/components/ToggleButtonGroup/types.ts b/packages/ui/src/components/ToggleButtonGroup/types.ts index d0de983f66..16ca11d482 100644 --- a/packages/ui/src/components/ToggleButtonGroup/types.ts +++ b/packages/ui/src/components/ToggleButtonGroup/types.ts @@ -14,14 +14,13 @@ * limitations under the License. */ -import type { Breakpoint } from '../..'; import type { ToggleButtonGroupProps as AriaToggleButtonGroupProps } from 'react-aria-components'; +import { Responsive } from '../../types'; /** @public */ export interface ToggleButtonGroupProps - extends AriaToggleButtonGroupProps { - orientation?: - | 'horizontal' - | 'vertical' - | Partial>; + extends Omit { + orientation?: Responsive< + NonNullable + >; } diff --git a/packages/ui/src/hooks/useStyles.ts b/packages/ui/src/hooks/useStyles.ts index cc05f5f362..384b25d913 100644 --- a/packages/ui/src/hooks/useStyles.ts +++ b/packages/ui/src/hooks/useStyles.ts @@ -23,10 +23,13 @@ import { utilityClassMap } from '../utils/utilityClassMap'; * @param breakpoint - The current breakpoint * @returns The resolved value for the current breakpoint */ -function resolveResponsiveValue( - value: string | Record, +export function resolveResponsiveValue( + value: T | Partial> | undefined, breakpoint: string, -): string | undefined { +): T | undefined { + if (value === undefined) { + return undefined; + } if (typeof value === 'string') { return value; } From fa9b11f853c8b450e7f1f166ed7d66af6a2a9611 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Wed, 31 Dec 2025 16:57:12 +0100 Subject: [PATCH 05/13] refactor: try to fit in useStyle Signed-off-by: Gabriel Dugny --- .../ToggleButtonGroup/ToggleButtonGroup.tsx | 14 +++++++++----- packages/ui/src/hooks/useStyles.ts | 18 +++++++++++++++--- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx index cec9bd90db..8e536f4406 100644 --- a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx @@ -18,15 +18,13 @@ import clsx from 'clsx'; import { forwardRef, Ref } from 'react'; import { ToggleButtonGroup as AriaToggleButtonGroup } from 'react-aria-components'; import type { ToggleButtonGroupProps } from './types'; -import { resolveResponsiveValue, useStyles } from '../../hooks/useStyles'; +import { useStyles } from '../../hooks/useStyles'; import { ToggleButtonGroupDefinition } from './definition'; import styles from './ToggleButtonGroup.module.css'; -import { useBreakpoint } from '../../hooks/useBreakpoint'; /** @public */ export const ToggleButtonGroup = forwardRef( (props: ToggleButtonGroupProps, ref: Ref) => { - const { breakpoint } = useBreakpoint(); const { classNames, dataAttributes, cleanedProps } = useStyles( ToggleButtonGroupDefinition, { @@ -35,13 +33,19 @@ export const ToggleButtonGroup = forwardRef( }, ); - const { className, children, orientation, ...rest } = cleanedProps; + const { + className, + children, + orientation: _orientation, + ...rest + } = cleanedProps; + const resolvedOrientation = dataAttributes['data-orientation']; return ( diff --git a/packages/ui/src/hooks/useStyles.ts b/packages/ui/src/hooks/useStyles.ts index 384b25d913..c00623e579 100644 --- a/packages/ui/src/hooks/useStyles.ts +++ b/packages/ui/src/hooks/useStyles.ts @@ -17,13 +17,23 @@ import { useBreakpoint, breakpoints } from './useBreakpoint'; import type { ComponentDefinition } from '../types'; import { utilityClassMap } from '../utils/utilityClassMap'; +type DataAttributeValue = V extends string ? V : string; + +type DataAttributesOf = T extends { + dataAttributes: infer D extends Record; +} + ? { + [K in keyof D as `data-${K & string}`]?: DataAttributeValue; + } + : Record; + /** * Resolve a responsive value based on the current breakpoint * @param value - The responsive value (string or object with breakpoint keys) * @param breakpoint - The current breakpoint * @returns The resolved value for the current breakpoint */ -export function resolveResponsiveValue( +function resolveResponsiveValue( value: T | Partial> | undefined, breakpoint: string, ): T | undefined { @@ -69,7 +79,7 @@ export function useStyles< props: P = {} as P, ): { classNames: T['classNames']; - dataAttributes: Record; + dataAttributes: DataAttributesOf & Record; utilityClasses: string; style: React.CSSProperties; cleanedProps: P; @@ -91,6 +101,7 @@ export function useStyles< const incomingStyle = props.style || {}; // Generate data attributes from component definition + // Keep this writable without running into TS2862 ("generic and can only be indexed for reading") const dataAttributes: Record = {}; for (const key of dataAttributeNames) { const value = props[key]; @@ -197,7 +208,8 @@ export function useStyles< return { classNames, - dataAttributes, + dataAttributes: dataAttributes as DataAttributesOf & + Record, utilityClasses: utilityClassList.join(' '), style: mergedStyle, cleanedProps, From 66434bccd7a92cd2704cbf7563c1e33b7bc78fb5 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Wed, 31 Dec 2025 18:09:49 +0100 Subject: [PATCH 06/13] chore: api report Signed-off-by: Gabriel Dugny --- packages/ui/report.api.md | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 35edc52d09..bc339b4026 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -51,6 +51,8 @@ import type { TagGroupProps as TagGroupProps_2 } from 'react-aria-components'; import type { TagListProps } from 'react-aria-components'; import type { TagProps as TagProps_2 } from 'react-aria-components'; import type { TextFieldProps as TextFieldProps_2 } from 'react-aria-components'; +import type { ToggleButtonGroupProps as ToggleButtonGroupProps_2 } from 'react-aria-components'; +import type { ToggleButtonProps as ToggleButtonProps_2 } from 'react-aria-components'; import { TooltipProps as TooltipProps_2 } from 'react-aria-components'; import { TooltipTriggerComponentProps } from 'react-aria-components'; @@ -1814,6 +1816,64 @@ export type TextVariants = // @public (undocumented) export type TextWeights = 'regular' | 'bold'; +// @public (undocumented) +export const ToggleButton: ForwardRefExoticComponent< + ToggleButtonProps & RefAttributes +>; + +// @public +export const ToggleButtonDefinition: { + readonly classNames: { + readonly root: 'bui-ToggleButton'; + readonly content: 'bui-ToggleButtonContent'; + }; + readonly dataAttributes: { + readonly size: readonly ['small', 'medium']; + readonly variant: readonly ['primary', 'secondary']; + }; +}; + +// @public (undocumented) +export const ToggleButtonGroup: ForwardRefExoticComponent< + ToggleButtonGroupProps & RefAttributes +>; + +// @public +export const ToggleButtonGroupDefinition: { + readonly classNames: { + readonly root: 'bui-ToggleButtonGroup'; + }; + readonly dataAttributes: { + readonly orientation: readonly ['horizontal', 'vertical']; + }; +}; + +// @public (undocumented) +export interface ToggleButtonGroupProps + extends Omit { + // (undocumented) + orientation?: Responsive< + NonNullable + >; +} + +// @public +export interface ToggleButtonProps extends ToggleButtonProps_2 { + // (undocumented) + children?: ReactNode; + // (undocumented) + iconEnd?: ReactElement; + // (undocumented) + iconStart?: ReactElement; + // (undocumented) + size?: 'small' | 'medium' | Partial>; + // (undocumented) + variant?: + | 'primary' + | 'secondary' + | Partial>; +} + // @public (undocumented) export const Tooltip: ForwardRefExoticComponent< TooltipProps & RefAttributes From 7c1f6cc6339ed40494d73bb9a81ad157fa93e8c0 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Thu, 8 Jan 2026 12:05:40 +0100 Subject: [PATCH 07/13] feat: support surfaces Signed-off-by: Gabriel Dugny --- docs-ui/src/content/toggle-button.props.ts | 6 ++ packages/ui/report.api.md | 1 + .../ToggleButton/ToggleButton.module.css | 64 +++++++++++++++---- .../components/ToggleButton/ToggleButton.tsx | 7 +- .../ui/src/components/ToggleButton/types.ts | 3 + 5 files changed, 69 insertions(+), 12 deletions(-) diff --git a/docs-ui/src/content/toggle-button.props.ts b/docs-ui/src/content/toggle-button.props.ts index 71625e7e6f..31acd558c7 100644 --- a/docs-ui/src/content/toggle-button.props.ts +++ b/docs-ui/src/content/toggle-button.props.ts @@ -17,6 +17,12 @@ export const toggleButtonPropDefs: Record = { default: 'small', responsive: true, }, + onSurface: { + type: 'enum', + values: ['0', '1', '2', '3', 'danger', 'warning', 'success', 'auto'], + description: 'Surface level this toggle is placed on', + responsive: true, + }, iconStart: { type: 'enum', values: ['ReactNode'] }, iconEnd: { type: 'enum', values: ['ReactNode'] }, isSelected: { type: 'boolean' }, diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index bc339b4026..56b716ea94 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -1865,6 +1865,7 @@ export interface ToggleButtonProps extends ToggleButtonProps_2 { iconEnd?: ReactElement; // (undocumented) iconStart?: ReactElement; + onSurface?: Responsive; // (undocumented) size?: 'small' | 'medium' | Partial>; // (undocumented) diff --git a/packages/ui/src/components/ToggleButton/ToggleButton.module.css b/packages/ui/src/components/ToggleButton/ToggleButton.module.css index ad3b173f04..f11c8bac45 100644 --- a/packages/ui/src/components/ToggleButton/ToggleButton.module.css +++ b/packages/ui/src/components/ToggleButton/ToggleButton.module.css @@ -36,9 +36,57 @@ } .bui-ToggleButton[data-variant='secondary'] { - background: transparent; + background-color: var(--bui-bg-neutral-on-surface-0); color: var(--bui-fg-primary); - box-shadow: inset 0 0 0 1px var(--bui-border); + box-shadow: none; + + &[data-hovered]:not([data-disabled]) { + background-color: var(--bui-bg-neutral-on-surface-0-hover); + } + + &[data-pressed]:not([data-disabled]), + &[data-selected]:not([data-disabled]) { + background-color: var(--bui-bg-neutral-on-surface-0-pressed); + } + + &[data-on-surface='1'] { + background-color: var(--bui-bg-neutral-on-surface-1); + + &[data-hovered]:not([data-disabled]) { + background-color: var(--bui-bg-neutral-on-surface-1-hover); + } + + &[data-pressed]:not([data-disabled]), + &[data-selected]:not([data-disabled]) { + background-color: var(--bui-bg-neutral-on-surface-1-pressed); + } + } + + &[data-on-surface='2'] { + background-color: var(--bui-bg-neutral-on-surface-2); + + &[data-hovered]:not([data-disabled]) { + background-color: var(--bui-bg-neutral-on-surface-2-hover); + } + + &[data-pressed]:not([data-disabled]), + &[data-selected]:not([data-disabled]) { + background-color: var(--bui-bg-neutral-on-surface-2-pressed); + } + } + + &[data-on-surface='3'] { + background-color: var(--bui-bg-neutral-on-surface-3); + + &[data-hovered]:not([data-disabled]) { + background-color: var(--bui-bg-neutral-on-surface-3-hover); + } + + &[data-pressed]:not([data-disabled]), + &[data-selected]:not([data-disabled]) { + background-color: var(--bui-bg-neutral-on-surface-3-pressed); + } + } } .bui-ToggleButton[data-variant='primary'][data-selected], @@ -51,21 +99,15 @@ background: var(--bui-bg-surface-2); } - .bui-ToggleButton[data-variant='secondary'][data-selected], - .bui-ToggleButton[data-variant='secondary'][data-pressed] { - background: var(--bui-bg-surface-2); - box-shadow: inset 0 0 0 1px var(--bui-border-hover); - } - .bui-ToggleButton[data-focus-visible] { - outline: 2px solid var(--bui-ring); - outline-offset: 2px; + outline: none; + box-shadow: inset 0 0 0 2px var(--bui-ring); } .bui-ToggleButton[data-disabled] { cursor: not-allowed; color: var(--bui-fg-disabled); - background: var(--bui-bg-disabled); + background: var(--bui-bg-neutral-on-surface-0-disabled); box-shadow: inset 0 0 0 1px var(--bui-border-disabled); } diff --git a/packages/ui/src/components/ToggleButton/ToggleButton.tsx b/packages/ui/src/components/ToggleButton/ToggleButton.tsx index e688b29c96..211ff03b6d 100644 --- a/packages/ui/src/components/ToggleButton/ToggleButton.tsx +++ b/packages/ui/src/components/ToggleButton/ToggleButton.tsx @@ -21,6 +21,7 @@ import type { ToggleButtonProps } from './types'; import { useStyles } from '../../hooks/useStyles'; import { ToggleButtonDefinition } from './definition'; import styles from './ToggleButton.module.css'; +import { useSurface } from '../../hooks/useSurface'; /** @public */ export const ToggleButton = forwardRef( @@ -34,13 +35,17 @@ export const ToggleButton = forwardRef( }, ); - const { children, className, iconStart, iconEnd, ...rest } = cleanedProps; + const { children, className, iconStart, iconEnd, onSurface, ...rest } = + cleanedProps; + + const { surface } = useSurface({ onSurface }); return ( ; } From 88f35891174e99982b60de651b1cee6d29e76c62 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Thu, 8 Jan 2026 12:09:23 +0100 Subject: [PATCH 08/13] docs: links Signed-off-by: Gabriel Dugny --- docs-ui/src/content/toggle-button-group.mdx | 2 +- docs-ui/src/content/toggle-button.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs-ui/src/content/toggle-button-group.mdx b/docs-ui/src/content/toggle-button-group.mdx index fa979d65fe..de5ac6fcee 100644 --- a/docs-ui/src/content/toggle-button-group.mdx +++ b/docs-ui/src/content/toggle-button-group.mdx @@ -20,7 +20,7 @@ import { ToggleButtonGroupDefinition } from '../utils/definitions'; Date: Fri, 9 Jan 2026 17:02:30 +0100 Subject: [PATCH 09/13] docs: changeset Signed-off-by: Gabriel Dugny --- .changeset/rude-trains-marry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/rude-trains-marry.md b/.changeset/rude-trains-marry.md index e4c41351c5..0246ff6c9e 100644 --- a/.changeset/rude-trains-marry.md +++ b/.changeset/rude-trains-marry.md @@ -2,4 +2,4 @@ '@backstage/ui': minor --- -feat: Introduce new `ToggleButton` & `ToggleButtonGroup` components +Introduce new `ToggleButton` & `ToggleButtonGroup` components in Backstage UI From 92562d14af217c3de66b70f9ab784e89c85d7551 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Fri, 9 Jan 2026 17:48:58 +0100 Subject: [PATCH 10/13] docs: Add surface examples to the ToggleButton/ToggleButtonGroups Signed-off-by: Gabriel Dugny --- docs-ui/src/content/toggle-button-group.mdx | 11 ++ .../src/content/toggle-button-group.props.ts | 86 +++++++- docs-ui/src/content/toggle-button.props.ts | 39 +++- .../ToggleButton/ToggleButton.stories.tsx | 39 +++- .../ToggleButtonGroup.stories.tsx | 183 ++++++++++++++++++ 5 files changed, 348 insertions(+), 10 deletions(-) diff --git a/docs-ui/src/content/toggle-button-group.mdx b/docs-ui/src/content/toggle-button-group.mdx index de5ac6fcee..cc9110189c 100644 --- a/docs-ui/src/content/toggle-button-group.mdx +++ b/docs-ui/src/content/toggle-button-group.mdx @@ -11,6 +11,7 @@ import { toggleButtonGroupDisallowEmptySnippet, toggleButtonGroupIconsSnippet, toggleButtonGroupIconsOnlySnippet, + toggleButtonGroupVariantsSnippet, } from './toggle-button-group.props'; import { ToggleButtonGroupSnippet } from '@/snippets/stories-snippets'; import { PageTitle } from '@/components/PageTitle'; @@ -40,6 +41,16 @@ import { ToggleButtonGroupDefinition } from '../utils/definitions'; ## Examples +### Variants + +} + code={toggleButtonGroupVariantsSnippet} +/> + ### Single Selection Cats `; -export const toggleButtonGroupSingleSnippet = ` - Apples - Oranges - Bananas +export const toggleButtonGroupSingleSnippet = ` + Dogs + Cats + Birds `; export const toggleButtonGroupMultipleSnippet = ` @@ -73,3 +73,81 @@ export const toggleButtonGroupIconsOnlySnippet = ` `; + +export const toggleButtonGroupVariantsSnippet = ` + + Default + + + Primary 1 + Option 2 + Option 3 + + + Secondary 1 + Option 2 + Option 3 + + + + + On Surface 0 + + + Primary 1 + Option 2 + Option 3 + + + Secondary 1 + Option 2 + Option 3 + + + + + On Surface 1 + + + Primary 1 + Option 2 + Option 3 + + + Secondary 1 + Option 2 + Option 3 + + + + + On Surface 2 + + + Primary 1 + Option 2 + Option 3 + + + Secondary 1 + Option 2 + Option 3 + + + + + On Surface 3 + + + Primary 1 + Option 2 + Option 3 + + + Secondary 1 + Option 2 + Option 3 + + + +`; diff --git a/docs-ui/src/content/toggle-button.props.ts b/docs-ui/src/content/toggle-button.props.ts index 31acd558c7..596b7af71b 100644 --- a/docs-ui/src/content/toggle-button.props.ts +++ b/docs-ui/src/content/toggle-button.props.ts @@ -38,9 +38,42 @@ export const toggleButtonUsageSnippet = `import { ToggleButton } from '@backstag Toggle`; -export const toggleButtonVariantsSnippet = ` - Primary - Secondary +export const toggleButtonVariantsSnippet = ` + + Default + + Primary + Secondary + + + + On Surface 0 + + Primary + Secondary + + + + On Surface 1 + + Primary + Secondary + + + + On Surface 2 + + Primary + Secondary + + + + On Surface 3 + + Primary + Secondary + + `; export const toggleButtonSizesSnippet = ` diff --git a/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx b/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx index 55395542e3..36fa774ab5 100644 --- a/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx +++ b/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx @@ -62,9 +62,42 @@ export const Variants = meta.story({ }, }, render: () => ( - - Primary - Secondary + + + Default + + Primary + Secondary + + + + On Surface 0 + + Primary + Secondary + + + + On Surface 1 + + Primary + Secondary + + + + On Surface 2 + + Primary + Secondary + + + + On Surface 3 + + Primary + Secondary + + ), }); diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx index 69e48d5789..f023757cca 100644 --- a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx @@ -72,6 +72,189 @@ export const MultipleSelection = meta.story({ ), }); +export const Variants = meta.story({ + args: { + selectionMode: 'single', + defaultSelectedKeys: ['option1'], + }, + parameters: { + argTypes: { + selectionMode: { + control: false, + }, + }, + }, + render: () => ( + + + Default + + + + Primary 1 + + + Option 2 + + + Option 3 + + + + + Secondary 1 + + + Option 2 + + + Option 3 + + + + + + On Surface 0 + + + + Primary 1 + + + Option 2 + + + Option 3 + + + + + Secondary 1 + + + Option 2 + + + Option 3 + + + + + + On Surface 1 + + + + Primary 1 + + + Option 2 + + + Option 3 + + + + + Secondary 1 + + + Option 2 + + + Option 3 + + + + + + On Surface 2 + + + + Primary 1 + + + Option 2 + + + Option 3 + + + + + Secondary 1 + + + Option 2 + + + Option 3 + + + + + + On Surface 3 + + + + Primary 1 + + + Option 2 + + + Option 3 + + + + + Secondary 1 + + + Option 2 + + + Option 3 + + + + + + ), +}); + export const DisabledGroup = meta.story({ args: { selectionMode: 'single', From 9b26bad524637264739a4c66b411dc2a6a0d00cd Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Fri, 9 Jan 2026 18:30:54 +0100 Subject: [PATCH 11/13] chore: remove variants for now Signed-off-by: Gabriel Dugny --- docs-ui/src/content/toggle-button-group.mdx | 8 +- .../src/content/toggle-button-group.props.ts | 57 +++----- docs-ui/src/content/toggle-button.mdx | 12 +- docs-ui/src/content/toggle-button.props.ts | 23 +-- .../ToggleButton/ToggleButton.module.css | 77 +++------- .../ToggleButton/ToggleButton.stories.tsx | 32 ++--- .../components/ToggleButton/ToggleButton.tsx | 1 - .../src/components/ToggleButton/definition.ts | 1 - .../ui/src/components/ToggleButton/types.ts | 4 - .../ToggleButtonGroup.module.css | 4 - .../ToggleButtonGroup.stories.tsx | 132 +++--------------- .../ToggleButtonGroup/ToggleButtonGroup.tsx | 40 ++++-- .../src/components/ToggleButtonGroup/types.ts | 1 + 13 files changed, 107 insertions(+), 285 deletions(-) diff --git a/docs-ui/src/content/toggle-button-group.mdx b/docs-ui/src/content/toggle-button-group.mdx index cc9110189c..19034f0ac5 100644 --- a/docs-ui/src/content/toggle-button-group.mdx +++ b/docs-ui/src/content/toggle-button-group.mdx @@ -11,7 +11,7 @@ import { toggleButtonGroupDisallowEmptySnippet, toggleButtonGroupIconsSnippet, toggleButtonGroupIconsOnlySnippet, - toggleButtonGroupVariantsSnippet, + toggleButtonGroupSurfacesSnippet, } from './toggle-button-group.props'; import { ToggleButtonGroupSnippet } from '@/snippets/stories-snippets'; import { PageTitle } from '@/components/PageTitle'; @@ -41,14 +41,14 @@ import { ToggleButtonGroupDefinition } from '../utils/definitions'; ## Examples -### Variants +### Surfaces } - code={toggleButtonGroupVariantsSnippet} + preview={} + code={toggleButtonGroupSurfacesSnippet} /> ### Single Selection diff --git a/docs-ui/src/content/toggle-button-group.props.ts b/docs-ui/src/content/toggle-button-group.props.ts index 63fdd2afd3..d1c9c915dc 100644 --- a/docs-ui/src/content/toggle-button-group.props.ts +++ b/docs-ui/src/content/toggle-button-group.props.ts @@ -74,19 +74,14 @@ export const toggleButtonGroupIconsOnlySnippet = ` `; -export const toggleButtonGroupVariantsSnippet = ` +export const toggleButtonGroupSurfacesSnippet = ` Default - Primary 1 - Option 2 - Option 3 - - - Secondary 1 - Option 2 - Option 3 + Option 1 + Option 2 + Option 3 @@ -94,14 +89,9 @@ export const toggleButtonGroupVariantsSnippet = ` - Primary 1 - Option 2 - Option 3 - - - Secondary 1 - Option 2 - Option 3 + Option 1 + Option 2 + Option 3 @@ -109,14 +99,9 @@ export const toggleButtonGroupVariantsSnippet = ` - Primary 1 - Option 2 - Option 3 - - - Secondary 1 - Option 2 - Option 3 + Option 1 + Option 2 + Option 3 @@ -124,14 +109,9 @@ export const toggleButtonGroupVariantsSnippet = ` - Primary 1 - Option 2 - Option 3 - - - Secondary 1 - Option 2 - Option 3 + Option 1 + Option 2 + Option 3 @@ -139,14 +119,9 @@ export const toggleButtonGroupVariantsSnippet = ` - Primary 1 - Option 2 - Option 3 - - - Secondary 1 - Option 2 - Option 3 + Option 1 + Option 2 + Option 3 diff --git a/docs-ui/src/content/toggle-button.mdx b/docs-ui/src/content/toggle-button.mdx index 48afdededc..593d97b02a 100644 --- a/docs-ui/src/content/toggle-button.mdx +++ b/docs-ui/src/content/toggle-button.mdx @@ -5,7 +5,7 @@ import { ToggleButtonSnippet } from '@/snippets/stories-snippets'; import { toggleButtonPropDefs, toggleButtonUsageSnippet, - toggleButtonVariantsSnippet, + toggleButtonSurfacesSnippet, toggleButtonSizesSnippet, toggleButtonIconsSnippet, toggleButtonDisabledSnippet, @@ -24,8 +24,8 @@ import { ToggleButtonDefinition } from '../utils/definitions'; } - code={toggleButtonVariantsSnippet} + preview={} + code={toggleButtonUsageSnippet} /> ## Usage @@ -38,14 +38,14 @@ import { ToggleButtonDefinition } from '../utils/definitions'; ## Examples -### Variants +### Surfaces } - code={toggleButtonVariantsSnippet} + preview={} + code={toggleButtonSurfacesSnippet} /> ### Sizes diff --git a/docs-ui/src/content/toggle-button.props.ts b/docs-ui/src/content/toggle-button.props.ts index 596b7af71b..9df1a24791 100644 --- a/docs-ui/src/content/toggle-button.props.ts +++ b/docs-ui/src/content/toggle-button.props.ts @@ -5,12 +5,6 @@ import { } from '@/utils/propDefs'; export const toggleButtonPropDefs: Record = { - variant: { - type: 'enum', - values: ['primary', 'secondary'], - default: 'primary', - responsive: true, - }, size: { type: 'enum', values: ['small', 'medium'], @@ -38,40 +32,35 @@ export const toggleButtonUsageSnippet = `import { ToggleButton } from '@backstag Toggle`; -export const toggleButtonVariantsSnippet = ` +export const toggleButtonSurfacesSnippet = ` Default - Primary - Secondary + Toggle On Surface 0 - Primary - Secondary + Toggle On Surface 1 - Primary - Secondary + Toggle On Surface 2 - Primary - Secondary + Toggle On Surface 3 - Primary - Secondary + Toggle `; diff --git a/packages/ui/src/components/ToggleButton/ToggleButton.module.css b/packages/ui/src/components/ToggleButton/ToggleButton.module.css index f11c8bac45..e23c4d53eb 100644 --- a/packages/ui/src/components/ToggleButton/ToggleButton.module.css +++ b/packages/ui/src/components/ToggleButton/ToggleButton.module.css @@ -33,72 +33,32 @@ cursor: pointer; transition: background-color 150ms ease, box-shadow 150ms ease, color 150ms ease, transform 100ms ease; - } - .bui-ToggleButton[data-variant='secondary'] { - background-color: var(--bui-bg-neutral-on-surface-0); - color: var(--bui-fg-primary); - box-shadow: none; - - &[data-hovered]:not([data-disabled]) { - background-color: var(--bui-bg-neutral-on-surface-0-hover); + &[data-selected], + &[data-pressed] { + background: var(--bui-bg-solid); + color: var(--bui-fg-solid); } - &[data-pressed]:not([data-disabled]), - &[data-selected]:not([data-disabled]) { - background-color: var(--bui-bg-neutral-on-surface-0-pressed); + &:not([data-selected])[data-hovered] { + background: var(--bui-bg-surface-2); } - &[data-on-surface='1'] { - background-color: var(--bui-bg-neutral-on-surface-1); - - &[data-hovered]:not([data-disabled]) { - background-color: var(--bui-bg-neutral-on-surface-1-hover); - } - - &[data-pressed]:not([data-disabled]), - &[data-selected]:not([data-disabled]) { - background-color: var(--bui-bg-neutral-on-surface-1-pressed); - } + &[data-disabled] { + background: var(--bui-bg-neutral-on-surface-0-disabled); + color: var(--bui-fg-disabled); } - &[data-on-surface='2'] { - background-color: var(--bui-bg-neutral-on-surface-2); - - &[data-hovered]:not([data-disabled]) { - background-color: var(--bui-bg-neutral-on-surface-2-hover); - } - - &[data-pressed]:not([data-disabled]), - &[data-selected]:not([data-disabled]) { - background-color: var(--bui-bg-neutral-on-surface-2-pressed); - } + &[data-disabled][data-hovered] { + background: var(--bui-bg-neutral-on-surface-0-disabled); } - &[data-on-surface='3'] { - background-color: var(--bui-bg-neutral-on-surface-3); - - &[data-hovered]:not([data-disabled]) { - background-color: var(--bui-bg-neutral-on-surface-3-hover); - } - - &[data-pressed]:not([data-disabled]), - &[data-selected]:not([data-disabled]) { - background-color: var(--bui-bg-neutral-on-surface-3-pressed); - } + &[data-disabled][data-selected] { + background: var(--bui-bg-solid-disabled); + color: var(--bui-fg-disabled); } } - .bui-ToggleButton[data-variant='primary'][data-selected], - .bui-ToggleButton[data-variant='primary'][data-pressed] { - background: var(--bui-bg-solid); - color: var(--bui-fg-solid); - } - - .bui-ToggleButton[data-variant='primary']:not([data-selected])[data-hovered] { - background: var(--bui-bg-surface-2); - } - .bui-ToggleButton[data-focus-visible] { outline: none; box-shadow: inset 0 0 0 2px var(--bui-ring); @@ -106,15 +66,16 @@ .bui-ToggleButton[data-disabled] { cursor: not-allowed; - color: var(--bui-fg-disabled); - background: var(--bui-bg-neutral-on-surface-0-disabled); - box-shadow: inset 0 0 0 1px var(--bui-border-disabled); } - .bui-ToggleButton[data-pressed]:not([data-disabled]) { + .bui-ToggleButton[data-pressed] { transform: scale(0.98); } + .bui-ToggleButton[data-disabled][data-pressed] { + transform: none; + } + .bui-ToggleButton[data-size='small'] { height: 2rem; font-size: var(--bui-font-size-3); diff --git a/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx b/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx index 36fa774ab5..f4be5cb46b 100644 --- a/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx +++ b/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx @@ -37,10 +37,6 @@ const meta = preview.meta({ control: 'select', options: ['small', 'medium'], }, - variant: { - control: 'select', - options: ['primary', 'secondary'], - }, }, }); @@ -50,13 +46,13 @@ export const Default = meta.story({ }, }); -export const Variants = meta.story({ +export const Surfaces = meta.story({ args: { children: 'Toggle', }, parameters: { argTypes: { - variant: { + size: { control: false, }, }, @@ -66,36 +62,31 @@ export const Variants = meta.story({ Default - Primary - Secondary + Toggle On Surface 0 - Primary - Secondary + Toggle On Surface 1 - Primary - Secondary + Toggle On Surface 2 - Primary - Secondary + Toggle On Surface 3 - Primary - Secondary + Toggle @@ -176,13 +167,8 @@ export const MixedIcons = meta.story({ export const Disabled = meta.story({ render: () => ( - - Primary - - - Secondary - - + Disabled + Selected diff --git a/packages/ui/src/components/ToggleButton/ToggleButton.tsx b/packages/ui/src/components/ToggleButton/ToggleButton.tsx index 211ff03b6d..dd11554621 100644 --- a/packages/ui/src/components/ToggleButton/ToggleButton.tsx +++ b/packages/ui/src/components/ToggleButton/ToggleButton.tsx @@ -30,7 +30,6 @@ export const ToggleButton = forwardRef( ToggleButtonDefinition, { size: 'small', - variant: 'primary', ...props, }, ); diff --git a/packages/ui/src/components/ToggleButton/definition.ts b/packages/ui/src/components/ToggleButton/definition.ts index 78e026eed8..98c22fef35 100644 --- a/packages/ui/src/components/ToggleButton/definition.ts +++ b/packages/ui/src/components/ToggleButton/definition.ts @@ -27,6 +27,5 @@ export const ToggleButtonDefinition = { }, dataAttributes: { size: ['small', 'medium'] as const, - variant: ['primary', 'secondary'] as const, }, } as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/ToggleButton/types.ts b/packages/ui/src/components/ToggleButton/types.ts index bf6c08f521..aba31e4f25 100644 --- a/packages/ui/src/components/ToggleButton/types.ts +++ b/packages/ui/src/components/ToggleButton/types.ts @@ -26,10 +26,6 @@ import type { Responsive, Surface } from '../../types'; */ export interface ToggleButtonProps extends AriaToggleButtonProps { size?: 'small' | 'medium' | Partial>; - variant?: - | 'primary' - | 'secondary' - | Partial>; iconStart?: ReactElement; iconEnd?: ReactElement; children?: ReactNode; diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css index 173679f64b..36309ad9dd 100644 --- a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css @@ -39,10 +39,6 @@ border: 0; } - .bui-ToggleButtonGroup :global(.bui-ToggleButton)[data-variant='secondary'] { - box-shadow: none; - } - /* Horizontal radius rules (default orientation) */ .bui-ToggleButtonGroup:not([data-orientation='vertical']) > :global(.bui-ToggleButton) { diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx index f023757cca..70f3810a49 100644 --- a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx @@ -72,7 +72,7 @@ export const MultipleSelection = meta.story({ ), }); -export const Variants = meta.story({ +export const Surfaces = meta.story({ args: { selectionMode: 'single', defaultSelectedKeys: ['option1'], @@ -93,29 +93,9 @@ export const Variants = meta.story({ selectionMode="single" defaultSelectedKeys={['option1']} > - - Primary 1 - - - Option 2 - - - Option 3 - - - - - Secondary 1 - - - Option 2 - - - Option 3 - + Option 1 + Option 2 + Option 3 @@ -126,29 +106,9 @@ export const Variants = meta.story({ selectionMode="single" defaultSelectedKeys={['option1']} > - - Primary 1 - - - Option 2 - - - Option 3 - - - - - Secondary 1 - - - Option 2 - - - Option 3 - + Option 1 + Option 2 + Option 3 @@ -159,29 +119,9 @@ export const Variants = meta.story({ selectionMode="single" defaultSelectedKeys={['option1']} > - - Primary 1 - - - Option 2 - - - Option 3 - - - - - Secondary 1 - - - Option 2 - - - Option 3 - + Option 1 + Option 2 + Option 3 @@ -192,29 +132,9 @@ export const Variants = meta.story({ selectionMode="single" defaultSelectedKeys={['option1']} > - - Primary 1 - - - Option 2 - - - Option 3 - - - - - Secondary 1 - - - Option 2 - - - Option 3 - + Option 1 + Option 2 + Option 3 @@ -225,29 +145,9 @@ export const Variants = meta.story({ selectionMode="single" defaultSelectedKeys={['option1']} > - - Primary 1 - - - Option 2 - - - Option 3 - - - - - Secondary 1 - - - Option 2 - - - Option 3 - + Option 1 + Option 2 + Option 3 diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx index 8e536f4406..908b5881dc 100644 --- a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx @@ -15,12 +15,28 @@ */ import clsx from 'clsx'; -import { forwardRef, Ref } from 'react'; +import { forwardRef, Ref, createContext, useContext } from 'react'; import { ToggleButtonGroup as AriaToggleButtonGroup } from 'react-aria-components'; import type { ToggleButtonGroupProps } from './types'; import { useStyles } from '../../hooks/useStyles'; import { ToggleButtonGroupDefinition } from './definition'; import styles from './ToggleButtonGroup.module.css'; +import type { Breakpoint } from '../..'; + +/** @internal */ +export interface ToggleButtonGroupContextValue {} + +const ToggleButtonGroupContext = createContext< + ToggleButtonGroupContextValue | undefined +>(undefined); + +/** + * Hook to access the ToggleButtonGroup context. + * @internal + */ +export const useToggleButtonGroupContext = () => { + return useContext(ToggleButtonGroupContext); +}; /** @public */ export const ToggleButtonGroup = forwardRef( @@ -41,16 +57,20 @@ export const ToggleButtonGroup = forwardRef( } = cleanedProps; const resolvedOrientation = dataAttributes['data-orientation']; + const contextValue: ToggleButtonGroupContextValue = {}; + return ( - - {children} - + + + {children} + + ); }, ); diff --git a/packages/ui/src/components/ToggleButtonGroup/types.ts b/packages/ui/src/components/ToggleButtonGroup/types.ts index 16ca11d482..e01b9afda0 100644 --- a/packages/ui/src/components/ToggleButtonGroup/types.ts +++ b/packages/ui/src/components/ToggleButtonGroup/types.ts @@ -16,6 +16,7 @@ import type { ToggleButtonGroupProps as AriaToggleButtonGroupProps } from 'react-aria-components'; import { Responsive } from '../../types'; +import type { Breakpoint } from '../..'; /** @public */ export interface ToggleButtonGroupProps From 44530400dc27f2b48ead22de0941556e6954f2f7 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Sat, 10 Jan 2026 13:15:26 +0100 Subject: [PATCH 12/13] chore: remove Responsive orientation Signed-off-by: Gabriel Dugny --- packages/ui/report.api.md | 10 +------- .../ToggleButtonGroup.stories.tsx | 25 ------------------- .../ToggleButtonGroup/ToggleButtonGroup.tsx | 11 +------- .../src/components/ToggleButtonGroup/types.ts | 6 +---- packages/ui/src/hooks/useStyles.ts | 25 ++++--------------- 5 files changed, 8 insertions(+), 69 deletions(-) diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 56b716ea94..afd08072ee 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -1829,7 +1829,6 @@ export const ToggleButtonDefinition: { }; readonly dataAttributes: { readonly size: readonly ['small', 'medium']; - readonly variant: readonly ['primary', 'secondary']; }; }; @@ -1852,9 +1851,7 @@ export const ToggleButtonGroupDefinition: { export interface ToggleButtonGroupProps extends Omit { // (undocumented) - orientation?: Responsive< - NonNullable - >; + orientation?: NonNullable; } // @public @@ -1868,11 +1865,6 @@ export interface ToggleButtonProps extends ToggleButtonProps_2 { onSurface?: Responsive; // (undocumented) size?: 'small' | 'medium' | Partial>; - // (undocumented) - variant?: - | 'primary' - | 'secondary' - | Partial>; } // @public (undocumented) diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx index 70f3810a49..30e53f0669 100644 --- a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx @@ -21,7 +21,6 @@ import { Flex } from '../Flex'; import { Text } from '../Text'; import { useState } from 'react'; import type { Selection } from 'react-aria-components'; -import { useBreakpoint } from '../../hooks/useBreakpoint'; import { RiCloudLine, RiStarLine, @@ -209,30 +208,6 @@ export const Orientation = meta.story({ ), }); -export const ResponsiveOrientation = meta.story({ - render: () => { - const { breakpoint } = useBreakpoint(); // For testing purposes only (display breakpoint) - - return ( - - - Breakpoint: {breakpoint} (orientation: vertical on initial/sm, - horizontal from md+) - - - Morning - Afternoon - Evening - - - ); - }, -}); - export const ControlledGroup = meta.story({ render: () => { const [selectedKeys, setSelectedKeys] = useState( diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx index 908b5881dc..6934b42eb6 100644 --- a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx @@ -21,7 +21,6 @@ import type { ToggleButtonGroupProps } from './types'; import { useStyles } from '../../hooks/useStyles'; import { ToggleButtonGroupDefinition } from './definition'; import styles from './ToggleButtonGroup.module.css'; -import type { Breakpoint } from '../..'; /** @internal */ export interface ToggleButtonGroupContextValue {} @@ -44,18 +43,11 @@ export const ToggleButtonGroup = forwardRef( const { classNames, dataAttributes, cleanedProps } = useStyles( ToggleButtonGroupDefinition, { - orientation: 'horizontal' as const, ...props, }, ); - const { - className, - children, - orientation: _orientation, - ...rest - } = cleanedProps; - const resolvedOrientation = dataAttributes['data-orientation']; + const { className, children, ...rest } = cleanedProps; const contextValue: ToggleButtonGroupContextValue = {}; @@ -64,7 +56,6 @@ export const ToggleButtonGroup = forwardRef( diff --git a/packages/ui/src/components/ToggleButtonGroup/types.ts b/packages/ui/src/components/ToggleButtonGroup/types.ts index e01b9afda0..7a57708750 100644 --- a/packages/ui/src/components/ToggleButtonGroup/types.ts +++ b/packages/ui/src/components/ToggleButtonGroup/types.ts @@ -15,13 +15,9 @@ */ import type { ToggleButtonGroupProps as AriaToggleButtonGroupProps } from 'react-aria-components'; -import { Responsive } from '../../types'; -import type { Breakpoint } from '../..'; /** @public */ export interface ToggleButtonGroupProps extends Omit { - orientation?: Responsive< - NonNullable - >; + orientation?: NonNullable; } diff --git a/packages/ui/src/hooks/useStyles.ts b/packages/ui/src/hooks/useStyles.ts index c00623e579..cc05f5f362 100644 --- a/packages/ui/src/hooks/useStyles.ts +++ b/packages/ui/src/hooks/useStyles.ts @@ -17,29 +17,16 @@ import { useBreakpoint, breakpoints } from './useBreakpoint'; import type { ComponentDefinition } from '../types'; import { utilityClassMap } from '../utils/utilityClassMap'; -type DataAttributeValue = V extends string ? V : string; - -type DataAttributesOf = T extends { - dataAttributes: infer D extends Record; -} - ? { - [K in keyof D as `data-${K & string}`]?: DataAttributeValue; - } - : Record; - /** * Resolve a responsive value based on the current breakpoint * @param value - The responsive value (string or object with breakpoint keys) * @param breakpoint - The current breakpoint * @returns The resolved value for the current breakpoint */ -function resolveResponsiveValue( - value: T | Partial> | undefined, +function resolveResponsiveValue( + value: string | Record, breakpoint: string, -): T | undefined { - if (value === undefined) { - return undefined; - } +): string | undefined { if (typeof value === 'string') { return value; } @@ -79,7 +66,7 @@ export function useStyles< props: P = {} as P, ): { classNames: T['classNames']; - dataAttributes: DataAttributesOf & Record; + dataAttributes: Record; utilityClasses: string; style: React.CSSProperties; cleanedProps: P; @@ -101,7 +88,6 @@ export function useStyles< const incomingStyle = props.style || {}; // Generate data attributes from component definition - // Keep this writable without running into TS2862 ("generic and can only be indexed for reading") const dataAttributes: Record = {}; for (const key of dataAttributeNames) { const value = props[key]; @@ -208,8 +194,7 @@ export function useStyles< return { classNames, - dataAttributes: dataAttributes as DataAttributesOf & - Record, + dataAttributes, utilityClasses: utilityClassList.join(' '), style: mergedStyle, cleanedProps, From 6921b2c824ce47ea4f5a3115617ffe42ab5c809c Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Sat, 10 Jan 2026 14:05:22 +0100 Subject: [PATCH 13/13] chore: support render function, document it, remove now unused POC context Signed-off-by: Gabriel Dugny --- .../src/content/toggle-button-group.props.ts | 36 ++-- docs-ui/src/content/toggle-button.mdx | 13 ++ docs-ui/src/content/toggle-button.props.ts | 122 +++++++++++- packages/ui/report.api.md | 2 - .../ToggleButton/ToggleButton.stories.tsx | 180 ++++++++++++------ .../components/ToggleButton/ToggleButton.tsx | 24 ++- .../ui/src/components/ToggleButton/types.ts | 3 +- .../ToggleButtonGroup.module.css | 1 + .../ToggleButtonGroup/ToggleButtonGroup.tsx | 37 +--- 9 files changed, 300 insertions(+), 118 deletions(-) diff --git a/docs-ui/src/content/toggle-button-group.props.ts b/docs-ui/src/content/toggle-button-group.props.ts index d1c9c915dc..b8f12941de 100644 --- a/docs-ui/src/content/toggle-button-group.props.ts +++ b/docs-ui/src/content/toggle-button-group.props.ts @@ -30,6 +30,7 @@ export const toggleButtonGroupUsageSnippet = `import { ToggleButtonGroup, Toggle Dogs Cats + Birds `; export const toggleButtonGroupSingleSnippet = ` @@ -45,9 +46,9 @@ export const toggleButtonGroupMultipleSnippet = ``; export const toggleButtonGroupVerticalSnippet = ` - Low - Medium - High + Morning + Afternoon + Evening `; export const toggleButtonGroupDisabledSnippet = ` @@ -62,16 +63,29 @@ export const toggleButtonGroupDisallowEmptySnippet = `Three `; -export const toggleButtonGroupIconsSnippet = ` - Cloud - Starred - Next +export const toggleButtonGroupIconsSnippet = `import { RiCloudLine, RiStarFill, RiStarLine, RiArrowRightSLine } from '@remixicon/react'; + + + } /> + } + /> + }> + Star + + }> + Next + `; -export const toggleButtonGroupIconsOnlySnippet = ` - - - +export const toggleButtonGroupIconsOnlySnippet = `import { RiCloudLine, RiStarLine, RiArrowRightSLine } from '@remixicon/react'; + + + } /> + } /> + } /> `; export const toggleButtonGroupSurfacesSnippet = ` diff --git a/docs-ui/src/content/toggle-button.mdx b/docs-ui/src/content/toggle-button.mdx index 593d97b02a..78750ec365 100644 --- a/docs-ui/src/content/toggle-button.mdx +++ b/docs-ui/src/content/toggle-button.mdx @@ -10,6 +10,7 @@ import { toggleButtonIconsSnippet, toggleButtonDisabledSnippet, toggleButtonControlledSnippet, + toggleButtonFunctionChildrenSnippet, } from './toggle-button.props'; import { PageTitle } from '@/components/PageTitle'; import { Theming } from '@/components/Theming'; @@ -88,6 +89,18 @@ import { ToggleButtonDefinition } from '../utils/definitions'; code={toggleButtonControlledSnippet} /> +### Dynamic Content with Function Children + +The `children` prop can be a function that receives render props, allowing you to dynamically customize the button content based on component state (such as `isSelected`, `isDisabled`, `isHovered`, etc.). + +} + code={toggleButtonFunctionChildrenSnippet} +/> + diff --git a/docs-ui/src/content/toggle-button.props.ts b/docs-ui/src/content/toggle-button.props.ts index 9df1a24791..9115b29603 100644 --- a/docs-ui/src/content/toggle-button.props.ts +++ b/docs-ui/src/content/toggle-button.props.ts @@ -23,7 +23,12 @@ export const toggleButtonPropDefs: Record = { defaultSelected: { type: 'boolean' }, onChange: { type: 'enum', values: ['(isSelected: boolean) => void'] }, isDisabled: { type: 'boolean', default: 'false' }, - children: { type: 'enum', values: ['ReactNode'] }, + children: { + type: 'enum', + values: ['ReactNode', '(values: ToggleButtonRenderProps) => ReactNode'], + description: + 'The children of the component. A function may be provided to alter the children based on component state (such as `isSelected`, `isDisabled`, `isHovered`, etc.).', + }, ...classNamePropDefs, ...stylePropDefs, }; @@ -70,9 +75,12 @@ export const toggleButtonSizesSnippet = ` Medium `; -export const toggleButtonIconsSnippet = ` - Favorite - Confirm +export const toggleButtonIconsSnippet = `import { RiStarLine, RiStarFill, RiCheckLine } from '@remixicon/react'; + + + }>Favorite + } defaultSelected>Starred + }>Confirm `; export const toggleButtonDisabledSnippet = ` @@ -80,8 +88,108 @@ export const toggleButtonDisabledSnippet = ` Selected `; -export const toggleButtonControlledSnippet = `const [selected, setSelected] = useState(false); +export const toggleButtonControlledSnippet = `import { useState } from 'react'; +import { RiStarFill, RiStarLine } from '@remixicon/react'; - - {selected ? 'On' : 'Off'} +const [selected, setSelected] = useState(false); + + : } +> + {selected ? 'Starred' : 'Not starred'} `; + +export const toggleButtonFunctionChildrenSnippet = `import { RiStarFill, RiStarLine } from '@remixicon/react'; + + + Example 1: Selection State + + + {({ isSelected }) => (isSelected ? '✓ Selected' : 'Not Selected')} + + + {({ isSelected }) => (isSelected ? '✓ Selected' : 'Not Selected')} + + + + + + Example 2: Multiple States + + + {({ isSelected, isHovered }) => { + const states = []; + if (isSelected) states.push('on'); + else states.push('off'); + if (isHovered) states.push('hovered'); + return \`Email (\${states.join(', ')})\`; + }} + + + {({ isSelected, isHovered }) => { + const states = []; + if (isSelected) states.push('on'); + else states.push('off'); + if (isHovered) states.push('hovered'); + return \`Push (\${states.join(', ')})\`; + }} + + + + + + Example 3: Conditional Icons + + + {({ isSelected }) => ( + <> + {isSelected ? : } + {isSelected ? 'Starred' : 'Star'} + + )} + + + + + + Example 4: Status Indicators + + + {({ isSelected }) => ( + + + Active + + )} + + + {({ isSelected }) => ( + + + Inactive + + )} + + + +`; diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index afd08072ee..b7b6604421 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -1856,8 +1856,6 @@ export interface ToggleButtonGroupProps // @public export interface ToggleButtonProps extends ToggleButtonProps_2 { - // (undocumented) - children?: ReactNode; // (undocumented) iconEnd?: ReactElement; // (undocumented) diff --git a/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx b/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx index f4be5cb46b..14183e6ede 100644 --- a/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx +++ b/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx @@ -16,13 +16,11 @@ import preview from '../../../../../.storybook/preview'; import { ToggleButton } from './ToggleButton'; -import { ToggleButtonGroup } from '../ToggleButtonGroup/ToggleButtonGroup'; import { Flex } from '../Flex'; import { Text } from '../Text'; import { useState } from 'react'; import { RiCheckLine, - RiHeartLine, RiStarFill, RiStarLine, RiCloudLine, @@ -150,20 +148,6 @@ export const IconsAndText = meta.story({ ), }); -export const MixedIcons = meta.story({ - render: () => ( - - }> - Cloud - - } /> - }> - Next - - - ), -}); - export const Disabled = meta.story({ render: () => ( @@ -194,52 +178,128 @@ export const Controlled = meta.story({ }, }); -export const GroupSingle = meta.story({ - render: () => ( - - Apples - Oranges - Bananas - - ), +export const FunctionChildren = meta.story({ + render: () => { + const [disabled, setDisabled] = useState(false); + return ( + + + + {({ isDisabled, isSelected }) => + isDisabled + ? `Disabled ${isSelected ? '(Selected)' : '(Unselected)'}` + : `Enabled ${isSelected ? '(Selected)' : '(Unselected)'}` + } + + setDisabled(!disabled)} + > + {disabled ? 'Enable' : 'Disable'} + + + + Toggle the button to change the disabled state and see text update + + + ); + }, }); -export const GroupMultiple = meta.story({ - render: () => ( - - Dogs - Cats - Rabbits - - ), -}); +export const DynamicContent = meta.story({ + render: () => { + return ( + + + Example 1: Selection State + + + {({ isSelected }) => (isSelected ? '✓ Selected' : 'Not Selected')} + + + {({ isSelected }) => (isSelected ? '✓ Selected' : 'Not Selected')} + + + -export const GroupWithIcons = meta.story({ - render: () => ( - - }> - Chill - - }> - Focus - - }> - Party - - - ), -}); + + Example 2: Multiple States + + + {({ isSelected, isHovered }) => { + const states = []; + if (isSelected) states.push('on'); + else states.push('off'); + if (isHovered) states.push('hovered'); + return `Email (${states.join(', ')})`; + }} + + + {({ isSelected, isHovered }) => { + const states = []; + if (isSelected) states.push('on'); + else states.push('off'); + if (isHovered) states.push('hovered'); + return `Push (${states.join(', ')})`; + }} + + + -export const VerticalGroup = meta.story({ - render: () => ( - - Low - Medium - High - - ), + + Example 3: Conditional Icons + + + {({ isSelected }) => ( + <> + {isSelected ? : } + {isSelected ? 'Starred' : 'Star'} + + )} + + + + + + Example 4: Status Indicators + + + {({ isSelected }) => ( + + + Active + + )} + + + {({ isSelected }) => ( + + + Inactive + + )} + + + + + ); + }, }); diff --git a/packages/ui/src/components/ToggleButton/ToggleButton.tsx b/packages/ui/src/components/ToggleButton/ToggleButton.tsx index dd11554621..9c96304a80 100644 --- a/packages/ui/src/components/ToggleButton/ToggleButton.tsx +++ b/packages/ui/src/components/ToggleButton/ToggleButton.tsx @@ -47,14 +47,22 @@ export const ToggleButton = forwardRef( {...(typeof surface === 'string' ? { 'data-on-surface': surface } : {})} {...rest} > - - {iconStart} - {children} - {iconEnd} - + {renderProps => { + // If children is a function, call it with render props; otherwise use children as-is + const renderedChildren = + typeof children === 'function' ? children(renderProps) : children; + + return ( + + {iconStart} + {renderedChildren} + {iconEnd} + + ); + }} ); }, diff --git a/packages/ui/src/components/ToggleButton/types.ts b/packages/ui/src/components/ToggleButton/types.ts index aba31e4f25..de70bd3c29 100644 --- a/packages/ui/src/components/ToggleButton/types.ts +++ b/packages/ui/src/components/ToggleButton/types.ts @@ -15,7 +15,7 @@ */ import type { Breakpoint } from '../..'; -import type { ReactElement, ReactNode } from 'react'; +import type { ReactElement } from 'react'; import type { ToggleButtonProps as AriaToggleButtonProps } from 'react-aria-components'; import type { Responsive, Surface } from '../../types'; @@ -28,7 +28,6 @@ export interface ToggleButtonProps extends AriaToggleButtonProps { size?: 'small' | 'medium' | Partial>; iconStart?: ReactElement; iconEnd?: ReactElement; - children?: ReactNode; /** Surface the toggle button is placed on. Defaults to context surface if available */ onSurface?: Responsive; } diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css index 36309ad9dd..90bedd1756 100644 --- a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css @@ -24,6 +24,7 @@ border-radius: var(--bui-radius-2); overflow: hidden; box-shadow: inset 0 0 0 1px var(--bui-border); + width: fit-content; } .bui-ToggleButtonGroup[data-orientation='vertical'] { diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx index 6934b42eb6..43449f8472 100644 --- a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx @@ -15,28 +15,13 @@ */ import clsx from 'clsx'; -import { forwardRef, Ref, createContext, useContext } from 'react'; +import { forwardRef, Ref } from 'react'; import { ToggleButtonGroup as AriaToggleButtonGroup } from 'react-aria-components'; import type { ToggleButtonGroupProps } from './types'; import { useStyles } from '../../hooks/useStyles'; import { ToggleButtonGroupDefinition } from './definition'; import styles from './ToggleButtonGroup.module.css'; -/** @internal */ -export interface ToggleButtonGroupContextValue {} - -const ToggleButtonGroupContext = createContext< - ToggleButtonGroupContextValue | undefined ->(undefined); - -/** - * Hook to access the ToggleButtonGroup context. - * @internal - */ -export const useToggleButtonGroupContext = () => { - return useContext(ToggleButtonGroupContext); -}; - /** @public */ export const ToggleButtonGroup = forwardRef( (props: ToggleButtonGroupProps, ref: Ref) => { @@ -49,19 +34,15 @@ export const ToggleButtonGroup = forwardRef( const { className, children, ...rest } = cleanedProps; - const contextValue: ToggleButtonGroupContextValue = {}; - return ( - - - {children} - - + + {children} + ); }, );