diff --git a/.github/workflows/check-packages.yml b/.github/workflows/check-packages.yml index 216e2ba2c7dd4..2d488b5c44ed0 100644 --- a/.github/workflows/check-packages.yml +++ b/.github/workflows/check-packages.yml @@ -30,11 +30,10 @@ jobs: run: | echo "yarn.lock changed! Verifying package deduplication ..." - yarn run dedupe --check --json - - if [[ $? -ne 0 ]]; then - echo "Your changes introduced package duplication 🚨" - echo "Run 'yarn run dedupe' to fix those." + if ! yarn run dedupe --check; then + echo + echo "::error::Your changes introduced package duplication - run 'yarn run dedupe' to fix." + exit 1 else echo "No duplicate packages introduced ✅" fi diff --git a/.vscode/settings.json b/.vscode/settings.json index 41454c24a61a1..c0e8e7a492731 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -69,12 +69,10 @@ "[handlebars]": { "editor.formatOnSave": false }, - "javascript.preferences.importModuleSpecifier": "relative", - "javascript.preferences.importModuleSpecifierEnding": "index", - "typescript.preferences.importModuleSpecifier": "relative", - "typescript.preferences.importModuleSpecifierEnding": "index", - "typescript.tsdk": "./node_modules/typescript/lib", - "typescript.reportStyleChecksAsWarnings": false, + "js/ts.preferences.importModuleSpecifier": "relative", + "js/ts.preferences.importModuleSpecifierEnding": "index", + "js/ts.tsdk.path": "./node_modules/typescript/lib", + "js/ts.reportStyleChecksAsWarnings": false, "cSpell.words": ["beachball", "borderless", "fluentui", "griffel", "spinbutton", "tabster"], "nxConsole.generateAiAgentRules": true, // Maximum number of requests for the chat agent - Prevents AI agents from stopping mid-session diff --git a/azure-pipelines.release-headless-experimental.yml b/azure-pipelines.release-headless-experimental.yml index 5ecde8859c686..f0f7491c6a63a 100644 --- a/azure-pipelines.release-headless-experimental.yml +++ b/azure-pipelines.release-headless-experimental.yml @@ -125,10 +125,12 @@ extends: displayName: test - script: | - yarn beachball publish -b origin/$(validation.branchPath) --access public -y -n $(npmToken) --no-push --tag experimental --config scripts/beachball/src/release-headless.config.js + yarn beachball publish -b origin/$(validation.branchPath) --no-push --tag experimental --config scripts/beachball/src/release-headless.config.js git reset --hard origin/$(validation.branchPath) displayName: Publish changes and bump versions condition: and(succeeded(), not(${{ parameters.dryRun }})) + env: + NPM_TOKEN: $(npmToken) - template: .devops/templates/cleanup.yml@self parameters: diff --git a/azure-pipelines.release-vnext-experimental.yml b/azure-pipelines.release-vnext-experimental.yml index b327c25135eda..be0e54ae30bfa 100644 --- a/azure-pipelines.release-vnext-experimental.yml +++ b/azure-pipelines.release-vnext-experimental.yml @@ -118,10 +118,12 @@ extends: displayName: test - script: | - yarn beachball publish -b origin/$(validation.branchPath) --access public -y -n $(npmToken) --no-push --tag experimental --config scripts/beachball/src/release-vNext.config.js + yarn beachball publish -b origin/$(validation.branchPath) --no-push --tag experimental --config scripts/beachball/src/release-vNext.config.js git reset --hard origin/$(validation.branchPath) displayName: Publish changes and bump versions condition: and(succeeded(), not(${{ parameters.dryRun }})) + env: + NPM_TOKEN: $(npmToken) - template: .devops/templates/cleanup.yml@self parameters: diff --git a/azure-pipelines.release-vnext-nightly.yml b/azure-pipelines.release-vnext-nightly.yml index b2a0642d680f9..f80c50372ca3c 100644 --- a/azure-pipelines.release-vnext-nightly.yml +++ b/azure-pipelines.release-vnext-nightly.yml @@ -103,10 +103,12 @@ extends: displayName: test - script: | - yarn publish:beachball -b origin/$(Build.SourceBranchName) -n $(npmToken) --no-push --tag nightly --config scripts/beachball/src/release-vNext.config.js + yarn beachball publish -b origin/$(Build.SourceBranchName) --no-push --tag nightly --config scripts/beachball/src/release-vNext.config.js git reset --hard origin/$(Build.SourceBranchName) displayName: Publish changes and bump versions condition: not(${{ parameters.dryRun }}) + env: + NPM_TOKEN: $(npmToken) - template: .devops/templates/cleanup.yml@self parameters: diff --git a/azure-pipelines.release-vnext.yml b/azure-pipelines.release-vnext.yml index 95193a420b71a..831d2d45b3748 100644 --- a/azure-pipelines.release-vnext.yml +++ b/azure-pipelines.release-vnext.yml @@ -95,10 +95,11 @@ extends: condition: not(${{ parameters.dryRun }}) - script: | - yarn publish:beachball -n $(npmToken) --config scripts/beachball/src/release-vNext.config.js --message 'release: applying package updates - react-components' + yarn beachball publish --config scripts/beachball/src/release-vNext.config.js --message 'release: applying package updates - react-components' git reset --hard origin/master env: GITHUB_PAT: $(githubPAT) + NPM_TOKEN: $(npmToken) displayName: Publish changes and bump versions condition: not(${{ parameters.dryRun }}) diff --git a/azure-pipelines.release.headless.yml b/azure-pipelines.release.headless.yml index 9058a6fcb17a0..9fad65c70cd94 100644 --- a/azure-pipelines.release.headless.yml +++ b/azure-pipelines.release.headless.yml @@ -84,10 +84,11 @@ extends: displayName: lint - script: | - yarn publish:beachball -n $(npmToken) --config scripts/beachball/src/release-headless.config.js --message 'release: applying package updates - react-headless' + yarn beachball publish --config scripts/beachball/src/release-headless.config.js --message 'release: applying package updates - react-headless' git reset --hard origin/master env: GITHUB_PAT: $(githubPAT) + NPM_TOKEN: $(npmToken) displayName: Publish changes and bump versions condition: not(${{ parameters.dryRun }}) diff --git a/azure-pipelines.release.tools-experimental.yml b/azure-pipelines.release.tools-experimental.yml index 86a09c8e65add..518fe3874503e 100644 --- a/azure-pipelines.release.tools-experimental.yml +++ b/azure-pipelines.release.tools-experimental.yml @@ -117,10 +117,12 @@ extends: displayName: test - script: | - yarn beachball publish -b origin/$(validation.branchPath) --access public -y -n $(npmToken) --no-push --tag experimental --config scripts/beachball/src/release-tools.config.js + yarn beachball publish -b origin/$(validation.branchPath) --no-push --tag experimental --config scripts/beachball/src/release-tools.config.js git reset --hard origin/$(validation.branchPath) displayName: Publish changes and bump versions condition: and(succeeded(), not(${{ parameters.dryRun }})) + env: + NPM_TOKEN: $(npmToken) - template: .devops/templates/cleanup.yml@self parameters: diff --git a/azure-pipelines.release.tools.yml b/azure-pipelines.release.tools.yml index f7d7f42bab460..cb1701fd80441 100644 --- a/azure-pipelines.release.tools.yml +++ b/azure-pipelines.release.tools.yml @@ -83,10 +83,11 @@ extends: displayName: lint - script: | - yarn publish:beachball -n $(npmToken) --config scripts/beachball/src/release-tools.config.js --message 'release: applying package updates - tools' + yarn beachball publish --config scripts/beachball/src/release-tools.config.js --message 'release: applying package updates - tools' git reset --hard origin/master env: GITHUB_PAT: $(githubPAT) + NPM_TOKEN: $(npmToken) displayName: Publish changes and bump versions condition: not(${{ parameters.dryRun }}) diff --git a/azure-pipelines.release.web-components.yml b/azure-pipelines.release.web-components.yml index de2d9bc1ee07a..7e7fd8d0a0fae 100644 --- a/azure-pipelines.release.web-components.yml +++ b/azure-pipelines.release.web-components.yml @@ -81,10 +81,11 @@ extends: displayName: Build, Test, Lint - script: | - yarn publish:beachball -n $(npmToken) --config scripts/beachball/src/release-web-components.config.js --message 'release: applying package updates - web-components' + yarn beachball publish --config scripts/beachball/src/release-web-components.config.js --message 'release: applying package updates - web-components' git reset --hard origin/master env: GITHUB_PAT: $(githubPAT) + NPM_TOKEN: $(npmToken) displayName: Publish changes and bump versions condition: not(${{ parameters.dryRun }}) diff --git a/azure-pipelines.release.yml b/azure-pipelines.release.yml index ecabd7c10955a..6bfc06bedbe29 100644 --- a/azure-pipelines.release.yml +++ b/azure-pipelines.release.yml @@ -127,11 +127,12 @@ extends: displayName: verify packaged assets - script: | - yarn publish:beachball -n $(npmToken) --config scripts/beachball/src/release-v8.config.js --message 'release: applying package updates - react v8' + yarn beachball publish --config scripts/beachball/src/release-v8.config.js --message 'release: applying package updates - react v8' git reset --hard origin/master condition: and(succeeded(), not(${{ parameters.dryRun }})) env: GITHUB_PAT: $(githubPAT) + NPM_TOKEN: $(npmToken) displayName: Publish changes and bump versions - script: | diff --git a/beachball.config.js b/beachball.config.js index 472e34ca7deee..af2178ece96f5 100644 --- a/beachball.config.js +++ b/beachball.config.js @@ -1 +1 @@ -module.exports = require('./scripts/beachball/base.config.json'); +module.exports = require('./scripts/beachball/base.config'); diff --git a/change/@fluentui-react-headless-components-preview-931596e2-bc46-4837-a7cc-db12b9edd5cd.json b/change/@fluentui-react-headless-components-preview-931596e2-bc46-4837-a7cc-db12b9edd5cd.json new file mode 100644 index 0000000000000..d9e0ca2793c45 --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-931596e2-bc46-4837-a7cc-db12b9edd5cd.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "feat: add swatch picker components", + "packageName": "@fluentui/react-headless-components-preview", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-swatch-picker-c346c0ef-e11a-45a1-91b0-4efbf20a3127.json b/change/@fluentui-react-swatch-picker-c346c0ef-e11a-45a1-91b0-4efbf20a3127.json new file mode 100644 index 0000000000000..6a3dfca6b4645 --- /dev/null +++ b/change/@fluentui-react-swatch-picker-c346c0ef-e11a-45a1-91b0-4efbf20a3127.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: expose headless base APIs", + "packageName": "@fluentui/react-swatch-picker", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/fluentui-eslint-plugin-a417f93b-a140-4c40-a4e9-074fab196688.json b/change/fluentui-eslint-plugin-a417f93b-a140-4c40-a4e9-074fab196688.json deleted file mode 100644 index c6f73ed4930d9..0000000000000 --- a/change/fluentui-eslint-plugin-a417f93b-a140-4c40-a4e9-074fab196688.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "minor", - "comment": "chore: enforce consistent type imports in react packages", - "packageName": "@fluentui/eslint-plugin", - "email": "copilot@microsoft.com", - "dependentChangeType": "patch" -} diff --git a/change/fluentui-theme-designer-2a88231d-0358-4d1a-bb01-9f602e9306ec.json b/change/fluentui-theme-designer-2a88231d-0358-4d1a-bb01-9f602e9306ec.json deleted file mode 100644 index 6c9c934700c16..0000000000000 --- a/change/fluentui-theme-designer-2a88231d-0358-4d1a-bb01-9f602e9306ec.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "none", - "comment": "chore: enforce consistent type imports", - "packageName": "@fluentui/theme-designer", - "email": "copilot@microsoft.com", - "dependentChangeType": "none" -} diff --git a/package.json b/package.json index 47a8d1e74a010..4f1d7f8f64459 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ }, "scripts": { "dedupe": "yarn dedupe --strategy highest", - "change": "beachball change --no-commit", + "change": "beachball change", "check:change": "beachball check", "check:modified-files": "node -r ./scripts/ts-node/src/register ./scripts/executors/src/check-for-modified-files", "check:affected": "node ./scripts/executors/src/checkIfPackagesAffected.js", @@ -23,7 +23,6 @@ "generate-version-files": "node -r ./scripts/ts-node/src/register ./scripts/generators/src/generate-version-files", "postinstall": "husky && node ./scripts/package-manager/src/postinstall.js", "preinstall": "node ./scripts/package-manager/src/preinstall.js", - "publish:beachball": "beachball publish -b origin/master --access public -y", "start": "node -r ./scripts/ts-node/src/register ./scripts/executors/src/start", "generate": "node -r ./scripts/ts-node/src/register ./scripts/executors/src/generate-ui", "clean": "nx run-many -t clean --verbose", @@ -165,7 +164,6 @@ "@types/webpack-env": "1.18.8", "@types/webpack-hot-middleware": "2.25.9", "@types/yargs": "13.0.11", - "@types/yargs-unparser": "2.0.1", "@typescript-eslint/eslint-plugin": "8.46.2", "@typescript-eslint/rule-tester": "^8.46.2", "autoprefixer": "10.2.1", @@ -178,7 +176,7 @@ "babel-plugin-module-resolver": "5.0.3", "babel-plugin-react-compiler": "1.0.0", "babel-plugin-tester": "10.1.0", - "beachball": "2.31.0", + "beachball": "3.0.0-alpha.7", "chalk": "4.1.0", "chrome-remote-interface": "0.28.2", "ci-info": "3.9.0", @@ -283,7 +281,6 @@ "react-shadow": "20.3.0", "react-test-renderer": "19.2.0", "react-window": "^1.8.6", - "read-pkg-up": "7.0.1", "remark-gfm": "4.0.1", "replace-in-file": "6.1.0", "resolve": "1.22.8", @@ -331,10 +328,8 @@ "webpack-dev-server": "4.15.2", "webpack-hot-middleware": "2.26.1", "webpack-merge": "5.10.0", - "workspace-tools": "0.29.1", "yargs": "13.3.2", - "yargs-parser": "13.1.2", - "yargs-unparser": "2.0.0" + "yargs-parser": "13.1.2" }, "dependencies": { "@fluentui/react-icons-northstar": "0.66.5", diff --git a/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js b/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js index d2b7af34c33e3..0197c67f0fac3 100644 --- a/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js +++ b/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js @@ -37,6 +37,7 @@ import * as Skeleton from '@fluentui/react-headless-components-preview/skeleton' import * as Slider from '@fluentui/react-headless-components-preview/slider'; import * as SpinButton from '@fluentui/react-headless-components-preview/spin-button'; import * as Spinner from '@fluentui/react-headless-components-preview/spinner'; +import * as SwatchPicker from '@fluentui/react-headless-components-preview/swatch-picker'; 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'; @@ -89,6 +90,7 @@ console.log({ Slider, SpinButton, Spinner, + SwatchPicker, Switch, TabList, Tag, diff --git a/packages/react-components/react-headless-components-preview/library/etc/swatch-picker.api.md b/packages/react-components/react-headless-components-preview/library/etc/swatch-picker.api.md new file mode 100644 index 0000000000000..d4f2f18cc1f36 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/etc/swatch-picker.api.md @@ -0,0 +1,136 @@ +## 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 { ColorSwatchBaseState } from '@fluentui/react-swatch-picker'; +import { ColorSwatchBaseProps as ColorSwatchProps } from '@fluentui/react-swatch-picker'; +import { ColorSwatchSlots } from '@fluentui/react-swatch-picker'; +import type { EmptySwatchBaseState } from '@fluentui/react-swatch-picker'; +import { EmptySwatchBaseProps as EmptySwatchProps } from '@fluentui/react-swatch-picker'; +import { EmptySwatchSlots } from '@fluentui/react-swatch-picker'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { ImageSwatchBaseState } from '@fluentui/react-swatch-picker'; +import { ImageSwatchBaseProps as ImageSwatchProps } from '@fluentui/react-swatch-picker'; +import { ImageSwatchSlots } from '@fluentui/react-swatch-picker'; +import type * as React_2 from 'react'; +import { renderColorSwatch_unstable as renderColorSwatch } from '@fluentui/react-swatch-picker'; +import { renderEmptySwatch_unstable as renderEmptySwatch } from '@fluentui/react-swatch-picker'; +import { renderImageSwatch_unstable as renderImageSwatch } from '@fluentui/react-swatch-picker'; +import { renderSwatchPicker_unstable as renderSwatchPicker } from '@fluentui/react-swatch-picker'; +import { renderSwatchPickerRow_unstable as renderSwatchPickerRow } from '@fluentui/react-swatch-picker'; +import type { SwatchPickerBaseState } from '@fluentui/react-swatch-picker'; +import { SwatchPickerBaseProps as SwatchPickerProps } from '@fluentui/react-swatch-picker'; +import type { SwatchPickerRowBaseState } from '@fluentui/react-swatch-picker'; +import { SwatchPickerRowBaseProps as SwatchPickerRowProps } from '@fluentui/react-swatch-picker'; +import { SwatchPickerRowSlots } from '@fluentui/react-swatch-picker'; +import { SwatchPickerSlots } from '@fluentui/react-swatch-picker'; +import { useSwatchPickerContextValue_unstable as useSwatchPickerContextValue } from '@fluentui/react-swatch-picker'; +import { useSwatchPickerContextValues } from '@fluentui/react-swatch-picker'; + +// @public (undocumented) +export const ColorSwatch: ForwardRefComponent; + +export { ColorSwatchProps } + +export { ColorSwatchSlots } + +// @public (undocumented) +export type ColorSwatchState = ColorSwatchBaseState & { + root: { + 'data-selected'?: string; + 'data-disabled'?: string; + }; +}; + +// @public (undocumented) +export const EmptySwatch: ForwardRefComponent; + +export { EmptySwatchProps } + +export { EmptySwatchSlots } + +// @public (undocumented) +export type EmptySwatchState = EmptySwatchBaseState & { + root: { + 'data-selected'?: string; + 'data-disabled'?: string; + }; +}; + +// @public (undocumented) +export const ImageSwatch: ForwardRefComponent; + +export { ImageSwatchProps } + +export { ImageSwatchSlots } + +// @public (undocumented) +export type ImageSwatchState = ImageSwatchBaseState & { + root: { + 'data-selected'?: string; + }; +}; + +export { renderColorSwatch } + +export { renderEmptySwatch } + +export { renderImageSwatch } + +export { renderSwatchPicker } + +export { renderSwatchPickerRow } + +// @public +export const SwatchPicker: ForwardRefComponent; + +export { SwatchPickerProps } + +// @public (undocumented) +export const SwatchPickerRow: ForwardRefComponent; + +export { SwatchPickerRowProps } + +export { SwatchPickerRowSlots } + +// @public (undocumented) +export type SwatchPickerRowState = SwatchPickerRowBaseState & { + root: SwatchPickerRowBaseState['root'] & { + focusgrouprow?: string; + }; +}; + +export { SwatchPickerSlots } + +// @public (undocumented) +export type SwatchPickerState = SwatchPickerBaseState & { + root: { + 'data-layout'?: SwatchPickerBaseState['layout']; + focusgroup?: string; + }; +}; + +// @public (undocumented) +export const useColorSwatch: (props: ColorSwatchProps, ref: React_2.Ref) => ColorSwatchState; + +// @public (undocumented) +export const useEmptySwatch: (props: EmptySwatchProps, ref: React_2.Ref) => EmptySwatchState; + +// @public (undocumented) +export const useImageSwatch: (props: ImageSwatchProps, ref: React_2.Ref) => ImageSwatchState; + +// @public (undocumented) +export const useSwatchPicker: (props: SwatchPickerProps, ref: React_2.Ref) => SwatchPickerState; + +export { useSwatchPickerContextValue } + +export { useSwatchPickerContextValues } + +// @public (undocumented) +export const useSwatchPickerRow: (props: SwatchPickerRowProps, ref: React_2.Ref) => SwatchPickerRowState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-headless-components-preview/library/package.json b/packages/react-components/react-headless-components-preview/library/package.json index 2a32c5be1b217..e6500eb795359 100644 --- a/packages/react-components/react-headless-components-preview/library/package.json +++ b/packages/react-components/react-headless-components-preview/library/package.json @@ -57,6 +57,7 @@ "@fluentui/react-slider": "^9.6.4", "@fluentui/react-spinbutton": "^9.6.4", "@fluentui/react-spinner": "^9.8.4", + "@fluentui/react-swatch-picker": "^9.5.4", "@fluentui/react-switch": "^9.7.4", "@fluentui/react-tabs": "^9.12.3", "@fluentui/react-tabster": "^9.26.16", @@ -323,6 +324,12 @@ "import": "./lib/spinner.js", "require": "./lib-commonjs/spinner.js" }, + "./swatch-picker": { + "types": "./dist/swatch-picker.d.ts", + "node": "./lib-commonjs/swatch-picker.js", + "import": "./lib/swatch-picker.js", + "require": "./lib-commonjs/swatch-picker.js" + }, "./switch": { "types": "./dist/switch.d.ts", "node": "./lib-commonjs/switch.js", diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/ColorSwatch.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/ColorSwatch.test.tsx new file mode 100644 index 0000000000000..3ada7f41c6b38 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/ColorSwatch.test.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../../testing/isConformant'; +import { ColorSwatch } from './ColorSwatch'; +import { SwatchPicker } from '../SwatchPicker'; + +describe('ColorSwatch', () => { + isConformant({ + Component: ColorSwatch, + displayName: 'ColorSwatch', + disabledTests: ['has-top-level-file-extra'], + }); + + it('emits selected and disabled state attributes', () => { + const { getByRole } = render(, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + const swatch = getByRole('radio'); + expect(swatch).toHaveAttribute('aria-label', 'Pink'); + expect(swatch).toHaveAttribute('aria-checked', 'true'); + expect(swatch).toHaveAttribute('data-selected'); + expect(swatch).toHaveAttribute('data-disabled'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/ColorSwatch.tsx b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/ColorSwatch.tsx new file mode 100644 index 0000000000000..20678a3304a61 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/ColorSwatch.tsx @@ -0,0 +1,14 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { ColorSwatchProps } from './ColorSwatch.types'; +import { useColorSwatch } from './useColorSwatch'; +import { renderColorSwatch } from './renderColorSwatch'; + +export const ColorSwatch: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useColorSwatch(props, ref); + return renderColorSwatch(state); +}); + +ColorSwatch.displayName = 'ColorSwatch'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/ColorSwatch.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/ColorSwatch.types.ts new file mode 100644 index 0000000000000..7505a6bcdac7f --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/ColorSwatch.types.ts @@ -0,0 +1,16 @@ +import type { ColorSwatchBaseState } from '@fluentui/react-swatch-picker'; + +export type { ColorSwatchBaseProps as ColorSwatchProps, ColorSwatchSlots } from '@fluentui/react-swatch-picker'; + +export type ColorSwatchState = ColorSwatchBaseState & { + root: { + /** + * Whether ColorSwatch is selected + */ + 'data-selected'?: string; + /** + * Whether ColorSwatch is disabled + */ + 'data-disabled'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/index.ts new file mode 100644 index 0000000000000..8114ede193133 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/index.ts @@ -0,0 +1,4 @@ +export { ColorSwatch } from './ColorSwatch'; +export { renderColorSwatch } from './renderColorSwatch'; +export { useColorSwatch } from './useColorSwatch'; +export type { ColorSwatchSlots, ColorSwatchProps, ColorSwatchState } from './ColorSwatch.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/renderColorSwatch.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/renderColorSwatch.ts new file mode 100644 index 0000000000000..16f0c344cf809 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/renderColorSwatch.ts @@ -0,0 +1 @@ +export { renderColorSwatch_unstable as renderColorSwatch } from '@fluentui/react-swatch-picker'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/useColorSwatch.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/useColorSwatch.ts new file mode 100644 index 0000000000000..055de0d123e37 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ColorSwatch/useColorSwatch.ts @@ -0,0 +1,17 @@ +'use client'; + +import type * as React from 'react'; +import { useColorSwatchBase_unstable } from '@fluentui/react-swatch-picker'; +import { stringifyDataAttribute } from '../../../utils'; +import type { ColorSwatchProps, ColorSwatchState } from './ColorSwatch.types'; + +export const useColorSwatch = (props: ColorSwatchProps, ref: React.Ref): ColorSwatchState => { + const state: ColorSwatchState = useColorSwatchBase_unstable(props, ref); + + // eslint-disable-next-line react-hooks/immutability + state.root['data-selected'] = stringifyDataAttribute(state.selected); + // eslint-disable-next-line react-hooks/immutability + state.root['data-disabled'] = stringifyDataAttribute(state.disabled); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/EmptySwatch.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/EmptySwatch.test.tsx new file mode 100644 index 0000000000000..ee7adf43040ff --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/EmptySwatch.test.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../../testing/isConformant'; +import { EmptySwatch } from './EmptySwatch'; + +describe('EmptySwatch', () => { + isConformant({ Component: EmptySwatch, displayName: 'EmptySwatch', disabledTests: ['has-top-level-file-extra'] }); + + it('renders a native radio swatch with state attributes', () => { + const swatch = render().getByRole('radio'); + + expect(swatch).toHaveAttribute('aria-checked', 'false'); + expect(swatch).not.toHaveAttribute('data-selected'); + expect(swatch).toHaveAttribute('data-disabled'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/EmptySwatch.tsx b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/EmptySwatch.tsx new file mode 100644 index 0000000000000..0be33ea73d7ce --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/EmptySwatch.tsx @@ -0,0 +1,14 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { EmptySwatchProps } from './EmptySwatch.types'; +import { useEmptySwatch } from './useEmptySwatch'; +import { renderEmptySwatch } from './renderEmptySwatch'; + +export const EmptySwatch: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useEmptySwatch(props, ref); + return renderEmptySwatch(state); +}); + +EmptySwatch.displayName = 'EmptySwatch'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/EmptySwatch.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/EmptySwatch.types.ts new file mode 100644 index 0000000000000..60e97faf83298 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/EmptySwatch.types.ts @@ -0,0 +1,16 @@ +import type { EmptySwatchBaseState } from '@fluentui/react-swatch-picker'; + +export type { EmptySwatchBaseProps as EmptySwatchProps, EmptySwatchSlots } from '@fluentui/react-swatch-picker'; + +export type EmptySwatchState = EmptySwatchBaseState & { + root: { + /** + * Whether EmptySwatch is selected + */ + 'data-selected'?: string; + /** + * Whether EmptySwatch is disabled + */ + 'data-disabled'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/index.ts new file mode 100644 index 0000000000000..8a106691b6b56 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/index.ts @@ -0,0 +1,4 @@ +export { EmptySwatch } from './EmptySwatch'; +export { renderEmptySwatch } from './renderEmptySwatch'; +export { useEmptySwatch } from './useEmptySwatch'; +export type { EmptySwatchSlots, EmptySwatchProps, EmptySwatchState } from './EmptySwatch.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/renderEmptySwatch.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/renderEmptySwatch.ts new file mode 100644 index 0000000000000..9099162e6159f --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/renderEmptySwatch.ts @@ -0,0 +1 @@ +export { renderEmptySwatch_unstable as renderEmptySwatch } from '@fluentui/react-swatch-picker'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/useEmptySwatch.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/useEmptySwatch.ts new file mode 100644 index 0000000000000..20c685ba560a7 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/EmptySwatch/useEmptySwatch.ts @@ -0,0 +1,17 @@ +'use client'; + +import type * as React from 'react'; +import { useEmptySwatchBase_unstable } from '@fluentui/react-swatch-picker'; +import { stringifyDataAttribute } from '../../../utils'; +import type { EmptySwatchProps, EmptySwatchState } from './EmptySwatch.types'; + +export const useEmptySwatch = (props: EmptySwatchProps, ref: React.Ref): EmptySwatchState => { + const state: EmptySwatchState = useEmptySwatchBase_unstable(props, ref); + + // eslint-disable-next-line react-hooks/immutability + state.root['data-selected'] = stringifyDataAttribute(state.root['aria-checked']); + // eslint-disable-next-line react-hooks/immutability + state.root['data-disabled'] = stringifyDataAttribute(state.root.disabled); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/ImageSwatch.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/ImageSwatch.test.tsx new file mode 100644 index 0000000000000..8e6f342d5933c --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/ImageSwatch.test.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../../testing/isConformant'; +import { ImageSwatch } from './ImageSwatch'; +import { SwatchPicker } from '../SwatchPicker'; + +describe('ImageSwatch', () => { + isConformant({ Component: ImageSwatch, displayName: 'ImageSwatch', disabledTests: ['has-top-level-file-extra'] }); + + it('renders an image swatch with its selection state', () => { + const { getByRole } = render(, { + wrapper: ({ children }) => {children}, + }); + + expect(getByRole('radio')).toHaveAttribute('data-selected'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/ImageSwatch.tsx b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/ImageSwatch.tsx new file mode 100644 index 0000000000000..c4af99e23b62c --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/ImageSwatch.tsx @@ -0,0 +1,14 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { ImageSwatchProps } from './ImageSwatch.types'; +import { useImageSwatch } from './useImageSwatch'; +import { renderImageSwatch } from './renderImageSwatch'; + +export const ImageSwatch: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useImageSwatch(props, ref); + return renderImageSwatch(state); +}); + +ImageSwatch.displayName = 'ImageSwatch'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/ImageSwatch.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/ImageSwatch.types.ts new file mode 100644 index 0000000000000..5accc268e485a --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/ImageSwatch.types.ts @@ -0,0 +1,12 @@ +import type { ImageSwatchBaseState } from '@fluentui/react-swatch-picker'; + +export type { ImageSwatchSlots, ImageSwatchBaseProps as ImageSwatchProps } from '@fluentui/react-swatch-picker'; + +export type ImageSwatchState = ImageSwatchBaseState & { + root: { + /** + * Whether ImageSwatch is selected + */ + 'data-selected'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/index.ts new file mode 100644 index 0000000000000..fb44b96d644b6 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/index.ts @@ -0,0 +1,4 @@ +export { ImageSwatch } from './ImageSwatch'; +export { renderImageSwatch } from './renderImageSwatch'; +export { useImageSwatch } from './useImageSwatch'; +export type { ImageSwatchSlots, ImageSwatchProps, ImageSwatchState } from './ImageSwatch.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/renderImageSwatch.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/renderImageSwatch.ts new file mode 100644 index 0000000000000..c9a6d31f52513 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/renderImageSwatch.ts @@ -0,0 +1 @@ +export { renderImageSwatch_unstable as renderImageSwatch } from '@fluentui/react-swatch-picker'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/useImageSwatch.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/useImageSwatch.ts new file mode 100644 index 0000000000000..e73fc3309cfe2 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/ImageSwatch/useImageSwatch.ts @@ -0,0 +1,15 @@ +'use client'; + +import type * as React from 'react'; +import { useImageSwatchBase_unstable } from '@fluentui/react-swatch-picker'; +import { stringifyDataAttribute } from '../../../utils/stringifyDataAttribute'; +import type { ImageSwatchProps, ImageSwatchState } from './ImageSwatch.types'; + +export const useImageSwatch = (props: ImageSwatchProps, ref: React.Ref): ImageSwatchState => { + const state: ImageSwatchState = useImageSwatchBase_unstable(props, ref); + + // eslint-disable-next-line react-hooks/immutability + state.root['data-selected'] = stringifyDataAttribute(state.selected); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPicker.cy.tsx b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPicker.cy.tsx new file mode 100644 index 0000000000000..4146058e23aa7 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPicker.cy.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { mount as mountBase } from '@fluentui/scripts-cypress'; +import { polyfillBodyAndObserve } from '@microsoft/focusgroup-polyfill'; +import type { JSXElement } from '@fluentui/react-utilities'; + +import { Provider } from '../Provider'; +import { SwatchPicker } from './SwatchPicker'; +import { ColorSwatch } from './ColorSwatch/ColorSwatch'; +import { SwatchPickerRow } from './SwatchPickerRow/SwatchPickerRow'; + +polyfillBodyAndObserve(); + +const mount = (element: JSXElement) => mountBase({element}); + +describe('SwatchPicker', () => { + // TODO: Enable this test once the focusgroup-polyfill is updated to support the new arrow key navigation behavior + it.skip('moves focus through a grid with arrow keys and wraps', () => { + mount( + + + + + + + + + + + + , + ); + + cy.get('[role="grid"]').should('have.attr', 'focusgroup', 'grid manual rowflow'); + cy.get('[role="grid"]').should('not.have.attr', 'data-tabster'); + cy.get('[aria-label="Red"]').focus().realPress('ArrowRight'); + cy.get('[aria-label="Yellow"]').should('be.focused').realPress('ArrowDown'); + cy.get('[aria-label="Violet"]').should('be.focused').realPress('ArrowRight'); + cy.get('[aria-label="Red"]').should('be.focused'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPicker.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPicker.test.tsx new file mode 100644 index 0000000000000..653838d5a1895 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPicker.test.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { isConformant } from '../../testing/isConformant'; +import { SwatchPicker } from './SwatchPicker'; +import { ColorSwatch } from './ColorSwatch/ColorSwatch'; +import { SwatchPickerRow } from './SwatchPickerRow/SwatchPickerRow'; + +describe('SwatchPicker', () => { + isConformant({ Component: SwatchPicker, displayName: 'SwatchPicker' }); + + it('renders semantic state attributes and forwards selection', async () => { + const onSelectionChange = jest.fn(); + const { getByRole } = render( + + + , + ); + + expect(getByRole('grid')).toHaveAttribute('data-layout', 'grid'); + + await userEvent.click(getByRole('gridcell')); + expect(onSelectionChange).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ selectedValue: 'pink', selectedSwatch: '#f09' }), + ); + }); + + it.each([ + ['row', 'radiogroup', 'radiogroup'], + ['grid', 'grid', 'grid manual rowflow'], + ] as const)('uses focusgroup for arrow navigation in a %s', (layout, role, focusgroup) => { + const { getByRole } = render( + + + , + ); + + expect(getByRole(role)).toHaveAttribute('focusgroup', focusgroup); + }); + + it('does not add grid arrow navigation in tab mode', async () => { + const onKeyDown = jest.fn((event: React.KeyboardEvent) => event.preventDefault()); + const { getByLabelText } = render( + + + + + + , + ); + const red = getByLabelText('Red'); + const green = getByLabelText('Green'); + + red.focus(); + await userEvent.keyboard('{ArrowRight}'); + + expect(onKeyDown).toHaveBeenCalledTimes(1); + expect(red).toHaveFocus(); + expect(green).not.toHaveAttribute('tabindex', '-1'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPicker.tsx b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPicker.tsx new file mode 100644 index 0000000000000..643bbfccc3d75 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPicker.tsx @@ -0,0 +1,21 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +import type { SwatchPickerProps } from './SwatchPicker.types'; +import { useSwatchPicker, useSwatchPickerContextValues } from './useSwatchPicker'; +import { renderSwatchPicker } from './renderSwatchPicker'; + +/** + * SwatchPicker represents a collection of swatches that can be selected. It is used + * to display a set of colors or images for the user to choose from. + */ +export const SwatchPicker: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useSwatchPicker(props, ref); + const contextValues = useSwatchPickerContextValues(state); + + return renderSwatchPicker(state, contextValues); +}); + +SwatchPicker.displayName = 'SwatchPicker'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPicker.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPicker.types.ts new file mode 100644 index 0000000000000..dd6b7a951c10e --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPicker.types.ts @@ -0,0 +1,16 @@ +import type { SwatchPickerBaseState } from '@fluentui/react-swatch-picker'; + +export type { SwatchPickerBaseProps as SwatchPickerProps, SwatchPickerSlots } from '@fluentui/react-swatch-picker'; + +export type SwatchPickerState = SwatchPickerBaseState & { + root: { + /** + * Whether SwatchPicker is row or grid + */ + 'data-layout'?: SwatchPickerBaseState['layout']; + /** + * Arrow navigation mode for SwatchPicker + */ + focusgroup?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/SwatchPickerRow.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/SwatchPickerRow.test.tsx new file mode 100644 index 0000000000000..1d0ab0d80a027 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/SwatchPickerRow.test.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../../testing/isConformant'; +import { SwatchPickerRow } from './SwatchPickerRow'; + +describe('SwatchPickerRow', () => { + isConformant({ + Component: SwatchPickerRow, + displayName: 'SwatchPickerRow', + disabledTests: ['has-top-level-file-extra'], + }); + + it('renders a row', () => { + const row = render(Colors).getByRole('row'); + + expect(row).toHaveTextContent('Colors'); + expect(row).toHaveAttribute('focusgrouprow'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/SwatchPickerRow.tsx b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/SwatchPickerRow.tsx new file mode 100644 index 0000000000000..52f98c1786aee --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/SwatchPickerRow.tsx @@ -0,0 +1,14 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { SwatchPickerRowProps } from './SwatchPickerRow.types'; +import { useSwatchPickerRow } from './useSwatchPickerRow'; +import { renderSwatchPickerRow } from './renderSwatchPickerRow'; + +export const SwatchPickerRow: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useSwatchPickerRow(props, ref); + return renderSwatchPickerRow(state); +}); + +SwatchPickerRow.displayName = 'SwatchPickerRow'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/SwatchPickerRow.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/SwatchPickerRow.types.ts new file mode 100644 index 0000000000000..4068d05ad1271 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/SwatchPickerRow.types.ts @@ -0,0 +1,15 @@ +import type { SwatchPickerRowBaseState } from '@fluentui/react-swatch-picker'; + +export type { + SwatchPickerRowBaseProps as SwatchPickerRowProps, + SwatchPickerRowSlots, +} from '@fluentui/react-swatch-picker'; + +export type SwatchPickerRowState = SwatchPickerRowBaseState & { + root: SwatchPickerRowBaseState['root'] & { + /** + * The `focusgrouprow` attribute is used to indicate that the row is part of a focus group for keyboard navigation. + */ + focusgrouprow?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/index.ts new file mode 100644 index 0000000000000..2354688677978 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/index.ts @@ -0,0 +1,4 @@ +export { SwatchPickerRow } from './SwatchPickerRow'; +export { renderSwatchPickerRow } from './renderSwatchPickerRow'; +export { useSwatchPickerRow } from './useSwatchPickerRow'; +export type { SwatchPickerRowSlots, SwatchPickerRowProps, SwatchPickerRowState } from './SwatchPickerRow.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/renderSwatchPickerRow.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/renderSwatchPickerRow.ts new file mode 100644 index 0000000000000..b07e650b1c734 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/renderSwatchPickerRow.ts @@ -0,0 +1 @@ +export { renderSwatchPickerRow_unstable as renderSwatchPickerRow } from '@fluentui/react-swatch-picker'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/useSwatchPickerRow.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/useSwatchPickerRow.ts new file mode 100644 index 0000000000000..04a5d97d6773f --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/SwatchPickerRow/useSwatchPickerRow.ts @@ -0,0 +1,17 @@ +'use client'; + +import type * as React from 'react'; +import { useSwatchPickerRowBase_unstable } from '@fluentui/react-swatch-picker'; +import type { SwatchPickerRowProps, SwatchPickerRowState } from './SwatchPickerRow.types'; + +export const useSwatchPickerRow = ( + props: SwatchPickerRowProps, + ref: React.Ref, +): SwatchPickerRowState => { + const state: SwatchPickerRowState = useSwatchPickerRowBase_unstable(props, ref); + + // eslint-disable-next-line react-hooks/immutability + state.root.focusgrouprow = ''; + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/index.ts new file mode 100644 index 0000000000000..444ead4b97890 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/index.ts @@ -0,0 +1,16 @@ +export { SwatchPicker } from './SwatchPicker'; +export { renderSwatchPicker } from './renderSwatchPicker'; +export { useSwatchPicker, useSwatchPickerContextValues, useSwatchPickerContextValue } from './useSwatchPicker'; +export type { SwatchPickerSlots, SwatchPickerProps, SwatchPickerState } from './SwatchPicker.types'; + +export type { ColorSwatchSlots, ColorSwatchProps, ColorSwatchState } from './ColorSwatch'; +export { ColorSwatch, useColorSwatch, renderColorSwatch } from './ColorSwatch'; + +export type { EmptySwatchSlots, EmptySwatchProps, EmptySwatchState } from './EmptySwatch'; +export { EmptySwatch, useEmptySwatch, renderEmptySwatch } from './EmptySwatch'; + +export type { ImageSwatchSlots, ImageSwatchProps, ImageSwatchState } from './ImageSwatch'; +export { ImageSwatch, useImageSwatch, renderImageSwatch } from './ImageSwatch'; + +export type { SwatchPickerRowSlots, SwatchPickerRowProps, SwatchPickerRowState } from './SwatchPickerRow'; +export { SwatchPickerRow, useSwatchPickerRow, renderSwatchPickerRow } from './SwatchPickerRow'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/renderSwatchPicker.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/renderSwatchPicker.ts new file mode 100644 index 0000000000000..5d80a2ef99b8c --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/renderSwatchPicker.ts @@ -0,0 +1 @@ +export { renderSwatchPicker_unstable as renderSwatchPicker } from '@fluentui/react-swatch-picker'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/useSwatchPicker.ts b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/useSwatchPicker.ts new file mode 100644 index 0000000000000..16212bc1d8441 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SwatchPicker/useSwatchPicker.ts @@ -0,0 +1,26 @@ +'use client'; + +import type * as React from 'react'; +import { useSwatchPickerBase_unstable } from '@fluentui/react-swatch-picker'; +import type { SwatchPickerProps, SwatchPickerState } from './SwatchPicker.types'; + +export { + useSwatchPickerContextValues, + useSwatchPickerContextValue_unstable as useSwatchPickerContextValue, +} from '@fluentui/react-swatch-picker'; + +export const useSwatchPicker = (props: SwatchPickerProps, ref: React.Ref): SwatchPickerState => { + const { focusMode = 'arrow', layout = 'row' } = props; + + const baseState: SwatchPickerState = useSwatchPickerBase_unstable(props, ref); + + // eslint-disable-next-line react-hooks/immutability + baseState.root['data-layout'] = layout; + + if (focusMode === 'arrow') { + // eslint-disable-next-line react-hooks/immutability + baseState.root.focusgroup = baseState.isGrid ? 'grid manual rowflow' : 'radiogroup'; + } + + return baseState; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/swatch-picker.ts b/packages/react-components/react-headless-components-preview/library/src/swatch-picker.ts new file mode 100644 index 0000000000000..e9b333fe83f85 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/swatch-picker.ts @@ -0,0 +1,36 @@ +export { + SwatchPicker, + renderSwatchPicker, + useSwatchPicker, + useSwatchPickerContextValues, + useSwatchPickerContextValue, + ColorSwatch, + renderColorSwatch, + useColorSwatch, + EmptySwatch, + renderEmptySwatch, + useEmptySwatch, + ImageSwatch, + renderImageSwatch, + useImageSwatch, + SwatchPickerRow, + renderSwatchPickerRow, + useSwatchPickerRow, +} from './components/SwatchPicker'; +export type { + SwatchPickerSlots, + SwatchPickerProps, + SwatchPickerState, + ColorSwatchSlots, + ColorSwatchProps, + ColorSwatchState, + EmptySwatchSlots, + EmptySwatchProps, + EmptySwatchState, + ImageSwatchSlots, + ImageSwatchProps, + ImageSwatchState, + SwatchPickerRowSlots, + SwatchPickerRowProps, + SwatchPickerRowState, +} from './components/SwatchPicker'; diff --git a/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/SwatchPickerDefault.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/SwatchPickerDefault.stories.tsx new file mode 100644 index 0000000000000..46a00aa58f899 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/SwatchPickerDefault.stories.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { ColorSwatch, SwatchPicker } from '@fluentui/react-headless-components-preview/swatch-picker'; +import styles from './swatch-picker.module.css'; + +const colors = [ + ['#e11d48', 'Red'], + ['#f97316', 'Orange'], + ['#84cc16', 'Green'], + ['#06b6d4', 'Cyan'], + ['#2563eb', 'Blue'], + ['#9333ea', 'Purple'], +]; + +export const Default = (): React.ReactNode => { + const [selected, setSelected] = React.useState(colors[0][0]); + + return ( +
+ setSelected(data.selectedValue)} + className={styles.picker} + focusMode="arrow" + layout="row" + > + {colors.map(([color, label]) => ( + + ))} + +
+
+ ); +}; diff --git a/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/SwatchPickerDescription.md b/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/SwatchPickerDescription.md new file mode 100644 index 0000000000000..1eee8aefdad7e --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/SwatchPickerDescription.md @@ -0,0 +1,7 @@ +SwatchPicker provides a semantic radio group or grid of selectable color and image swatches. + +Use `ColorSwatch`, `ImageSwatch`, and `EmptySwatch` as children of `SwatchPicker`. Give the picker +an accessible name and give each swatch an accessible name. + +When `layout="grid"`, group swatches into `SwatchPickerRow` children. The row structure provides +the grid semantics and coordinates used by arrow-key navigation; CSS controls only the visual layout. diff --git a/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/SwatchPickerEmpty.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/SwatchPickerEmpty.stories.tsx new file mode 100644 index 0000000000000..2ceec7eadd935 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/SwatchPickerEmpty.stories.tsx @@ -0,0 +1,92 @@ +import * as React from 'react'; +import { ColorSwatch, EmptySwatch, SwatchPicker } from '@fluentui/react-headless-components-preview/swatch-picker'; +import styles from './swatch-picker.module.css'; + +const ITEMS_LIMIT = 8; + +const defaultItems = [ + { color: '#e11d48', value: '#e11d48', label: 'Red' }, + { color: '#f97316', value: '#f97316', label: 'Orange' }, + { color: '#84cc16', value: '#84cc16', label: 'Green' }, + { color: '#06b6d4', value: '#06b6d4', label: 'Cyan' }, +]; + +export const EmptySwatchExample = (): React.ReactNode => { + const [items, setItems] = React.useState(defaultItems); + const [selectedValue, setSelectedValue] = React.useState(defaultItems[0].value); + const [customColor, setCustomColor] = React.useState('#2563eb'); + const addedSwatchRef = React.useRef(null); + + const emptyItems = Array.from({ length: ITEMS_LIMIT - items.length }); + + const addColor = () => { + const value = `${customColor}-${items.length}`; + setItems(currentItems => [...currentItems, { color: customColor, value, label: customColor }]); + setSelectedValue(value); + }; + + React.useEffect(() => { + if (items.length > defaultItems.length) { + addedSwatchRef.current?.focus(); + } + }, [items.length]); + + return ( +
+ setSelectedValue(data.selectedValue)} + className={styles.emptyPicker} + > + {items.map((item, index) => ( + defaultItems.length ? addedSwatchRef : undefined} + color={item.color} + value={item.value} + aria-label={item.label} + className={styles.swatch} + /> + ))} + {emptyItems.map((_, index) => ( + + ))} + + +
+ +
+ setCustomColor(event.target.value)} + /> + + +
+
+ ); +}; + +EmptySwatchExample.parameters = { + docs: { + description: { + story: 'Empty swatches reserve space for colors that can be added later.', + }, + }, +}; diff --git a/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/SwatchPickerGrid.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/SwatchPickerGrid.stories.tsx new file mode 100644 index 0000000000000..2fc4738ec6278 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/SwatchPickerGrid.stories.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { ColorSwatch, SwatchPicker, SwatchPickerRow } from '@fluentui/react-headless-components-preview/swatch-picker'; +import styles from './swatch-picker.module.css'; + +const colors = [ + [ + ['#e11d48', 'Red'], + ['#f97316', 'Orange'], + ['#84cc16', 'Green'], + ], + [ + ['#06b6d4', 'Cyan'], + ['#2563eb', 'Blue'], + ['#9333ea', 'Purple'], + ], +]; + +export const Grid = (): React.ReactNode => { + const [selected, setSelected] = React.useState(colors[0][0][0]); + + return ( +
+ setSelected(data.selectedValue)} + layout="grid" + className={styles.grid} + > + {colors.map((row, rowIndex) => ( + + {row.map(([color, label]) => ( + + ))} + + ))} + +
+
+ ); +}; diff --git a/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/SwatchPickerImage.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/SwatchPickerImage.stories.tsx new file mode 100644 index 0000000000000..973d3b9e53702 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/SwatchPickerImage.stories.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { ImageSwatch, SwatchPicker } from '@fluentui/react-headless-components-preview/swatch-picker'; +import styles from './swatch-picker.module.css'; + +const images = [ + { + swatchSrc: 'https://fabricweb.azureedge.net/fabric-website/assets/images/swatch-picker/sea-swatch.jpg', + value: 'sea', + label: 'Sea', + fullImageSrc: 'https://fabricweb.azureedge.net/fabric-website/assets/images/swatch-picker/sea-full-img.jpg', + }, + { + swatchSrc: 'https://fabricweb.azureedge.net/fabric-website/assets/images/swatch-picker/bridge-swatch.jpg', + value: 'bridge', + label: 'Bridge', + fullImageSrc: 'https://fabricweb.azureedge.net/fabric-website/assets/images/swatch-picker/bridge-full-img.jpg', + }, + { + swatchSrc: 'https://fabricweb.azureedge.net/fabric-website/assets/images/swatch-picker/park-swatch.jpg', + value: 'park', + label: 'Park', + fullImageSrc: 'https://fabricweb.azureedge.net/fabric-website/assets/images/swatch-picker/park-full-img.jpg', + }, +]; + +export const ImageSwatchExample = (): React.ReactNode => { + const [selectedValue, setSelectedValue] = React.useState('bridge'); + const selectedImage = images.find(image => image.value === selectedValue) ?? images[0]; + + return ( +
+ setSelectedValue(data.selectedValue)} + className={styles.imagePicker} + > + {images.map(image => ( + + ))} + + {`${selectedImage.label} +
+ ); +}; + +ImageSwatchExample.parameters = { + docs: { + description: { + story: 'A swatch can use an image thumbnail and control a larger image preview.', + }, + }, +}; diff --git a/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/index.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/index.stories.tsx new file mode 100644 index 0000000000000..825c6e85f6405 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/index.stories.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { + ColorSwatch, + EmptySwatch, + ImageSwatch, + SwatchPicker, + SwatchPickerRow, +} from '@fluentui/react-headless-components-preview/swatch-picker'; +import descriptionMd from './SwatchPickerDescription.md'; +import styles from './swatch-picker.module.css'; +import { getBrowserSupportNotice } from '../shared/browserSupportNotice'; + +export { Default } from './SwatchPickerDefault.stories'; +export { EmptySwatchExample } from './SwatchPickerEmpty.stories'; +export { Grid } from './SwatchPickerGrid.stories'; +export { ImageSwatchExample } from './SwatchPickerImage.stories'; + +export default { + title: 'Components/SwatchPicker', + component: SwatchPicker, + subcomponents: { ColorSwatch, EmptySwatch, ImageSwatch, SwatchPickerRow }, + parameters: { + docs: { + description: { + component: descriptionMd + getBrowserSupportNotice('SwatchPicker'), + }, + }, + }, + decorators: [ + (Story: React.ComponentType): React.ReactNode => ( +
+ +
+ ), + ], +}; diff --git a/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/swatch-picker.module.css b/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/swatch-picker.module.css new file mode 100644 index 0000000000000..10417aa37f8c7 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/SwatchPicker/swatch-picker.module.css @@ -0,0 +1,154 @@ +.story { + display: block; + padding: var(--space-6); + color: var(--text); + background: var(--bg); +} + +.demo { + display: grid; + gap: var(--space-4); +} + +.picker { + display: grid; + gap: var(--space-2); + grid-template-columns: repeat(6, var(--space-8)); +} + +.grid { + display: grid; + gap: var(--space-2); +} + +.gridRow { + display: grid; + grid-template-columns: repeat(3, var(--space-8)); + gap: var(--space-2); +} + +.emptyPicker { + display: grid; + grid-template-columns: repeat(8, var(--space-8)); + gap: var(--space-2); +} + +.swatch { + width: var(--space-8); + height: var(--space-8); + border: var(--stroke-thin) solid var(--border); + border-radius: var(--radius-sm); + cursor: pointer; + background: var(--fui-SwatchPicker--color); +} + +.emptySwatch { + width: var(--space-8); + height: var(--space-8); + padding: 0; + border: var(--stroke-thin) dashed var(--border-stronger); + border-radius: var(--radius-sm); + background: transparent; +} + +.imageDemo { + display: grid; + gap: var(--space-5); +} + +.imagePicker { + display: flex; + gap: var(--space-3); +} + +.imageSwatch { + width: calc(var(--space-16) + var(--space-10) - var(--space-1)); + height: calc(var(--space-16) + var(--space-10) - var(--space-1)); + padding: 0; + border: var(--stroke-thin) solid var(--border); + border-radius: var(--radius-sm); + background-position: center; + background-size: cover; + cursor: pointer; +} + +.imagePreview { + display: block; + width: 100%; + max-width: calc(var(--space-16) * 10); + aspect-ratio: 16 / 9; + border-radius: var(--radius-sm); + object-fit: cover; +} + +.actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-2); +} + +.colorInput { + width: var(--space-10); + height: var(--space-10); + padding: var(--space-1); + border: var(--stroke-thin) solid var(--border-strong); + border-radius: var(--radius-sm); + background: var(--bg); +} + +.button { + min-height: var(--space-10); + padding-inline: var(--space-4); + border: var(--stroke-thin) solid var(--border-strong); + border-radius: var(--radius-sm); + color: var(--text); + background: var(--bg-elev); +} + +.button:hover:not(:disabled) { + border-color: var(--accent); +} + +.button:disabled { + color: var(--text-faint); + cursor: not-allowed; +} + +.swatch[aria-checked='true'], +.swatch[aria-selected='true'], +.imageSwatch[aria-checked='true'], +.imageSwatch[aria-selected='true'] { + border-color: var(--text); + box-shadow: inset 0 0 var(--stroke-thick) var(--text); +} + +.swatch:focus-visible, +.imageSwatch:focus-visible, +.button:focus-visible, +.colorInput:focus-visible { + outline: var(--stroke-thick) solid var(--accent-strong); + outline-offset: var(--space-1); +} + +@media (forced-colors: active) { + .swatch[aria-checked='true'], + .swatch[aria-selected='true'], + .imageSwatch[aria-checked='true'], + .imageSwatch[aria-selected='true'] { + border-color: Highlight; + box-shadow: inset 0 0 var(--stroke-thick) Highlight; + } +} + +.selectedColor { + width: calc(var(--space-16) + var(--space-8) + var(--space-1)); + height: calc(var(--space-16) + var(--space-8) + var(--space-1)); + border: var(--stroke-thin) solid var(--border); +} + +@media (forced-colors: active) { + .selectedColor { + forced-color-adjust: none; + } +} diff --git a/packages/react-components/react-headless-components-preview/stories/src/shared/browserSupportNotice.ts b/packages/react-components/react-headless-components-preview/stories/src/shared/browserSupportNotice.ts index 14e13b74f40a3..6b5d52f6861a0 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/shared/browserSupportNotice.ts +++ b/packages/react-components/react-headless-components-preview/stories/src/shared/browserSupportNotice.ts @@ -22,6 +22,7 @@ const COMPONENT_FEATURES: Record = { Toolbar: ['focusgroup'], TabList: ['focusgroup'], Nav: ['focusgroup'], + SwatchPicker: ['focusgroup'], }; const BROWSER_SUPPORT_DOCS_PATH = '?path=/docs/overview-browser-support--docs'; diff --git a/packages/react-components/react-swatch-picker/library/etc/react-swatch-picker.api.md b/packages/react-components/react-swatch-picker/library/etc/react-swatch-picker.api.md index ee9ecdafdd803..17d1b20ffe8c9 100644 --- a/packages/react-components/react-swatch-picker/library/etc/react-swatch-picker.api.md +++ b/packages/react-components/react-swatch-picker/library/etc/react-swatch-picker.api.md @@ -18,6 +18,12 @@ import type { SlotClassNames } from '@fluentui/react-utilities'; // @public export const ColorSwatch: ForwardRefComponent; +// @public (undocumented) +export type ColorSwatchBaseProps = Omit; + +// @public (undocumented) +export type ColorSwatchBaseState = Omit; + // @public (undocumented) export const colorSwatchClassNames: SlotClassNames; @@ -44,6 +50,12 @@ export type ColorSwatchState = ComponentState & Pick; +// @public (undocumented) +export type EmptySwatchBaseProps = ComponentProps; + +// @public (undocumented) +export type EmptySwatchBaseState = ComponentState; + // @public (undocumented) export const emptySwatchClassNames: SlotClassNames; @@ -61,6 +73,12 @@ export type EmptySwatchState = ComponentState & Pick; +// @public (undocumented) +export type ImageSwatchBaseProps = Omit; + +// @public (undocumented) +export type ImageSwatchBaseState = Omit; + // @public (undocumented) export const imageSwatchClassNames: SlotClassNames; @@ -81,22 +99,22 @@ export type ImageSwatchState = ComponentState & Pick JSXElement; +export const renderColorSwatch_unstable: (state: ColorSwatchBaseState) => JSXElement; // @public -export const renderEmptySwatch_unstable: (state: EmptySwatchState) => JSXElement; +export const renderEmptySwatch_unstable: (state: EmptySwatchBaseState) => JSXElement; // @public -export const renderImageSwatch_unstable: (state: ImageSwatchState) => JSXElement; +export const renderImageSwatch_unstable: (state: ImageSwatchBaseState) => JSXElement; // @public -export const renderSwatchPicker_unstable: (state: SwatchPickerState, contextValues: SwatchPickerContextValues) => JSXElement; +export const renderSwatchPicker_unstable: (state: SwatchPickerBaseState, contextValues: SwatchPickerContextValues) => JSXElement; // @public (undocumented) export const renderSwatchPickerGrid: (props: SwatchPickerGridProps) => JSXElement[]; // @public -export const renderSwatchPickerRow_unstable: (state: SwatchPickerRowState) => JSXElement; +export const renderSwatchPickerRow_unstable: (state: SwatchPickerRowBaseState) => JSXElement; // @public (undocumented) export const swatchCSSVars: { @@ -107,6 +125,12 @@ export const swatchCSSVars: { // @public export const SwatchPicker: ForwardRefComponent; +// @public (undocumented) +export type SwatchPickerBaseProps = Omit; + +// @public (undocumented) +export type SwatchPickerBaseState = Omit; + // @public (undocumented) export const swatchPickerClassNames: SlotClassNames; @@ -165,6 +189,12 @@ export const SwatchPickerProvider: React_2.Provider & // @public export const SwatchPickerRow: ForwardRefComponent; +// @public (undocumented) +export type SwatchPickerRowBaseProps = ComponentProps; + +// @public (undocumented) +export type SwatchPickerRowBaseState = ComponentState; + // @public (undocumented) export const swatchPickerRowClassNames: SlotClassNames; @@ -195,24 +225,36 @@ export type SwatchProps = ImageSwatchProps | ColorSwatchProps; // @public export const useColorSwatch_unstable: (props: ColorSwatchProps, ref: React_2.Ref) => ColorSwatchState; +// @public +export const useColorSwatchBase_unstable: (props: ColorSwatchBaseProps, ref: React_2.Ref) => ColorSwatchBaseState; + // @public export const useColorSwatchStyles_unstable: (state: ColorSwatchState) => ColorSwatchState; // @public export const useEmptySwatch_unstable: (props: EmptySwatchProps, ref: React_2.Ref) => EmptySwatchState; +// @public +export const useEmptySwatchBase_unstable: (props: EmptySwatchBaseProps, ref: React_2.Ref) => EmptySwatchBaseState; + // @public export const useEmptySwatchStyles_unstable: (state: EmptySwatchState) => EmptySwatchState; // @public export const useImageSwatch_unstable: (props: ImageSwatchProps, ref: React_2.Ref) => ImageSwatchState; +// @public +export const useImageSwatchBase_unstable: (props: ImageSwatchBaseProps, ref: React_2.Ref) => ImageSwatchBaseState; + // @public export const useImageSwatchStyles_unstable: (state: ImageSwatchState) => ImageSwatchState; // @public export const useSwatchPicker_unstable: (props: SwatchPickerProps, ref: React_2.Ref) => SwatchPickerState; +// @public +export const useSwatchPickerBase_unstable: (props: SwatchPickerBaseProps, ref: React_2.Ref) => SwatchPickerBaseState; + // @public (undocumented) export const useSwatchPickerContextValue_unstable: (selector: ContextSelector) => T; @@ -222,6 +264,9 @@ export const useSwatchPickerContextValues: (state: SwatchPickerState) => SwatchP // @public export const useSwatchPickerRow_unstable: (props: SwatchPickerRowProps, ref: React_2.Ref) => SwatchPickerRowState; +// @public +export const useSwatchPickerRowBase_unstable: (props: SwatchPickerRowBaseProps, ref: React_2.Ref) => SwatchPickerRowBaseState; + // @public export const useSwatchPickerRowStyles_unstable: (state: SwatchPickerRowState) => SwatchPickerRowState; diff --git a/packages/react-components/react-swatch-picker/library/src/ColorSwatch.ts b/packages/react-components/react-swatch-picker/library/src/ColorSwatch.ts index 49d83f567eace..75dae5f4d87fe 100644 --- a/packages/react-components/react-swatch-picker/library/src/ColorSwatch.ts +++ b/packages/react-components/react-swatch-picker/library/src/ColorSwatch.ts @@ -1,4 +1,10 @@ -export type { ColorSwatchProps, ColorSwatchSlots, ColorSwatchState } from './components/ColorSwatch/index'; +export type { + ColorSwatchBaseProps, + ColorSwatchBaseState, + ColorSwatchProps, + ColorSwatchSlots, + ColorSwatchState, +} from './components/ColorSwatch/index'; export { ColorSwatch, colorSwatchClassNames, @@ -6,4 +12,5 @@ export { swatchCSSVars, useColorSwatchStyles_unstable, useColorSwatch_unstable, + useColorSwatchBase_unstable, } from './components/ColorSwatch/index'; diff --git a/packages/react-components/react-swatch-picker/library/src/EmptySwatch.ts b/packages/react-components/react-swatch-picker/library/src/EmptySwatch.ts index e3f07b3ca09b1..8b7bea451c4b7 100644 --- a/packages/react-components/react-swatch-picker/library/src/EmptySwatch.ts +++ b/packages/react-components/react-swatch-picker/library/src/EmptySwatch.ts @@ -1,8 +1,15 @@ -export type { EmptySwatchProps, EmptySwatchSlots, EmptySwatchState } from './components/EmptySwatch/index'; +export type { + EmptySwatchBaseProps, + EmptySwatchBaseState, + EmptySwatchProps, + EmptySwatchSlots, + EmptySwatchState, +} from './components/EmptySwatch/index'; export { EmptySwatch, emptySwatchClassNames, renderEmptySwatch_unstable, + useEmptySwatchBase_unstable, useEmptySwatchStyles_unstable, useEmptySwatch_unstable, } from './components/EmptySwatch/index'; diff --git a/packages/react-components/react-swatch-picker/library/src/ImageSwatch.ts b/packages/react-components/react-swatch-picker/library/src/ImageSwatch.ts index 91d5b933fce0d..e726858537267 100644 --- a/packages/react-components/react-swatch-picker/library/src/ImageSwatch.ts +++ b/packages/react-components/react-swatch-picker/library/src/ImageSwatch.ts @@ -1,8 +1,15 @@ -export type { ImageSwatchProps, ImageSwatchSlots, ImageSwatchState } from './components/ImageSwatch/index'; +export type { + ImageSwatchBaseProps, + ImageSwatchBaseState, + ImageSwatchProps, + ImageSwatchSlots, + ImageSwatchState, +} from './components/ImageSwatch/index'; export { ImageSwatch, imageSwatchClassNames, renderImageSwatch_unstable, useImageSwatchStyles_unstable, useImageSwatch_unstable, + useImageSwatchBase_unstable, } from './components/ImageSwatch/index'; diff --git a/packages/react-components/react-swatch-picker/library/src/SwatchPicker.ts b/packages/react-components/react-swatch-picker/library/src/SwatchPicker.ts index f745cebd312d8..4260050a1c1f5 100644 --- a/packages/react-components/react-swatch-picker/library/src/SwatchPicker.ts +++ b/packages/react-components/react-swatch-picker/library/src/SwatchPicker.ts @@ -1,4 +1,6 @@ export type { + SwatchPickerBaseProps, + SwatchPickerBaseState, SwatchPickerOnSelectEventHandler, SwatchPickerOnSelectionChangeData, SwatchPickerProps, @@ -11,4 +13,5 @@ export { swatchPickerClassNames, useSwatchPickerStyles_unstable, useSwatchPicker_unstable, + useSwatchPickerBase_unstable, } from './components/SwatchPicker/index'; diff --git a/packages/react-components/react-swatch-picker/library/src/SwatchPickerRow.ts b/packages/react-components/react-swatch-picker/library/src/SwatchPickerRow.ts index e895c28862f58..271d0159c9363 100644 --- a/packages/react-components/react-swatch-picker/library/src/SwatchPickerRow.ts +++ b/packages/react-components/react-swatch-picker/library/src/SwatchPickerRow.ts @@ -1,4 +1,6 @@ export type { + SwatchPickerRowBaseProps, + SwatchPickerRowBaseState, SwatchPickerRowProps, SwatchPickerRowSlots, SwatchPickerRowState, @@ -9,4 +11,5 @@ export { swatchPickerRowClassNames, useSwatchPickerRowStyles_unstable, useSwatchPickerRow_unstable, + useSwatchPickerRowBase_unstable, } from './components/SwatchPickerRow/index'; diff --git a/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/ColorSwatch.types.ts b/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/ColorSwatch.types.ts index af41ad9d8b8e4..7b2a43968a71a 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/ColorSwatch.types.ts +++ b/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/ColorSwatch.types.ts @@ -33,6 +33,8 @@ export type ColorSwatchProps = ComponentProps & value: string; }; +export type ColorSwatchBaseProps = Omit; + /** * State used in rendering ColorSwatch */ @@ -40,3 +42,5 @@ export type ColorSwatchState = ComponentState & Pick & { selected: boolean; }; + +export type ColorSwatchBaseState = Omit; diff --git a/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/index.ts b/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/index.ts index 73095cbd7936a..a9a4b2398b42d 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/index.ts +++ b/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/index.ts @@ -1,5 +1,11 @@ export { ColorSwatch } from './ColorSwatch'; -export type { ColorSwatchProps, ColorSwatchSlots, ColorSwatchState } from './ColorSwatch.types'; +export type { + ColorSwatchBaseProps, + ColorSwatchBaseState, + ColorSwatchProps, + ColorSwatchSlots, + ColorSwatchState, +} from './ColorSwatch.types'; export { renderColorSwatch_unstable } from './renderColorSwatch'; -export { useColorSwatch_unstable } from './useColorSwatch'; +export { useColorSwatch_unstable, useColorSwatchBase_unstable } from './useColorSwatch'; export { colorSwatchClassNames, swatchCSSVars, useColorSwatchStyles_unstable } from './useColorSwatchStyles.styles'; diff --git a/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/renderColorSwatch.tsx b/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/renderColorSwatch.tsx index ba11faab441cd..4171bcd84f1b5 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/renderColorSwatch.tsx +++ b/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/renderColorSwatch.tsx @@ -3,12 +3,12 @@ import { assertSlots } from '@fluentui/react-utilities'; import type { JSXElement } from '@fluentui/react-utilities'; -import type { ColorSwatchState, ColorSwatchSlots } from './ColorSwatch.types'; +import type { ColorSwatchBaseState, ColorSwatchSlots } from './ColorSwatch.types'; /** * Render the final JSX of ColorSwatch */ -export const renderColorSwatch_unstable = (state: ColorSwatchState): JSXElement => { +export const renderColorSwatch_unstable = (state: ColorSwatchBaseState): JSXElement => { assertSlots(state); return ( diff --git a/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/useColorSwatch.test.tsx b/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/useColorSwatch.test.tsx new file mode 100644 index 0000000000000..bb51de42edf6c --- /dev/null +++ b/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/useColorSwatch.test.tsx @@ -0,0 +1,95 @@ +import * as React from 'react'; +import { act, renderHook } from '@testing-library/react-hooks'; +import { SwatchPickerProvider, swatchPickerContextDefaultValue } from '../../contexts/swatchPicker'; +import type { SwatchPickerContextValue } from '../../contexts/swatchPicker'; +import { useColorSwatch_unstable } from './useColorSwatch'; + +const props = { color: '#ff0000', value: 'red' }; + +const createWrapper = + (value: Partial) => + ({ children }: { children: React.ReactNode }) => + {children}; + +describe('useColorSwatch', () => { + it('uses the default size and shape', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useColorSwatch_unstable(props, ref)); + + expect(result.current.size).toBe('medium'); + expect(result.current.shape).toBe('square'); + }); + + it('uses the size and shape from context', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useColorSwatch_unstable(props, ref), { + wrapper: createWrapper({ shape: 'circular', size: 'large' }), + }); + + expect(result.current.size).toBe('large'); + expect(result.current.shape).toBe('circular'); + }); + + it('prefers the size and shape props over context', () => { + const ref = React.createRef(); + const { result } = renderHook( + () => useColorSwatch_unstable({ ...props, shape: 'rounded', size: 'extra-small' }, ref), + { wrapper: createWrapper({ shape: 'circular', size: 'large' }) }, + ); + + expect(result.current.size).toBe('extra-small'); + expect(result.current.shape).toBe('rounded'); + }); + + it('uses the radio role outside of a grid', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useColorSwatch_unstable(props, ref)); + + expect(result.current.root.role).toBe('radio'); + expect(result.current.root['aria-checked']).toBe(false); + }); + + it('uses the gridcell role inside a grid', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useColorSwatch_unstable(props, ref), { + wrapper: createWrapper({ isGrid: true }), + }); + + expect(result.current.root.role).toBe('gridcell'); + expect(result.current.root['aria-selected']).toBe(false); + }); + + it('uses the selected value from context', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useColorSwatch_unstable(props, ref), { + wrapper: createWrapper({ selectedValue: 'red' }), + }); + + expect(result.current.selected).toBe(true); + expect(result.current.root['aria-checked']).toBe(true); + }); + + it('renders a disabled icon by default', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useColorSwatch_unstable(props, ref)); + + expect(result.current.disabledIcon?.children).toBeDefined(); + }); + + it('forwards requested selection changes', () => { + const requestSelectionChange = jest.fn(); + const event = {} as React.MouseEvent; + const ref = React.createRef(); + const { result } = renderHook(() => useColorSwatch_unstable(props, ref), { + wrapper: createWrapper({ requestSelectionChange }), + }); + + act(() => result.current.root.onClick?.(event)); + + expect(requestSelectionChange).toHaveBeenCalledTimes(1); + expect(requestSelectionChange).toHaveBeenCalledWith(event, { + selectedValue: 'red', + selectedSwatch: '#ff0000', + }); + }); +}); diff --git a/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/useColorSwatch.tsx b/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/useColorSwatch.tsx index c2bada3e516a1..1eb0a19b74b55 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/useColorSwatch.tsx +++ b/packages/react-components/react-swatch-picker/library/src/components/ColorSwatch/useColorSwatch.tsx @@ -2,28 +2,30 @@ import * as React from 'react'; import { slot, useEventCallback, getIntrinsicElementProps, mergeCallbacks } from '@fluentui/react-utilities'; -import type { ColorSwatchProps, ColorSwatchState } from './ColorSwatch.types'; +import type { + ColorSwatchBaseProps, + ColorSwatchBaseState, + ColorSwatchProps, + ColorSwatchState, +} from './ColorSwatch.types'; import { useSwatchPickerContextValue_unstable } from '../../contexts/swatchPicker'; import { swatchCSSVars } from './useColorSwatchStyles.styles'; import { ProhibitedFilled } from '@fluentui/react-icons'; import { tokens } from '@fluentui/react-theme'; /** - * Create the state required to render ColorSwatch. + * Create the basee state required to render unstyled ColorSwatch. * - * The returned state can be modified with hooks such as useColorSwatchStyles_unstable, - * before being passed to renderColorSwatch_unstable. + * The returned state can be modified with hooks before being passed to renderColorSwatch_unstable. * * @param props - props from this instance of ColorSwatch * @param ref - reference to root HTMLButtonElement of ColorSwatch */ -export const useColorSwatch_unstable = ( - props: ColorSwatchProps, +export const useColorSwatchBase_unstable = ( + props: ColorSwatchBaseProps, ref: React.Ref, -): ColorSwatchState => { - const { borderColor, color, disabled, disabledIcon, icon, value, onClick, size, shape, style, ...rest } = props; - const _size = useSwatchPickerContextValue_unstable(ctx => ctx.size); - const _shape = useSwatchPickerContextValue_unstable(ctx => ctx.shape); +): ColorSwatchBaseState => { + const { borderColor, color, disabled, disabledIcon, icon, value, onClick, style, ...rest } = props; const isGrid = useSwatchPickerContextValue_unstable(ctx => ctx.isGrid); const requestSelectionChange = useSwatchPickerContextValue_unstable(ctx => ctx.requestSelectionChange); @@ -52,10 +54,6 @@ export const useColorSwatch_unstable = ( const iconShorthand = slot.optional(icon, { elementType: 'span' }); const disabledIconShorthand = slot.optional(disabledIcon, { - defaultProps: { - children: , - }, - renderByDefault: true, elementType: 'span', }); @@ -84,10 +82,42 @@ export const useColorSwatch_unstable = ( icon: iconShorthand, disabledIcon: disabledIconShorthand, disabled, - size: size ?? _size, - shape: shape ?? _shape, selected, color, value, }; }; + +/** + * Create the state required to render ColorSwatch. + * + * The returned state can be modified with hooks such as useColorSwatchStyles_unstable, + * before being passed to renderColorSwatch_unstable. + * + * @param props - props from this instance of ColorSwatch + * @param ref - reference to root HTMLButtonElement of ColorSwatch + */ +export const useColorSwatch_unstable = ( + props: ColorSwatchProps, + ref: React.Ref, +): ColorSwatchState => { + const sizeFromContext = useSwatchPickerContextValue_unstable(ctx => ctx.size); + const shapeFromContext = useSwatchPickerContextValue_unstable(ctx => ctx.shape); + const { size = sizeFromContext, shape = shapeFromContext, disabledIcon, ...rest } = props; + + const baseState = useColorSwatchBase_unstable(rest, ref); + + return { + ...baseState, + size, + shape, + disabledIcon: slot.optional(disabledIcon, { + defaultProps: { + ...baseState.disabledIcon, + children: , + }, + renderByDefault: true, + elementType: 'span', + }), + }; +}; diff --git a/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/EmptySwatch.types.ts b/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/EmptySwatch.types.ts index f3f1ccace6d40..8a22612bf2c6b 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/EmptySwatch.types.ts +++ b/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/EmptySwatch.types.ts @@ -10,7 +10,11 @@ export type EmptySwatchSlots = { */ export type EmptySwatchProps = ComponentProps & Pick; +export type EmptySwatchBaseProps = ComponentProps; + /** * State used in rendering EmptySwatch */ export type EmptySwatchState = ComponentState & Pick; + +export type EmptySwatchBaseState = ComponentState; diff --git a/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/index.ts b/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/index.ts index 53a36de504647..b705fcb1e8757 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/index.ts +++ b/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/index.ts @@ -1,5 +1,11 @@ export { EmptySwatch } from './EmptySwatch'; -export type { EmptySwatchProps, EmptySwatchSlots, EmptySwatchState } from './EmptySwatch.types'; +export type { + EmptySwatchBaseProps, + EmptySwatchBaseState, + EmptySwatchProps, + EmptySwatchSlots, + EmptySwatchState, +} from './EmptySwatch.types'; export { renderEmptySwatch_unstable } from './renderEmptySwatch'; -export { useEmptySwatch_unstable } from './useEmptySwatch'; +export { useEmptySwatch_unstable, useEmptySwatchBase_unstable } from './useEmptySwatch'; export { emptySwatchClassNames, useEmptySwatchStyles_unstable } from './useEmptySwatchStyles.styles'; diff --git a/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/renderEmptySwatch.tsx b/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/renderEmptySwatch.tsx index 47785923e2401..603c0b3cf2392 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/renderEmptySwatch.tsx +++ b/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/renderEmptySwatch.tsx @@ -3,12 +3,12 @@ import { assertSlots } from '@fluentui/react-utilities'; import type { JSXElement } from '@fluentui/react-utilities'; -import type { EmptySwatchState, EmptySwatchSlots } from './EmptySwatch.types'; +import type { EmptySwatchBaseState, EmptySwatchSlots } from './EmptySwatch.types'; /** * Render the final JSX of EmptySwatch */ -export const renderEmptySwatch_unstable = (state: EmptySwatchState): JSXElement => { +export const renderEmptySwatch_unstable = (state: EmptySwatchBaseState): JSXElement => { assertSlots(state); return ; diff --git a/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/useEmptySwatch.test.tsx b/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/useEmptySwatch.test.tsx new file mode 100644 index 0000000000000..df5c19eacd98d --- /dev/null +++ b/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/useEmptySwatch.test.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import { SwatchPickerProvider, swatchPickerContextDefaultValue } from '../../contexts/swatchPicker'; +import type { SwatchPickerContextValue } from '../../contexts/swatchPicker'; +import { useEmptySwatch_unstable } from './useEmptySwatch'; + +const createWrapper = + (value: Partial) => + ({ children }: { children: React.ReactNode }) => + {children}; + +describe('useEmptySwatch', () => { + it('uses the default size and shape', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useEmptySwatch_unstable({}, ref)); + + expect(result.current.size).toBe('medium'); + expect(result.current.shape).toBe('square'); + }); + + it('uses the size and shape from context', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useEmptySwatch_unstable({}, ref), { + wrapper: createWrapper({ shape: 'circular', size: 'large' }), + }); + + expect(result.current.size).toBe('large'); + expect(result.current.shape).toBe('circular'); + }); + + it('prefers the size and shape props over context', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useEmptySwatch_unstable({ shape: 'rounded', size: 'extra-small' }, ref), { + wrapper: createWrapper({ shape: 'circular', size: 'large' }), + }); + + expect(result.current.size).toBe('extra-small'); + expect(result.current.shape).toBe('rounded'); + }); + + it('uses the radio role outside of a grid', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useEmptySwatch_unstable({}, ref)); + + expect(result.current.root.role).toBe('radio'); + expect(result.current.root['aria-checked']).toBe(false); + }); + + it('uses the gridcell role inside a grid', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useEmptySwatch_unstable({}, ref), { + wrapper: createWrapper({ isGrid: true }), + }); + + expect(result.current.root.role).toBe('gridcell'); + expect(result.current.root['aria-checked']).toBeUndefined(); + }); +}); diff --git a/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/useEmptySwatch.ts b/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/useEmptySwatch.ts index 830969f5f35e8..fcb74d989a010 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/useEmptySwatch.ts +++ b/packages/react-components/react-swatch-picker/library/src/components/EmptySwatch/useEmptySwatch.ts @@ -2,25 +2,26 @@ import type * as React from 'react'; import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; -import type { EmptySwatchProps, EmptySwatchState } from './EmptySwatch.types'; +import type { + EmptySwatchBaseProps, + EmptySwatchBaseState, + EmptySwatchProps, + EmptySwatchState, +} from './EmptySwatch.types'; import { useSwatchPickerContextValue_unstable } from '../../contexts/swatchPicker'; /** - * Create the state required to render EmptySwatch. + * Create the base state required to render unstyled EmptySwatch. * - * The returned state can be modified with hooks such as useEmptySwatchStyles_unstable, - * before being passed to renderEmptySwatch_unstable. + * The returned state can be modified with hooks before being passed to renderEmptySwatch_unstable. * * @param props - props from this instance of EmptySwatch * @param ref - reference to root HTMLDivElement of EmptySwatch */ -export const useEmptySwatch_unstable = ( - props: EmptySwatchProps, +export const useEmptySwatchBase_unstable = ( + props: EmptySwatchBaseProps, ref: React.Ref, -): EmptySwatchState => { - const { size, shape, ...rest } = props; - const _size = useSwatchPickerContextValue_unstable(ctx => ctx.size); - const _shape = useSwatchPickerContextValue_unstable(ctx => ctx.shape); +): EmptySwatchBaseState => { const isGrid = useSwatchPickerContextValue_unstable(ctx => ctx.isGrid); const role = isGrid ? 'gridcell' : 'radio'; @@ -34,11 +35,35 @@ export const useEmptySwatch_unstable = ( ref, role, ...a11yProps, - ...rest, + ...props, }), { elementType: 'button' }, ), - size: size ?? _size, - shape: shape ?? _shape, + }; +}; + +/** + * Create the state required to render EmptySwatch. + * + * The returned state can be modified with hooks such as useEmptySwatchStyles_unstable, + * before being passed to renderEmptySwatch_unstable. + * + * @param props - props from this instance of EmptySwatch + * @param ref - reference to root HTMLDivElement of EmptySwatch + */ +export const useEmptySwatch_unstable = ( + props: EmptySwatchProps, + ref: React.Ref, +): EmptySwatchState => { + const sizeFromContext = useSwatchPickerContextValue_unstable(ctx => ctx.size); + const shapeFromContext = useSwatchPickerContextValue_unstable(ctx => ctx.shape); + const { size = sizeFromContext, shape = shapeFromContext, ...rest } = props; + + const baseState = useEmptySwatchBase_unstable(rest, ref); + + return { + ...baseState, + size, + shape, }; }; diff --git a/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/ImageSwatch.types.ts b/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/ImageSwatch.types.ts index 5ed9117013589..f4a0ccb2d2b8d 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/ImageSwatch.types.ts +++ b/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/ImageSwatch.types.ts @@ -21,6 +21,8 @@ export type ImageSwatchProps = ComponentProps & value: string; }; +export type ImageSwatchBaseProps = Omit; + /** * State used in rendering ImageSwatch */ @@ -28,3 +30,5 @@ export type ImageSwatchState = ComponentState & Pick & { selected: boolean; }; + +export type ImageSwatchBaseState = Omit; diff --git a/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/index.ts b/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/index.ts index bd86939248ce1..16a5a998b7473 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/index.ts +++ b/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/index.ts @@ -1,5 +1,11 @@ export { ImageSwatch } from './ImageSwatch'; -export type { ImageSwatchProps, ImageSwatchSlots, ImageSwatchState } from './ImageSwatch.types'; +export type { + ImageSwatchBaseProps, + ImageSwatchBaseState, + ImageSwatchProps, + ImageSwatchSlots, + ImageSwatchState, +} from './ImageSwatch.types'; export { renderImageSwatch_unstable } from './renderImageSwatch'; -export { useImageSwatch_unstable } from './useImageSwatch'; +export { useImageSwatch_unstable, useImageSwatchBase_unstable } from './useImageSwatch'; export { imageSwatchClassNames, useImageSwatchStyles_unstable } from './useImageSwatchStyles.styles'; diff --git a/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/renderImageSwatch.tsx b/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/renderImageSwatch.tsx index 1c9ec30fab848..8aea8fce31bcf 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/renderImageSwatch.tsx +++ b/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/renderImageSwatch.tsx @@ -3,12 +3,12 @@ import { assertSlots } from '@fluentui/react-utilities'; import type { JSXElement } from '@fluentui/react-utilities'; -import type { ImageSwatchState, ImageSwatchSlots } from './ImageSwatch.types'; +import type { ImageSwatchBaseState, ImageSwatchSlots } from './ImageSwatch.types'; /** * Render the final JSX of ImageSwatch */ -export const renderImageSwatch_unstable = (state: ImageSwatchState): JSXElement => { +export const renderImageSwatch_unstable = (state: ImageSwatchBaseState): JSXElement => { assertSlots(state); return ; diff --git a/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/useImageSwatch.test.tsx b/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/useImageSwatch.test.tsx new file mode 100644 index 0000000000000..dba680ca09b92 --- /dev/null +++ b/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/useImageSwatch.test.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import { act, renderHook } from '@testing-library/react-hooks'; +import { SwatchPickerProvider, swatchPickerContextDefaultValue } from '../../contexts/swatchPicker'; +import type { SwatchPickerContextValue } from '../../contexts/swatchPicker'; +import { useImageSwatch_unstable } from './useImageSwatch'; + +const props = { src: 'image.png', value: 'image' }; + +const createWrapper = + (value: Partial) => + ({ children }: { children: React.ReactNode }) => + {children}; + +describe('useImageSwatch', () => { + it('uses the default size and shape', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useImageSwatch_unstable(props, ref)); + + expect(result.current.size).toBe('medium'); + expect(result.current.shape).toBe('square'); + }); + + it('uses the size and shape from context', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useImageSwatch_unstable(props, ref), { + wrapper: createWrapper({ shape: 'circular', size: 'large' }), + }); + + expect(result.current.size).toBe('large'); + expect(result.current.shape).toBe('circular'); + }); + + it('prefers the size and shape from context over props', () => { + const ref = React.createRef(); + const { result } = renderHook( + () => useImageSwatch_unstable({ ...props, shape: 'rounded', size: 'extra-small' }, ref), + { wrapper: createWrapper({ shape: 'circular', size: 'large' }) }, + ); + + expect(result.current.size).toBe('large'); + expect(result.current.shape).toBe('circular'); + }); + + it('uses the radio role outside of a grid', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useImageSwatch_unstable(props, ref)); + + expect(result.current.root.role).toBe('radio'); + expect(result.current.root['aria-checked']).toBe(false); + }); + + it('uses the gridcell role inside a grid', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useImageSwatch_unstable(props, ref), { + wrapper: createWrapper({ isGrid: true }), + }); + + expect(result.current.root.role).toBe('gridcell'); + expect(result.current.root['aria-selected']).toBe(false); + }); + + it('uses the selected value from context', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useImageSwatch_unstable(props, ref), { + wrapper: createWrapper({ selectedValue: 'image' }), + }); + + expect(result.current.selected).toBe(true); + expect(result.current.root['aria-checked']).toBe(true); + }); + + it('forwards requested selection changes', () => { + const requestSelectionChange = jest.fn(); + const event = {} as React.MouseEvent; + const ref = React.createRef(); + const { result } = renderHook(() => useImageSwatch_unstable(props, ref), { + wrapper: createWrapper({ requestSelectionChange }), + }); + + act(() => result.current.root.onClick?.(event)); + + expect(requestSelectionChange).toHaveBeenCalledTimes(1); + expect(requestSelectionChange).toHaveBeenCalledWith(event, { + selectedValue: 'image', + selectedSwatch: 'image.png', + }); + }); +}); diff --git a/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/useImageSwatch.ts b/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/useImageSwatch.ts index 4030a53f5a8e6..1dfbefb011fd3 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/useImageSwatch.ts +++ b/packages/react-components/react-swatch-picker/library/src/components/ImageSwatch/useImageSwatch.ts @@ -2,25 +2,27 @@ import type * as React from 'react'; import { getIntrinsicElementProps, slot, useEventCallback, mergeCallbacks } from '@fluentui/react-utilities'; -import type { ImageSwatchProps, ImageSwatchState } from './ImageSwatch.types'; +import type { + ImageSwatchBaseProps, + ImageSwatchBaseState, + ImageSwatchProps, + ImageSwatchState, +} from './ImageSwatch.types'; import { useSwatchPickerContextValue_unstable } from '../../contexts/swatchPicker'; /** - * Create the state required to render ImageSwatch. + * Create the base state required to render unstyled ImageSwatch. * - * The returned state can be modified with hooks such as useImageSwatchStyles_unstable, - * before being passed to renderImageSwatch_unstable. + * The returned state can be modified with hooks before being passed to renderImageSwatch_unstable. * * @param props - props from this instance of ImageSwatch * @param ref - reference to root HTMLDivElement of ImageSwatch */ -export const useImageSwatch_unstable = ( - props: ImageSwatchProps, +export const useImageSwatchBase_unstable = ( + props: ImageSwatchBaseProps, ref: React.Ref, -): ImageSwatchState => { +): ImageSwatchBaseState => { const { src, value, onClick, style, ...rest } = props; - const size = useSwatchPickerContextValue_unstable(ctx => ctx.size); - const shape = useSwatchPickerContextValue_unstable(ctx => ctx.shape); const isGrid = useSwatchPickerContextValue_unstable(ctx => ctx.isGrid); const requestSelectionChange = useSwatchPickerContextValue_unstable(ctx => ctx.requestSelectionChange); @@ -62,7 +64,32 @@ export const useImageSwatch_unstable = ( ), value, selected, - size, - shape, + }; +}; + +/** + * Create the state required to render ImageSwatch. + * + * The returned state can be modified with hooks such as useImageSwatchStyles_unstable, + * before being passed to renderImageSwatch_unstable. + * + * @param props - props from this instance of ImageSwatch + * @param ref - reference to root HTMLDivElement of ImageSwatch + */ +export const useImageSwatch_unstable = ( + props: ImageSwatchProps, + ref: React.Ref, +): ImageSwatchState => { + const sizeFromContext = useSwatchPickerContextValue_unstable(ctx => ctx.size); + const shapeFromContext = useSwatchPickerContextValue_unstable(ctx => ctx.shape); + + const { size: _size, shape: _shape, ...rest } = props; + + const baseState = useImageSwatchBase_unstable(rest, ref); + + return { + ...baseState, + size: sizeFromContext, + shape: shapeFromContext, }; }; diff --git a/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/SwatchPicker.types.ts b/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/SwatchPicker.types.ts index 5e25a408535d4..a91811406e5da 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/SwatchPicker.types.ts +++ b/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/SwatchPicker.types.ts @@ -69,6 +69,8 @@ export type SwatchPickerProps = ComponentProps & { spacing?: 'small' | 'medium'; }; +export type SwatchPickerBaseProps = Omit; + /** * State used in rendering SwatchPicker */ @@ -77,3 +79,5 @@ export type SwatchPickerState = ComponentState & Pick & { isGrid: boolean; }; + +export type SwatchPickerBaseState = Omit; diff --git a/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/index.ts b/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/index.ts index aeb175311cc05..52a626a0c4e96 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/index.ts +++ b/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/index.ts @@ -1,5 +1,7 @@ export { SwatchPicker } from './SwatchPicker'; export type { + SwatchPickerBaseProps, + SwatchPickerBaseState, SwatchPickerOnSelectEventHandler, SwatchPickerOnSelectionChangeData, SwatchPickerProps, @@ -7,5 +9,5 @@ export type { SwatchPickerState, } from './SwatchPicker.types'; export { renderSwatchPicker_unstable } from './renderSwatchPicker'; -export { useSwatchPicker_unstable } from './useSwatchPicker'; +export { useSwatchPicker_unstable, useSwatchPickerBase_unstable } from './useSwatchPicker'; export { swatchPickerClassNames, useSwatchPickerStyles_unstable } from './useSwatchPickerStyles.styles'; diff --git a/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/renderSwatchPicker.tsx b/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/renderSwatchPicker.tsx index 27e90ef25a8ff..1150b6fc0ac35 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/renderSwatchPicker.tsx +++ b/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/renderSwatchPicker.tsx @@ -5,13 +5,13 @@ import { assertSlots } from '@fluentui/react-utilities'; import type { JSXElement } from '@fluentui/react-utilities'; import { SwatchPickerProvider } from '../../contexts/swatchPicker'; import type { SwatchPickerContextValues } from '../../contexts/swatchPicker'; -import type { SwatchPickerState, SwatchPickerSlots } from './SwatchPicker.types'; +import type { SwatchPickerBaseState, SwatchPickerSlots } from './SwatchPicker.types'; /** * Render the final JSX of SwatchPicker */ export const renderSwatchPicker_unstable = ( - state: SwatchPickerState, + state: SwatchPickerBaseState, contextValues: SwatchPickerContextValues, ): JSXElement => { assertSlots(state); diff --git a/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/useSwatchPicker.test.tsx b/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/useSwatchPicker.test.tsx new file mode 100644 index 0000000000000..0dde4482f4966 --- /dev/null +++ b/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/useSwatchPicker.test.tsx @@ -0,0 +1,80 @@ +import * as React from 'react'; +import { act, renderHook } from '@testing-library/react-hooks'; +import { useSwatchPicker_unstable } from './useSwatchPicker'; + +describe('useSwatchPicker', () => { + it('uses the default size, shape and spacing', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useSwatchPicker_unstable({}, ref)); + + expect(result.current.size).toBe('medium'); + expect(result.current.shape).toBeUndefined(); + expect(result.current.spacing).toBe('medium'); + }); + + it('uses the size, shape and spacing props', () => { + const ref = React.createRef(); + const { result } = renderHook(() => + useSwatchPicker_unstable({ shape: 'circular', size: 'large', spacing: 'small' }, ref), + ); + + expect(result.current.size).toBe('large'); + expect(result.current.shape).toBe('circular'); + expect(result.current.spacing).toBe('small'); + }); + + it('uses the radiogroup role by default', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useSwatchPicker_unstable({}, ref)); + + expect(result.current.isGrid).toBe(false); + expect(result.current.root.role).toBe('radiogroup'); + }); + + it('uses the grid role for the grid layout', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useSwatchPicker_unstable({ layout: 'grid' }, ref)); + + expect(result.current.isGrid).toBe(true); + expect(result.current.root.role).toBe('grid'); + }); + + it('applies arrow navigation attributes by default', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useSwatchPicker_unstable({}, ref)); + + expect(result.current.root).toHaveProperty('data-tabster', expect.any(String)); + }); + + it('does not apply arrow navigation attributes when focusMode is tab', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useSwatchPicker_unstable({ focusMode: 'tab' }, ref)); + + expect(result.current.root).not.toHaveProperty('data-tabster'); + }); + + it('uses the default selected value', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useSwatchPicker_unstable({ defaultSelectedValue: 'red' }, ref)); + + expect(result.current.selectedValue).toBe('red'); + }); + + it('forwards requested selection changes', () => { + const onSelectionChange = jest.fn(); + const event = {} as React.MouseEvent; + const ref = React.createRef(); + const { result } = renderHook(() => useSwatchPicker_unstable({ onSelectionChange }, ref)); + + act(() => result.current.requestSelectionChange(event, { selectedValue: 'red', selectedSwatch: '#ff0000' })); + + expect(result.current.selectedValue).toBe('red'); + expect(onSelectionChange).toHaveBeenCalledTimes(1); + expect(onSelectionChange).toHaveBeenCalledWith(event, { + type: 'click', + event, + selectedValue: 'red', + selectedSwatch: '#ff0000', + }); + }); +}); diff --git a/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/useSwatchPicker.ts b/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/useSwatchPicker.ts index accf01d22a880..7351c28179ae0 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/useSwatchPicker.ts +++ b/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/useSwatchPicker.ts @@ -6,41 +6,31 @@ import { useFieldControlProps_unstable } from '@fluentui/react-field'; import { useArrowNavigationGroup } from '@fluentui/react-tabster'; import { getIntrinsicElementProps, useControllableState, useEventCallback, slot } from '@fluentui/react-utilities'; -import type { SwatchPickerProps, SwatchPickerState } from './SwatchPicker.types'; +import type { + SwatchPickerBaseProps, + SwatchPickerBaseState, + SwatchPickerProps, + SwatchPickerState, +} from './SwatchPicker.types'; /** - * Create the state required to render SwatchPicker. + * Create the base state required to render unstyled SwatchPicker. * - * The returned state can be modified with hooks such as useSwatchPickerStyles_unstable, - * before being passed to renderSwatchPicker_unstable. + * The returned state can be modified with hooks before being passed to renderSwatchPicker_unstable. * * @param props - props from this instance of SwatchPicker * @param ref - reference to root HTMLElement of SwatchPicker */ -export const useSwatchPicker_unstable = ( - props: SwatchPickerProps, +export const useSwatchPickerBase_unstable = ( + props: SwatchPickerBaseProps, ref: React.Ref, -): SwatchPickerState => { +): SwatchPickerBaseState => { // Merge props from surrounding , if any props = useFieldControlProps_unstable(props); - const { - focusMode = 'arrow', - layout, - onSelectionChange, - size = 'medium', - shape, - spacing = 'medium', - style, - ...rest - } = props; + const { layout, onSelectionChange, style, ...rest } = props; const isGrid = layout === 'grid'; - const focusAttributes = useArrowNavigationGroup({ - circular: true, - axis: isGrid ? 'grid-linear' : 'both', - memorizeCurrent: true, - }); const role = isGrid ? 'grid' : 'radiogroup'; @@ -68,7 +58,6 @@ export const useSwatchPicker_unstable = ( getIntrinsicElementProps('div', { ref, role, - ...(focusMode === 'arrow' ? focusAttributes : {}), ...rest, }), { elementType: 'div' }, @@ -76,6 +65,38 @@ export const useSwatchPicker_unstable = ( isGrid, requestSelectionChange, selectedValue, + }; +}; + +/** + * Create the state required to render SwatchPicker. + * + * The returned state can be modified with hooks such as useSwatchPickerStyles_unstable, + * before being passed to renderSwatchPicker_unstable. + * + * @param props - props from this instance of SwatchPicker + * @param ref - reference to root HTMLElement of SwatchPicker + */ +export const useSwatchPicker_unstable = ( + props: SwatchPickerProps, + ref: React.Ref, +): SwatchPickerState => { + const { focusMode = 'arrow', size = 'medium', shape, spacing = 'medium', ...rest } = props; + + const baseState = useSwatchPickerBase_unstable(rest, ref); + + const focusAttributes = useArrowNavigationGroup({ + circular: true, + axis: baseState.isGrid ? 'grid-linear' : 'both', + memorizeCurrent: true, + }); + + return { + ...baseState, + root: { + ...baseState.root, + ...(focusMode === 'arrow' ? focusAttributes : {}), + }, size, shape, spacing, diff --git a/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/SwatchPickerRow.types.ts b/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/SwatchPickerRow.types.ts index c72bf702d2a07..6bccf9f90b874 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/SwatchPickerRow.types.ts +++ b/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/SwatchPickerRow.types.ts @@ -10,7 +10,11 @@ export type SwatchPickerRowSlots = { */ export type SwatchPickerRowProps = ComponentProps; +export type SwatchPickerRowBaseProps = ComponentProps; + /** * State used in rendering SwatchPickerRow */ export type SwatchPickerRowState = ComponentState & Pick; + +export type SwatchPickerRowBaseState = ComponentState; diff --git a/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/index.ts b/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/index.ts index c948a0da0e7ce..43447884be283 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/index.ts +++ b/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/index.ts @@ -1,5 +1,11 @@ export { SwatchPickerRow } from './SwatchPickerRow'; -export type { SwatchPickerRowProps, SwatchPickerRowSlots, SwatchPickerRowState } from './SwatchPickerRow.types'; +export type { + SwatchPickerRowBaseProps, + SwatchPickerRowBaseState, + SwatchPickerRowProps, + SwatchPickerRowSlots, + SwatchPickerRowState, +} from './SwatchPickerRow.types'; export { renderSwatchPickerRow_unstable } from './renderSwatchPickerRow'; -export { useSwatchPickerRow_unstable } from './useSwatchPickerRow'; +export { useSwatchPickerRow_unstable, useSwatchPickerRowBase_unstable } from './useSwatchPickerRow'; export { swatchPickerRowClassNames, useSwatchPickerRowStyles_unstable } from './useSwatchPickerRowStyles.styles'; diff --git a/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/renderSwatchPickerRow.tsx b/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/renderSwatchPickerRow.tsx index b5d04b63ee252..308b3279e7768 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/renderSwatchPickerRow.tsx +++ b/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/renderSwatchPickerRow.tsx @@ -3,12 +3,12 @@ import { assertSlots } from '@fluentui/react-utilities'; import type { JSXElement } from '@fluentui/react-utilities'; -import type { SwatchPickerRowState, SwatchPickerRowSlots } from './SwatchPickerRow.types'; +import type { SwatchPickerRowBaseState, SwatchPickerRowSlots } from './SwatchPickerRow.types'; /** * Render the final JSX of SwatchPickerRow */ -export const renderSwatchPickerRow_unstable = (state: SwatchPickerRowState): JSXElement => { +export const renderSwatchPickerRow_unstable = (state: SwatchPickerRowBaseState): JSXElement => { assertSlots(state); return ; }; diff --git a/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/useSwatchPickerRow.test.tsx b/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/useSwatchPickerRow.test.tsx new file mode 100644 index 0000000000000..8ec74ef1478bb --- /dev/null +++ b/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/useSwatchPickerRow.test.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import { SwatchPickerProvider, swatchPickerContextDefaultValue } from '../../contexts/swatchPicker'; +import { useSwatchPickerRow_unstable } from './useSwatchPickerRow'; + +const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + +describe('useSwatchPickerRow', () => { + it('uses the row role', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useSwatchPickerRow_unstable({}, ref)); + + expect(result.current.root.role).toBe('row'); + }); + + it('uses the default spacing', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useSwatchPickerRow_unstable({}, ref)); + + expect(result.current.spacing).toBe('medium'); + }); + + it('uses the spacing from context', () => { + const ref = React.createRef(); + const { result } = renderHook(() => useSwatchPickerRow_unstable({}, ref), { wrapper }); + + expect(result.current.spacing).toBe('small'); + }); +}); diff --git a/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/useSwatchPickerRow.ts b/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/useSwatchPickerRow.ts index 5f13568814b49..935a8507b7508 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/useSwatchPickerRow.ts +++ b/packages/react-components/react-swatch-picker/library/src/components/SwatchPickerRow/useSwatchPickerRow.ts @@ -2,24 +2,27 @@ import type * as React from 'react'; import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; -import type { SwatchPickerRowProps, SwatchPickerRowState } from './SwatchPickerRow.types'; +import type { + SwatchPickerRowBaseProps, + SwatchPickerRowBaseState, + SwatchPickerRowProps, + SwatchPickerRowState, +} from './SwatchPickerRow.types'; import { useSwatchPickerContextValue_unstable } from '../../contexts/swatchPicker'; /** - * Create the state required to render SwatchPickerRow. + * Create the base state required to render unstyled SwatchPickerRow. * - * The returned state can be modified with hooks such as useSwatchPickerRowStyles_unstable, - * before being passed to renderSwatchPickerRow_unstable. + * The returned state can be modified with hooks before being passed to renderSwatchPickerRow_unstable. * * @param props - props from this instance of SwatchPickerRow * @param ref - reference to root HTMLDivElement of SwatchPickerRow */ -export const useSwatchPickerRow_unstable = ( - props: SwatchPickerRowProps, +export const useSwatchPickerRowBase_unstable = ( + props: SwatchPickerRowBaseProps, ref: React.Ref, -): SwatchPickerRowState => { +): SwatchPickerRowBaseState => { const { style, ...rest } = props; - const spacing = useSwatchPickerContextValue_unstable(ctx => ctx.spacing); return { components: { @@ -33,6 +36,27 @@ export const useSwatchPickerRow_unstable = ( }), { elementType: 'div' }, ), + }; +}; + +/** + * Create the state required to render SwatchPickerRow. + * + * The returned state can be modified with hooks such as useSwatchPickerRowStyles_unstable, + * before being passed to renderSwatchPickerRow_unstable. + * + * @param props - props from this instance of SwatchPickerRow + * @param ref - reference to root HTMLDivElement of SwatchPickerRow + */ +export const useSwatchPickerRow_unstable = ( + props: SwatchPickerRowProps, + ref: React.Ref, +): SwatchPickerRowState => { + const spacing = useSwatchPickerContextValue_unstable(ctx => ctx.spacing); + const baseState = useSwatchPickerRowBase_unstable(props, ref); + + return { + ...baseState, spacing, }; }; diff --git a/packages/react-components/react-swatch-picker/library/src/index.ts b/packages/react-components/react-swatch-picker/library/src/index.ts index 7a4d2157f9c6b..e531f78d5311f 100644 --- a/packages/react-components/react-swatch-picker/library/src/index.ts +++ b/packages/react-components/react-swatch-picker/library/src/index.ts @@ -3,6 +3,7 @@ export { renderSwatchPicker_unstable, useSwatchPickerStyles_unstable, useSwatchPicker_unstable, + useSwatchPickerBase_unstable, swatchPickerClassNames, } from './SwatchPicker'; export { @@ -13,6 +14,8 @@ export { } from './contexts'; export type { SwatchPickerContextValue, SwatchPickerContextValues } from './contexts'; export type { + SwatchPickerBaseProps, + SwatchPickerBaseState, SwatchPickerProps, SwatchPickerSlots, SwatchPickerState, @@ -24,23 +27,44 @@ export { renderColorSwatch_unstable, useColorSwatchStyles_unstable, useColorSwatch_unstable, + useColorSwatchBase_unstable, colorSwatchClassNames, swatchCSSVars, } from './ColorSwatch'; -export type { ColorSwatchProps, ColorSwatchSlots, ColorSwatchState } from './ColorSwatch'; +export type { + ColorSwatchBaseProps, + ColorSwatchBaseState, + ColorSwatchProps, + ColorSwatchSlots, + ColorSwatchState, +} from './ColorSwatch'; export { ImageSwatch, renderImageSwatch_unstable, useImageSwatchStyles_unstable, useImageSwatch_unstable, + useImageSwatchBase_unstable, imageSwatchClassNames, } from './ImageSwatch'; -export type { ImageSwatchProps, ImageSwatchSlots, ImageSwatchState } from './ImageSwatch'; -export type { SwatchPickerRowProps, SwatchPickerRowSlots, SwatchPickerRowState } from './SwatchPickerRow'; +export type { + ImageSwatchBaseProps, + ImageSwatchBaseState, + ImageSwatchProps, + ImageSwatchSlots, + ImageSwatchState, +} from './ImageSwatch'; +export type { + SwatchPickerRowBaseProps, + SwatchPickerRowBaseState, + SwatchPickerRowProps, + SwatchPickerRowSlots, + SwatchPickerRowState, +} from './SwatchPickerRow'; export { SwatchPickerRow, renderSwatchPickerRow_unstable, useSwatchPickerRowStyles_unstable, + useSwatchPickerRowBase_unstable, useSwatchPickerRow_unstable, swatchPickerRowClassNames, } from './SwatchPickerRow'; @@ -51,6 +75,13 @@ export { renderEmptySwatch_unstable, useEmptySwatchStyles_unstable, useEmptySwatch_unstable, + useEmptySwatchBase_unstable, emptySwatchClassNames, } from './EmptySwatch'; -export type { EmptySwatchProps, EmptySwatchSlots, EmptySwatchState } from './EmptySwatch'; +export type { + EmptySwatchBaseProps, + EmptySwatchBaseState, + EmptySwatchProps, + EmptySwatchSlots, + EmptySwatchState, +} from './EmptySwatch'; diff --git a/scripts/beachball/base.config.js b/scripts/beachball/base.config.js new file mode 100644 index 0000000000000..e3af0b964d46f --- /dev/null +++ b/scripts/beachball/base.config.js @@ -0,0 +1,36 @@ +/** @satisfies {import('beachball').BeachballConfig} */ +const config = { + access: 'public', + // Target branch WITHOUT remote, since people may have multiple remotes and choose different naming schemes. + // Specifying this helps beachball more quickly determine the target branch + remote at runtime. + branch: 'master', + commit: false, + disallowedChangeTypes: ['major'], + tag: 'latest', + generateChangelog: true, + ignorePatterns: [ + '**/*.{shot,snap}', + '**/*.{test,spec,cy}.{ts,tsx}', + '**/*.stories.{ts,tsx}', + '**/.eslintrc.*', + '**/eslint.config.*', + '**/rit.config.js', + '**/__fixtures__/**', + '**/__mocks__/**', + '**/docs/**', + '**/stories/**', + '**/.storybook/**', + '**/bundle-size/**', + '**/common/isConformant.ts', + '**/src/testing/**', + '**/src/e2e/**', + '**/config/tests.js', + '**/jest.config.js', + '**/SPEC*.md', + '**/tests/**', + ], + scope: ['!packages/fluentui/*'], + changehint: "Run 'yarn change' to generate a change file", +}; + +module.exports = config; diff --git a/scripts/beachball/base.config.json b/scripts/beachball/base.config.json deleted file mode 100644 index a4666c0fdd9e5..0000000000000 --- a/scripts/beachball/base.config.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "disallowedChangeTypes": ["major"], - "tag": "latest", - "generateChangelog": true, - "ignorePatterns": [ - "**/*.{shot,snap}", - "**/*.{test,spec,cy}.{ts,tsx}", - "**/*.stories.{ts,tsx}", - "**/.eslintrc.*", - "**/eslint.config.*", - "**/rit.config.js", - "**/__fixtures__/**", - "**/__mocks__/**", - "**/docs/**", - "**/stories/**", - "**/.storybook/**", - "**/bundle-size/**", - "**/common/isConformant.ts", - "**/src/testing/**", - "**/src/e2e/**", - "**/config/tests.js", - "**/jest.config.js", - "**/SPEC*.md", - "**/tests/**" - ], - "scope": ["!packages/fluentui/*"], - "changehint": "Run 'yarn change' to generate a change file" -} diff --git a/scripts/beachball/src/config.test.ts b/scripts/beachball/src/config.test.ts index 6ffbfdb434281..0c1c3244b03f6 100644 --- a/scripts/beachball/src/config.test.ts +++ b/scripts/beachball/src/config.test.ts @@ -28,7 +28,10 @@ describe(`beachball configs`, () => { it(`should generate shared config`, () => { expect(sharedConfig).toEqual({ + access: 'public', + branch: 'origin/master', changehint: "Run 'yarn change' to generate a change file", + commit: false, disallowedChangeTypes: ['major'], generateChangelog: true, hooks: { @@ -55,6 +58,7 @@ describe(`beachball configs`, () => { '**/SPEC*.md', '**/tests/**', ], + registry: 'https://registry.npmjs.org', scope: ['!packages/fluentui/*'], tag: 'latest', changelog: { @@ -135,7 +139,7 @@ describe(`beachball configs`, () => { expect(vNextConfig.changelog.groups).toEqual([ { changelogPath: 'packages/react-components/react-components', - masterPackageName: '@fluentui/react-components', + mainPackageName: '@fluentui/react-components', include: includeScopes, }, ]); diff --git a/scripts/beachball/src/customRenderers.ts b/scripts/beachball/src/customRenderers.ts index 2b0bd0a2db3e3..90a68739e4ca4 100644 --- a/scripts/beachball/src/customRenderers.ts +++ b/scripts/beachball/src/customRenderers.ts @@ -42,6 +42,9 @@ export async function renderEntry(entry: ChangelogEntry): Promise { } async function _getPrNumber(entry: ChangelogEntry): Promise { + if (!entry.commit || entry.commit === 'not available') { + return undefined; + } // Look for (presumably) the PR number at the end of the first line of the commit try { // Get the actual commit message which should contain the PR number diff --git a/scripts/beachball/src/shared.config.ts b/scripts/beachball/src/shared.config.ts index 29ec60a524896..b0038d3755fd3 100644 --- a/scripts/beachball/src/shared.config.ts +++ b/scripts/beachball/src/shared.config.ts @@ -1,17 +1,21 @@ import { execSync } from 'child_process'; -import * as fs from 'fs'; -import * as path from 'path'; import type { BeachballConfig } from 'beachball'; import { renderEntry, renderHeader } from './customRenderers'; +import baseConfig from '../base.config'; -const baseConfig: typeof import('../base.config.json') = JSON.parse( - fs.readFileSync(path.resolve(__dirname, '../base.config.json'), { encoding: 'utf8' }), -); - -export const config: typeof baseConfig & Required> = { +/** + * Shared Beachball release config. + */ +export const config: typeof baseConfig & + Required> = { ...baseConfig, + // This can't be in the base config because people might use different names for remotes, + // but it should be safe in release pipelines. + branch: 'origin/master', + // In beachball v3 alpha, this is required if NPM_TOKEN is used. + registry: 'https://registry.npmjs.org', changelog: { customRenderers: { renderHeader, diff --git a/scripts/beachball/src/utils.ts b/scripts/beachball/src/utils.ts index 350e347d384b7..204683f655972 100644 --- a/scripts/beachball/src/utils.ts +++ b/scripts/beachball/src/utils.ts @@ -14,7 +14,7 @@ export function getConfig({ version }: { version: 'headless' }): { scope: string export function getConfig({ version }: { version: 'vNext' }): { scope: string[]; groupConfig: { - masterPackageName: string; + mainPackageName: string; changelogPath: string; include: string[]; }; @@ -26,7 +26,7 @@ export function getConfig({ version }: { version: 'v8' | 'vNext' | 'web-componen return { scope: [...vNextPaths], groupConfig: { - masterPackageName: '@fluentui/react-components', + mainPackageName: '@fluentui/react-components', changelogPath: 'packages/react-components/react-components', include: vNextPaths, }, diff --git a/scripts/beachball/tsconfig.json b/scripts/beachball/tsconfig.json index cd12b9358f4bc..a95e1d6f4a43c 100644 --- a/scripts/beachball/tsconfig.json +++ b/scripts/beachball/tsconfig.json @@ -7,8 +7,7 @@ "allowJs": true, "checkJs": true, "sourceMap": true, - "noUnusedLocals": true, - "resolveJsonModule": true + "noUnusedLocals": true }, "include": [], "files": [], diff --git a/scripts/update-release-notes/src/changelogsAndTags.ts b/scripts/update-release-notes/src/changelogsAndTags.ts index a07f20f83e9d2..efd9a24125fba 100644 --- a/scripts/update-release-notes/src/changelogsAndTags.ts +++ b/scripts/update-release-notes/src/changelogsAndTags.ts @@ -26,7 +26,7 @@ export function getTagToChangelogMap(maxAgeDays?: number): Map { const { commit, author: authorEmail } = entry; - + if (!commit || commit === 'not available') { + return undefined; + } const pr = await getPullRequestForCommit({ commit, github, diff --git a/tools/workspace-plugin/src/generators/prepare-initial-release/index.spec.ts b/tools/workspace-plugin/src/generators/prepare-initial-release/index.spec.ts index edbf880cf8e81..b74e66a0a4b2d 100644 --- a/tools/workspace-plugin/src/generators/prepare-initial-release/index.spec.ts +++ b/tools/workspace-plugin/src/generators/prepare-initial-release/index.spec.ts @@ -173,7 +173,7 @@ describe('prepare-initial-release generator', () => { expect(execCalls.length).toEqual(1); expect(execCalls[0].cmd).toMatchInlineSnapshot( - `"yarn change --message 'feat: release compat package' --type patch --package @proj/react-one-compat"`, + `"yarn change --message \\"feat: release compat package\\" --type patch --package @proj/react-one-compat"`, ); expect(execCalls[0].args).toMatchInlineSnapshot( { cwd: expect.any(String) }, @@ -273,7 +273,7 @@ describe('prepare-initial-release generator', () => { expect(execCalls.length).toEqual(1); expect(execCalls[0].cmd).toMatchInlineSnapshot( - `"yarn change --message 'feat: release preview package' --type minor --package @proj/react-one-preview"`, + `"yarn change --message \\"feat: release preview package\\" --type minor --package @proj/react-one-preview"`, ); expect(execCalls[0].args).toMatchInlineSnapshot( { cwd: expect.any(String) }, @@ -548,7 +548,7 @@ describe('prepare-initial-release generator', () => { expect(execCalls.length).toEqual(3); expect(execCalls[0].cmd).toMatchInlineSnapshot( - `"yarn change --message 'feat: release stable' --type minor --package @proj/react-one"`, + `"yarn change --message \\"feat: release stable\\" --type minor --package @proj/react-one"`, ); expect(execCalls[0].args).toMatchInlineSnapshot( { cwd: expect.any(String) }, @@ -561,7 +561,7 @@ describe('prepare-initial-release generator', () => { ); expect(execCalls[1].cmd).toMatchInlineSnapshot( - `"yarn change --message 'feat: add @proj/react-one to suite' --type minor --package @proj/react-components"`, + `"yarn change --message \\"feat: add @proj/react-one to suite\\" --type minor --package @proj/react-components"`, ); expect(execCalls[1].args).toMatchInlineSnapshot( { cwd: expect.any(String) }, diff --git a/tools/workspace-plugin/src/generators/prepare-initial-release/index.ts b/tools/workspace-plugin/src/generators/prepare-initial-release/index.ts index e5ae740281b08..a1b4b544b3fa0 100644 --- a/tools/workspace-plugin/src/generators/prepare-initial-release/index.ts +++ b/tools/workspace-plugin/src/generators/prepare-initial-release/index.ts @@ -444,7 +444,7 @@ function generateChangefileTask( projectName: string, options: { changeType: 'minor' | 'patch'; message: string }, ) { - const cmd = `yarn change --message '${options.message}' --type ${options.changeType} --package ${projectName}`; + const cmd = `yarn change --message "${options.message}" --type ${options.changeType} --package ${projectName}`; return execSync(cmd, { cwd: workspaceRoot, stdio: 'inherit' }); } diff --git a/yarn.lock b/yarn.lock index ff372e8e73a61..3dd37ebeb344f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2920,7 +2920,6 @@ __metadata: "@types/webpack-env": "npm:1.18.8" "@types/webpack-hot-middleware": "npm:2.25.9" "@types/yargs": "npm:13.0.11" - "@types/yargs-unparser": "npm:2.0.1" "@typescript-eslint/eslint-plugin": "npm:8.46.2" "@typescript-eslint/rule-tester": "npm:^8.46.2" autoprefixer: "npm:10.2.1" @@ -2933,7 +2932,7 @@ __metadata: babel-plugin-module-resolver: "npm:5.0.3" babel-plugin-react-compiler: "npm:1.0.0" babel-plugin-tester: "npm:10.1.0" - beachball: "npm:2.31.0" + beachball: "npm:3.0.0-alpha.7" chalk: "npm:4.1.0" chrome-remote-interface: "npm:0.28.2" ci-info: "npm:3.9.0" @@ -3039,7 +3038,6 @@ __metadata: react-shadow: "npm:20.3.0" react-test-renderer: "npm:19.2.0" react-window: "npm:^1.8.6" - read-pkg-up: "npm:7.0.1" remark-gfm: "npm:4.0.1" replace-in-file: "npm:6.1.0" resolve: "npm:1.22.8" @@ -3087,10 +3085,8 @@ __metadata: webpack-dev-server: "npm:4.15.2" webpack-hot-middleware: "npm:2.26.1" webpack-merge: "npm:5.10.0" - workspace-tools: "npm:0.29.1" yargs: "npm:13.3.2" yargs-parser: "npm:13.1.2" - yargs-unparser: "npm:2.0.0" languageName: unknown linkType: soft @@ -4359,6 +4355,7 @@ __metadata: "@fluentui/react-slider": "npm:^9.6.4" "@fluentui/react-spinbutton": "npm:^9.6.4" "@fluentui/react-spinner": "npm:^9.8.4" + "@fluentui/react-swatch-picker": "npm:^9.5.4" "@fluentui/react-switch": "npm:^9.7.4" "@fluentui/react-tabs": "npm:^9.12.3" "@fluentui/react-tabster": "npm:^9.26.16" @@ -10703,13 +10700,6 @@ __metadata: languageName: node linkType: hard -"@types/normalize-package-data@npm:^2.4.0": - version: 2.4.0 - resolution: "@types/normalize-package-data@npm:2.4.0" - checksum: 10c0/f5504a9fe5cb6b82d9d2fb7982e4681f51bd31dc6c4750f833ca6499a2372294e58c0e2e0f5d78066f3c212c553c85cdbf653c5d6035f902d00822e7f3590c28 - languageName: node - linkType: hard - "@types/parse-json@npm:^4.0.0": version: 4.0.0 resolution: "@types/parse-json@npm:4.0.0" @@ -10717,6 +10707,13 @@ __metadata: languageName: node linkType: hard +"@types/parse-path@npm:^7.0.0": + version: 7.0.3 + resolution: "@types/parse-path@npm:7.0.3" + checksum: 10c0/8344b6c7acba4e4e5a8d542f56f53c297685fa92f9b0c085d7532cc7e1b661432cecfc1c75c76cdb0d161c95679b6ecfe0573d9fef7c836962aacf604150a984 + languageName: node + linkType: hard + "@types/parse5@npm:^5.0.0": version: 5.0.3 resolution: "@types/parse5@npm:5.0.3" @@ -11142,13 +11139,6 @@ __metadata: languageName: node linkType: hard -"@types/yargs-unparser@npm:2.0.1": - version: 2.0.1 - resolution: "@types/yargs-unparser@npm:2.0.1" - checksum: 10c0/fdb85acc6d530e3d980f110588f5f48ae51e9e8477f056d539c70401a3e95d53a0c03c712292a683b63048a3c2033eab6c0fc06bd2541b4f03f046f177028ee7 - languageName: node - linkType: hard - "@types/yargs@npm:13.0.11": version: 13.0.11 resolution: "@types/yargs@npm:13.0.11" @@ -11640,6 +11630,13 @@ __metadata: languageName: node linkType: hard +"@vercel/detect-agent@npm:^1.2.1": + version: 1.2.3 + resolution: "@vercel/detect-agent@npm:1.2.3" + checksum: 10c0/12eb2e11603b0e9ff2fe89f969eac2327315a0f71b68acba2e4fe71f2ee6d282c519ccb7aa3b43e9b748a6db00517b88aa93628a20dbdab9ef6d8a003f8980e9 + languageName: node + linkType: hard + "@vitest/expect@npm:3.2.4": version: 3.2.4 resolution: "@vitest/expect@npm:3.2.4" @@ -13453,25 +13450,21 @@ __metadata: languageName: node linkType: hard -"beachball@npm:2.31.0": - version: 2.31.0 - resolution: "beachball@npm:2.31.0" +"beachball@npm:3.0.0-alpha.7": + version: 3.0.0-alpha.7 + resolution: "beachball@npm:3.0.0-alpha.7" dependencies: - cosmiconfig: "npm:^7.0.0" - execa: "npm:^5.0.0" - fs-extra: "npm:^10.0.0" - lodash: "npm:^4.17.15" - minimatch: "npm:^3.0.4" - p-limit: "npm:^3.0.2" - prompts: "npm:^2.1.0" - semver: "npm:^7.0.0" - toposort: "npm:^2.0.2" - uuid: "npm:^9.0.0" - workspace-tools: "npm:^0.27.0" - yargs-parser: "npm:^21.0.0" + "@vercel/detect-agent": "npm:^1.2.1" + commander: "npm:^14.0.3" + nano-spawn: "npm:^2.1.0" + p-graph: "npm:^3.0.0-alpha.4" + p-limit: "npm:^3.1.0" + prompts: "npm:^2.4.2" + semver: "npm:^7.7.4" + workspace-tools: "npm:^0.42.0" bin: beachball: bin/beachball.js - checksum: 10c0/355ad1f7f252e320f9e900bb68a4774cc39a86e9771b1df9dc4078b99eae60f8125134aa87724642120d2673e09ffeab0f09801c17e48622a0fe621249e71bbd + checksum: 10c0/5f4bd3f00bf5c99dcd40887fcc099ef808f80f4c939066fed0c2ff12217e65a644f53dac1f3f08babac52e3d82970ce9cbac67890886a5a8fde1d2748f54f41c languageName: node linkType: hard @@ -14868,6 +14861,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^14.0.3": + version: 14.0.3 + resolution: "commander@npm:14.0.3" + checksum: 10c0/755652564bbf56ff2ff083313912b326450d3f8d8c85f4b71416539c9a05c3c67dbd206821ca72635bf6b160e2afdefcb458e86b317827d5cb333b69ce7f1a24 + languageName: node + linkType: hard + "commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -18249,7 +18249,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.0.3, fast-glob@npm:^3.1.1, fast-glob@npm:^3.2.5, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": +"fast-glob@npm:^3.0.3, fast-glob@npm:^3.1.1, fast-glob@npm:^3.2.5, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2, fast-glob@npm:^3.3.3": version: 3.3.3 resolution: "fast-glob@npm:3.3.3" dependencies: @@ -19367,22 +19367,22 @@ __metadata: languageName: node linkType: hard -"git-up@npm:^7.0.0": - version: 7.0.0 - resolution: "git-up@npm:7.0.0" +"git-up@npm:^8.1.0": + version: 8.1.1 + resolution: "git-up@npm:8.1.1" dependencies: is-ssh: "npm:^1.4.0" - parse-url: "npm:^8.1.0" - checksum: 10c0/a3fa02e1a63c7c824b5ebbf23f4a9a6b34dd80031114c5dd8adb7ef53493642e39d3d80dfef4025a452128400c35c2c138d20a0f6ae5d7d7ef70d9ba13083d34 + parse-url: "npm:^9.2.0" + checksum: 10c0/2cc4461d8565a3f7a1ecd3d262a58ddb8df0a67f7f7d4915df2913c460b2e88ae570a6ea810700a6d22fb3b9e4bea8dd10a8eb469900ddc12e35c62208608c03 languageName: node linkType: hard -"git-url-parse@npm:^13.0.0": - version: 13.1.0 - resolution: "git-url-parse@npm:13.1.0" +"git-url-parse@npm:^16.1.0": + version: 16.1.0 + resolution: "git-url-parse@npm:16.1.0" dependencies: - git-up: "npm:^7.0.0" - checksum: 10c0/2ef6126c42d999e240dbcdf1e96172cf7a2044ffa1ef78a518acf823df9bbe2a1ea9e6b443d42948e3c581e4d899559afc4c1de024b3eaa8eb6a4229f73285aa + git-up: "npm:^8.1.0" + checksum: 10c0/b8f5ebcbd5b2baf9f1bb77a217376f0247c47fe1d42811ccaac3015768eebb0759a59051f758e50e70adf5c67ae059d1975bf6b750164f36bfd39138d11b940b languageName: node linkType: hard @@ -19583,7 +19583,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:11.1.0, globby@npm:^11.0.0, globby@npm:^11.0.1, globby@npm:^11.1.0": +"globby@npm:11.1.0, globby@npm:^11.0.1, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -22187,25 +22187,25 @@ __metadata: linkType: hard "js-yaml@npm:^3.10.0, js-yaml@npm:^3.13.1": - version: 3.13.1 - resolution: "js-yaml@npm:3.13.1" + version: 3.15.0 + resolution: "js-yaml@npm:3.15.0" dependencies: argparse: "npm:^1.0.7" esprima: "npm:^4.0.0" bin: js-yaml: bin/js-yaml.js - checksum: 10c0/6a4f78b998d2eb58964cc5e051c031865bf292dc3c156a8057cf468d9e60a8739f4e8f607a267e97f09eb8d08263b8262df57eddb16b920ec5a04a259c3b4960 + checksum: 10c0/ca966bd354ac5b1b7a4694ebdba46526796aa3a6a99529fa540af2abf85918bd155a50ccc0166b413130a00622999973754458ec01e7095bc902177bfdbd5b64 languageName: node linkType: hard -"js-yaml@npm:^4.0.0, js-yaml@npm:^4.1.0": - version: 4.1.0 - resolution: "js-yaml@npm:4.1.0" +"js-yaml@npm:^4.0.0, js-yaml@npm:^4.1.0, js-yaml@npm:^4.2.0": + version: 4.3.0 + resolution: "js-yaml@npm:4.3.0" dependencies: argparse: "npm:^2.0.1" bin: js-yaml: bin/js-yaml.js - checksum: 10c0/184a24b4eaacfce40ad9074c64fd42ac83cf74d8c8cd137718d456ced75051229e5061b8633c3366b8aada17945a7a356b337828c19da92b51ae62126575018f + checksum: 10c0/058b30473d6915ca5b4feb11e2f7d4d97242f98d00a798ed48dd90b46b7c640398afe9128c5db22c5300f8c6528fe2a174b9a93f351a70ebc28c6203938d8bff languageName: node linkType: hard @@ -25054,6 +25054,13 @@ __metadata: languageName: node linkType: hard +"nano-spawn@npm:^2.1.0": + version: 2.1.0 + resolution: "nano-spawn@npm:2.1.0" + checksum: 10c0/3becc67ed9ab630b6572feab69a4ef468891ad1f89d5c8643f14a2044cf32ba64533033506208039b1e3d9ddcb2f5f4f87ec360f13b3c4f0774304aedf0f0290 + languageName: node + linkType: hard + "nano-staged@npm:0.9.0": version: 0.9.0 resolution: "nano-staged@npm:0.9.0" @@ -25349,7 +25356,7 @@ __metadata: languageName: node linkType: hard -"normalize-package-data@npm:^2.3.2, normalize-package-data@npm:^2.5.0": +"normalize-package-data@npm:^2.3.2": version: 2.5.0 resolution: "normalize-package-data@npm:2.5.0" dependencies: @@ -25851,6 +25858,13 @@ __metadata: languageName: node linkType: hard +"p-graph@npm:^3.0.0-alpha.4": + version: 3.0.0-alpha.4 + resolution: "p-graph@npm:3.0.0-alpha.4" + checksum: 10c0/84452847e6cb761aedcfe1d2d02faff2fd8e9155807e97e7beb90006d2e14e46c2f55c51b3578198a1d28b9c3e576bef9980a063e47b5ff681655d1433c6a03f + languageName: node + linkType: hard + "p-limit@npm:^2.0.0, p-limit@npm:^2.2.0": version: 2.3.0 resolution: "p-limit@npm:2.3.0" @@ -26208,12 +26222,13 @@ __metadata: languageName: node linkType: hard -"parse-url@npm:^8.1.0": - version: 8.1.0 - resolution: "parse-url@npm:8.1.0" +"parse-url@npm:^9.2.0": + version: 9.2.0 + resolution: "parse-url@npm:9.2.0" dependencies: + "@types/parse-path": "npm:^7.0.0" parse-path: "npm:^7.0.0" - checksum: 10c0/68b95afdf4bbf72e57c7ab66f8757c935fff888f7e2b0f1e06098b4faa19e06b6b743bddaed5bc8df4f0c2de6fc475355d787373b2fdd40092be9e4e4b996648 + checksum: 10c0/b8f56cdb01e76616255dff82544f4b5ab4378f6f4bac8604ed6fde03a75b0f71c547d92688386d8f22f38fad3c928c075abf69458677c6185da76c841bfd7a93 languageName: node linkType: hard @@ -26968,7 +26983,7 @@ __metadata: languageName: node linkType: hard -"prompts@npm:^2.1.0, prompts@npm:^2.4.0": +"prompts@npm:^2.4.0, prompts@npm:^2.4.2": version: 2.4.2 resolution: "prompts@npm:2.4.2" dependencies: @@ -27498,17 +27513,6 @@ __metadata: languageName: node linkType: hard -"read-pkg-up@npm:7.0.1": - version: 7.0.1 - resolution: "read-pkg-up@npm:7.0.1" - dependencies: - find-up: "npm:^4.1.0" - read-pkg: "npm:^5.2.0" - type-fest: "npm:^0.8.1" - checksum: 10c0/82b3ac9fd7c6ca1bdc1d7253eb1091a98ff3d195ee0a45386582ce3e69f90266163c34121e6a0a02f1630073a6c0585f7880b3865efcae9c452fa667f02ca385 - languageName: node - linkType: hard - "read-pkg-up@npm:^1.0.1": version: 1.0.1 resolution: "read-pkg-up@npm:1.0.1" @@ -27530,18 +27534,6 @@ __metadata: languageName: node linkType: hard -"read-pkg@npm:^5.2.0": - version: 5.2.0 - resolution: "read-pkg@npm:5.2.0" - dependencies: - "@types/normalize-package-data": "npm:^2.4.0" - normalize-package-data: "npm:^2.5.0" - parse-json: "npm:^5.0.0" - type-fest: "npm:^0.6.0" - checksum: 10c0/b51a17d4b51418e777029e3a7694c9bd6c578a5ab99db544764a0b0f2c7c0f58f8a6bc101f86a6fceb8ba6d237d67c89acf6170f6b98695d0420ddc86cf109fb - languageName: node - linkType: hard - "read-yaml-file@npm:2.1.0": version: 2.1.0 resolution: "read-yaml-file@npm:2.1.0" @@ -30689,13 +30681,6 @@ __metadata: languageName: node linkType: hard -"toposort@npm:^2.0.2": - version: 2.0.2 - resolution: "toposort@npm:2.0.2" - checksum: 10c0/ab9ca91fce4b972ccae9e2f539d755bf799a0c7eb60da07fd985fce0f14c159ed1e92305ff55697693b5bc13e300f5417db90e2593b127d421c9f6c440950222 - languageName: node - linkType: hard - "totalist@npm:^3.0.0": version: 3.0.1 resolution: "totalist@npm:3.0.1" @@ -31063,20 +31048,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^0.6.0": - version: 0.6.0 - resolution: "type-fest@npm:0.6.0" - checksum: 10c0/0c585c26416fce9ecb5691873a1301b5aff54673c7999b6f925691ed01f5b9232db408cdbb0bd003d19f5ae284322523f44092d1f81ca0a48f11f7cf0be8cd38 - languageName: node - linkType: hard - -"type-fest@npm:^0.8.1": - version: 0.8.1 - resolution: "type-fest@npm:0.8.1" - checksum: 10c0/dffbb99329da2aa840f506d376c863bd55f5636f4741ad6e65e82f5ce47e6914108f44f340a0b74009b0cb5d09d6752ae83203e53e98b1192cf80ecee5651636 - languageName: node - linkType: hard - "type-fest@npm:^4.41.0": version: 4.41.0 resolution: "type-fest@npm:4.41.0" @@ -31925,15 +31896,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.0": - version: 9.0.0 - resolution: "uuid@npm:9.0.0" - bin: - uuid: dist/bin/uuid - checksum: 10c0/8867e438990d1d33ac61093e2e4e3477a2148b844e4fa9e3c2360fa4399292429c4b6ec64537eb1659c97b2d10db349c673ad58b50e2824a11e0d3630de3c056 - languageName: node - linkType: hard - "uvu@npm:^0.5.0": version: 0.5.6 resolution: "uvu@npm:0.5.6" @@ -32715,31 +32677,17 @@ __metadata: languageName: node linkType: hard -"workspace-tools@npm:0.29.1": - version: 0.29.1 - resolution: "workspace-tools@npm:0.29.1" +"workspace-tools@npm:^0.42.0": + version: 0.42.0 + resolution: "workspace-tools@npm:0.42.0" dependencies: "@yarnpkg/lockfile": "npm:^1.1.0" - git-url-parse: "npm:^13.0.0" - globby: "npm:^11.0.0" + fast-glob: "npm:^3.3.3" + git-url-parse: "npm:^16.1.0" jju: "npm:^1.4.0" - js-yaml: "npm:^4.1.0" - micromatch: "npm:^4.0.0" - checksum: 10c0/d33659484c1abc93aa262b844889ff9293b48f0c61c21ec707e530fb5061283b9e3dcd5754e85fae078da7879bcb8d88d496c5ee18fb1c38d3192101b56d038e - languageName: node - linkType: hard - -"workspace-tools@npm:^0.27.0": - version: 0.27.0 - resolution: "workspace-tools@npm:0.27.0" - dependencies: - "@yarnpkg/lockfile": "npm:^1.1.0" - git-url-parse: "npm:^13.0.0" - globby: "npm:^11.0.0" - jju: "npm:^1.4.0" - js-yaml: "npm:^4.1.0" - micromatch: "npm:^4.0.0" - checksum: 10c0/8b117f56c055bd8350e22d0a4ec72fc86e27ff7f1359a04d51d9f27a3798928031e3c099ce3a8cf8eda9a49e37222ffeb9b314dad8e428fad3c364abbbae8b73 + js-yaml: "npm:^4.2.0" + micromatch: "npm:^4.0.8" + checksum: 10c0/a39edf688de49f3e183ab57ffd0b0740350048b2c5fc656bfee93ec9c438bd90b07b98033e52709f8d92b7eef65fc5c8c70632e47f972601650706819db3d114 languageName: node linkType: hard @@ -32976,7 +32924,7 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:21.1.1, yargs-parser@npm:^21.0.0, yargs-parser@npm:^21.1.1": +"yargs-parser@npm:21.1.1, yargs-parser@npm:^21.1.1": version: 21.1.1 resolution: "yargs-parser@npm:21.1.1" checksum: 10c0/f84b5e48169479d2f402239c59f084cfd1c3acc197a05c59b98bab067452e6b3ea46d4dd8ba2985ba7b3d32a343d77df0debd6b343e5dae3da2aab2cdf5886b2 @@ -33017,7 +32965,7 @@ __metadata: languageName: node linkType: hard -"yargs-unparser@npm:2.0.0, yargs-unparser@npm:^2.0.0": +"yargs-unparser@npm:^2.0.0": version: 2.0.0 resolution: "yargs-unparser@npm:2.0.0" dependencies: