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 d2c8a5852cc33..d360a88129b2d 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,21 @@ 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 { 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 { 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 { 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 { 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'; @@ -679,6 +694,24 @@ 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 renderToolbarToggleButton: (state: ButtonBaseState) => JSXElement; + // @public export const SearchBox: ForwardRefComponent; @@ -872,6 +905,98 @@ export type ToggleButtonState = ToggleButtonBaseState & { }; }; +// @public +export const Toolbar: ForwardRefComponent; + +// @public +export const ToolbarButton: ForwardRefComponent; + +// @public (undocumented) +export type ToolbarButtonProps = ToolbarButtonBaseProps; + +// @public (undocumented) +export type ToolbarButtonState = ToolbarButtonBaseState & { + 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 = ToolbarDividerBaseProps; + +// @public (undocumented) +export type ToolbarDividerState = ToolbarDividerBaseState & { + 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 = ToolbarBaseProps; + +// @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 = ToolbarBaseState & { + root: { + 'data-vertical'?: string; + focusgroup?: string; + }; +}; + +// @public +export const ToolbarToggleButton: ForwardRefComponent; + +// @public (undocumented) +export type ToolbarToggleButtonProps = ToolbarToggleButtonBaseProps; + +// @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; @@ -1015,6 +1140,30 @@ 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; + +// @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/package.json b/packages/react-components/react-headless-components-preview/library/package.json index 1be5a59499e99..ade91f1f9ef84 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 0000000000000..4d45490eeec8f --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Toolbar.ts @@ -0,0 +1,17 @@ +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'; + +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/Toolbar.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.test.tsx new file mode 100644 index 0000000000000..7f968091f75eb --- /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.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.tsx new file mode 100644 index 0000000000000..faf5a48f91111 --- /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 0000000000000..8986de93a7148 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/Toolbar.types.ts @@ -0,0 +1,26 @@ +import type { + ToolbarSlots as ToolbarBaseSlots, + ToolbarBaseProps, + ToolbarContextValues as ToolbarBaseContextValues, + 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 to define the focus behavior of the toolbar's children + */ + focusgroup?: 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 0000000000000..bb96bc71172e8 --- /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 0000000000000..29df0fdacaa84 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarButton/ToolbarButton.types.ts @@ -0,0 +1,27 @@ +import type { ToolbarButtonBaseProps, 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 0000000000000..2c48846fe2a2b --- /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 0000000000000..39d0379b140c6 --- /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 0000000000000..84adc2de59637 --- /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 { useToolbarButtonBase_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 = useToolbarButtonBase_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 0000000000000..d33baf6031204 --- /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 0000000000000..7b2ef2e547a37 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarDivider/ToolbarDivider.types.ts @@ -0,0 +1,13 @@ +import type { ToolbarDividerBaseProps, 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 0000000000000..59f0e3fdce28f --- /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 0000000000000..63672a0ca69d6 --- /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 0000000000000..0da7b9b3d653e --- /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 { useToolbarDividerBase_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 = 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); + + 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 0000000000000..7e1ccdd6b45e0 --- /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 0000000000000..0f4ac1770ffb6 --- /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 0000000000000..8363da310a1e3 --- /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 0000000000000..860612164408b --- /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 0000000000000..9ef95e923044b --- /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 0000000000000..b3e1b21a71745 --- /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 0000000000000..1622fcdf4fcac --- /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 0000000000000..4ae7d0012b280 --- /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 0000000000000..8f484e270d3d0 --- /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 0000000000000..42414f7d1c684 --- /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/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 0000000000000..c4614362023a5 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.test.tsx @@ -0,0 +1,54 @@ +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 { getAllByRole } = render( + + + Bold + + + Italic + + , + ); + + 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', () => { + const { getByRole } = render( + + + Bold + + + Italic + + + , + ); + + 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.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.tsx new file mode 100644 index 0000000000000..eb98441578aec --- /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 0000000000000..a365af79bf191 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/ToolbarToggleButton/ToolbarToggleButton.types.ts @@ -0,0 +1,27 @@ +import type { ToolbarToggleButtonBaseProps, ToolbarToggleButtonBaseState } from '@fluentui/react-toolbar'; + +export type ToolbarToggleButtonProps = ToolbarToggleButtonBaseProps; + +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 0000000000000..ba9cdb7a9b866 --- /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 0000000000000..72da03a292c01 --- /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 0000000000000..0e78539ffef2e --- /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 new file mode 100644 index 0000000000000..fe2b0167081de --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/index.ts @@ -0,0 +1,29 @@ +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'; + +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/components/Toolbar/renderToolbar.ts b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/renderToolbar.ts new file mode 100644 index 0000000000000..c40f34f4d3952 --- /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 0000000000000..ca94c553a1b17 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Toolbar/useToolbar.ts @@ -0,0 +1,38 @@ +'use client'; + +import type * as React from 'react'; +import { + useToolbarBase_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 = useToolbarBase_unstable(props, ref); + + state.root.focusgroup = `toolbar ${state.vertical ? 'block' : 'inline'}`; + state.root['data-vertical'] = stringifyDataAttribute(state.vertical); + + 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 356387c60c464..d1dacb2e2bc3a 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,42 @@ 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, + ToolbarToggleButton, + renderToolbarToggleButton, + useToolbarToggleButton, +} from './Toolbar'; +export type { + ToolbarSlots, + ToolbarProps, + ToolbarState, + ToolbarContextValues, + ToolbarButtonProps, + ToolbarButtonState, + ToolbarDividerProps, + ToolbarDividerState, + ToolbarGroupProps, + 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 new file mode 100644 index 0000000000000..74240b193b67c --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarDefault.stories.tsx @@ -0,0 +1,114 @@ +import * as React from 'react'; +import { + Toolbar, + ToolbarButton, + ToolbarDivider, + ToolbarGroup, + ToolbarRadioGroup, + ToolbarToggleButton, +} 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-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 alignIcons = { + left: TextAlignLeftRegular, + center: TextAlignCenterRegular, + right: TextAlignRightRegular, +}; + +export const Default = (): React.ReactNode => { + const [align, setAlign] = React.useState('left'); + + return ( + + } /> + } /> + } /> + + + + + } + onClick={() => undefined} + /> + } + onClick={() => undefined} + /> + } + onClick={() => undefined} + /> + } + /> + + + + + + {Object.entries(alignIcons).map(([option, Icon]) => { + 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 0000000000000..5a649e846071f --- /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/ToolbarToggleButton.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarToggleButton.stories.tsx new file mode 100644 index 0000000000000..5e59d29965bef --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Toolbar/ToolbarToggleButton.stories.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { Toolbar, ToolbarGroup, ToolbarToggleButton } from '@fluentui/react-headless-components-preview'; +import { 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', + 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', +}; + +export const Toggle = (): React.ReactNode => { + return ( + + + } + /> + } + /> + } + /> + + + ); +}; 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 0000000000000..f19a1ed789924 --- /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 0000000000000..9f597f8d66238 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Toolbar/index.stories.tsx @@ -0,0 +1,27 @@ +import { + Toolbar, + ToolbarButton, + 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, ToolbarToggleButton }, + parameters: { + docs: { + description: { + component: descriptionMd, + }, + }, + }, +};