From 010c8383a8a9e776f80953cc76681eaf8c8749a2 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Thu, 16 Apr 2026 16:27:44 +0200 Subject: [PATCH 1/4] feat(react-headless-components-preview): add Toolbar component --- .../react-headless-components-preview.api.md | 126 ++++++++++++++++++ .../library/package.json | 1 + .../library/src/Toolbar.ts | 14 ++ .../src/components/Toolbar/Toolbar.tsx | 19 +++ .../src/components/Toolbar/Toolbar.types.ts | 25 ++++ .../Toolbar/ToolbarButton/ToolbarButton.tsx | 19 +++ .../ToolbarButton/ToolbarButton.types.ts | 30 +++++ .../components/Toolbar/ToolbarButton/index.ts | 4 + .../ToolbarButton/renderToolbarButton.ts | 6 + .../Toolbar/ToolbarButton/useToolbarButton.ts | 28 ++++ .../Toolbar/ToolbarDivider/ToolbarDivider.tsx | 19 +++ .../ToolbarDivider/ToolbarDivider.types.ts | 16 +++ .../Toolbar/ToolbarDivider/index.ts | 4 + .../ToolbarDivider/renderToolbarDivider.ts | 6 + .../ToolbarDivider/useToolbarDivider.ts | 22 +++ .../Toolbar/ToolbarGroup/ToolbarGroup.tsx | 19 +++ .../ToolbarGroup/ToolbarGroup.types.ts | 15 +++ .../components/Toolbar/ToolbarGroup/index.ts | 4 + .../ToolbarGroup/renderToolbarGroup.ts | 6 + .../Toolbar/ToolbarGroup/useToolbarGroup.ts | 22 +++ .../ToolbarRadioGroup/ToolbarRadioGroup.tsx | 19 +++ .../ToolbarRadioGroup.types.ts | 19 +++ .../Toolbar/ToolbarRadioGroup/index.ts | 4 + .../renderToolbarRadioGroup.ts | 6 + .../ToolbarRadioGroup/useToolbarRadioGroup.ts | 20 +++ .../library/src/components/Toolbar/index.ts | 24 ++++ .../src/components/Toolbar/renderToolbar.ts | 6 + .../src/components/Toolbar/useToolbar.ts | 39 ++++++ .../library/src/index.ts | 34 +++++ .../src/Toolbar/ToolbarDefault.stories.tsx | 83 ++++++++++++ .../stories/src/Toolbar/ToolbarDescription.md | 3 + .../src/Toolbar/ToolbarVertical.stories.tsx | 38 ++++++ .../stories/src/Toolbar/index.stories.tsx | 25 ++++ 33 files changed, 725 insertions(+) create mode 100644 packages/react-components/react-headless-components-preview/library/src/Toolbar.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.tsx create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.types.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/ToolbarButton.tsx create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/ToolbarButton.types.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/index.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/renderToolbarButton.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/useToolbarButton.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/ToolbarDivider.tsx create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/ToolbarDivider.types.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/index.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/renderToolbarDivider.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/useToolbarDivider.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/ToolbarGroup.tsx create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/ToolbarGroup.types.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/index.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/renderToolbarGroup.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/useToolbarGroup.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/ToolbarRadioGroup.tsx create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/ToolbarRadioGroup.types.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/index.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/renderToolbarRadioGroup.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/useToolbarRadioGroup.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/index.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/renderToolbar.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/useToolbar.ts create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarDefault.stories.tsx create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarDescription.md create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarVertical.stories.tsx create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Toolbar/index.stories.tsx diff --git a/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md b/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md index d2c8a5852cc338..0576925a82e46c 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md @@ -142,6 +142,20 @@ import { TextareaBaseState } from '@fluentui/react-textarea'; import type { TextareaSlots as TextareaSlots_2 } from '@fluentui/react-textarea'; import type { ToggleButtonBaseProps } from '@fluentui/react-button'; import type { ToggleButtonBaseState } from '@fluentui/react-button'; +import { ToolbarBaseState } from '@fluentui/react-toolbar'; +import type { ToolbarButtonProps as ToolbarButtonProps_2 } from '@fluentui/react-toolbar'; +import type { ToolbarButtonState as ToolbarButtonState_2 } from '@fluentui/react-toolbar'; +import { ToolbarContextValue } from '@fluentui/react-toolbar'; +import { ToolbarContextValues as ToolbarContextValues_2 } from '@fluentui/react-toolbar'; +import type { ToolbarDividerProps as ToolbarDividerProps_2 } from '@fluentui/react-toolbar'; +import type { ToolbarDividerState as ToolbarDividerState_2 } 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 { ToolbarProps as ToolbarProps_2 } 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'; +import type { ToolbarState as ToolbarState_2 } from '@fluentui/react-toolbar'; import { useMessageBarBodyContextValues_unstable } from '@fluentui/react-message-bar'; import { useFluent_unstable as useProviderContext } from '@fluentui/react-shared-contexts'; @@ -679,6 +693,21 @@ export const renderTextarea: (state: TextareaBaseState) => JSXElement; // @public export const renderToggleButton: (state: ButtonBaseState) => JSXElement; +// @public +export const renderToolbar: (state: ToolbarBaseState, contextValues: ToolbarContextValues_2) => JSXElement; + +// @public +export const renderToolbarButton: (state: ButtonBaseState) => JSXElement; + +// @public +export const renderToolbarDivider: (state: DividerBaseState) => JSXElement; + +// @public +export const renderToolbarGroup: (state: ToolbarGroupState_2) => JSXElement; + +// @public +export const renderToolbarRadioGroup: (state: ToolbarGroupState_2) => JSXElement; + // @public export const SearchBox: ForwardRefComponent; @@ -872,6 +901,82 @@ export type ToggleButtonState = ToggleButtonBaseState & { }; }; +// @public +export const Toolbar: ForwardRefComponent; + +// @public +export const ToolbarButton: ForwardRefComponent; + +// @public (undocumented) +export type ToolbarButtonProps = ToolbarButtonProps_2; + +// @public (undocumented) +export type ToolbarButtonState = ToolbarButtonState_2 & { + root: { + 'data-vertical'?: string; + 'data-disabled'?: string; + 'data-disabled-focusable'?: string; + 'data-icon-only'?: string; + }; +}; + +// @public (undocumented) +export type ToolbarContextValues = ToolbarContextValues_2; + +// @public +export const ToolbarDivider: ForwardRefComponent; + +// @public (undocumented) +export type ToolbarDividerProps = ToolbarDividerProps_2; + +// @public (undocumented) +export type ToolbarDividerState = ToolbarDividerState_2 & { + root: { + 'data-vertical'?: string; + }; +}; + +// @public +export const ToolbarGroup: ForwardRefComponent; + +// @public (undocumented) +export type ToolbarGroupProps = ToolbarGroupProps_2; + +// @public (undocumented) +export type ToolbarGroupState = ToolbarGroupState_2 & { + root: { + 'data-vertical'?: string; + }; +}; + +// @public (undocumented) +export type ToolbarProps = ToolbarProps_2; + +// @public +export const ToolbarRadioGroup: ForwardRefComponent; + +// @public (undocumented) +export type ToolbarRadioGroupProps = ToolbarRadioGroupProps_2; + +// @public (undocumented) +export type ToolbarRadioGroupState = ToolbarRadioGroupState_2 & { + vertical?: boolean; + root: { + 'data-vertical'?: string; + }; +}; + +// @public (undocumented) +export type ToolbarSlots = ToolbarSlots_2; + +// @public (undocumented) +export type ToolbarState = ToolbarState_2 & { + root: { + 'data-vertical'?: string; + 'data-size'?: string; + }; +}; + // @public export const useAccordion: (props: AccordionProps, ref: React_2.Ref) => AccordionState; @@ -1015,6 +1120,27 @@ export const useTextarea: (props: TextareaProps, ref: React_2.Ref) => ToggleButtonState; +// @public +export const useToolbar: (props: ToolbarProps, ref: React_2.Ref) => ToolbarState; + +// @public +export const useToolbarButton: (props: ToolbarButtonProps, ref: React_2.Ref) => ToolbarButtonState; + +// @public +export const useToolbarContext: (selector: ContextSelector) => T; + +// @public +export const useToolbarContextValues: (state: ToolbarState) => ToolbarContextValues; + +// @public +export const useToolbarDivider: (props: ToolbarDividerProps, ref: React_2.Ref) => ToolbarDividerState; + +// @public +export const useToolbarGroup: (props: ToolbarGroupProps, ref: React_2.Ref) => ToolbarGroupState; + +// @public +export const useToolbarRadioGroup: (props: ToolbarRadioGroupProps, ref: React_2.Ref) => ToolbarRadioGroupState; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/react-components/react-headless-components-preview/library/package.json b/packages/react-components/react-headless-components-preview/library/package.json index 1be5a59499e998..ade91f1f9ef84c 100644 --- a/packages/react-components/react-headless-components-preview/library/package.json +++ b/packages/react-components/react-headless-components-preview/library/package.json @@ -49,6 +49,7 @@ "@fluentui/react-tabs": "^9.11.2", "@fluentui/react-tags": "^9.7.19", "@fluentui/react-textarea": "^9.7.0", + "@fluentui/react-toolbar": "^9.7.7", "@fluentui/react-utilities": "^9.26.2", "@swc/helpers": "^0.5.1" }, 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 new file mode 100644 index 00000000000000..e16346d472a13f --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Toolbar.ts @@ -0,0 +1,14 @@ +export { Toolbar, renderToolbar, useToolbar, useToolbarContext, useToolbarContextValues } from './components/Toolbar'; +export type { ToolbarSlots, ToolbarProps, ToolbarState, ToolbarContextValues } from './components/Toolbar'; + +export { ToolbarButton, renderToolbarButton, useToolbarButton } from './components/Toolbar'; +export type { ToolbarButtonProps, ToolbarButtonState } from './components/Toolbar'; + +export { ToolbarDivider, renderToolbarDivider, useToolbarDivider } from './components/Toolbar'; +export type { ToolbarDividerProps, ToolbarDividerState } from './components/Toolbar'; + +export { ToolbarGroup, renderToolbarGroup, useToolbarGroup } from './components/Toolbar'; +export type { ToolbarGroupProps, ToolbarGroupState } from './components/Toolbar'; + +export { ToolbarRadioGroup, renderToolbarRadioGroup, useToolbarRadioGroup } from './components/Toolbar'; +export type { ToolbarRadioGroupProps, ToolbarRadioGroupState } from './components/Toolbar'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.tsx new file mode 100644 index 00000000000000..faf5a48f91111d --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { ToolbarProps } from './Toolbar.types'; +import { useToolbar, useToolbarContextValues } from './useToolbar'; +import { renderToolbar } from './renderToolbar'; + +/** + * A toolbar component that provides a container for grouping a set of controls such as buttons and menu items. + */ +export const Toolbar: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useToolbar(props, ref); + const contextValues = useToolbarContextValues(state); + + return renderToolbar(state, contextValues); +}); + +Toolbar.displayName = 'Toolbar'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.types.ts new file mode 100644 index 00000000000000..32b24d6b19d2e8 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.types.ts @@ -0,0 +1,25 @@ +import type { + ToolbarSlots as ToolbarBaseSlots, + ToolbarProps as ToolbarBaseProps, + ToolbarContextValues as ToolbarBaseContextValues, + ToolbarState as ToolbarBaseState, +} from '@fluentui/react-toolbar'; + +export type ToolbarSlots = ToolbarBaseSlots; + +export type ToolbarProps = ToolbarBaseProps; + +export type ToolbarState = ToolbarBaseState & { + root: { + /** + * Data attribute set when the toolbar is vertically oriented. + */ + 'data-vertical'?: string; + /** + * Data attribute reflecting the current size of the toolbar. Value is 'small', 'medium', or 'large'. + */ + 'data-size'?: string; + }; +}; + +export type ToolbarContextValues = ToolbarBaseContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/ToolbarButton.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/ToolbarButton.tsx new file mode 100644 index 00000000000000..bb96bc71172e85 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/ToolbarButton.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { ToolbarButtonProps } from './ToolbarButton.types'; +import { useToolbarButton } from './useToolbarButton'; +import { renderToolbarButton } from './renderToolbarButton'; + +/** + * A button component designed to be used inside a Toolbar, inheriting toolbar context such as size. + */ +export const ToolbarButton: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useToolbarButton(props, ref); + + return renderToolbarButton(state); + // Casting is required due to lack of distributive union to support unions on @types/react +}) as ForwardRefComponent; + +ToolbarButton.displayName = 'ToolbarButton'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/ToolbarButton.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/ToolbarButton.types.ts new file mode 100644 index 00000000000000..fb0bd9932a1bf5 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/ToolbarButton.types.ts @@ -0,0 +1,30 @@ +import type { + ToolbarButtonProps as ToolbarButtonBaseProps, + ToolbarButtonState as ToolbarButtonBaseState, +} from '@fluentui/react-toolbar'; + +export type ToolbarButtonProps = ToolbarButtonBaseProps; + +export type ToolbarButtonState = ToolbarButtonBaseState & { + root: { + /** + * Data attribute set when the button is in a vertically oriented toolbar. + */ + 'data-vertical'?: string; + + /** + * 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; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/index.ts new file mode 100644 index 00000000000000..2c48846fe2a2b6 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/index.ts @@ -0,0 +1,4 @@ +export { ToolbarButton } from './ToolbarButton'; +export { renderToolbarButton } from './renderToolbarButton'; +export { useToolbarButton } from './useToolbarButton'; +export type { ToolbarButtonProps, ToolbarButtonState } from './ToolbarButton.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/renderToolbarButton.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/renderToolbarButton.ts new file mode 100644 index 00000000000000..39d0379b140c6d --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/renderToolbarButton.ts @@ -0,0 +1,6 @@ +import { renderButton_unstable } from '@fluentui/react-button'; + +/** + * Renders the final JSX of the ToolbarButton component, given the state. + */ +export const renderToolbarButton = renderButton_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/useToolbarButton.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/useToolbarButton.ts new file mode 100644 index 00000000000000..1f08af81c4355a --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/useToolbarButton.ts @@ -0,0 +1,28 @@ +'use client'; + +import type * as React from 'react'; +import { useToolbarButton_unstable } from '@fluentui/react-toolbar'; + +import type { ToolbarButtonProps, ToolbarButtonState } from './ToolbarButton.types'; +import { stringifyDataAttribute } from '../../../utils'; + +/** + * Returns the state for a ToolbarButton component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderToolbarButton`. + */ +export const useToolbarButton = ( + props: ToolbarButtonProps, + ref: React.Ref, +): ToolbarButtonState => { + 'use no memo'; + + const state: ToolbarButtonState = useToolbarButton_unstable(props, ref); + + // Set data attributes for vertical, disabled, disabledFocusable, and iconOnly states to simplify styling. + state.root['data-vertical'] = stringifyDataAttribute(state.vertical); + state.root['data-disabled'] = stringifyDataAttribute(state.disabled); + state.root['data-disabled-focusable'] = stringifyDataAttribute(state.disabledFocusable); + state.root['data-icon-only'] = stringifyDataAttribute(state.iconOnly); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/ToolbarDivider.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/ToolbarDivider.tsx new file mode 100644 index 00000000000000..d33baf60312045 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/ToolbarDivider.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { ToolbarDividerProps } from './ToolbarDivider.types'; +import { useToolbarDivider } from './useToolbarDivider'; +import { renderToolbarDivider } from './renderToolbarDivider'; + +/** + * A divider component designed to be used inside a Toolbar to visually separate groups of controls. + * Its orientation is automatically inverted relative to the toolbar's orientation. + */ +export const ToolbarDivider: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useToolbarDivider(props, ref); + + return renderToolbarDivider(state); +}); + +ToolbarDivider.displayName = 'ToolbarDivider'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/ToolbarDivider.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/ToolbarDivider.types.ts new file mode 100644 index 00000000000000..322ee3b6e454ac --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/ToolbarDivider.types.ts @@ -0,0 +1,16 @@ +import type { + ToolbarDividerProps as ToolbarDividerBaseProps, + ToolbarDividerState as ToolbarDividerBaseState, +} from '@fluentui/react-toolbar'; + +export type ToolbarDividerProps = ToolbarDividerBaseProps; + +export type ToolbarDividerState = ToolbarDividerBaseState & { + root: { + /** + * Data attribute reflecting the actual orientation of the divider element. + * Note: the toolbar divider's orientation is inverted relative to the toolbar's orientation. + */ + 'data-vertical'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/index.ts new file mode 100644 index 00000000000000..59f0e3fdce28f9 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/index.ts @@ -0,0 +1,4 @@ +export { ToolbarDivider } from './ToolbarDivider'; +export { renderToolbarDivider } from './renderToolbarDivider'; +export { useToolbarDivider } from './useToolbarDivider'; +export type { ToolbarDividerProps, ToolbarDividerState } from './ToolbarDivider.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/renderToolbarDivider.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/renderToolbarDivider.ts new file mode 100644 index 00000000000000..63672a0ca69d68 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/renderToolbarDivider.ts @@ -0,0 +1,6 @@ +import { renderDivider_unstable } from '@fluentui/react-divider'; + +/** + * Renders the final JSX of the ToolbarDivider component, given the state. + */ +export const renderToolbarDivider = renderDivider_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/useToolbarDivider.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/useToolbarDivider.ts new file mode 100644 index 00000000000000..dbb469e6fe21ba --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/useToolbarDivider.ts @@ -0,0 +1,22 @@ +'use client'; + +import type * as React from 'react'; +import { useToolbarDivider_unstable } from '@fluentui/react-toolbar'; + +import type { ToolbarDividerProps, ToolbarDividerState } from './ToolbarDivider.types'; +import { stringifyDataAttribute } from '../../../utils'; + +/** + * Returns the state for a ToolbarDivider component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderToolbarDivider`. + */ +export const useToolbarDivider = (props: ToolbarDividerProps, ref: React.Ref): ToolbarDividerState => { + 'use no memo'; + + const state: ToolbarDividerState = useToolbarDivider_unstable(props, ref); + + // Set data-vertical based on the resolved orientation of the divider (already inverted relative to the toolbar). + state.root['data-vertical'] = stringifyDataAttribute(state.vertical); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/ToolbarGroup.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/ToolbarGroup.tsx new file mode 100644 index 00000000000000..7e1ccdd6b45e0b --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/ToolbarGroup.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { ToolbarGroupProps } from './ToolbarGroup.types'; +import { useToolbarGroup } from './useToolbarGroup'; +import { renderToolbarGroup } from './renderToolbarGroup'; + +/** + * A group component used inside a Toolbar to visually and semantically group related controls. + */ +export const ToolbarGroup: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useToolbarGroup(props, ref); + + return renderToolbarGroup(state); + // Casting is required due to lack of distributive union to support unions on @types/react +}) as ForwardRefComponent; + +ToolbarGroup.displayName = 'ToolbarGroup'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/ToolbarGroup.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/ToolbarGroup.types.ts new file mode 100644 index 00000000000000..0f4ac1770ffb64 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/ToolbarGroup.types.ts @@ -0,0 +1,15 @@ +import type { + ToolbarGroupProps as ToolbarGroupBaseProps, + ToolbarGroupState as ToolbarGroupBaseState, +} from '@fluentui/react-toolbar'; + +export type ToolbarGroupProps = ToolbarGroupBaseProps; + +export type ToolbarGroupState = ToolbarGroupBaseState & { + root: { + /** + * Data attribute set when the toolbar group is in a vertically oriented toolbar. + */ + 'data-vertical'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/index.ts new file mode 100644 index 00000000000000..8363da310a1e36 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/index.ts @@ -0,0 +1,4 @@ +export { ToolbarGroup } from './ToolbarGroup'; +export { renderToolbarGroup } from './renderToolbarGroup'; +export { useToolbarGroup } from './useToolbarGroup'; +export type { ToolbarGroupProps, ToolbarGroupState } from './ToolbarGroup.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/renderToolbarGroup.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/renderToolbarGroup.ts new file mode 100644 index 00000000000000..860612164408be --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/renderToolbarGroup.ts @@ -0,0 +1,6 @@ +import { renderToolbarGroup_unstable } from '@fluentui/react-toolbar'; + +/** + * Renders the final JSX of the ToolbarGroup component, given the state. + */ +export const renderToolbarGroup = renderToolbarGroup_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/useToolbarGroup.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/useToolbarGroup.ts new file mode 100644 index 00000000000000..9ef95e923044b6 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarGroup/useToolbarGroup.ts @@ -0,0 +1,22 @@ +'use client'; + +import type * as React from 'react'; +import { useToolbarGroup_unstable } from '@fluentui/react-toolbar'; + +import type { ToolbarGroupProps, ToolbarGroupState } from './ToolbarGroup.types'; +import { stringifyDataAttribute } from '../../../utils'; + +/** + * Returns the state for a ToolbarGroup component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderToolbarGroup`. + */ +export const useToolbarGroup = (props: ToolbarGroupProps, ref: React.Ref): ToolbarGroupState => { + 'use no memo'; + + const state: ToolbarGroupState = useToolbarGroup_unstable(props, ref); + + // Set data-vertical based on the toolbar context orientation. + state.root['data-vertical'] = stringifyDataAttribute(state.vertical); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/ToolbarRadioGroup.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/ToolbarRadioGroup.tsx new file mode 100644 index 00000000000000..b3e1b21a717452 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/ToolbarRadioGroup.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { ToolbarRadioGroupProps } from './ToolbarRadioGroup.types'; +import { useToolbarRadioGroup } from './useToolbarRadioGroup'; +import { renderToolbarRadioGroup } from './renderToolbarRadioGroup'; + +/** + * A radio group component used inside a Toolbar to group mutually exclusive toggle controls. + */ +export const ToolbarRadioGroup: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useToolbarRadioGroup(props, ref); + + return renderToolbarRadioGroup(state); + // Casting is required due to lack of distributive union to support unions on @types/react +}) as ForwardRefComponent; + +ToolbarRadioGroup.displayName = 'ToolbarRadioGroup'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/ToolbarRadioGroup.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/ToolbarRadioGroup.types.ts new file mode 100644 index 00000000000000..1622fcdf4fcac0 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/ToolbarRadioGroup.types.ts @@ -0,0 +1,19 @@ +import type { + ToolbarRadioGroupProps as ToolbarRadioGroupBaseProps, + ToolbarRadioGroupState as ToolbarRadioGroupBaseState, +} from '@fluentui/react-toolbar'; + +export type ToolbarRadioGroupProps = ToolbarRadioGroupBaseProps; + +export type ToolbarRadioGroupState = ToolbarRadioGroupBaseState & { + /** + * Whether the toolbar group is in a vertically oriented toolbar. + */ + vertical?: boolean; + root: { + /** + * Data attribute set when the toolbar radio group is in a vertically oriented toolbar. + */ + 'data-vertical'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/index.ts new file mode 100644 index 00000000000000..4ae7d0012b2802 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/index.ts @@ -0,0 +1,4 @@ +export { ToolbarRadioGroup } from './ToolbarRadioGroup'; +export { renderToolbarRadioGroup } from './renderToolbarRadioGroup'; +export { useToolbarRadioGroup } from './useToolbarRadioGroup'; +export type { ToolbarRadioGroupProps, ToolbarRadioGroupState } from './ToolbarRadioGroup.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/renderToolbarRadioGroup.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/renderToolbarRadioGroup.ts new file mode 100644 index 00000000000000..8f484e270d3d06 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/renderToolbarRadioGroup.ts @@ -0,0 +1,6 @@ +import { renderToolbarGroup_unstable } from '@fluentui/react-toolbar'; + +/** + * Renders the final JSX of the ToolbarRadioGroup component, given the state. + */ +export const renderToolbarRadioGroup = renderToolbarGroup_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/useToolbarRadioGroup.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/useToolbarRadioGroup.ts new file mode 100644 index 00000000000000..42414f7d1c6845 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarRadioGroup/useToolbarRadioGroup.ts @@ -0,0 +1,20 @@ +'use client'; + +import type * as React from 'react'; +import { useToolbarGroup } from '../ToolbarGroup/useToolbarGroup'; + +import type { ToolbarRadioGroupProps, ToolbarRadioGroupState } from './ToolbarRadioGroup.types'; + +/** + * Returns the state for a ToolbarRadioGroup component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderToolbarRadioGroup`. + */ +export const useToolbarRadioGroup = ( + props: ToolbarRadioGroupProps, + ref: React.Ref, +): ToolbarRadioGroupState => { + 'use no memo'; + + // ToolbarRadioGroup reuses ToolbarGroup logic with role='radiogroup'. + return useToolbarGroup({ role: 'radiogroup', ...props }, ref) as unknown as ToolbarRadioGroupState; +}; 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 new file mode 100644 index 00000000000000..e44958bfc7f169 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/index.ts @@ -0,0 +1,24 @@ +export { Toolbar } from './Toolbar'; +export { renderToolbar } from './renderToolbar'; +export { useToolbar, useToolbarContext, useToolbarContextValues } from './useToolbar'; +export type { ToolbarSlots, ToolbarProps, ToolbarState, ToolbarContextValues } from './Toolbar.types'; + +export { ToolbarButton } from './ToolbarButton'; +export { renderToolbarButton } from './ToolbarButton'; +export { useToolbarButton } from './ToolbarButton'; +export type { ToolbarButtonProps, ToolbarButtonState } from './ToolbarButton'; + +export { ToolbarDivider } from './ToolbarDivider'; +export { renderToolbarDivider } from './ToolbarDivider'; +export { useToolbarDivider } from './ToolbarDivider'; +export type { ToolbarDividerProps, ToolbarDividerState } from './ToolbarDivider'; + +export { ToolbarGroup } from './ToolbarGroup'; +export { renderToolbarGroup } from './ToolbarGroup'; +export { useToolbarGroup } from './ToolbarGroup'; +export type { ToolbarGroupProps, ToolbarGroupState } from './ToolbarGroup'; + +export { ToolbarRadioGroup } from './ToolbarRadioGroup'; +export { renderToolbarRadioGroup } from './ToolbarRadioGroup'; +export { useToolbarRadioGroup } from './ToolbarRadioGroup'; +export type { ToolbarRadioGroupProps, ToolbarRadioGroupState } from './ToolbarRadioGroup'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/renderToolbar.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/renderToolbar.ts new file mode 100644 index 00000000000000..c40f34f4d39521 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/renderToolbar.ts @@ -0,0 +1,6 @@ +import { renderToolbar_unstable } from '@fluentui/react-toolbar'; + +/** + * Renders the final JSX of the Toolbar component, given the state and context values. + */ +export const renderToolbar = renderToolbar_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/useToolbar.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/useToolbar.ts new file mode 100644 index 00000000000000..efa1f64f9d83ba --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/useToolbar.ts @@ -0,0 +1,39 @@ +'use client'; + +import type * as React from 'react'; +import { + useToolbar_unstable, + useToolbarContext_unstable, + useToolbarContextValues_unstable, +} from '@fluentui/react-toolbar'; + +import type { ToolbarProps, ToolbarState, ToolbarContextValues } from './Toolbar.types'; +import { stringifyDataAttribute } from '../../utils'; + +/** + * Returns the state for a Toolbar component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderToolbar`. + */ +export const useToolbar = (props: ToolbarProps, ref: React.Ref): ToolbarState => { + 'use no memo'; + + const state: ToolbarState = useToolbar_unstable(props, ref); + + // Set data attributes for vertical and size states to simplify styling of these states. + state.root['data-vertical'] = stringifyDataAttribute(state.vertical); + state.root['data-size'] = state.size; + + return state; +}; + +/** + * Returns the context of the toolbar, which is used to pass information about the toolbar to its children. + */ +export const useToolbarContext = useToolbarContext_unstable; + +/** + * Maps the state of the toolbar to the values that are passed through context to its children. + */ +export const useToolbarContextValues = useToolbarContextValues_unstable as ( + state: ToolbarState, +) => ToolbarContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/index.ts b/packages/react-components/react-headless-components-preview/library/src/index.ts index 356387c60c4644..0edd39b5412c5e 100644 --- a/packages/react-components/react-headless-components-preview/library/src/index.ts +++ b/packages/react-components/react-headless-components-preview/library/src/index.ts @@ -188,3 +188,37 @@ export type { TextareaSlots, TextareaProps, TextareaState } from './Textarea'; export { ToggleButton, renderToggleButton, useToggleButton } from './ToggleButton'; export type { ToggleButtonSlots, ToggleButtonProps, ToggleButtonState } from './ToggleButton'; + +export { + Toolbar, + renderToolbar, + useToolbar, + useToolbarContext, + useToolbarContextValues, + ToolbarButton, + renderToolbarButton, + useToolbarButton, + ToolbarDivider, + renderToolbarDivider, + useToolbarDivider, + ToolbarGroup, + renderToolbarGroup, + useToolbarGroup, + ToolbarRadioGroup, + renderToolbarRadioGroup, + useToolbarRadioGroup, +} from './Toolbar'; +export type { + ToolbarSlots, + ToolbarProps, + ToolbarState, + ToolbarContextValues, + ToolbarButtonProps, + ToolbarButtonState, + ToolbarDividerProps, + ToolbarDividerState, + ToolbarGroupProps, + ToolbarGroupState, + ToolbarRadioGroupProps, + ToolbarRadioGroupState, +} from './Toolbar'; diff --git a/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarDefault.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarDefault.stories.tsx new file mode 100644 index 00000000000000..ca119c46b7f0c2 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarDefault.stories.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; +import { + Toolbar, + ToolbarButton, + ToolbarDivider, + ToolbarGroup, + ToolbarRadioGroup, +} from '@fluentui/react-headless-components-preview'; +import { + CutRegular, + CopyRegular, + ClipboardPasteRegular, + TextBoldRegular, + TextItalicRegular, + TextUnderlineRegular, + TextAlignLeftRegular, + TextAlignCenterRegular, + TextAlignRightRegular, +} from '@fluentui/react-icons'; + +const classes = { + toolbar: + 'inline-flex items-center gap-0.5 rounded-lg border border-gray-200 bg-white p-1 shadow-sm data-[vertical]:flex-col', + button: + 'flex items-center justify-center size-8 rounded border-none bg-transparent p-0 text-gray-700 cursor-pointer ' + + 'hover:bg-gray-100 active:bg-gray-200 ' + + 'focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 ' + + 'data-[disabled]:opacity-40 data-[disabled]:cursor-not-allowed data-[disabled]:pointer-events-none', + activeButton: + 'flex items-center justify-center size-8 rounded border-none p-0 text-blue-700 bg-blue-50 cursor-pointer ' + + 'hover:bg-blue-100 active:bg-blue-200 ' + + 'focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1', + divider: 'mx-0.5 w-px self-stretch bg-gray-200 data-[vertical]:h-px data-[vertical]:w-auto data-[vertical]:my-0.5', + group: 'flex items-center gap-0.5 data-[vertical]:flex-col', +}; + +const alignOptions = ['left', 'center', 'right'] as const; +type AlignOption = (typeof alignOptions)[number]; + +const alignIcons: Record>> = { + left: TextAlignLeftRegular, + center: TextAlignCenterRegular, + right: TextAlignRightRegular, +}; + +export const Default = (): React.ReactNode => { + const [align, setAlign] = React.useState('left'); + + return ( + + } /> + } /> + } /> + + + + + } /> + } /> + } /> + } /> + + + + + + {alignOptions.map(option => { + const Icon = alignIcons[option]; + return ( + } + onClick={() => setAlign(option)} + /> + ); + })} + + + ); +}; diff --git a/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarDescription.md b/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarDescription.md new file mode 100644 index 00000000000000..5a649e846071fb --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarDescription.md @@ -0,0 +1,3 @@ +A toolbar provides a container for grouping a set of controls such as buttons, menu buttons, or checkboxes. + +Toolbar exposes semantic data attributes on all its sub-components to simplify headless styling: diff --git a/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarVertical.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarVertical.stories.tsx new file mode 100644 index 00000000000000..f19a1ed7899249 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarVertical.stories.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { Toolbar, ToolbarButton, ToolbarDivider, ToolbarGroup } from '@fluentui/react-headless-components-preview'; +import { + CutRegular, + CopyRegular, + ClipboardPasteRegular, + TextBoldRegular, + TextItalicRegular, + TextUnderlineRegular, +} from '@fluentui/react-icons'; + +const classes = { + toolbar: + 'inline-flex items-center gap-0.5 rounded-lg border border-gray-200 bg-white p-1 shadow-sm data-[vertical]:flex-col', + button: + 'flex items-center justify-center size-8 rounded border-none bg-transparent p-0 text-gray-700 cursor-pointer ' + + 'hover:bg-gray-100 active:bg-gray-200 ' + + 'focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 ' + + 'data-[disabled]:opacity-40 data-[disabled]:cursor-not-allowed data-[disabled]:pointer-events-none', + divider: 'mx-0.5 w-px self-stretch bg-gray-200 data-[vertical]:h-px data-[vertical]:w-auto data-[vertical]:my-0.5', + group: 'flex items-center gap-0.5 data-[vertical]:flex-col', +}; + +export const Vertical = (): React.ReactNode => ( + + } /> + } /> + } /> + + + + + } /> + } /> + } /> + + +); 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 new file mode 100644 index 00000000000000..2aeb7a902f18a9 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Toolbar/index.stories.tsx @@ -0,0 +1,25 @@ +import { + Toolbar, + ToolbarButton, + ToolbarDivider, + ToolbarGroup, + ToolbarRadioGroup, +} from '@fluentui/react-headless-components-preview'; + +import descriptionMd from './ToolbarDescription.md'; + +export { Default } from './ToolbarDefault.stories'; +export { Vertical } from './ToolbarVertical.stories'; + +export default { + title: 'Headless Components/Toolbar', + component: Toolbar, + subcomponents: { ToolbarButton, ToolbarDivider, ToolbarGroup, ToolbarRadioGroup }, + parameters: { + docs: { + description: { + component: descriptionMd, + }, + }, + }, +}; From 14f7435bf39fb1a57af4d7c2b925764eb7b57020 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Tue, 21 Apr 2026 18:17:02 +0200 Subject: [PATCH 2/4] fixup --- .../react-headless-components-preview.api.md | 26 +++++++++ .../library/src/Toolbar.ts | 3 + .../ToolbarToggleButton.test.tsx | 34 +++++++++++ .../ToolbarToggleButton.tsx | 18 ++++++ .../ToolbarToggleButton.types.ts | 32 +++++++++++ .../Toolbar/ToolbarToggleButton/index.ts | 4 ++ .../renderToolbarToggleButton.ts | 6 ++ .../useToolbarToggleButton.ts | 27 +++++++++ .../library/src/components/Toolbar/index.ts | 5 ++ .../library/src/index.ts | 5 ++ .../src/Toolbar/ToolbarDefault.stories.tsx | 57 ++++++++++++++----- .../Toolbar/ToolbarToggleButton.stories.tsx | 46 +++++++++++++++ .../stories/src/Toolbar/index.stories.tsx | 4 +- 13 files changed, 253 insertions(+), 14 deletions(-) create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.test.tsx create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.tsx create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.types.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/index.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/renderToolbarToggleButton.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/useToolbarToggleButton.ts create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarToggleButton.stories.tsx diff --git a/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md b/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md index 0576925a82e46c..c3116fe2db1b09 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md @@ -156,6 +156,8 @@ import type { ToolbarRadioGroupProps as ToolbarRadioGroupProps_2 } from '@fluent import type { ToolbarRadioGroupState as ToolbarRadioGroupState_2 } from '@fluentui/react-toolbar'; import type { ToolbarSlots as ToolbarSlots_2 } from '@fluentui/react-toolbar'; import type { ToolbarState as ToolbarState_2 } from '@fluentui/react-toolbar'; +import type { ToolbarToggleButtonBaseProps } from '@fluentui/react-toolbar'; +import type { ToolbarToggleButtonBaseState } from '@fluentui/react-toolbar'; import { useMessageBarBodyContextValues_unstable } from '@fluentui/react-message-bar'; import { useFluent_unstable as useProviderContext } from '@fluentui/react-shared-contexts'; @@ -708,6 +710,9 @@ export const renderToolbarGroup: (state: ToolbarGroupState_2) => JSXElement; // @public export const renderToolbarRadioGroup: (state: ToolbarGroupState_2) => JSXElement; +// @public +export const renderToolbarToggleButton: (state: ButtonBaseState) => JSXElement; + // @public export const SearchBox: ForwardRefComponent; @@ -977,6 +982,24 @@ export type ToolbarState = ToolbarState_2 & { }; }; +// @public +export const ToolbarToggleButton: ForwardRefComponent; + +// @public (undocumented) +export type ToolbarToggleButtonProps = ToolbarToggleButtonBaseProps & { + vertical?: boolean; +}; + +// @public (undocumented) +export type ToolbarToggleButtonState = ToolbarToggleButtonBaseState & { + root: { + 'data-disabled'?: string; + 'data-disabled-focusable'?: string; + 'data-icon-only'?: string; + 'data-checked'?: string; + }; +}; + // @public export const useAccordion: (props: AccordionProps, ref: React_2.Ref) => AccordionState; @@ -1141,6 +1164,9 @@ export const useToolbarGroup: (props: ToolbarGroupProps, ref: React_2.Ref) => ToolbarRadioGroupState; +// @public +export const useToolbarToggleButton: (props: ToolbarToggleButtonProps, ref: React_2.Ref) => ToolbarToggleButtonState; + // (No @packageDocumentation comment for this package) ``` 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 e16346d472a13f..4d45490eeec8f9 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 @@ -12,3 +12,6 @@ export type { ToolbarGroupProps, ToolbarGroupState } from './components/Toolbar' export { ToolbarRadioGroup, renderToolbarRadioGroup, useToolbarRadioGroup } from './components/Toolbar'; export type { ToolbarRadioGroupProps, ToolbarRadioGroupState } 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/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.test.tsx new file mode 100644 index 00000000000000..d36ee9207ecede --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.test.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { Toolbar } from '../Toolbar'; +import { ToolbarToggleButton } from './ToolbarToggleButton'; + +describe('ToolbarToggleButton', () => { + it('renders unchecked by default', () => { + const { getByRole } = render( + + + Bold + + , + ); + + const button = getByRole('button', { name: 'Bold' }); + expect(button).toHaveAttribute('aria-pressed', 'false'); + expect(button).not.toHaveAttribute('data-checked'); + }); + + it('renders checked when value is in toolbar checkedValues', () => { + const { getByRole } = render( + + + Bold + + , + ); + + const button = getByRole('button', { name: 'Bold' }); + expect(button).toHaveAttribute('aria-pressed', 'true'); + expect(button).toHaveAttribute('data-checked'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.tsx new file mode 100644 index 00000000000000..eb98441578aec0 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.tsx @@ -0,0 +1,18 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { ToolbarToggleButtonProps } from './ToolbarToggleButton.types'; +import { useToolbarToggleButton } from './useToolbarToggleButton'; +import { renderToolbarToggleButton } from './renderToolbarToggleButton'; + +/** + * A toggle button designed to be used inside a Toolbar with toolbar toggle-group behavior. + */ +export const ToolbarToggleButton: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useToolbarToggleButton(props, ref); + + return renderToolbarToggleButton(state); +}); + +ToolbarToggleButton.displayName = 'ToolbarToggleButton'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.types.ts new file mode 100644 index 00000000000000..725cd5391ebf85 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.types.ts @@ -0,0 +1,32 @@ +import type { ToolbarToggleButtonBaseProps, ToolbarToggleButtonBaseState } from '@fluentui/react-toolbar'; + +export type ToolbarToggleButtonProps = ToolbarToggleButtonBaseProps & { + /** + * Whether the toolbar toggle button is in a vertically oriented toolbar. + */ + vertical?: boolean; +}; + +export type ToolbarToggleButtonState = ToolbarToggleButtonBaseState & { + 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 (pressed) state. + */ + 'data-checked'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/index.ts new file mode 100644 index 00000000000000..ba9cdb7a9b8661 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/index.ts @@ -0,0 +1,4 @@ +export { ToolbarToggleButton } from './ToolbarToggleButton'; +export { renderToolbarToggleButton } from './renderToolbarToggleButton'; +export { useToolbarToggleButton } from './useToolbarToggleButton'; +export type { ToolbarToggleButtonProps, ToolbarToggleButtonState } from './ToolbarToggleButton.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/renderToolbarToggleButton.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/renderToolbarToggleButton.ts new file mode 100644 index 00000000000000..72da03a292c01e --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/renderToolbarToggleButton.ts @@ -0,0 +1,6 @@ +import { renderToggleButton_unstable } from '@fluentui/react-button'; + +/** + * Renders the final JSX of the ToolbarToggleButton component, given the state. + */ +export const renderToolbarToggleButton = renderToggleButton_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/useToolbarToggleButton.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/useToolbarToggleButton.ts new file mode 100644 index 00000000000000..0e78539ffef2e3 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/useToolbarToggleButton.ts @@ -0,0 +1,27 @@ +'use client'; + +import type * as React from 'react'; +import { useToolbarToggleButtonBase_unstable } from '@fluentui/react-toolbar'; + +import type { ToolbarToggleButtonProps, ToolbarToggleButtonState } from './ToolbarToggleButton.types'; +import { stringifyDataAttribute } from '../../../utils'; + +/** + * Returns the state for a ToolbarToggleButton component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderToolbarToggleButton`. + */ +export const useToolbarToggleButton = ( + props: ToolbarToggleButtonProps, + ref: React.Ref, +): ToolbarToggleButtonState => { + 'use no memo'; + + const state: ToolbarToggleButtonState = useToolbarToggleButtonBase_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 e44958bfc7f169..fe2b0167081de6 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 @@ -22,3 +22,8 @@ export { ToolbarRadioGroup } from './ToolbarRadioGroup'; export { renderToolbarRadioGroup } from './ToolbarRadioGroup'; export { useToolbarRadioGroup } from './ToolbarRadioGroup'; export type { ToolbarRadioGroupProps, ToolbarRadioGroupState } from './ToolbarRadioGroup'; + +export { ToolbarToggleButton } from './ToolbarToggleButton'; +export { renderToolbarToggleButton } from './ToolbarToggleButton'; +export { useToolbarToggleButton } from './ToolbarToggleButton'; +export type { ToolbarToggleButtonProps, ToolbarToggleButtonState } from './ToolbarToggleButton'; diff --git a/packages/react-components/react-headless-components-preview/library/src/index.ts b/packages/react-components/react-headless-components-preview/library/src/index.ts index 0edd39b5412c5e..d1dacb2e2bc3a4 100644 --- a/packages/react-components/react-headless-components-preview/library/src/index.ts +++ b/packages/react-components/react-headless-components-preview/library/src/index.ts @@ -207,6 +207,9 @@ export { ToolbarRadioGroup, renderToolbarRadioGroup, useToolbarRadioGroup, + ToolbarToggleButton, + renderToolbarToggleButton, + useToolbarToggleButton, } from './Toolbar'; export type { ToolbarSlots, @@ -221,4 +224,6 @@ export type { ToolbarGroupState, ToolbarRadioGroupProps, ToolbarRadioGroupState, + ToolbarToggleButtonProps, + ToolbarToggleButtonState, } from './Toolbar'; diff --git a/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarDefault.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarDefault.stories.tsx index ca119c46b7f0c2..74240b193b67c7 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarDefault.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarDefault.stories.tsx @@ -5,6 +5,7 @@ import { ToolbarDivider, ToolbarGroup, ToolbarRadioGroup, + ToolbarToggleButton, } from '@fluentui/react-headless-components-preview'; import { CutRegular, @@ -29,22 +30,25 @@ const classes = { activeButton: 'flex items-center justify-center size-8 rounded border-none p-0 text-blue-700 bg-blue-50 cursor-pointer ' + 'hover:bg-blue-100 active:bg-blue-200 ' + - 'focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1', + 'focus:outline-none focus-visible:ring-2 focus-visible:ring-black focus-visible:ring-offset-1', + toggleButton: + 'flex items-center justify-center size-8 rounded border-none bg-transparent p-0 text-gray-700 cursor-pointer ' + + 'hover:bg-gray-100 active:bg-gray-200 ' + + 'focus:outline-none focus-visible:ring-2 focus-visible:ring-black focus-visible:ring-offset-1 ' + + 'data-[checked]:text-blue-700 data-[checked]:bg-blue-50 ' + + 'data-[disabled]:opacity-40 data-[disabled]:cursor-not-allowed data-[disabled]:pointer-events-none', divider: 'mx-0.5 w-px self-stretch bg-gray-200 data-[vertical]:h-px data-[vertical]:w-auto data-[vertical]:my-0.5', group: 'flex items-center gap-0.5 data-[vertical]:flex-col', }; -const alignOptions = ['left', 'center', 'right'] as const; -type AlignOption = (typeof alignOptions)[number]; - -const alignIcons: Record>> = { +const alignIcons = { left: TextAlignLeftRegular, center: TextAlignCenterRegular, right: TextAlignRightRegular, }; export const Default = (): React.ReactNode => { - const [align, setAlign] = React.useState('left'); + const [align, setAlign] = React.useState('left'); return ( @@ -54,18 +58,45 @@ export const Default = (): React.ReactNode => { - - } /> - } /> - } /> - } /> + + } + onClick={() => undefined} + /> + } + onClick={() => undefined} + /> + } + onClick={() => undefined} + /> + } + /> - {alignOptions.map(option => { - const Icon = alignIcons[option]; + {Object.entries(alignIcons).map(([option, Icon]) => { return ( { + return ( + + + } + /> + } + /> + } + /> + + + ); +}; 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 2aeb7a902f18a9..9f597f8d662380 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 @@ -4,17 +4,19 @@ import { ToolbarDivider, ToolbarGroup, ToolbarRadioGroup, + ToolbarToggleButton, } from '@fluentui/react-headless-components-preview'; import descriptionMd from './ToolbarDescription.md'; export { Default } from './ToolbarDefault.stories'; export { Vertical } from './ToolbarVertical.stories'; +export { Toggle } from './ToolbarToggleButton.stories'; export default { title: 'Headless Components/Toolbar', component: Toolbar, - subcomponents: { ToolbarButton, ToolbarDivider, ToolbarGroup, ToolbarRadioGroup }, + subcomponents: { ToolbarButton, ToolbarDivider, ToolbarGroup, ToolbarRadioGroup, ToolbarToggleButton }, parameters: { docs: { description: { From 0450181981b5c7ff76515a912906c79026e0c611 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Wed, 22 Apr 2026 17:17:09 +0200 Subject: [PATCH 3/4] review comments --- .../react-headless-components-preview.api.md | 4 +-- .../ToolbarToggleButton.test.tsx | 34 +++++++++++++++---- .../ToolbarToggleButton.types.ts | 7 +--- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md b/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md index c3116fe2db1b09..9e18a88d5ac153 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md @@ -986,9 +986,7 @@ export type ToolbarState = ToolbarState_2 & { export const ToolbarToggleButton: ForwardRefComponent; // @public (undocumented) -export type ToolbarToggleButtonProps = ToolbarToggleButtonBaseProps & { - vertical?: boolean; -}; +export type ToolbarToggleButtonProps = ToolbarToggleButtonBaseProps; // @public (undocumented) export type ToolbarToggleButtonState = ToolbarToggleButtonBaseState & { diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.test.tsx index d36ee9207ecede..c4614362023a56 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.test.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.test.tsx @@ -5,17 +5,21 @@ import { ToolbarToggleButton } from './ToolbarToggleButton'; describe('ToolbarToggleButton', () => { it('renders unchecked by default', () => { - const { getByRole } = render( + const { getAllByRole } = render( Bold + + Italic + , ); - const button = getByRole('button', { name: 'Bold' }); - expect(button).toHaveAttribute('aria-pressed', 'false'); - expect(button).not.toHaveAttribute('data-checked'); + getAllByRole('button').forEach(button => { + expect(button).toHaveAttribute('aria-pressed', 'false'); + expect(button).not.toHaveAttribute('data-checked'); + }); }); it('renders checked when value is in toolbar checkedValues', () => { @@ -24,11 +28,27 @@ describe('ToolbarToggleButton', () => { Bold + + Italic + + , ); - const button = getByRole('button', { name: 'Bold' }); - expect(button).toHaveAttribute('aria-pressed', 'true'); - expect(button).toHaveAttribute('data-checked'); + const button1 = getByRole('button', { name: 'Bold' }); + expect(button1).toHaveAttribute('aria-pressed', 'true'); + expect(button1).toHaveAttribute('data-checked'); + + const button2 = getByRole('button', { name: 'Italic' }); + expect(button2).toHaveAttribute('aria-pressed', 'false'); + expect(button2).not.toHaveAttribute('data-checked'); + expect(button2).toBeDisabled(); + expect(button2).toHaveAttribute('data-disabled'); + + const button3 = getByRole('button', { name: 'Underline' }); + expect(button3).toHaveAttribute('aria-pressed', 'false'); + expect(button3).not.toHaveAttribute('data-checked'); + expect(button3).not.toBeDisabled(); + expect(button3).toHaveAttribute('data-icon-only'); }); }); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.types.ts index 725cd5391ebf85..a365af79bf1916 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.types.ts @@ -1,11 +1,6 @@ import type { ToolbarToggleButtonBaseProps, ToolbarToggleButtonBaseState } from '@fluentui/react-toolbar'; -export type ToolbarToggleButtonProps = ToolbarToggleButtonBaseProps & { - /** - * Whether the toolbar toggle button is in a vertically oriented toolbar. - */ - vertical?: boolean; -}; +export type ToolbarToggleButtonProps = ToolbarToggleButtonBaseProps; export type ToolbarToggleButtonState = ToolbarToggleButtonBaseState & { root: { From b9551b2457868ef43f8671af94f386ab15636773 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Wed, 22 Apr 2026 19:10:14 +0200 Subject: [PATCH 4/4] fixup --- .../react-headless-components-preview.api.md | 25 ++++---- .../src/components/Toolbar/Toolbar.test.tsx | 58 +++++++++++++++++++ .../src/components/Toolbar/Toolbar.types.ts | 9 +-- .../ToolbarButton/ToolbarButton.types.ts | 5 +- .../Toolbar/ToolbarButton/useToolbarButton.ts | 4 +- .../ToolbarDivider/ToolbarDivider.types.ts | 5 +- .../ToolbarDivider/useToolbarDivider.ts | 4 +- .../src/components/Toolbar/useToolbar.ts | 7 +-- 8 files changed, 84 insertions(+), 33 deletions(-) create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.test.tsx diff --git a/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md b/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md index 9e18a88d5ac153..d360a88129b2d3 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md @@ -142,20 +142,19 @@ import { TextareaBaseState } from '@fluentui/react-textarea'; import type { TextareaSlots as TextareaSlots_2 } from '@fluentui/react-textarea'; import type { ToggleButtonBaseProps } from '@fluentui/react-button'; import type { ToggleButtonBaseState } from '@fluentui/react-button'; +import type { ToolbarBaseProps } from '@fluentui/react-toolbar'; import { ToolbarBaseState } from '@fluentui/react-toolbar'; -import type { ToolbarButtonProps as ToolbarButtonProps_2 } from '@fluentui/react-toolbar'; -import type { ToolbarButtonState as ToolbarButtonState_2 } from '@fluentui/react-toolbar'; +import type { ToolbarButtonBaseProps } from '@fluentui/react-toolbar'; +import type { ToolbarButtonBaseState } from '@fluentui/react-toolbar'; import { ToolbarContextValue } from '@fluentui/react-toolbar'; import { ToolbarContextValues as ToolbarContextValues_2 } from '@fluentui/react-toolbar'; -import type { ToolbarDividerProps as ToolbarDividerProps_2 } from '@fluentui/react-toolbar'; -import type { ToolbarDividerState as ToolbarDividerState_2 } from '@fluentui/react-toolbar'; +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 { ToolbarProps as ToolbarProps_2 } 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'; -import type { ToolbarState as ToolbarState_2 } from '@fluentui/react-toolbar'; import type { ToolbarToggleButtonBaseProps } from '@fluentui/react-toolbar'; import type { ToolbarToggleButtonBaseState } from '@fluentui/react-toolbar'; import { useMessageBarBodyContextValues_unstable } from '@fluentui/react-message-bar'; @@ -913,10 +912,10 @@ export const Toolbar: ForwardRefComponent; export const ToolbarButton: ForwardRefComponent; // @public (undocumented) -export type ToolbarButtonProps = ToolbarButtonProps_2; +export type ToolbarButtonProps = ToolbarButtonBaseProps; // @public (undocumented) -export type ToolbarButtonState = ToolbarButtonState_2 & { +export type ToolbarButtonState = ToolbarButtonBaseState & { root: { 'data-vertical'?: string; 'data-disabled'?: string; @@ -932,10 +931,10 @@ export type ToolbarContextValues = ToolbarContextValues_2; export const ToolbarDivider: ForwardRefComponent; // @public (undocumented) -export type ToolbarDividerProps = ToolbarDividerProps_2; +export type ToolbarDividerProps = ToolbarDividerBaseProps; // @public (undocumented) -export type ToolbarDividerState = ToolbarDividerState_2 & { +export type ToolbarDividerState = ToolbarDividerBaseState & { root: { 'data-vertical'?: string; }; @@ -955,7 +954,7 @@ export type ToolbarGroupState = ToolbarGroupState_2 & { }; // @public (undocumented) -export type ToolbarProps = ToolbarProps_2; +export type ToolbarProps = ToolbarBaseProps; // @public export const ToolbarRadioGroup: ForwardRefComponent; @@ -975,10 +974,10 @@ export type ToolbarRadioGroupState = ToolbarRadioGroupState_2 & { export type ToolbarSlots = ToolbarSlots_2; // @public (undocumented) -export type ToolbarState = ToolbarState_2 & { +export type ToolbarState = ToolbarBaseState & { root: { 'data-vertical'?: string; - 'data-size'?: string; + focusgroup?: string; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.test.tsx new file mode 100644 index 00000000000000..7f968091f75eb2 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.test.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { Toolbar } from '.'; +import { ToolbarButton } from './ToolbarButton'; +import { ToolbarToggleButton } from './ToolbarToggleButton'; + +describe('Toolbar', () => { + it('renders unchecked by default', () => { + const { getByRole, getAllByRole } = render( + + + Bold + + + Italic + + , + ); + + expect(getByRole('toolbar')).toHaveAttribute('focusgroup', 'toolbar inline'); + expect(getByRole('toolbar')).not.toHaveAttribute('data-vertical'); + + expect(getAllByRole('button')).toHaveLength(2); + }); + + it('renders checked when value is in toolbar checkedValues', () => { + const { getByRole } = render( + + + Bold + + + Italic + + + , + ); + + expect(getByRole('toolbar')).toHaveAttribute('focusgroup', 'toolbar block'); + expect(getByRole('toolbar')).toHaveAttribute('data-vertical'); + + const button1 = getByRole('button', { name: 'Bold' }); + expect(button1).toHaveAttribute('aria-pressed', 'true'); + expect(button1).toHaveAttribute('data-checked'); + + const button2 = getByRole('button', { name: 'Italic' }); + expect(button2).toHaveAttribute('aria-pressed', 'false'); + expect(button2).not.toHaveAttribute('data-checked'); + expect(button2).toBeDisabled(); + expect(button2).toHaveAttribute('data-disabled'); + + const button3 = getByRole('button', { name: 'Underline' }); + expect(button3).toHaveAttribute('aria-pressed', 'false'); + expect(button3).not.toHaveAttribute('data-checked'); + expect(button3).not.toBeDisabled(); + expect(button3).toHaveAttribute('data-icon-only'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.types.ts index 32b24d6b19d2e8..8986de93a7148c 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.types.ts @@ -1,8 +1,8 @@ import type { ToolbarSlots as ToolbarBaseSlots, - ToolbarProps as ToolbarBaseProps, + ToolbarBaseProps, ToolbarContextValues as ToolbarBaseContextValues, - ToolbarState as ToolbarBaseState, + ToolbarBaseState, } from '@fluentui/react-toolbar'; export type ToolbarSlots = ToolbarBaseSlots; @@ -15,10 +15,11 @@ export type ToolbarState = ToolbarBaseState & { * Data attribute set when the toolbar is vertically oriented. */ 'data-vertical'?: string; + /** - * Data attribute reflecting the current size of the toolbar. Value is 'small', 'medium', or 'large'. + * Data attribute to define the focus behavior of the toolbar's children */ - 'data-size'?: string; + focusgroup?: string; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/ToolbarButton.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/ToolbarButton.types.ts index fb0bd9932a1bf5..29df0fdacaa849 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/ToolbarButton.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/ToolbarButton.types.ts @@ -1,7 +1,4 @@ -import type { - ToolbarButtonProps as ToolbarButtonBaseProps, - ToolbarButtonState as ToolbarButtonBaseState, -} from '@fluentui/react-toolbar'; +import type { ToolbarButtonBaseProps, ToolbarButtonBaseState } from '@fluentui/react-toolbar'; export type ToolbarButtonProps = ToolbarButtonBaseProps; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/useToolbarButton.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/useToolbarButton.ts index 1f08af81c4355a..84adc2de596377 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/useToolbarButton.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/useToolbarButton.ts @@ -1,7 +1,7 @@ 'use client'; import type * as React from 'react'; -import { useToolbarButton_unstable } from '@fluentui/react-toolbar'; +import { useToolbarButtonBase_unstable } from '@fluentui/react-toolbar'; import type { ToolbarButtonProps, ToolbarButtonState } from './ToolbarButton.types'; import { stringifyDataAttribute } from '../../../utils'; @@ -16,7 +16,7 @@ export const useToolbarButton = ( ): ToolbarButtonState => { 'use no memo'; - const state: ToolbarButtonState = useToolbarButton_unstable(props, ref); + const state: ToolbarButtonState = useToolbarButtonBase_unstable(props, ref); // Set data attributes for vertical, disabled, disabledFocusable, and iconOnly states to simplify styling. state.root['data-vertical'] = stringifyDataAttribute(state.vertical); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/ToolbarDivider.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/ToolbarDivider.types.ts index 322ee3b6e454ac..7b2ef2e547a37d 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/ToolbarDivider.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/ToolbarDivider.types.ts @@ -1,7 +1,4 @@ -import type { - ToolbarDividerProps as ToolbarDividerBaseProps, - ToolbarDividerState as ToolbarDividerBaseState, -} from '@fluentui/react-toolbar'; +import type { ToolbarDividerBaseProps, ToolbarDividerBaseState } from '@fluentui/react-toolbar'; export type ToolbarDividerProps = ToolbarDividerBaseProps; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/useToolbarDivider.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/useToolbarDivider.ts index dbb469e6fe21ba..0da7b9b3d653e9 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/useToolbarDivider.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/useToolbarDivider.ts @@ -1,7 +1,7 @@ 'use client'; import type * as React from 'react'; -import { useToolbarDivider_unstable } from '@fluentui/react-toolbar'; +import { useToolbarDividerBase_unstable } from '@fluentui/react-toolbar'; import type { ToolbarDividerProps, ToolbarDividerState } from './ToolbarDivider.types'; import { stringifyDataAttribute } from '../../../utils'; @@ -13,7 +13,7 @@ import { stringifyDataAttribute } from '../../../utils'; export const useToolbarDivider = (props: ToolbarDividerProps, ref: React.Ref): ToolbarDividerState => { 'use no memo'; - const state: ToolbarDividerState = useToolbarDivider_unstable(props, ref); + const state: ToolbarDividerState = useToolbarDividerBase_unstable(props, ref); // Set data-vertical based on the resolved orientation of the divider (already inverted relative to the toolbar). state.root['data-vertical'] = stringifyDataAttribute(state.vertical); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/useToolbar.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/useToolbar.ts index efa1f64f9d83ba..ca94c553a1b176 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/useToolbar.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/useToolbar.ts @@ -2,7 +2,7 @@ import type * as React from 'react'; import { - useToolbar_unstable, + useToolbarBase_unstable, useToolbarContext_unstable, useToolbarContextValues_unstable, } from '@fluentui/react-toolbar'; @@ -17,11 +17,10 @@ import { stringifyDataAttribute } from '../../utils'; export const useToolbar = (props: ToolbarProps, ref: React.Ref): ToolbarState => { 'use no memo'; - const state: ToolbarState = useToolbar_unstable(props, ref); + const state: ToolbarState = useToolbarBase_unstable(props, ref); - // Set data attributes for vertical and size states to simplify styling of these states. + state.root.focusgroup = `toolbar ${state.vertical ? 'block' : 'inline'}`; state.root['data-vertical'] = stringifyDataAttribute(state.vertical); - state.root['data-size'] = state.size; return state; };