diff --git a/change/@fluentui-react-headless-components-preview-7df3cee6-6c57-48b6-966e-a855e917444b.json b/change/@fluentui-react-headless-components-preview-7df3cee6-6c57-48b6-966e-a855e917444b.json new file mode 100644 index 0000000000000..dd3abd5ec0e20 --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-7df3cee6-6c57-48b6-966e-a855e917444b.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "feat: add ToolbarRadioButton component", + "packageName": "@fluentui/react-headless-components-preview", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-headless-components-preview/library/etc/toolbar.api.md b/packages/react-components/react-headless-components-preview/library/etc/toolbar.api.md index db6c2162d881c..3ed290735b036 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/toolbar.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/toolbar.api.md @@ -20,6 +20,8 @@ import type { ToolbarDividerBaseProps } from '@fluentui/react-toolbar'; import type { ToolbarDividerBaseState } from '@fluentui/react-toolbar'; import type { ToolbarGroupProps as ToolbarGroupProps_2 } from '@fluentui/react-toolbar'; import { ToolbarGroupState as ToolbarGroupState_2 } from '@fluentui/react-toolbar'; +import type { ToolbarRadioButtonBaseProps } from '@fluentui/react-toolbar'; +import type { ToolbarRadioButtonBaseState } from '@fluentui/react-toolbar'; import type { ToolbarRadioGroupProps as ToolbarRadioGroupProps_2 } from '@fluentui/react-toolbar'; import type { ToolbarRadioGroupState as ToolbarRadioGroupState_2 } from '@fluentui/react-toolbar'; import type { ToolbarSlots as ToolbarSlots_2 } from '@fluentui/react-toolbar'; @@ -38,6 +40,9 @@ export const renderToolbarDivider: (state: DividerBaseState) => JSXElement; // @public export const renderToolbarGroup: (state: ToolbarGroupState_2) => JSXElement; +// @public +export const renderToolbarRadioButton: (state: ButtonBaseState) => JSXElement; + // @public export const renderToolbarRadioGroup: (state: ToolbarGroupState_2) => JSXElement; @@ -95,6 +100,22 @@ export type ToolbarGroupState = ToolbarGroupState_2 & { // @public (undocumented) export type ToolbarProps = ToolbarBaseProps; +// @public +export const ToolbarRadioButton: ForwardRefComponent; + +// @public (undocumented) +export type ToolbarRadioButtonProps = ToolbarRadioButtonBaseProps; + +// @public (undocumented) +export type ToolbarRadioButtonState = ToolbarRadioButtonBaseState & { + root: { + 'data-disabled'?: string; + 'data-disabled-focusable'?: string; + 'data-icon-only'?: string; + 'data-checked'?: string; + }; +}; + // @public export const ToolbarRadioGroup: ForwardRefComponent; @@ -154,6 +175,9 @@ export const useToolbarDivider: (props: ToolbarDividerProps, ref: React_2.Ref) => ToolbarGroupState; +// @public +export const useToolbarRadioButton: (props: ToolbarRadioButtonProps, ref: React_2.Ref) => ToolbarRadioButtonState; + // @public export const useToolbarRadioGroup: (props: ToolbarRadioGroupProps, ref: React_2.Ref) => ToolbarRadioGroupState; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioButton/ToolbarRadioButton.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioButton/ToolbarRadioButton.tsx new file mode 100644 index 0000000000000..bd9828105bb82 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioButton/ToolbarRadioButton.tsx @@ -0,0 +1,18 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { ToolbarRadioButtonProps } from './ToolbarRadioButton.types'; +import { useToolbarRadioButton } from './useToolbarRadioButton'; +import { renderToolbarRadioButton } from './renderToolbarRadioButton'; + +/** + * A radio button designed to be used inside a Toolbar with mutually exclusive selection behavior. + */ +export const ToolbarRadioButton: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useToolbarRadioButton(props, ref); + + return renderToolbarRadioButton(state); +}) as ForwardRefComponent; + +ToolbarRadioButton.displayName = 'ToolbarRadioButton'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioButton/ToolbarRadioButton.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioButton/ToolbarRadioButton.types.ts new file mode 100644 index 0000000000000..754ff0f68e227 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioButton/ToolbarRadioButton.types.ts @@ -0,0 +1,27 @@ +import type { ToolbarRadioButtonBaseProps, ToolbarRadioButtonBaseState } from '@fluentui/react-toolbar'; + +export type ToolbarRadioButtonProps = ToolbarRadioButtonBaseProps; + +export type ToolbarRadioButtonState = ToolbarRadioButtonBaseState & { + root: { + /** + * Data attribute set when the button is disabled. + */ + 'data-disabled'?: string; + + /** + * Data attribute set when the button is disabled but still focusable. + */ + 'data-disabled-focusable'?: string; + + /** + * Data attribute set when the button renders only an icon. + */ + 'data-icon-only'?: string; + + /** + * Data attribute set when the button is in a checked (selected) state. + */ + 'data-checked'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioButton/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioButton/index.ts new file mode 100644 index 0000000000000..f7941ffab915b --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioButton/index.ts @@ -0,0 +1,4 @@ +export { ToolbarRadioButton } from './ToolbarRadioButton'; +export { renderToolbarRadioButton } from './renderToolbarRadioButton'; +export { useToolbarRadioButton } from './useToolbarRadioButton'; +export type { ToolbarRadioButtonProps, ToolbarRadioButtonState } from './ToolbarRadioButton.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioButton/renderToolbarRadioButton.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioButton/renderToolbarRadioButton.ts new file mode 100644 index 0000000000000..7903c7e2f1819 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioButton/renderToolbarRadioButton.ts @@ -0,0 +1,6 @@ +import { renderToggleButton_unstable } from '@fluentui/react-button'; + +/** + * Renders the final JSX of the ToolbarRadioButton component, given the state. + */ +export const renderToolbarRadioButton = renderToggleButton_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioButton/useToolbarRadioButton.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioButton/useToolbarRadioButton.ts new file mode 100644 index 0000000000000..7a6d232d4870b --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioButton/useToolbarRadioButton.ts @@ -0,0 +1,27 @@ +'use client'; + +import type * as React from 'react'; +import { useToolbarRadioButtonBase_unstable } from '@fluentui/react-toolbar'; + +import type { ToolbarRadioButtonProps, ToolbarRadioButtonState } from './ToolbarRadioButton.types'; +import { stringifyDataAttribute } from '../../../utils'; + +/** + * Returns the state for a ToolbarRadioButton component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderToolbarRadioButton`. + */ +export const useToolbarRadioButton = ( + props: ToolbarRadioButtonProps, + ref: React.Ref, +): ToolbarRadioButtonState => { + 'use no memo'; + + const state: ToolbarRadioButtonState = useToolbarRadioButtonBase_unstable(props, ref); + + state.root['data-disabled'] = stringifyDataAttribute(state.disabled); + state.root['data-disabled-focusable'] = stringifyDataAttribute(state.disabledFocusable); + state.root['data-icon-only'] = stringifyDataAttribute(state.iconOnly); + state.root['data-checked'] = stringifyDataAttribute(state.checked); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/index.ts index fe2b0167081de..54099472cfb85 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/index.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/index.ts @@ -23,6 +23,11 @@ export { renderToolbarRadioGroup } from './ToolbarRadioGroup'; export { useToolbarRadioGroup } from './ToolbarRadioGroup'; export type { ToolbarRadioGroupProps, ToolbarRadioGroupState } from './ToolbarRadioGroup'; +export { ToolbarRadioButton } from './ToolbarRadioButton'; +export { renderToolbarRadioButton } from './ToolbarRadioButton'; +export { useToolbarRadioButton } from './ToolbarRadioButton'; +export type { ToolbarRadioButtonProps, ToolbarRadioButtonState } from './ToolbarRadioButton'; + export { ToolbarToggleButton } from './ToolbarToggleButton'; export { renderToolbarToggleButton } from './ToolbarToggleButton'; export { useToolbarToggleButton } from './ToolbarToggleButton'; diff --git a/packages/react-components/react-headless-components-preview/library/src/toolbar.ts b/packages/react-components/react-headless-components-preview/library/src/toolbar.ts index 4d45490eeec8f..0e7f28a3f0f80 100644 --- a/packages/react-components/react-headless-components-preview/library/src/toolbar.ts +++ b/packages/react-components/react-headless-components-preview/library/src/toolbar.ts @@ -13,5 +13,8 @@ export type { ToolbarGroupProps, ToolbarGroupState } from './components/Toolbar' export { ToolbarRadioGroup, renderToolbarRadioGroup, useToolbarRadioGroup } from './components/Toolbar'; export type { ToolbarRadioGroupProps, ToolbarRadioGroupState } from './components/Toolbar'; +export { ToolbarRadioButton, renderToolbarRadioButton, useToolbarRadioButton } from './components/Toolbar'; +export type { ToolbarRadioButtonProps, ToolbarRadioButtonState } from './components/Toolbar'; + export { ToolbarToggleButton, renderToolbarToggleButton, useToolbarToggleButton } from './components/Toolbar'; export type { ToolbarToggleButtonProps, ToolbarToggleButtonState } from './components/Toolbar'; diff --git a/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarRadioButton.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarRadioButton.stories.tsx new file mode 100644 index 0000000000000..802c34f412481 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarRadioButton.stories.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { Toolbar, ToolbarRadioButton, ToolbarRadioGroup } from '@fluentui/react-headless-components-preview/toolbar'; +import { TextAlignCenterRegular, TextAlignLeftRegular, TextAlignRightRegular } from '@fluentui/react-icons'; + +import styles from './toolbar.module.css'; + +const alignOptions = [ + { value: 'left', label: 'Align left', Icon: TextAlignLeftRegular }, + { value: 'center', label: 'Align center', Icon: TextAlignCenterRegular }, + { value: 'right', label: 'Align right', Icon: TextAlignRightRegular }, +] as const; + +export const RadioButton = (): React.ReactNode => ( + + + {alignOptions.map(({ value, label, Icon }) => ( + + + + ))} + + +); diff --git a/packages/react-components/react-headless-components-preview/stories/src/Toolbar/index.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Toolbar/index.stories.tsx index bebaafe5c3d28..66b4fdd6a1135 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Toolbar/index.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Toolbar/index.stories.tsx @@ -3,6 +3,7 @@ import { ToolbarButton, ToolbarDivider, ToolbarGroup, + ToolbarRadioButton, ToolbarRadioGroup, ToolbarToggleButton, } from '@fluentui/react-headless-components-preview/toolbar'; @@ -11,11 +12,19 @@ import descriptionMd from './ToolbarDescription.md'; export { Default } from './ToolbarDefault.stories'; export { Vertical } from './ToolbarVertical.stories'; export { Toggle } from './ToolbarToggleButton.stories'; +export { RadioButton } from './ToolbarRadioButton.stories'; export default { title: 'Headless Components/Toolbar', component: Toolbar, - subcomponents: { ToolbarButton, ToolbarDivider, ToolbarGroup, ToolbarRadioGroup, ToolbarToggleButton }, + subcomponents: { + ToolbarButton, + ToolbarDivider, + ToolbarGroup, + ToolbarRadioButton, + ToolbarRadioGroup, + ToolbarToggleButton, + }, parameters: { docs: { description: {