Skip to content

Commit c36cff5

Browse files
authored
feat(react-headless-components-preview): add Toolbar component (#35992)
1 parent 86cf4cb commit c36cff5

41 files changed

Lines changed: 1028 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,21 @@ import { TextareaBaseState } from '@fluentui/react-textarea';
142142
import type { TextareaSlots as TextareaSlots_2 } from '@fluentui/react-textarea';
143143
import type { ToggleButtonBaseProps } from '@fluentui/react-button';
144144
import type { ToggleButtonBaseState } from '@fluentui/react-button';
145+
import type { ToolbarBaseProps } from '@fluentui/react-toolbar';
146+
import { ToolbarBaseState } from '@fluentui/react-toolbar';
147+
import type { ToolbarButtonBaseProps } from '@fluentui/react-toolbar';
148+
import type { ToolbarButtonBaseState } from '@fluentui/react-toolbar';
149+
import { ToolbarContextValue } from '@fluentui/react-toolbar';
150+
import { ToolbarContextValues as ToolbarContextValues_2 } from '@fluentui/react-toolbar';
151+
import type { ToolbarDividerBaseProps } from '@fluentui/react-toolbar';
152+
import type { ToolbarDividerBaseState } from '@fluentui/react-toolbar';
153+
import type { ToolbarGroupProps as ToolbarGroupProps_2 } from '@fluentui/react-toolbar';
154+
import { ToolbarGroupState as ToolbarGroupState_2 } from '@fluentui/react-toolbar';
155+
import type { ToolbarRadioGroupProps as ToolbarRadioGroupProps_2 } from '@fluentui/react-toolbar';
156+
import type { ToolbarRadioGroupState as ToolbarRadioGroupState_2 } from '@fluentui/react-toolbar';
157+
import type { ToolbarSlots as ToolbarSlots_2 } from '@fluentui/react-toolbar';
158+
import type { ToolbarToggleButtonBaseProps } from '@fluentui/react-toolbar';
159+
import type { ToolbarToggleButtonBaseState } from '@fluentui/react-toolbar';
145160
import { useMessageBarBodyContextValues_unstable } from '@fluentui/react-message-bar';
146161
import { useFluent_unstable as useProviderContext } from '@fluentui/react-shared-contexts';
147162

@@ -679,6 +694,24 @@ export const renderTextarea: (state: TextareaBaseState) => JSXElement;
679694
// @public
680695
export const renderToggleButton: (state: ButtonBaseState) => JSXElement;
681696

697+
// @public
698+
export const renderToolbar: (state: ToolbarBaseState, contextValues: ToolbarContextValues_2) => JSXElement;
699+
700+
// @public
701+
export const renderToolbarButton: (state: ButtonBaseState) => JSXElement;
702+
703+
// @public
704+
export const renderToolbarDivider: (state: DividerBaseState) => JSXElement;
705+
706+
// @public
707+
export const renderToolbarGroup: (state: ToolbarGroupState_2) => JSXElement;
708+
709+
// @public
710+
export const renderToolbarRadioGroup: (state: ToolbarGroupState_2) => JSXElement;
711+
712+
// @public
713+
export const renderToolbarToggleButton: (state: ButtonBaseState) => JSXElement;
714+
682715
// @public
683716
export const SearchBox: ForwardRefComponent<SearchBoxProps>;
684717

@@ -872,6 +905,98 @@ export type ToggleButtonState = ToggleButtonBaseState & {
872905
};
873906
};
874907

908+
// @public
909+
export const Toolbar: ForwardRefComponent<ToolbarProps>;
910+
911+
// @public
912+
export const ToolbarButton: ForwardRefComponent<ToolbarButtonProps>;
913+
914+
// @public (undocumented)
915+
export type ToolbarButtonProps = ToolbarButtonBaseProps;
916+
917+
// @public (undocumented)
918+
export type ToolbarButtonState = ToolbarButtonBaseState & {
919+
root: {
920+
'data-vertical'?: string;
921+
'data-disabled'?: string;
922+
'data-disabled-focusable'?: string;
923+
'data-icon-only'?: string;
924+
};
925+
};
926+
927+
// @public (undocumented)
928+
export type ToolbarContextValues = ToolbarContextValues_2;
929+
930+
// @public
931+
export const ToolbarDivider: ForwardRefComponent<ToolbarDividerProps>;
932+
933+
// @public (undocumented)
934+
export type ToolbarDividerProps = ToolbarDividerBaseProps;
935+
936+
// @public (undocumented)
937+
export type ToolbarDividerState = ToolbarDividerBaseState & {
938+
root: {
939+
'data-vertical'?: string;
940+
};
941+
};
942+
943+
// @public
944+
export const ToolbarGroup: ForwardRefComponent<ToolbarGroupProps>;
945+
946+
// @public (undocumented)
947+
export type ToolbarGroupProps = ToolbarGroupProps_2;
948+
949+
// @public (undocumented)
950+
export type ToolbarGroupState = ToolbarGroupState_2 & {
951+
root: {
952+
'data-vertical'?: string;
953+
};
954+
};
955+
956+
// @public (undocumented)
957+
export type ToolbarProps = ToolbarBaseProps;
958+
959+
// @public
960+
export const ToolbarRadioGroup: ForwardRefComponent<ToolbarRadioGroupProps>;
961+
962+
// @public (undocumented)
963+
export type ToolbarRadioGroupProps = ToolbarRadioGroupProps_2;
964+
965+
// @public (undocumented)
966+
export type ToolbarRadioGroupState = ToolbarRadioGroupState_2 & {
967+
vertical?: boolean;
968+
root: {
969+
'data-vertical'?: string;
970+
};
971+
};
972+
973+
// @public (undocumented)
974+
export type ToolbarSlots = ToolbarSlots_2;
975+
976+
// @public (undocumented)
977+
export type ToolbarState = ToolbarBaseState & {
978+
root: {
979+
'data-vertical'?: string;
980+
focusgroup?: string;
981+
};
982+
};
983+
984+
// @public
985+
export const ToolbarToggleButton: ForwardRefComponent<ToolbarToggleButtonProps>;
986+
987+
// @public (undocumented)
988+
export type ToolbarToggleButtonProps = ToolbarToggleButtonBaseProps;
989+
990+
// @public (undocumented)
991+
export type ToolbarToggleButtonState = ToolbarToggleButtonBaseState & {
992+
root: {
993+
'data-disabled'?: string;
994+
'data-disabled-focusable'?: string;
995+
'data-icon-only'?: string;
996+
'data-checked'?: string;
997+
};
998+
};
999+
8751000
// @public
8761001
export const useAccordion: (props: AccordionProps, ref: React_2.Ref<HTMLElement>) => AccordionState;
8771002

@@ -1015,6 +1140,30 @@ export const useTextarea: (props: TextareaProps, ref: React_2.Ref<HTMLTextAreaEl
10151140
// @public
10161141
export const useToggleButton: (props: ToggleButtonProps, ref: React_2.Ref<HTMLButtonElement | HTMLAnchorElement>) => ToggleButtonState;
10171142

1143+
// @public
1144+
export const useToolbar: (props: ToolbarProps, ref: React_2.Ref<HTMLElement>) => ToolbarState;
1145+
1146+
// @public
1147+
export const useToolbarButton: (props: ToolbarButtonProps, ref: React_2.Ref<HTMLButtonElement | HTMLAnchorElement>) => ToolbarButtonState;
1148+
1149+
// @public
1150+
export const useToolbarContext: <T>(selector: ContextSelector<ToolbarContextValue, T>) => T;
1151+
1152+
// @public
1153+
export const useToolbarContextValues: (state: ToolbarState) => ToolbarContextValues;
1154+
1155+
// @public
1156+
export const useToolbarDivider: (props: ToolbarDividerProps, ref: React_2.Ref<HTMLElement>) => ToolbarDividerState;
1157+
1158+
// @public
1159+
export const useToolbarGroup: (props: ToolbarGroupProps, ref: React_2.Ref<HTMLDivElement>) => ToolbarGroupState;
1160+
1161+
// @public
1162+
export const useToolbarRadioGroup: (props: ToolbarRadioGroupProps, ref: React_2.Ref<HTMLDivElement>) => ToolbarRadioGroupState;
1163+
1164+
// @public
1165+
export const useToolbarToggleButton: (props: ToolbarToggleButtonProps, ref: React_2.Ref<HTMLButtonElement | HTMLAnchorElement>) => ToolbarToggleButtonState;
1166+
10181167
// (No @packageDocumentation comment for this package)
10191168

10201169
```

packages/react-components/react-headless-components-preview/library/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@fluentui/react-tabs": "^9.11.2",
5050
"@fluentui/react-tags": "^9.7.19",
5151
"@fluentui/react-textarea": "^9.7.0",
52+
"@fluentui/react-toolbar": "^9.7.7",
5253
"@fluentui/react-utilities": "^9.26.2",
5354
"@swc/helpers": "^0.5.1"
5455
},
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export { Toolbar, renderToolbar, useToolbar, useToolbarContext, useToolbarContextValues } from './components/Toolbar';
2+
export type { ToolbarSlots, ToolbarProps, ToolbarState, ToolbarContextValues } from './components/Toolbar';
3+
4+
export { ToolbarButton, renderToolbarButton, useToolbarButton } from './components/Toolbar';
5+
export type { ToolbarButtonProps, ToolbarButtonState } from './components/Toolbar';
6+
7+
export { ToolbarDivider, renderToolbarDivider, useToolbarDivider } from './components/Toolbar';
8+
export type { ToolbarDividerProps, ToolbarDividerState } from './components/Toolbar';
9+
10+
export { ToolbarGroup, renderToolbarGroup, useToolbarGroup } from './components/Toolbar';
11+
export type { ToolbarGroupProps, ToolbarGroupState } from './components/Toolbar';
12+
13+
export { ToolbarRadioGroup, renderToolbarRadioGroup, useToolbarRadioGroup } from './components/Toolbar';
14+
export type { ToolbarRadioGroupProps, ToolbarRadioGroupState } from './components/Toolbar';
15+
16+
export { ToolbarToggleButton, renderToolbarToggleButton, useToolbarToggleButton } from './components/Toolbar';
17+
export type { ToolbarToggleButtonProps, ToolbarToggleButtonState } from './components/Toolbar';
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as React from 'react';
2+
import { render } from '@testing-library/react';
3+
import { Toolbar } from '.';
4+
import { ToolbarButton } from './ToolbarButton';
5+
import { ToolbarToggleButton } from './ToolbarToggleButton';
6+
7+
describe('Toolbar', () => {
8+
it('renders unchecked by default', () => {
9+
const { getByRole, getAllByRole } = render(
10+
<Toolbar>
11+
<ToolbarButton name="format" value="bold">
12+
Bold
13+
</ToolbarButton>
14+
<ToolbarToggleButton name="format" value="italic">
15+
Italic
16+
</ToolbarToggleButton>
17+
</Toolbar>,
18+
);
19+
20+
expect(getByRole('toolbar')).toHaveAttribute('focusgroup', 'toolbar inline wrap');
21+
expect(getByRole('toolbar')).not.toHaveAttribute('data-vertical');
22+
23+
expect(getAllByRole('button')).toHaveLength(2);
24+
});
25+
26+
it('renders checked when value is in toolbar checkedValues', () => {
27+
const { getByRole } = render(
28+
<Toolbar defaultCheckedValues={{ format: ['bold'] }} vertical>
29+
<ToolbarToggleButton name="format" value="bold">
30+
Bold
31+
</ToolbarToggleButton>
32+
<ToolbarToggleButton name="format" value="italic" disabled>
33+
Italic
34+
</ToolbarToggleButton>
35+
<ToolbarToggleButton name="format" value="underline" icon="U" aria-label="Underline" />
36+
</Toolbar>,
37+
);
38+
39+
expect(getByRole('toolbar')).toHaveAttribute('focusgroup', 'toolbar block wrap');
40+
expect(getByRole('toolbar')).toHaveAttribute('data-vertical');
41+
42+
const button1 = getByRole('button', { name: 'Bold' });
43+
expect(button1).toHaveAttribute('aria-pressed', 'true');
44+
expect(button1).toHaveAttribute('data-checked');
45+
46+
const button2 = getByRole('button', { name: 'Italic' });
47+
expect(button2).toHaveAttribute('aria-pressed', 'false');
48+
expect(button2).not.toHaveAttribute('data-checked');
49+
expect(button2).toBeDisabled();
50+
expect(button2).toHaveAttribute('data-disabled');
51+
52+
const button3 = getByRole('button', { name: 'Underline' });
53+
expect(button3).toHaveAttribute('aria-pressed', 'false');
54+
expect(button3).not.toHaveAttribute('data-checked');
55+
expect(button3).not.toBeDisabled();
56+
expect(button3).toHaveAttribute('data-icon-only');
57+
});
58+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use client';
2+
3+
import * as React from 'react';
4+
import type { ForwardRefComponent } from '@fluentui/react-utilities';
5+
import type { ToolbarProps } from './Toolbar.types';
6+
import { useToolbar, useToolbarContextValues } from './useToolbar';
7+
import { renderToolbar } from './renderToolbar';
8+
9+
/**
10+
* A toolbar component that provides a container for grouping a set of controls such as buttons and menu items.
11+
*/
12+
export const Toolbar: ForwardRefComponent<ToolbarProps> = React.forwardRef((props, ref) => {
13+
const state = useToolbar(props, ref);
14+
const contextValues = useToolbarContextValues(state);
15+
16+
return renderToolbar(state, contextValues);
17+
});
18+
19+
Toolbar.displayName = 'Toolbar';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type {
2+
ToolbarSlots as ToolbarBaseSlots,
3+
ToolbarBaseProps,
4+
ToolbarContextValues as ToolbarBaseContextValues,
5+
ToolbarBaseState,
6+
} from '@fluentui/react-toolbar';
7+
8+
export type ToolbarSlots = ToolbarBaseSlots;
9+
10+
export type ToolbarProps = ToolbarBaseProps;
11+
12+
export type ToolbarState = ToolbarBaseState & {
13+
root: {
14+
/**
15+
* Data attribute set when the toolbar is vertically oriented.
16+
*/
17+
'data-vertical'?: string;
18+
19+
/**
20+
* Data attribute to define the focus behavior of the toolbar's children
21+
*/
22+
focusgroup?: string;
23+
};
24+
};
25+
26+
export type ToolbarContextValues = ToolbarBaseContextValues;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use client';
2+
3+
import * as React from 'react';
4+
import type { ForwardRefComponent } from '@fluentui/react-utilities';
5+
import type { ToolbarButtonProps } from './ToolbarButton.types';
6+
import { useToolbarButton } from './useToolbarButton';
7+
import { renderToolbarButton } from './renderToolbarButton';
8+
9+
/**
10+
* A button component designed to be used inside a Toolbar, inheriting toolbar context such as size.
11+
*/
12+
export const ToolbarButton: ForwardRefComponent<ToolbarButtonProps> = React.forwardRef((props, ref) => {
13+
const state = useToolbarButton(props, ref);
14+
15+
return renderToolbarButton(state);
16+
// Casting is required due to lack of distributive union to support unions on @types/react
17+
}) as ForwardRefComponent<ToolbarButtonProps>;
18+
19+
ToolbarButton.displayName = 'ToolbarButton';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { ToolbarButtonBaseProps, ToolbarButtonBaseState } from '@fluentui/react-toolbar';
2+
3+
export type ToolbarButtonProps = ToolbarButtonBaseProps;
4+
5+
export type ToolbarButtonState = ToolbarButtonBaseState & {
6+
root: {
7+
/**
8+
* Data attribute set when the button is in a vertically oriented toolbar.
9+
*/
10+
'data-vertical'?: string;
11+
12+
/**
13+
* Data attribute set when the button is disabled.
14+
*/
15+
'data-disabled'?: string;
16+
17+
/**
18+
* Data attribute set when the button is disabled but still focusable.
19+
*/
20+
'data-disabled-focusable'?: string;
21+
22+
/**
23+
* Data attribute set when the button renders only an icon.
24+
*/
25+
'data-icon-only'?: string;
26+
};
27+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { ToolbarButton } from './ToolbarButton';
2+
export { renderToolbarButton } from './renderToolbarButton';
3+
export { useToolbarButton } from './useToolbarButton';
4+
export type { ToolbarButtonProps, ToolbarButtonState } from './ToolbarButton.types';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { renderButton_unstable } from '@fluentui/react-button';
2+
3+
/**
4+
* Renders the final JSX of the ToolbarButton component, given the state.
5+
*/
6+
export const renderToolbarButton = renderButton_unstable;

0 commit comments

Comments
 (0)