Skip to content

feat(react-headless-components-preview): add swatch picker controls - #36535

Open
Dmytro Kirpa (dmytrokirpa) wants to merge 6 commits into
feat/swatch-picker-headless-basefrom
feat/headless-swatch-picker
Open

feat(react-headless-components-preview): add swatch picker controls#36535
Dmytro Kirpa (dmytrokirpa) wants to merge 6 commits into
feat/swatch-picker-headless-basefrom
feat/headless-swatch-picker

Conversation

@dmytrokirpa

Copy link
Copy Markdown
Contributor

Motivation

Add an unstyled SwatchPicker family to @fluentui/react-headless-components-preview. The controls reuse the state and interaction behavior extracted in #36534 while leaving layout and visual treatment to consumers.

Changes

  • add headless SwatchPicker, SwatchPickerRow, ColorSwatch, ImageSwatch, and EmptySwatch components
  • compose the controls from @fluentui/react-swatch-picker base hooks and shared context
  • add grid navigation behavior and state data attributes for consumer styling
  • add tests and Storybook examples
  • export the family through the swatch-picker package entry point
  • add API output, bundle-size coverage, dependency metadata, and a change file

Review guidance

Review this PR relative to feat/swatch-picker-headless-base. The component layer intentionally adds no Griffel styles; story styles are consumer examples only.

Validation

  • yarn nx run-many -t lint test type-check bundle-size -p react-headless-components-preview --nxBail

PR stack

  1. test(react-swatch-picker): add hook state coverage #36533
  2. feat(react-swatch-picker): expose headless base APIs #36534
  3. This PR: add headless SwatchPicker controls

Depends on #36534 and should be merged last.

This comment was marked as outdated.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 46 out of 47 changed files in this pull request and generated no new comments.

Suppressed comments (3)

packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/useSwatchPicker.ts:17

  • The documented state-attribute contract is not implemented here. useSwatchPickerBase_unstable does not emit data-layout or data-focus-mode, and this wrapper only adds navigation props; similarly, the direct Color/Empty wrappers never emit the advertised data-selected/data-disabled attributes. Consumers therefore cannot use the styling API promised by the PR and SwatchPickerDescription.md. Please decorate each affected base state, extend the corresponding state types, and assert these attributes in the component tests.
    change/@fluentui-react-headless-components-preview-931596e2-bc46-4837-a7cc-db12b9edd5cd.json:3
  • Beachball comments should omit the conventional-commit prefix because packageName on line 4 already supplies the package context. Keep the release-note text as the user-facing change description only.
  "comment": "feat: add swatch picker components",

packages/react-components/react-headless-components-preview/library/src/hooks/useGridNavigation/useGridNavigation.ts:201

  • Navigation keys are only canceled when a destination is found. At a non-circular boundary, in a single-cell grid, or when the target column has no enabled cell, ArrowUp/ArrowDown/Home/End retain their browser default and can scroll the page while focus remains in the grid. Cancel recognized navigation keys once a valid current cell is found, regardless of whether focus moves.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Pull request demo site: URL


if (focusMode === 'arrow') {
// eslint-disable-next-line react-hooks/immutability
baseState.root['focusgroup'] = baseState.isGrid ? 'grid' : 'radiogroup';

@dmytrokirpa Dmytro Kirpa (dmytrokirpa) Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

depends on microsoft/polyfills#64, if that won't be merged we need to revert the grid part and replace it with Tabster hook

@@ -0,0 +1,54 @@
export {
// SwatchPicker

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these comments are not useful, let's omit them


Use `ColorSwatch`, `ImageSwatch`, and `EmptySwatch` as children of `SwatchPicker`. Give the picker
an accessible name and give each swatch an accessible name. The headless components expose
`data-layout`, `data-focus-mode`, `data-selected`, and `data-disabled` for styling behavior.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aren't we missing data-layout and data-focus-mode?

],
];

export const Grid = (): React.ReactNode => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried in deployed SB and could not navigate with arrow keys in any axes, only by tabbing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

);
});

it('uses focusgroup for arrow navigation in a row', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's also add to SB the polyfill notice, similar to TabList:

Image

Comment on lines +1 to +7
import type {
ImageSwatchBaseProps,
ImageSwatchBaseState,
ImageSwatchSlots as ImageSwatchBaseSlots,
} from '@fluentui/react-swatch-picker';

export type ImageSwatchProps = ImageSwatchBaseProps;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import type {
ImageSwatchBaseProps,
ImageSwatchBaseState,
ImageSwatchSlots as ImageSwatchBaseSlots,
} from '@fluentui/react-swatch-picker';
export type ImageSwatchProps = ImageSwatchBaseProps;
import type {
ImageSwatchBaseState,
ImageSwatchSlots as ImageSwatchBaseSlots,
} from '@fluentui/react-swatch-picker';
export type { ImageSwatchBaseProps as ImageSwatchProps } from '@fluentui/react-swatch-picker';

Comment on lines +58 to +72
red.focus();
expect(yellow).toHaveAttribute('tabindex', '-1');

fireEvent.keyDown(red, { key: 'ArrowRight' });
expect(yellow).toHaveFocus();

fireEvent.keyDown(yellow, { key: 'ArrowDown' });
expect(violet).toHaveFocus();

fireEvent.keyDown(violet, { key: 'ArrowRight' });
expect(red).toHaveFocus();

fireEvent.keyDown(red, { key: 'ArrowUp' });
expect(green).toHaveFocus();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: shouldn't we use userEvent here instead of fireEvent? userEvent dispatches the events like they would happen if a user interacted with the document

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants