diff --git a/change/@fluentui-react-toolbar-3e1d7781-11bc-4929-b8e8-25aa0d674e4a.json b/change/@fluentui-react-toolbar-3e1d7781-11bc-4929-b8e8-25aa0d674e4a.json new file mode 100644 index 00000000000000..898aaab4f35355 --- /dev/null +++ b/change/@fluentui-react-toolbar-3e1d7781-11bc-4929-b8e8-25aa0d674e4a.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "feat: add toolbar controlled state example for toggle buttons", + "packageName": "@fluentui/react-toolbar", + "email": "chassunc@microsoft.com", + "dependentChangeType": "none" +} diff --git a/packages/react-components/react-toolbar/etc/react-toolbar.api.md b/packages/react-components/react-toolbar/etc/react-toolbar.api.md index 5b0eb5f6d980eb..e979e0788f10bb 100644 --- a/packages/react-components/react-toolbar/etc/react-toolbar.api.md +++ b/packages/react-components/react-toolbar/etc/react-toolbar.api.md @@ -53,8 +53,8 @@ export type ToolbarButtonState = ComponentState> & ButtonSt export const toolbarClassNames: SlotClassNames; // @public (undocumented) -export type ToolbarContextValue = { - size: ToolbarProps['size']; +export type ToolbarContextValue = Pick & { + handleToggleButton?: ToggableHandler; }; // @public (undocumented) @@ -74,6 +74,9 @@ export type ToolbarDividerState = ComponentState> & Divide // @public export type ToolbarProps = ComponentProps & { size?: 'small' | 'medium'; + checkedValues?: Record; + defaultCheckedValues?: Record; + onCheckedValueChange?: (e: ToolbarCheckedValueChangeEvent, data: ToolbarCheckedValueChangeData) => void; }; // @public @@ -104,18 +107,23 @@ export type ToolbarSlots = { }; // @public -export type ToolbarState = ComponentState & Required>; +export type ToolbarState = ComponentState & Required> & Pick & { + handleToggleButton: ToggableHandler; +}; // @public export const ToolbarToggleButton: ForwardRefComponent; // @public -export type ToolbarToggleButtonProps = ComponentProps & Partial> & { +export type ToolbarToggleButtonProps = ComponentProps & Partial> & { appearance?: 'primary' | 'subtle'; }; // @public -export type ToolbarToggleButtonState = ComponentState> & ToggleButtonState; +export type ToolbarToggleButtonState = ComponentState> & ToggleButtonState & Required> & { + name?: string; + value?: string; +}; // @public export const useToolbar_unstable: (props: ToolbarProps, ref: React_2.Ref) => ToolbarState; diff --git a/packages/react-components/react-toolbar/src/components/Toolbar/Toolbar.types.ts b/packages/react-components/react-toolbar/src/components/Toolbar/Toolbar.types.ts index a5973b91f84fc0..7dfe5f419247a8 100644 --- a/packages/react-components/react-toolbar/src/components/Toolbar/Toolbar.types.ts +++ b/packages/react-components/react-toolbar/src/components/Toolbar/Toolbar.types.ts @@ -1,9 +1,19 @@ +import * as React from 'react'; import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; export type ToolbarSlots = { root: Slot<'div'>; }; +export type ToolbarCheckedValueChangeData = { + /** The items for this value that are checked */ + checkedItems: string[]; + /** The name of the value */ + name: string; +}; + +export type ToolbarCheckedValueChangeEvent = React.MouseEvent | React.KeyboardEvent; + /** * Toolbar Props */ @@ -14,17 +24,52 @@ export type ToolbarProps = ComponentProps & { * @default medium */ size?: 'small' | 'medium'; + + /** + * Map of all checked values + */ + checkedValues?: Record; + + /** + * Default values to be checked on mount + */ + defaultCheckedValues?: Record; + + /** + * Callback when checked items change for value with a name + * + * @param event - React's original SyntheticEvent + * @param data - A data object with relevant information + */ + onCheckedValueChange?: (e: ToolbarCheckedValueChangeEvent, data: ToolbarCheckedValueChangeData) => void; }; /** * State used in rendering Toolbar */ -export type ToolbarState = ComponentState & Required>; +export type ToolbarState = ComponentState & + Required> & + Pick & { + /* + * Toggles the state of a ToggleButton item + */ + handleToggleButton: ToggableHandler; + }; -export type ToolbarContextValue = { - size: ToolbarProps['size']; +export type ToolbarContextValue = Pick & { + handleToggleButton?: ToggableHandler; }; export type ToolbarContextValues = { toolbar: ToolbarContextValue; }; + +export type UninitializedToolbarState = Omit & + Partial>; + +export type ToggableHandler = ( + e: React.MouseEvent | React.KeyboardEvent, + name?: string, + value?: string, + checked?: boolean, +) => void; diff --git a/packages/react-components/react-toolbar/src/components/Toolbar/ToolbarContext.tsx b/packages/react-components/react-toolbar/src/components/Toolbar/ToolbarContext.tsx index eaf3b5fc2475a3..931eb3077fb471 100644 --- a/packages/react-components/react-toolbar/src/components/Toolbar/ToolbarContext.tsx +++ b/packages/react-components/react-toolbar/src/components/Toolbar/ToolbarContext.tsx @@ -7,6 +7,7 @@ export const ToolbarContext = React.createContext null, }; export const useToolbarContext = () => React.useContext(ToolbarContext) ?? toolbarContextDefaultValue; diff --git a/packages/react-components/react-toolbar/src/components/Toolbar/useToolbar.ts b/packages/react-components/react-toolbar/src/components/Toolbar/useToolbar.ts index c56b2cfa5cb9c5..d2f291e5ae665d 100644 --- a/packages/react-components/react-toolbar/src/components/Toolbar/useToolbar.ts +++ b/packages/react-components/react-toolbar/src/components/Toolbar/useToolbar.ts @@ -1,6 +1,7 @@ import * as React from 'react'; +import { useEventCallback, useControllableState } from '@fluentui/react-utilities'; import { getNativeElementProps } from '@fluentui/react-utilities'; -import type { ToolbarProps, ToolbarState } from './Toolbar.types'; +import type { ToggableHandler, ToolbarProps, ToolbarState, UninitializedToolbarState } from './Toolbar.types'; import { useArrowNavigationGroup } from '@fluentui/react-tabster'; /** @@ -13,14 +14,14 @@ import { useArrowNavigationGroup } from '@fluentui/react-tabster'; * @param ref - reference to root HTMLElement of Toolbar */ export const useToolbar_unstable = (props: ToolbarProps, ref: React.Ref): ToolbarState => { + const { size = 'medium' } = props; + const arrowNavigationProps = useArrowNavigationGroup({ circular: true, axis: 'horizontal', }); - const { size = 'medium' } = props; - - return { + const initialState: UninitializedToolbarState = { size, // TODO add appropriate props/defaults @@ -36,6 +37,36 @@ export const useToolbar_unstable = (props: ToolbarProps, ref: React.Ref { + if (name && value) { + const checkedItems = checkedValues?.[name] || []; + const newCheckedItems = [...checkedItems]; + if (checked) { + newCheckedItems.splice(newCheckedItems.indexOf(value), 1); + } else { + newCheckedItems.push(value); + } + + onCheckedValueChange?.(e, { name, checkedItems: newCheckedItems }); + setCheckedValues(s => ({ ...s, [name]: newCheckedItems })); + } + }, + ); + + return { + ...initialState, + handleToggleButton, + checkedValues: checkedValues ?? {}, }; }; diff --git a/packages/react-components/react-toolbar/src/components/Toolbar/useToolbarContextValues.tsx b/packages/react-components/react-toolbar/src/components/Toolbar/useToolbarContextValues.tsx index 8b833d2817a408..cf3a26f5ba1720 100644 --- a/packages/react-components/react-toolbar/src/components/Toolbar/useToolbarContextValues.tsx +++ b/packages/react-components/react-toolbar/src/components/Toolbar/useToolbarContextValues.tsx @@ -1,10 +1,11 @@ import type { ToolbarContextValue, ToolbarContextValues, ToolbarState } from './Toolbar.types'; export function useToolbarContextValues_unstable(state: ToolbarState): ToolbarContextValues { - const { size } = state; + const { size, handleToggleButton } = state; // This context is created with "@fluentui/react-context-selector", these is no sense to memoize it const toolbar: ToolbarContextValue = { size, + handleToggleButton, }; return { toolbar }; diff --git a/packages/react-components/react-toolbar/src/components/ToolbarToggleButton/ToolbarToggleButton.tsx b/packages/react-components/react-toolbar/src/components/ToolbarToggleButton/ToolbarToggleButton.tsx index 2029f15969da3f..1edd82c8454be6 100644 --- a/packages/react-components/react-toolbar/src/components/ToolbarToggleButton/ToolbarToggleButton.tsx +++ b/packages/react-components/react-toolbar/src/components/ToolbarToggleButton/ToolbarToggleButton.tsx @@ -1,20 +1,14 @@ import * as React from 'react'; import type { ToolbarToggleButtonProps } from './ToolbarToggleButton.types'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; -import { - renderToggleButton_unstable, - useToggleButtonStyles_unstable, - useToggleButton_unstable, -} from '@fluentui/react-button'; -import { useToolbarContext } from '../Toolbar/ToolbarContext'; +import { renderToggleButton_unstable, useToggleButtonStyles_unstable } from '@fluentui/react-button'; +import { useToolbarToggleButton_unstable } from './useToolbarToggleButton'; /** * ToolbarToggleButton component */ export const ToolbarToggleButton: ForwardRefComponent = React.forwardRef((props, ref) => { - const { size } = useToolbarContext(); - - const state = useToggleButton_unstable({ size, ...props }, ref); + const state = useToolbarToggleButton_unstable(props, ref); useToggleButtonStyles_unstable(state); return renderToggleButton_unstable(state); diff --git a/packages/react-components/react-toolbar/src/components/ToolbarToggleButton/ToolbarToggleButton.types.ts b/packages/react-components/react-toolbar/src/components/ToolbarToggleButton/ToolbarToggleButton.types.ts index 4c6481259d2a4c..f833f492635251 100644 --- a/packages/react-components/react-toolbar/src/components/ToolbarToggleButton/ToolbarToggleButton.types.ts +++ b/packages/react-components/react-toolbar/src/components/ToolbarToggleButton/ToolbarToggleButton.types.ts @@ -5,11 +5,16 @@ import { ToggleButtonProps, ButtonSlots, ToggleButtonState } from '@fluentui/rea * ToolbarToggleButton Props */ export type ToolbarToggleButtonProps = ComponentProps & - Partial> & { + Partial> & { appearance?: 'primary' | 'subtle'; }; /** * State used in rendering ToolbarToggleButton */ -export type ToolbarToggleButtonState = ComponentState> & ToggleButtonState; +export type ToolbarToggleButtonState = ComponentState> & + ToggleButtonState & + Required> & { + name?: string; + value?: string; + }; diff --git a/packages/react-components/react-toolbar/src/components/ToolbarToggleButton/useToolbarToggleButton.ts b/packages/react-components/react-toolbar/src/components/ToolbarToggleButton/useToolbarToggleButton.ts new file mode 100644 index 00000000000000..891f4bf5703fdb --- /dev/null +++ b/packages/react-components/react-toolbar/src/components/ToolbarToggleButton/useToolbarToggleButton.ts @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { useToggleButton_unstable } from '@fluentui/react-button'; +import { useToolbarContext } from '../Toolbar/ToolbarContext'; +import { ToolbarToggleButtonProps, ToolbarToggleButtonState } from './ToolbarToggleButton.types'; + +/** + * Given user props, defines default props for the ToggleButton, calls useButtonState and useChecked, and returns + * processed state. + * @param props - User provided props to the ToggleButton component. + * @param ref - User provided ref to be passed to the ToggleButton component. + */ +export const useToolbarToggleButton_unstable = ( + props: ToolbarToggleButtonProps, + ref: React.Ref, +): ToolbarToggleButtonState => { + const { handleToggleButton, size } = useToolbarContext(); + const { onClick: onClickOriginal } = props; + const state = useToggleButton_unstable({ size, ...props }, ref) as ToolbarToggleButtonState; + + const handleOnClick = ( + e: React.MouseEvent & React.MouseEvent, + ) => { + if (state.disabled) { + e.preventDefault(); + e.stopPropagation(); + return; + } + + handleToggleButton?.(e, state.name, state.value, state.checked); + onClickOriginal?.(e); + }; + + state.root.onClick = handleOnClick; + return state; +}; diff --git a/packages/react-components/react-toolbar/src/stories/Toolbar/ToolbarControlledToggleButton.stories.tsx b/packages/react-components/react-toolbar/src/stories/Toolbar/ToolbarControlledToggleButton.stories.tsx new file mode 100644 index 00000000000000..0f9d5cd9dfab5d --- /dev/null +++ b/packages/react-components/react-toolbar/src/stories/Toolbar/ToolbarControlledToggleButton.stories.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { Toolbar, ToolbarToggleButton, ToolbarProps } from '@fluentui/react-toolbar'; + +export const ControlledToggleButton = () => { + const [checkedValues, setCheckedValues] = React.useState>({ edit: ['cut', 'paste'] }); + const onChange: ToolbarProps['onCheckedValueChange'] = (e, { name, checkedItems }) => { + setCheckedValues(s => { + return s ? { ...s, [name]: checkedItems } : { [name]: checkedItems }; + }); + }; + + return ( + + Enable Group + Enable Group + + ); +}; diff --git a/packages/react-components/react-toolbar/src/stories/Toolbar/index.stories.tsx b/packages/react-components/react-toolbar/src/stories/Toolbar/index.stories.tsx index 407ffe4868e548..d913547c93e9df 100644 --- a/packages/react-components/react-toolbar/src/stories/Toolbar/index.stories.tsx +++ b/packages/react-components/react-toolbar/src/stories/Toolbar/index.stories.tsx @@ -9,6 +9,7 @@ export { OverflowItems } from './ToolbarOverflow.stories'; export { WithTooltip } from './ToolbarWithTooltip.stories'; export { WithPopover } from './ToolbarWithPopover.stories'; export { Subtle } from './ToolbarSubtle.stories'; +export { ControlledToggleButton } from './ToolbarControlledToggleButton.stories'; export default { title: 'Preview Components/Toolbar',