Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "feat: add headless TagPicker",
"packageName": "@fluentui/react-headless-components-preview",
"email": "vgenaev@gmail.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import * as Switch from '@fluentui/react-headless-components-preview/switch';
import * as TabList from '@fluentui/react-headless-components-preview/tab-list';
import * as Tag from '@fluentui/react-headless-components-preview/tag';
import * as TagGroup from '@fluentui/react-headless-components-preview/tag-group';
import * as TagPicker from '@fluentui/react-headless-components-preview/tag-picker';
import * as TeachingPopover from '@fluentui/react-headless-components-preview/teaching-popover';
import * as Textarea from '@fluentui/react-headless-components-preview/textarea';
import * as Toast from '@fluentui/react-headless-components-preview/toast';
Expand Down Expand Up @@ -84,6 +85,7 @@ console.log({
TabList,
Tag,
TagGroup,
TagPicker,
TeachingPopover,
Textarea,
Toast,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
## API Report File for "@fluentui/react-headless-components-preview"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).

```ts

import type { ComponentProps } from '@fluentui/react-utilities';
import type { ComponentState } from '@fluentui/react-utilities';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
import { JSXElement } from '@fluentui/react-utilities';
import type { ListboxProps as ListboxProps_2 } from '@fluentui/react-combobox';
import type { OptionGroupProps } from '@fluentui/react-combobox';
import type { OptionGroupSlots } from '@fluentui/react-combobox';
import { OptionGroupState } from '@fluentui/react-combobox';
import type { OptionProps as OptionProps_2 } from '@fluentui/react-combobox';
import type { OptionSlots as OptionSlots_2 } from '@fluentui/react-combobox';
import type { OptionState as OptionState_2 } from '@fluentui/react-combobox';
import type * as React_2 from 'react';
import { renderTagPicker_unstable as renderTagPicker } from '@fluentui/react-tag-picker';
import type { Slot } from '@fluentui/react-utilities';
import type { TagGroupBaseState } from '@fluentui/react-tags';
import type { TagGroupContextValues } from '@fluentui/react-tags';
import type { TagPickerButtonBaseState } from '@fluentui/react-tag-picker';
import { TagPickerButtonBaseProps as TagPickerButtonProps } from '@fluentui/react-tag-picker';
import { TagPickerButtonSlots } from '@fluentui/react-tag-picker';
import { TagPickerContextValue } from '@fluentui/react-tag-picker';
import { TagPickerContextValues } from '@fluentui/react-tag-picker';
import type { TagPickerControlBaseState } from '@fluentui/react-tag-picker';
import { TagPickerControlProps } from '@fluentui/react-tag-picker';
import { TagPickerControlSlots } from '@fluentui/react-tag-picker';
import type { TagPickerGroupSlots } from '@fluentui/react-tag-picker';
import type { TagPickerInputBaseState } from '@fluentui/react-tag-picker';
import { TagPickerInputBaseProps as TagPickerInputProps } from '@fluentui/react-tag-picker';
import { TagPickerInputSlots } from '@fluentui/react-tag-picker';
import { TagPickerOnOpenChangeData } from '@fluentui/react-tag-picker';
import { TagPickerOnOptionSelectData } from '@fluentui/react-tag-picker';
import type { TagPickerProps as TagPickerProps_2 } from '@fluentui/react-tag-picker';
import { TagPickerSize } from '@fluentui/react-tag-picker';
import { TagPickerSlots } from '@fluentui/react-tag-picker';
import { TagPickerState } from '@fluentui/react-tag-picker';
import { useTagPickerContext_unstable } from '@fluentui/react-tag-picker';
import { useTagPickerFilter } from '@fluentui/react-tag-picker';

export { renderTagPicker }

// @public
export const renderTagPickerButton: (state: TagPickerButtonState) => JSXElement;

// @public
export const renderTagPickerControl: (state: TagPickerControlState) => JSXElement;

// @public
export const renderTagPickerGroup: (state: TagPickerGroupState, contextValues: TagGroupContextValues) => JSXElement | null;

// @public
export const renderTagPickerInput: (state: TagPickerInputState) => JSXElement;

// @public
export const renderTagPickerList: (state: TagPickerListState) => JSXElement;

// @public
export const renderTagPickerOption: (state: TagPickerOptionState) => JSXElement;

// @public
export const renderTagPickerOptionGroup: (state: OptionGroupState) => JSXElement;

// @public (undocumented)
export const TagPicker: ForwardRefComponent<TagPickerProps>;

// @public
export const TagPickerButton: ForwardRefComponent<TagPickerButtonProps>;

export { TagPickerButtonProps }

export { TagPickerButtonSlots }

// @public
export type TagPickerButtonState = TagPickerButtonBaseState & {
root: {
'data-disabled'?: string;
};
};

export { TagPickerContextValue }

export { TagPickerContextValues }

// @public
export const TagPickerControl: ForwardRefComponent<TagPickerControlProps>;

// @public
export type TagPickerControlInternalSlots = {
aside?: NonNullable<Slot<'span'>>;
};

export { TagPickerControlProps }

export { TagPickerControlSlots }

// @public
export type TagPickerControlState = TagPickerControlBaseState & {
root: {
'data-disabled'?: string;
'data-invalid'?: string;
};
};

// @public
export const TagPickerGroup: ForwardRefComponent<TagPickerGroupProps>;

// @public
export type TagPickerGroupProps = ComponentProps<TagPickerGroupSlots>;

export { TagPickerGroupSlots }

// @public
export type TagPickerGroupState = TagGroupBaseState & {
hasSelectedOptions: boolean;
root: {
focusgroup?: string;
'data-disabled'?: string;
};
};

// @public
export const TagPickerInput: ForwardRefComponent<TagPickerInputProps>;

export { TagPickerInputProps }

export { TagPickerInputSlots }

// @public
export type TagPickerInputState = TagPickerInputBaseState & {
root: {
'data-disabled'?: string;
};
};

// @public
export const TagPickerList: ForwardRefComponent<TagPickerListProps>;

// @public
export type TagPickerListProps = ComponentProps<TagPickerListSlots>;

// @public (undocumented)
export type TagPickerListSlots = {
root: Slot<typeof Listbox>;
};

// @public
export type TagPickerListState = ComponentState<TagPickerListSlots> & {
open: boolean;
};

export { TagPickerOnOpenChangeData }

export { TagPickerOnOptionSelectData }

// @public
export const TagPickerOption: ForwardRefComponent<TagPickerOptionProps>;

// @public
export const TagPickerOptionGroup: ForwardRefComponent<TagPickerOptionGroupProps>;

// @public
export type TagPickerOptionGroupProps = OptionGroupProps;

// @public (undocumented)
export type TagPickerOptionGroupSlots = OptionGroupSlots;

// @public
export type TagPickerOptionGroupState = OptionGroupState;

// @public
export type TagPickerOptionProps = OptionProps & {
media?: Slot<'span'>;
secondaryContent?: Slot<'span'>;
};

// @public (undocumented)
export type TagPickerOptionSlots = OptionSlots & {
media?: Slot<'span'>;
secondaryContent?: Slot<'span'>;
};

// @public
export type TagPickerOptionState = OptionState & {
components: OptionState['components'] & {
media: 'span';
secondaryContent: 'span';
};
media?: Slot<'span'>;
secondaryContent?: Slot<'span'>;
};

// @public (undocumented)
export type TagPickerProps = Omit<TagPickerProps_2, 'inline' | 'size' | 'appearance' | 'mountNode'>;

export { TagPickerSize }

export { TagPickerSlots }

export { TagPickerState }

// @public
export const useTagPicker: (props: TagPickerProps) => TagPickerState;

// @public
export const useTagPickerButton: (props: TagPickerButtonProps, ref: React_2.Ref<HTMLButtonElement>) => TagPickerButtonState;

export { useTagPickerContext_unstable }

// @public
export function useTagPickerContextValues(state: TagPickerState): TagPickerContextValues;

// @public
export const useTagPickerControl: (props: TagPickerControlProps, ref: React_2.Ref<HTMLDivElement>) => TagPickerControlState;

export { useTagPickerFilter }

// @public
export const useTagPickerGroup: (props: TagPickerGroupProps, ref: React_2.Ref<HTMLDivElement>) => TagPickerGroupState;

// @public
export const useTagPickerInput: (props: TagPickerInputProps, ref: React_2.Ref<HTMLInputElement>) => TagPickerInputState;

// @public
export const useTagPickerList: (props: TagPickerListProps, ref: React_2.Ref<HTMLDivElement>) => TagPickerListState;

// @public
export const useTagPickerOption: (props: TagPickerOptionProps, ref: React_2.Ref<HTMLElement>) => TagPickerOptionState;

// @public
export const useTagPickerOptionGroup: (props: TagPickerOptionGroupProps, ref: React_2.Ref<HTMLElement>) => TagPickerOptionGroupState;

// (No @packageDocumentation comment for this package)

```
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@fluentui/react-switch": "^9.7.3",
"@fluentui/react-tabs": "^9.12.2",
"@fluentui/react-tabster": "^9.26.15",
"@fluentui/react-tag-picker": "^9.8.8",
"@fluentui/react-tags": "^9.9.1",
"@fluentui/react-textarea": "^9.7.3",
"@fluentui/react-toolbar": "^9.8.1",
Expand Down Expand Up @@ -313,6 +314,12 @@
"import": "./lib/tag-group.js",
"require": "./lib-commonjs/tag-group.js"
},
"./tag-picker": {
"types": "./dist/tag-picker.d.ts",
"node": "./lib-commonjs/tag-picker.js",
"import": "./lib/tag-picker.js",
"require": "./lib-commonjs/tag-picker.js"
},
"./teaching-popover": {
"types": "./dist/teaching-popover.d.ts",
"node": "./lib-commonjs/teaching-popover.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as React from 'react';
import { render } from '@testing-library/react';
import { TagPicker } from './TagPicker';
import { TagPickerControl } from './TagPickerControl';
import { TagPickerGroup } from './TagPickerGroup';
import { TagPickerInput } from './TagPickerInput';
import { TagPickerList } from './TagPickerList';
import { TagPickerOption } from './TagPickerOption';
import { optionClassNames } from '@fluentui/react-combobox';
import { Tag } from '../Tag';

const renderTagPicker = (props: { selectedOptions?: string[]; disabled?: boolean } = {}) => {
const { selectedOptions = [], disabled } = props;
return render(
<TagPicker open disabled={disabled} selectedOptions={selectedOptions}>
<TagPickerControl>
<TagPickerGroup aria-label="Selected animals">
{selectedOptions.map(option => (
<Tag key={option} value={option}>
{option}
</Tag>
))}
</TagPickerGroup>
<TagPickerInput aria-label="Select animals" />
</TagPickerControl>
<TagPickerList>
<TagPickerOption>Cat</TagPickerOption>
<TagPickerOption disabled>Ferret</TagPickerOption>
<TagPickerOption>Dog</TagPickerOption>
</TagPickerList>
</TagPicker>,
);
};

describe('TagPicker', () => {
it('renders the input trigger and the options list when open', () => {
const { getByRole, getAllByRole } = renderTagPicker();

expect(getByRole('combobox')).toBeInTheDocument();
expect(getByRole('listbox')).toBeInTheDocument();
expect(getAllByRole('option')).toHaveLength(3);
});

it('sets data-disabled on disabled options', () => {
const { getAllByRole } = renderTagPicker();
const options = getAllByRole('option');

expect(options[0]).not.toHaveAttribute('data-disabled');
expect(options[1]).toHaveAttribute('data-disabled');
expect(options[2]).not.toHaveAttribute('data-disabled');
});

it('marks options with the option class so active-descendant arrow navigation can find them', () => {
const { getAllByRole } = renderTagPicker();

getAllByRole('option').forEach(option => {
expect(option).toHaveClass(optionClassNames.root);
});
});

it('renders selected options as tags and applies the focusgroup attribute on the group', () => {
const { getByRole } = renderTagPicker({ selectedOptions: ['Dog'] });

const group = getByRole('listbox', { name: 'Selected animals' });
expect(group).toHaveAttribute('focusgroup', 'toolbar inline wrap');
expect(group).toHaveTextContent('Dog');
});

it('does not render the group when nothing is selected', () => {
const { queryByRole } = renderTagPicker({ selectedOptions: [] });

expect(queryByRole('listbox', { name: 'Selected animals' })).not.toBeInTheDocument();
});

it('sets data-disabled on the control when disabled', () => {
const { getByRole } = renderTagPicker({ disabled: true });

expect(getByRole('combobox')).toBeDisabled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client';

import * as React from 'react';
import type { ForwardRefComponent } from '@fluentui/react-utilities';

import { useTagPicker } from './useTagPicker';
import { renderTagPicker } from './renderTagPicker';
import { useTagPickerContextValues } from './useTagPickerContextValues';
import type { TagPickerProps } from './TagPicker.types';

export const TagPicker: ForwardRefComponent<TagPickerProps> = React.forwardRef((props, _ref) => {
const state = useTagPicker(props);
const contextValues = useTagPickerContextValues(state);

return renderTagPicker(state, contextValues);
});

TagPicker.displayName = 'TagPicker';
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { TagPickerProps as TagPickerPropsBase } from '@fluentui/react-tag-picker';

export type {
TagPickerState,
TagPickerContextValues,
TagPickerSlots,
TagPickerSize,
TagPickerOnOpenChangeData,
TagPickerOnOptionSelectData,
} from '@fluentui/react-tag-picker';

export type TagPickerProps = Omit<TagPickerPropsBase, 'inline' | 'size' | 'appearance' | 'mountNode'>;
Loading
Loading