Skip to content

Commit bee2c5b

Browse files
authored
feat: add toolbar controlled state example for toggle buttons (#24380)
* feat: add toolbar controlled state example for toggle buttons * chore: add changes * chore: remove Menu usages in toolbar example * chore: use context passed handler * chore: add changes * chore: update api * chore: fix types * chore: update api * chore: stop spreading props into state * chore: fix types * chore: move logic to useToolbarToggleButton * chore: remove unnecessary change * chore: update api * chore: move size to the use hook * chore: move size to the use hook
1 parent 8d7e2af commit bee2c5b

File tree

11 files changed

+171
-25
lines changed

11 files changed

+171
-25
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "none",
3+
"comment": "feat: add toolbar controlled state example for toggle buttons",
4+
"packageName": "@fluentui/react-toolbar",
5+
"email": "[email protected]",
6+
"dependentChangeType": "none"
7+
}

packages/react-components/react-toolbar/etc/react-toolbar.api.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ export type ToolbarButtonState = ComponentState<Partial<ButtonSlots>> & ButtonSt
5353
export const toolbarClassNames: SlotClassNames<ToolbarSlots>;
5454

5555
// @public (undocumented)
56-
export type ToolbarContextValue = {
57-
size: ToolbarProps['size'];
56+
export type ToolbarContextValue = Pick<ToolbarProps, 'size'> & {
57+
handleToggleButton?: ToggableHandler;
5858
};
5959

6060
// @public (undocumented)
@@ -74,6 +74,9 @@ export type ToolbarDividerState = ComponentState<Partial<DividerSlots>> & Divide
7474
// @public
7575
export type ToolbarProps = ComponentProps<ToolbarSlots> & {
7676
size?: 'small' | 'medium';
77+
checkedValues?: Record<string, string[]>;
78+
defaultCheckedValues?: Record<string, string[]>;
79+
onCheckedValueChange?: (e: ToolbarCheckedValueChangeEvent, data: ToolbarCheckedValueChangeData) => void;
7780
};
7881

7982
// @public
@@ -104,18 +107,23 @@ export type ToolbarSlots = {
104107
};
105108

106109
// @public
107-
export type ToolbarState = ComponentState<ToolbarSlots> & Required<Pick<ToolbarProps, 'size'>>;
110+
export type ToolbarState = ComponentState<ToolbarSlots> & Required<Pick<ToolbarProps, 'size' | 'checkedValues'>> & Pick<ToolbarProps, 'defaultCheckedValues' | 'onCheckedValueChange'> & {
111+
handleToggleButton: ToggableHandler;
112+
};
108113

109114
// @public
110115
export const ToolbarToggleButton: ForwardRefComponent<ToolbarToggleButtonProps>;
111116

112117
// @public
113-
export type ToolbarToggleButtonProps = ComponentProps<ButtonSlots> & Partial<Pick<ToggleButtonProps, 'disabled' | 'disabledFocusable'>> & {
118+
export type ToolbarToggleButtonProps = ComponentProps<ButtonSlots> & Partial<Pick<ToggleButtonProps, 'disabled' | 'disabledFocusable' | 'size'>> & {
114119
appearance?: 'primary' | 'subtle';
115120
};
116121

117122
// @public
118-
export type ToolbarToggleButtonState = ComponentState<Partial<ButtonSlots>> & ToggleButtonState;
123+
export type ToolbarToggleButtonState = ComponentState<Partial<ButtonSlots>> & ToggleButtonState & Required<Pick<ToggleButtonProps, 'checked'>> & {
124+
name?: string;
125+
value?: string;
126+
};
119127

120128
// @public
121129
export const useToolbar_unstable: (props: ToolbarProps, ref: React_2.Ref<HTMLElement>) => ToolbarState;
Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1+
import * as React from 'react';
12
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
23

34
export type ToolbarSlots = {
45
root: Slot<'div'>;
56
};
67

8+
export type ToolbarCheckedValueChangeData = {
9+
/** The items for this value that are checked */
10+
checkedItems: string[];
11+
/** The name of the value */
12+
name: string;
13+
};
14+
15+
export type ToolbarCheckedValueChangeEvent = React.MouseEvent | React.KeyboardEvent;
16+
717
/**
818
* Toolbar Props
919
*/
@@ -14,17 +24,52 @@ export type ToolbarProps = ComponentProps<ToolbarSlots> & {
1424
* @default medium
1525
*/
1626
size?: 'small' | 'medium';
27+
28+
/**
29+
* Map of all checked values
30+
*/
31+
checkedValues?: Record<string, string[]>;
32+
33+
/**
34+
* Default values to be checked on mount
35+
*/
36+
defaultCheckedValues?: Record<string, string[]>;
37+
38+
/**
39+
* Callback when checked items change for value with a name
40+
*
41+
* @param event - React's original SyntheticEvent
42+
* @param data - A data object with relevant information
43+
*/
44+
onCheckedValueChange?: (e: ToolbarCheckedValueChangeEvent, data: ToolbarCheckedValueChangeData) => void;
1745
};
1846

1947
/**
2048
* State used in rendering Toolbar
2149
*/
22-
export type ToolbarState = ComponentState<ToolbarSlots> & Required<Pick<ToolbarProps, 'size'>>;
50+
export type ToolbarState = ComponentState<ToolbarSlots> &
51+
Required<Pick<ToolbarProps, 'size' | 'checkedValues'>> &
52+
Pick<ToolbarProps, 'defaultCheckedValues' | 'onCheckedValueChange'> & {
53+
/*
54+
* Toggles the state of a ToggleButton item
55+
*/
56+
handleToggleButton: ToggableHandler;
57+
};
2358

24-
export type ToolbarContextValue = {
25-
size: ToolbarProps['size'];
59+
export type ToolbarContextValue = Pick<ToolbarProps, 'size'> & {
60+
handleToggleButton?: ToggableHandler;
2661
};
2762

2863
export type ToolbarContextValues = {
2964
toolbar: ToolbarContextValue;
3065
};
66+
67+
export type UninitializedToolbarState = Omit<ToolbarState, 'checkedValues' | 'handleToggleButton'> &
68+
Partial<Pick<ToolbarState, 'checkedValues'>>;
69+
70+
export type ToggableHandler = (
71+
e: React.MouseEvent | React.KeyboardEvent,
72+
name?: string,
73+
value?: string,
74+
checked?: boolean,
75+
) => void;

packages/react-components/react-toolbar/src/components/Toolbar/ToolbarContext.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const ToolbarContext = React.createContext<ToolbarContextValue | undefine
77

88
const toolbarContextDefaultValue = {
99
size: 'medium' as 'medium',
10+
handleToggleButton: () => null,
1011
};
1112

1213
export const useToolbarContext = () => React.useContext(ToolbarContext) ?? toolbarContextDefaultValue;

packages/react-components/react-toolbar/src/components/Toolbar/useToolbar.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react';
2+
import { useEventCallback, useControllableState } from '@fluentui/react-utilities';
23
import { getNativeElementProps } from '@fluentui/react-utilities';
3-
import type { ToolbarProps, ToolbarState } from './Toolbar.types';
4+
import type { ToggableHandler, ToolbarProps, ToolbarState, UninitializedToolbarState } from './Toolbar.types';
45
import { useArrowNavigationGroup } from '@fluentui/react-tabster';
56

67
/**
@@ -13,14 +14,14 @@ import { useArrowNavigationGroup } from '@fluentui/react-tabster';
1314
* @param ref - reference to root HTMLElement of Toolbar
1415
*/
1516
export const useToolbar_unstable = (props: ToolbarProps, ref: React.Ref<HTMLElement>): ToolbarState => {
17+
const { size = 'medium' } = props;
18+
1619
const arrowNavigationProps = useArrowNavigationGroup({
1720
circular: true,
1821
axis: 'horizontal',
1922
});
2023

21-
const { size = 'medium' } = props;
22-
23-
return {
24+
const initialState: UninitializedToolbarState = {
2425
size,
2526

2627
// TODO add appropriate props/defaults
@@ -36,6 +37,36 @@ export const useToolbar_unstable = (props: ToolbarProps, ref: React.Ref<HTMLElem
3637
...arrowNavigationProps,
3738
...props,
3839
}),
39-
...props,
40+
};
41+
42+
const [checkedValues, setCheckedValues] = useControllableState({
43+
state: initialState.checkedValues,
44+
defaultState: initialState.defaultCheckedValues,
45+
initialState: {},
46+
});
47+
48+
const { onCheckedValueChange } = initialState;
49+
50+
const handleToggleButton: ToggableHandler = useEventCallback(
51+
(e: React.MouseEvent | React.KeyboardEvent, name?: string, value?: string, checked?: boolean) => {
52+
if (name && value) {
53+
const checkedItems = checkedValues?.[name] || [];
54+
const newCheckedItems = [...checkedItems];
55+
if (checked) {
56+
newCheckedItems.splice(newCheckedItems.indexOf(value), 1);
57+
} else {
58+
newCheckedItems.push(value);
59+
}
60+
61+
onCheckedValueChange?.(e, { name, checkedItems: newCheckedItems });
62+
setCheckedValues(s => ({ ...s, [name]: newCheckedItems }));
63+
}
64+
},
65+
);
66+
67+
return {
68+
...initialState,
69+
handleToggleButton,
70+
checkedValues: checkedValues ?? {},
4071
};
4172
};

packages/react-components/react-toolbar/src/components/Toolbar/useToolbarContextValues.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { ToolbarContextValue, ToolbarContextValues, ToolbarState } from './Toolbar.types';
22

33
export function useToolbarContextValues_unstable(state: ToolbarState): ToolbarContextValues {
4-
const { size } = state;
4+
const { size, handleToggleButton } = state;
55
// This context is created with "@fluentui/react-context-selector", these is no sense to memoize it
66
const toolbar: ToolbarContextValue = {
77
size,
8+
handleToggleButton,
89
};
910

1011
return { toolbar };

packages/react-components/react-toolbar/src/components/ToolbarToggleButton/ToolbarToggleButton.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
11
import * as React from 'react';
22
import type { ToolbarToggleButtonProps } from './ToolbarToggleButton.types';
33
import type { ForwardRefComponent } from '@fluentui/react-utilities';
4-
import {
5-
renderToggleButton_unstable,
6-
useToggleButtonStyles_unstable,
7-
useToggleButton_unstable,
8-
} from '@fluentui/react-button';
9-
import { useToolbarContext } from '../Toolbar/ToolbarContext';
4+
import { renderToggleButton_unstable, useToggleButtonStyles_unstable } from '@fluentui/react-button';
5+
import { useToolbarToggleButton_unstable } from './useToolbarToggleButton';
106

117
/**
128
* ToolbarToggleButton component
139
*/
1410
export const ToolbarToggleButton: ForwardRefComponent<ToolbarToggleButtonProps> = React.forwardRef((props, ref) => {
15-
const { size } = useToolbarContext();
16-
17-
const state = useToggleButton_unstable({ size, ...props }, ref);
11+
const state = useToolbarToggleButton_unstable(props, ref);
1812

1913
useToggleButtonStyles_unstable(state);
2014
return renderToggleButton_unstable(state);

packages/react-components/react-toolbar/src/components/ToolbarToggleButton/ToolbarToggleButton.types.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ import { ToggleButtonProps, ButtonSlots, ToggleButtonState } from '@fluentui/rea
55
* ToolbarToggleButton Props
66
*/
77
export type ToolbarToggleButtonProps = ComponentProps<ButtonSlots> &
8-
Partial<Pick<ToggleButtonProps, 'disabled' | 'disabledFocusable'>> & {
8+
Partial<Pick<ToggleButtonProps, 'disabled' | 'disabledFocusable' | 'size'>> & {
99
appearance?: 'primary' | 'subtle';
1010
};
1111

1212
/**
1313
* State used in rendering ToolbarToggleButton
1414
*/
15-
export type ToolbarToggleButtonState = ComponentState<Partial<ButtonSlots>> & ToggleButtonState;
15+
export type ToolbarToggleButtonState = ComponentState<Partial<ButtonSlots>> &
16+
ToggleButtonState &
17+
Required<Pick<ToggleButtonProps, 'checked'>> & {
18+
name?: string;
19+
value?: string;
20+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as React from 'react';
2+
import { useToggleButton_unstable } from '@fluentui/react-button';
3+
import { useToolbarContext } from '../Toolbar/ToolbarContext';
4+
import { ToolbarToggleButtonProps, ToolbarToggleButtonState } from './ToolbarToggleButton.types';
5+
6+
/**
7+
* Given user props, defines default props for the ToggleButton, calls useButtonState and useChecked, and returns
8+
* processed state.
9+
* @param props - User provided props to the ToggleButton component.
10+
* @param ref - User provided ref to be passed to the ToggleButton component.
11+
*/
12+
export const useToolbarToggleButton_unstable = (
13+
props: ToolbarToggleButtonProps,
14+
ref: React.Ref<HTMLButtonElement | HTMLAnchorElement>,
15+
): ToolbarToggleButtonState => {
16+
const { handleToggleButton, size } = useToolbarContext();
17+
const { onClick: onClickOriginal } = props;
18+
const state = useToggleButton_unstable({ size, ...props }, ref) as ToolbarToggleButtonState;
19+
20+
const handleOnClick = (
21+
e: React.MouseEvent<HTMLButtonElement, MouseEvent> & React.MouseEvent<HTMLAnchorElement, MouseEvent>,
22+
) => {
23+
if (state.disabled) {
24+
e.preventDefault();
25+
e.stopPropagation();
26+
return;
27+
}
28+
29+
handleToggleButton?.(e, state.name, state.value, state.checked);
30+
onClickOriginal?.(e);
31+
};
32+
33+
state.root.onClick = handleOnClick;
34+
return state;
35+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as React from 'react';
2+
import { Toolbar, ToolbarToggleButton, ToolbarProps } from '@fluentui/react-toolbar';
3+
4+
export const ControlledToggleButton = () => {
5+
const [checkedValues, setCheckedValues] = React.useState<Record<string, string[]>>({ edit: ['cut', 'paste'] });
6+
const onChange: ToolbarProps['onCheckedValueChange'] = (e, { name, checkedItems }) => {
7+
setCheckedValues(s => {
8+
return s ? { ...s, [name]: checkedItems } : { [name]: checkedItems };
9+
});
10+
};
11+
12+
return (
13+
<Toolbar checkedValues={checkedValues} onCheckedValueChange={onChange}>
14+
<ToolbarToggleButton name="group">Enable Group</ToolbarToggleButton>
15+
<ToolbarToggleButton name="group">Enable Group</ToolbarToggleButton>
16+
</Toolbar>
17+
);
18+
};

0 commit comments

Comments
 (0)