Skip to content

Commit 009b3f7

Browse files
committed
feat(react-headless-components-preview): implement TagPicker
1 parent fdf755c commit 009b3f7

54 files changed

Lines changed: 2226 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.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "feat: add headless TagPicker",
4+
"packageName": "@fluentui/react-headless-components-preview",
5+
"email": "vgenaev@gmail.com",
6+
"dependentChangeType": "patch"
7+
}

packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import * as Switch from '@fluentui/react-headless-components-preview/switch';
3737
import * as TabList from '@fluentui/react-headless-components-preview/tab-list';
3838
import * as Tag from '@fluentui/react-headless-components-preview/tag';
3939
import * as TagGroup from '@fluentui/react-headless-components-preview/tag-group';
40+
import * as TagPicker from '@fluentui/react-headless-components-preview/tag-picker';
4041
import * as TeachingPopover from '@fluentui/react-headless-components-preview/teaching-popover';
4142
import * as Textarea from '@fluentui/react-headless-components-preview/textarea';
4243
import * as Toast from '@fluentui/react-headless-components-preview/toast';
@@ -84,6 +85,7 @@ console.log({
8485
TabList,
8586
Tag,
8687
TagGroup,
88+
TagPicker,
8789
TeachingPopover,
8890
Textarea,
8991
Toast,
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
## API Report File for "@fluentui/react-headless-components-preview"
2+
3+
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4+
5+
```ts
6+
7+
import type { ComponentProps } from '@fluentui/react-utilities';
8+
import type { ComponentState } from '@fluentui/react-utilities';
9+
import type { ForwardRefComponent } from '@fluentui/react-utilities';
10+
import { JSXElement } from '@fluentui/react-utilities';
11+
import type { ListboxProps as ListboxProps_2 } from '@fluentui/react-combobox';
12+
import type { OptionGroupProps } from '@fluentui/react-combobox';
13+
import type { OptionGroupSlots } from '@fluentui/react-combobox';
14+
import { OptionGroupState } from '@fluentui/react-combobox';
15+
import type { OptionProps as OptionProps_2 } from '@fluentui/react-combobox';
16+
import type { OptionSlots as OptionSlots_2 } from '@fluentui/react-combobox';
17+
import type { OptionState as OptionState_2 } from '@fluentui/react-combobox';
18+
import type * as React_2 from 'react';
19+
import { renderTagPicker_unstable as renderTagPicker } from '@fluentui/react-tag-picker';
20+
import type { Slot } from '@fluentui/react-utilities';
21+
import type { TagGroupBaseState } from '@fluentui/react-tags';
22+
import type { TagGroupContextValues } from '@fluentui/react-tags';
23+
import type { TagPickerButtonBaseState } from '@fluentui/react-tag-picker';
24+
import { TagPickerButtonBaseProps as TagPickerButtonProps } from '@fluentui/react-tag-picker';
25+
import { TagPickerButtonSlots } from '@fluentui/react-tag-picker';
26+
import { TagPickerContextValue } from '@fluentui/react-tag-picker';
27+
import { TagPickerContextValues } from '@fluentui/react-tag-picker';
28+
import type { TagPickerControlBaseState } from '@fluentui/react-tag-picker';
29+
import { TagPickerControlProps } from '@fluentui/react-tag-picker';
30+
import { TagPickerControlSlots } from '@fluentui/react-tag-picker';
31+
import type { TagPickerGroupSlots } from '@fluentui/react-tag-picker';
32+
import type { TagPickerInputBaseState } from '@fluentui/react-tag-picker';
33+
import { TagPickerInputBaseProps as TagPickerInputProps } from '@fluentui/react-tag-picker';
34+
import { TagPickerInputSlots } from '@fluentui/react-tag-picker';
35+
import { TagPickerOnOpenChangeData } from '@fluentui/react-tag-picker';
36+
import { TagPickerOnOptionSelectData } from '@fluentui/react-tag-picker';
37+
import type { TagPickerProps as TagPickerProps_2 } from '@fluentui/react-tag-picker';
38+
import { TagPickerSize } from '@fluentui/react-tag-picker';
39+
import { TagPickerSlots } from '@fluentui/react-tag-picker';
40+
import { TagPickerState } from '@fluentui/react-tag-picker';
41+
import { useTagPickerContext_unstable } from '@fluentui/react-tag-picker';
42+
import { useTagPickerFilter } from '@fluentui/react-tag-picker';
43+
44+
export { renderTagPicker }
45+
46+
// @public
47+
export const renderTagPickerButton: (state: TagPickerButtonState) => JSXElement;
48+
49+
// @public
50+
export const renderTagPickerControl: (state: TagPickerControlState) => JSXElement;
51+
52+
// @public
53+
export function renderTagPickerGroup(state: TagPickerGroupState, contextValues: TagGroupContextValues): JSXElement | null;
54+
55+
// @public
56+
export const renderTagPickerInput: (state: TagPickerInputState) => JSXElement;
57+
58+
// @public
59+
export const renderTagPickerList: (state: TagPickerListState) => JSXElement;
60+
61+
// @public
62+
export const renderTagPickerOption: (state: TagPickerOptionState) => JSXElement;
63+
64+
// @public
65+
export const renderTagPickerOptionGroup: (state: OptionGroupState) => JSXElement;
66+
67+
// @public (undocumented)
68+
export const TagPicker: ForwardRefComponent<TagPickerProps>;
69+
70+
// @public
71+
export const TagPickerButton: ForwardRefComponent<TagPickerButtonProps>;
72+
73+
export { TagPickerButtonProps }
74+
75+
export { TagPickerButtonSlots }
76+
77+
// @public
78+
export type TagPickerButtonState = TagPickerButtonBaseState & {
79+
root: {
80+
'data-disabled'?: string;
81+
};
82+
};
83+
84+
export { TagPickerContextValue }
85+
86+
export { TagPickerContextValues }
87+
88+
// @public
89+
export const TagPickerControl: ForwardRefComponent<TagPickerControlProps>;
90+
91+
// @public
92+
export type TagPickerControlInternalSlots = {
93+
aside?: NonNullable<Slot<'span'>>;
94+
};
95+
96+
export { TagPickerControlProps }
97+
98+
export { TagPickerControlSlots }
99+
100+
// @public
101+
export type TagPickerControlState = TagPickerControlBaseState & {
102+
root: {
103+
'data-disabled'?: string;
104+
'data-invalid'?: string;
105+
};
106+
};
107+
108+
// @public
109+
export const TagPickerGroup: ForwardRefComponent<TagPickerGroupProps>;
110+
111+
// @public
112+
export type TagPickerGroupProps = ComponentProps<TagPickerGroupSlots>;
113+
114+
export { TagPickerGroupSlots }
115+
116+
// @public
117+
export type TagPickerGroupState = TagGroupBaseState & {
118+
hasSelectedOptions: boolean;
119+
root: {
120+
focusgroup?: string;
121+
'data-disabled'?: string;
122+
};
123+
};
124+
125+
// @public
126+
export const TagPickerInput: ForwardRefComponent<TagPickerInputProps>;
127+
128+
export { TagPickerInputProps }
129+
130+
export { TagPickerInputSlots }
131+
132+
// @public
133+
export type TagPickerInputState = TagPickerInputBaseState & {
134+
root: {
135+
'data-disabled'?: string;
136+
};
137+
};
138+
139+
// @public
140+
export const TagPickerList: ForwardRefComponent<TagPickerListProps>;
141+
142+
// @public
143+
export type TagPickerListProps = ComponentProps<TagPickerListSlots>;
144+
145+
// @public (undocumented)
146+
export type TagPickerListSlots = {
147+
root: Slot<typeof Listbox>;
148+
};
149+
150+
// @public
151+
export type TagPickerListState = ComponentState<TagPickerListSlots> & {
152+
open: boolean;
153+
};
154+
155+
export { TagPickerOnOpenChangeData }
156+
157+
export { TagPickerOnOptionSelectData }
158+
159+
// @public
160+
export const TagPickerOption: ForwardRefComponent<TagPickerOptionProps>;
161+
162+
// @public
163+
export const TagPickerOptionGroup: ForwardRefComponent<TagPickerOptionGroupProps>;
164+
165+
// @public
166+
export type TagPickerOptionGroupProps = OptionGroupProps;
167+
168+
// @public (undocumented)
169+
export type TagPickerOptionGroupSlots = OptionGroupSlots;
170+
171+
// @public
172+
export type TagPickerOptionGroupState = OptionGroupState;
173+
174+
// @public
175+
export type TagPickerOptionProps = OptionProps & {
176+
media?: Slot<'span'>;
177+
secondaryContent?: Slot<'span'>;
178+
};
179+
180+
// @public (undocumented)
181+
export type TagPickerOptionSlots = OptionSlots & {
182+
media?: Slot<'span'>;
183+
secondaryContent?: Slot<'span'>;
184+
};
185+
186+
// @public
187+
export type TagPickerOptionState = OptionState & {
188+
components: OptionState['components'] & {
189+
media: 'span';
190+
secondaryContent: 'span';
191+
};
192+
media?: Slot<'span'>;
193+
secondaryContent?: Slot<'span'>;
194+
};
195+
196+
// @public (undocumented)
197+
export type TagPickerProps = Omit<TagPickerProps_2, 'inline' | 'size' | 'appearance' | 'mountNode'>;
198+
199+
export { TagPickerSize }
200+
201+
export { TagPickerSlots }
202+
203+
export { TagPickerState }
204+
205+
// @public
206+
export const useTagPicker: (props: TagPickerProps) => TagPickerState;
207+
208+
// @public
209+
export const useTagPickerButton: (props: TagPickerButtonProps, ref: React_2.Ref<HTMLButtonElement>) => TagPickerButtonState;
210+
211+
export { useTagPickerContext_unstable }
212+
213+
// @public
214+
export function useTagPickerContextValues(state: TagPickerState): TagPickerContextValues;
215+
216+
// @public
217+
export const useTagPickerControl: (props: TagPickerControlProps, ref: React_2.Ref<HTMLDivElement>) => TagPickerControlState;
218+
219+
export { useTagPickerFilter }
220+
221+
// @public
222+
export const useTagPickerGroup: (props: TagPickerGroupProps, ref: React_2.Ref<HTMLDivElement>) => TagPickerGroupState;
223+
224+
// @public
225+
export const useTagPickerInput: (props: TagPickerInputProps, ref: React_2.Ref<HTMLInputElement>) => TagPickerInputState;
226+
227+
// @public
228+
export const useTagPickerList: (props: TagPickerListProps, ref: React_2.Ref<HTMLDivElement>) => TagPickerListState;
229+
230+
// @public
231+
export const useTagPickerOption: (props: TagPickerOptionProps, ref: React_2.Ref<HTMLElement>) => TagPickerOptionState;
232+
233+
// @public
234+
export const useTagPickerOptionGroup: (props: TagPickerOptionGroupProps, ref: React_2.Ref<HTMLElement>) => TagPickerOptionGroupState;
235+
236+
// (No @packageDocumentation comment for this package)
237+
238+
```

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@fluentui/react-switch": "^9.7.3",
5959
"@fluentui/react-tabs": "^9.12.2",
6060
"@fluentui/react-tabster": "^9.26.15",
61+
"@fluentui/react-tag-picker": "^9.8.8",
6162
"@fluentui/react-tags": "^9.9.1",
6263
"@fluentui/react-textarea": "^9.7.3",
6364
"@fluentui/react-toolbar": "^9.8.1",
@@ -313,6 +314,12 @@
313314
"import": "./lib/tag-group.js",
314315
"require": "./lib-commonjs/tag-group.js"
315316
},
317+
"./tag-picker": {
318+
"types": "./dist/tag-picker.d.ts",
319+
"node": "./lib-commonjs/tag-picker.js",
320+
"import": "./lib/tag-picker.js",
321+
"require": "./lib-commonjs/tag-picker.js"
322+
},
316323
"./teaching-popover": {
317324
"types": "./dist/teaching-popover.d.ts",
318325
"node": "./lib-commonjs/teaching-popover.js",
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import * as React from 'react';
2+
import { render } from '@testing-library/react';
3+
import { TagPicker } from './TagPicker';
4+
import { TagPickerControl } from './TagPickerControl';
5+
import { TagPickerGroup } from './TagPickerGroup';
6+
import { TagPickerInput } from './TagPickerInput';
7+
import { TagPickerList } from './TagPickerList';
8+
import { TagPickerOption } from './TagPickerOption';
9+
import { optionClassNames } from '@fluentui/react-combobox';
10+
import { Tag } from '../Tag';
11+
12+
const renderTagPicker = (props: { selectedOptions?: string[]; disabled?: boolean } = {}) => {
13+
const { selectedOptions = [], disabled } = props;
14+
return render(
15+
<TagPicker open disabled={disabled} selectedOptions={selectedOptions}>
16+
<TagPickerControl>
17+
<TagPickerGroup aria-label="Selected animals">
18+
{selectedOptions.map(option => (
19+
<Tag key={option} value={option}>
20+
{option}
21+
</Tag>
22+
))}
23+
</TagPickerGroup>
24+
<TagPickerInput aria-label="Select animals" />
25+
</TagPickerControl>
26+
<TagPickerList>
27+
<TagPickerOption>Cat</TagPickerOption>
28+
<TagPickerOption disabled>Ferret</TagPickerOption>
29+
<TagPickerOption>Dog</TagPickerOption>
30+
</TagPickerList>
31+
</TagPicker>,
32+
);
33+
};
34+
35+
describe('TagPicker', () => {
36+
it('renders the input trigger and the options list when open', () => {
37+
const { getByRole, getAllByRole } = renderTagPicker();
38+
39+
expect(getByRole('combobox')).toBeInTheDocument();
40+
expect(getByRole('listbox')).toBeInTheDocument();
41+
expect(getAllByRole('option')).toHaveLength(3);
42+
});
43+
44+
it('sets data-disabled on disabled options', () => {
45+
const { getAllByRole } = renderTagPicker();
46+
const options = getAllByRole('option');
47+
48+
expect(options[0]).not.toHaveAttribute('data-disabled');
49+
expect(options[1]).toHaveAttribute('data-disabled');
50+
expect(options[2]).not.toHaveAttribute('data-disabled');
51+
});
52+
53+
it('marks options with the option class so active-descendant arrow navigation can find them', () => {
54+
const { getAllByRole } = renderTagPicker();
55+
56+
getAllByRole('option').forEach(option => {
57+
expect(option).toHaveClass(optionClassNames.root);
58+
});
59+
});
60+
61+
it('renders selected options as tags and applies the focusgroup attribute on the group', () => {
62+
const { getByRole } = renderTagPicker({ selectedOptions: ['Dog'] });
63+
64+
const group = getByRole('listbox', { name: 'Selected animals' });
65+
expect(group).toHaveAttribute('focusgroup', 'toolbar inline wrap');
66+
expect(group).toHaveTextContent('Dog');
67+
});
68+
69+
it('does not render the group when nothing is selected', () => {
70+
const { queryByRole } = renderTagPicker({ selectedOptions: [] });
71+
72+
expect(queryByRole('listbox', { name: 'Selected animals' })).not.toBeInTheDocument();
73+
});
74+
75+
it('sets data-disabled on the control when disabled', () => {
76+
const { getByRole } = renderTagPicker({ disabled: true });
77+
78+
expect(getByRole('combobox')).toBeDisabled();
79+
});
80+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use client';
2+
3+
import * as React from 'react';
4+
import type { ForwardRefComponent } from '@fluentui/react-utilities';
5+
6+
import { useTagPicker } from './useTagPicker';
7+
import { renderTagPicker } from './renderTagPicker';
8+
import { useTagPickerContextValues } from './useTagPickerContextValues';
9+
import type { TagPickerProps } from './TagPicker.types';
10+
11+
export const TagPicker: ForwardRefComponent<TagPickerProps> = React.forwardRef((props, _ref) => {
12+
const state = useTagPicker(props);
13+
const contextValues = useTagPickerContextValues(state);
14+
15+
return renderTagPicker(state, contextValues);
16+
});
17+
18+
TagPicker.displayName = 'TagPicker';
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { TagPickerProps as TagPickerPropsBase } from '@fluentui/react-tag-picker';
2+
3+
export type {
4+
TagPickerState,
5+
TagPickerContextValues,
6+
TagPickerSlots,
7+
TagPickerSize,
8+
TagPickerOnOpenChangeData,
9+
TagPickerOnOptionSelectData,
10+
} from '@fluentui/react-tag-picker';
11+
12+
export type TagPickerProps = Omit<TagPickerPropsBase, 'inline' | 'size' | 'appearance' | 'mountNode'>;

0 commit comments

Comments
 (0)