From f1a15496cf7c7121f4d163e2394d5647fbe9eb96 Mon Sep 17 00:00:00 2001 From: Valentyna Date: Tue, 14 Jan 2025 04:54:42 -0800 Subject: [PATCH 01/45] feat(react-color-picker): Added `transparent` option to the AlphaSlider (#33572) --- ...-d36c4ad2-9cc0-4131-bdee-dd557b75c0bb.json | 7 +++ .../etc/react-color-picker-preview.api.md | 4 +- .../AlphaSlider/AlphaSlider.types.ts | 12 ++++- .../AlphaSlider/alphaSliderUtils.test.ts | 54 +++++++++++++++++++ .../AlphaSlider/alphaSliderUtils.ts | 36 +++++++++++++ .../AlphaSlider/useAlphaSliderState.ts | 16 +++--- .../ColorSlider/ColorSlider.types.ts | 2 +- .../AlphaSliderDefault.stories.tsx | 24 +++++++++ 8 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 change/@fluentui-react-color-picker-preview-d36c4ad2-9cc0-4131-bdee-dd557b75c0bb.json create mode 100644 packages/react-components/react-color-picker-preview/library/src/components/AlphaSlider/alphaSliderUtils.test.ts create mode 100644 packages/react-components/react-color-picker-preview/library/src/components/AlphaSlider/alphaSliderUtils.ts diff --git a/change/@fluentui-react-color-picker-preview-d36c4ad2-9cc0-4131-bdee-dd557b75c0bb.json b/change/@fluentui-react-color-picker-preview-d36c4ad2-9cc0-4131-bdee-dd557b75c0bb.json new file mode 100644 index 0000000000000..f30eab37953f9 --- /dev/null +++ b/change/@fluentui-react-color-picker-preview-d36c4ad2-9cc0-4131-bdee-dd557b75c0bb.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "feat: Added `transparent` option to the AlphaSlider", + "packageName": "@fluentui/react-color-picker-preview", + "email": "v.kozlova13@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-color-picker-preview/library/etc/react-color-picker-preview.api.md b/packages/react-components/react-color-picker-preview/library/etc/react-color-picker-preview.api.md index 0ff850de65940..8190eeb7f921c 100644 --- a/packages/react-components/react-color-picker-preview/library/etc/react-color-picker-preview.api.md +++ b/packages/react-components/react-color-picker-preview/library/etc/react-color-picker-preview.api.md @@ -20,7 +20,9 @@ export const AlphaSlider: ForwardRefComponent; export const alphaSliderClassNames: SlotClassNames; // @public -export type AlphaSliderProps = ColorSliderProps; +export type AlphaSliderProps = ColorSliderProps & { + transparency?: boolean; +}; // @public (undocumented) export type AlphaSliderSlots = ColorSliderSlots; diff --git a/packages/react-components/react-color-picker-preview/library/src/components/AlphaSlider/AlphaSlider.types.ts b/packages/react-components/react-color-picker-preview/library/src/components/AlphaSlider/AlphaSlider.types.ts index 7a471fa974f46..2f4c1195416b1 100644 --- a/packages/react-components/react-color-picker-preview/library/src/components/AlphaSlider/AlphaSlider.types.ts +++ b/packages/react-components/react-color-picker-preview/library/src/components/AlphaSlider/AlphaSlider.types.ts @@ -6,7 +6,17 @@ export type AlphaSliderSlots = ColorSliderSlots; /** * AlphaSlider Props */ -export type AlphaSliderProps = ColorSliderProps; +export type AlphaSliderProps = ColorSliderProps & { + /** + * The `transparency` property determines how the alpha channel is interpreted. + * - When `false`, the alpha channel represents the opacity of the color. + * - When `true`, the alpha channel represents the transparency of the color. + * For example, a 30% transparent color has 70% opacity. + * + * @defaultvalue false + */ + transparency?: boolean; +}; /** * State used in rendering AlphaSlider diff --git a/packages/react-components/react-color-picker-preview/library/src/components/AlphaSlider/alphaSliderUtils.test.ts b/packages/react-components/react-color-picker-preview/library/src/components/AlphaSlider/alphaSliderUtils.test.ts new file mode 100644 index 0000000000000..2756ee7164985 --- /dev/null +++ b/packages/react-components/react-color-picker-preview/library/src/components/AlphaSlider/alphaSliderUtils.test.ts @@ -0,0 +1,54 @@ +import { adjustToTransparency, calculateTransparencyValue, getSliderDirection } from './alphaSliderUtils'; + +describe('AlphaSlider Utils', () => { + describe('adjustToTransparency', () => { + it('should return 100 - value when transparency is true', () => { + expect(adjustToTransparency(30, true)).toBe(70); + }); + + it('should return value when transparency is false', () => { + expect(adjustToTransparency(30, false)).toBe(30); + }); + }); + + describe('calculateTransparencyValue', () => { + it('should return adjusted value when value is provided and transparency is true', () => { + expect(calculateTransparencyValue(true, 0.3)).toBe(70); + }); + + it('should return adjusted value when value is provided and transparency is false', () => { + expect(calculateTransparencyValue(false, 0.3)).toBe(30); + }); + + it('should return undefined when value is not provided', () => { + expect(calculateTransparencyValue(true)).toBeUndefined(); + expect(calculateTransparencyValue(false)).toBeUndefined(); + }); + }); + + describe('getSliderDirection', () => { + it('should return "180deg" when vertical is true and transparency is true', () => { + expect(getSliderDirection('ltr', true, true)).toBe('180deg'); + }); + + it('should return "0deg" when vertical is true and transparency is false', () => { + expect(getSliderDirection('ltr', true, false)).toBe('0deg'); + }); + + it('should return "90deg" when dir is "ltr" and transparency is false', () => { + expect(getSliderDirection('ltr', false, false)).toBe('90deg'); + }); + + it('should return "-90deg" when dir is "ltr" and transparency is true', () => { + expect(getSliderDirection('ltr', false, true)).toBe('-90deg'); + }); + + it('should return "-90deg" when dir is "rtl" and transparency is false', () => { + expect(getSliderDirection('rtl', false, false)).toBe('-90deg'); + }); + + it('should return "-90deg" when dir is "rtl" and transparency is true', () => { + expect(getSliderDirection('rtl', false, true)).toBe('-90deg'); + }); + }); +}); diff --git a/packages/react-components/react-color-picker-preview/library/src/components/AlphaSlider/alphaSliderUtils.ts b/packages/react-components/react-color-picker-preview/library/src/components/AlphaSlider/alphaSliderUtils.ts new file mode 100644 index 0000000000000..739fe0c7fcde8 --- /dev/null +++ b/packages/react-components/react-color-picker-preview/library/src/components/AlphaSlider/alphaSliderUtils.ts @@ -0,0 +1,36 @@ +/** + * Adjusts the given value based on the transparency flag. + * + * @param value - The numeric value to adjust. + * @param transparency - A boolean flag indicating whether to adjust for transparency. + * @returns The adjusted value. + */ +export function adjustToTransparency(value: number, transparency: boolean) { + return transparency ? 100 - value : value; +} + +/** + * Calculates the transparency value based on the given parameters. + * + * @param transparency - A boolean flag indicating whether to adjust for transparency. + * @param value - An optional numeric value to adjust. + * @returns The calculated transparency value or undefined if the value is not provided. + */ +export function calculateTransparencyValue(transparency: boolean, value?: number) { + return value !== undefined ? adjustToTransparency(value * 100, transparency) : undefined; +} + +/** + * Determines the direction of the slider based on the given parameters. + * + * @param dir - The text direction, either 'ltr' (left-to-right) or 'rtl' (right-to-left). + * @param vertical - A boolean indicating if the slider is vertical. + * @param transparency - A boolean indicating if the slider is for transparency. + * @returns The direction of the slider as a string representing degrees (e.g., '90deg'). + */ +export function getSliderDirection(dir: 'ltr' | 'rtl', vertical: boolean, transparency: boolean) { + if (vertical) { + return transparency ? '180deg' : '0deg'; + } + return dir === 'ltr' && !transparency ? '90deg' : '-90deg'; +} diff --git a/packages/react-components/react-color-picker-preview/library/src/components/AlphaSlider/useAlphaSliderState.ts b/packages/react-components/react-color-picker-preview/library/src/components/AlphaSlider/useAlphaSliderState.ts index 153975294f8e5..c1654f5eef77c 100644 --- a/packages/react-components/react-color-picker-preview/library/src/components/AlphaSlider/useAlphaSliderState.ts +++ b/packages/react-components/react-color-picker-preview/library/src/components/AlphaSlider/useAlphaSliderState.ts @@ -8,6 +8,7 @@ import { useColorPickerContextValue_unstable } from '../../contexts/colorPicker' import { MIN, MAX } from '../../utils/constants'; import { getPercent } from '../../utils/getPercent'; import type { HsvColor } from '../../types/color'; +import { adjustToTransparency, calculateTransparencyValue, getSliderDirection } from './alphaSliderUtils'; export const useAlphaSliderState_unstable = (state: AlphaSliderState, props: AlphaSliderProps) => { 'use no memo'; @@ -15,30 +16,33 @@ export const useAlphaSliderState_unstable = (state: AlphaSliderState, props: Alp const { dir } = useFluent(); const onChangeFromContext = useColorPickerContextValue_unstable(ctx => ctx.requestChange); const colorFromContext = useColorPickerContextValue_unstable(ctx => ctx.color); - const { color, onChange = onChangeFromContext } = props; + const { color, onChange = onChangeFromContext, transparency = false, vertical = false } = props; const hsvColor = color || colorFromContext; const hslColor = tinycolor(hsvColor).toHsl(); const [currentValue, setCurrentValue] = useControllableState({ - defaultState: props.defaultColor?.a ? props.defaultColor.a * 100 : undefined, - state: hsvColor?.a ? hsvColor.a * 100 : undefined, - initialState: 100, + defaultState: calculateTransparencyValue(transparency, props.defaultColor?.a), + state: calculateTransparencyValue(transparency, hsvColor?.a), + initialState: adjustToTransparency(100, transparency), }); + const clampedValue = clamp(currentValue, MIN, MAX); const valuePercent = getPercent(clampedValue, MIN, MAX); const inputOnChange = state.input.onChange; const _onChange: React.ChangeEventHandler = useEventCallback(event => { - const newValue = Number(event.target.value); + const newValue = adjustToTransparency(Number(event.target.value), transparency); const newColor: HsvColor = { ...hsvColor, a: newValue / 100 }; setCurrentValue(newValue); inputOnChange?.(event); onChange?.(event, { type: 'change', event, color: newColor }); }); + const sliderDirection = getSliderDirection(dir, vertical, transparency); + const rootVariables = { - [alphaSliderCSSVars.sliderDirectionVar]: state.vertical ? '0deg' : dir === 'ltr' ? '90deg' : '-90deg', + [alphaSliderCSSVars.sliderDirectionVar]: sliderDirection, [alphaSliderCSSVars.sliderProgressVar]: `${valuePercent}%`, [alphaSliderCSSVars.thumbColorVar]: `transparent`, [alphaSliderCSSVars.railColorVar]: `hsl(${hslColor.h} ${hslColor.s * 100}%, ${hslColor.l * 100}%)`, diff --git a/packages/react-components/react-color-picker-preview/library/src/components/ColorSlider/ColorSlider.types.ts b/packages/react-components/react-color-picker-preview/library/src/components/ColorSlider/ColorSlider.types.ts index ca7417aa765d3..162ff92bf3f6e 100644 --- a/packages/react-components/react-color-picker-preview/library/src/components/ColorSlider/ColorSlider.types.ts +++ b/packages/react-components/react-color-picker-preview/library/src/components/ColorSlider/ColorSlider.types.ts @@ -36,7 +36,7 @@ export type ColorSliderProps = Omit< vertical?: boolean; /** - * Color of the COlorPicker + * Color of the ColorPicker */ color?: HsvColor; diff --git a/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/AlphaSliderDefault.stories.tsx b/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/AlphaSliderDefault.stories.tsx index bce95432ff2cc..82867dbc8a908 100644 --- a/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/AlphaSliderDefault.stories.tsx +++ b/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/AlphaSliderDefault.stories.tsx @@ -27,13 +27,17 @@ export const AlphaSliderExample = (props: Partial) => { const styles = useStyles(); const [color, setColor] = React.useState(COLOR); + const [transparancyColor, setTransparancyColor] = React.useState(COLOR); const [value, setValue] = React.useState(COLOR.a * 100); const onSliderChange: AlphaSliderProps['onChange'] = (_, data) => { const alpha = data.color.a ?? 1; setColor({ ...data.color, a: alpha }); setValue(alpha * 100); }; + const onTransparancySliderChange: AlphaSliderProps['onChange'] = (_, data) => + setTransparancyColor({ ...data.color, a: data.color.a ?? 1 }); const resetSlider = () => setColor(COLOR); + const resetTransparencySlider = () => setTransparancyColor(COLOR); return (
@@ -48,6 +52,26 @@ export const AlphaSliderExample = (props: Partial) => { />
+

Transparency

+ + +
+
); }; From f66e15caa085fc79bff081c801ae465c24052d5f Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Tue, 14 Jan 2025 14:06:21 +0100 Subject: [PATCH 02/45] ci(gha): enable manual trigger for VRT and disable automatic PR trigger for testing purposes (#33606) --- .github/workflows/pr-vrt.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-vrt.yml b/.github/workflows/pr-vrt.yml index 344a1783f9f85..7cb8853eb8e60 100644 --- a/.github/workflows/pr-vrt.yml +++ b/.github/workflows/pr-vrt.yml @@ -1,6 +1,8 @@ name: VRT CI on: - pull_request: + # TODO: once testing is done enable pull_request trigger again + # pull_request: + workflow_dispatch: concurrency: # see https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#example-only-cancel-in-progress-jobs-or-runs-for-the-current-workflow From 8cf401d626def27ad679f9e53928533df9f9ef52 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Tue, 14 Jan 2025 15:06:36 +0100 Subject: [PATCH 03/45] fix(react-utilities): add autoCorrect and minLength input properties support to getNativeProps utility (#33642) --- ...-0b333279-3531-4fce-a0ee-8e9c05d6f830.json | 7 +++++ .../src/components/Input/Input.test.tsx | 11 ++++++++ .../src/utils/properties.test.ts | 28 ++++++++++++++++++- .../react-utilities/src/utils/properties.ts | 2 ++ 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 change/@fluentui-react-utilities-0b333279-3531-4fce-a0ee-8e9c05d6f830.json diff --git a/change/@fluentui-react-utilities-0b333279-3531-4fce-a0ee-8e9c05d6f830.json b/change/@fluentui-react-utilities-0b333279-3531-4fce-a0ee-8e9c05d6f830.json new file mode 100644 index 0000000000000..050e7fdba7a16 --- /dev/null +++ b/change/@fluentui-react-utilities-0b333279-3531-4fce-a0ee-8e9c05d6f830.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: add autoCorrect and minLength input properties support to getNativeProps utility", + "packageName": "@fluentui/react-utilities", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-input/library/src/components/Input/Input.test.tsx b/packages/react-components/react-input/library/src/components/Input/Input.test.tsx index 2d12b3fbca0d4..55f93f7356039 100644 --- a/packages/react-components/react-input/library/src/components/Input/Input.test.tsx +++ b/packages/react-components/react-input/library/src/components/Input/Input.test.tsx @@ -128,4 +128,15 @@ describe('Input', () => { expect(input.value).toBe('foo'); expect(spy).not.toHaveBeenCalled(); }); + + it('forwards native input props to the input element', () => { + renderedComponent = render(); + + expect(getInput()).toMatchObject({ + minLength: 1, + maxLength: 2, + }); + + expect(getInput().getAttribute('autocorrect')).toEqual('on'); + }); }); diff --git a/packages/react-components/react-utilities/src/utils/properties.test.ts b/packages/react-components/react-utilities/src/utils/properties.test.ts index 8b7a6a0735dd0..a2a2551990515 100644 --- a/packages/react-components/react-utilities/src/utils/properties.test.ts +++ b/packages/react-components/react-utilities/src/utils/properties.test.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { getNativeProps, divProperties } from './properties'; +import { getNativeProps, divProperties, inputProperties } from './properties'; describe('getNativeProps', () => { it('can pass through data tags', () => { @@ -43,6 +43,32 @@ describe('getNativeProps', () => { expect(typeof result.onClickCapture).toEqual('function'); }); + it('can pass through input props', () => { + const result = getNativeProps>( + { + autoCapitalize: 'off', + autoCorrect: 'on', + maxLength: 10, + minLength: 1, + value: '123', + // Non-input property + foobar: 1, + }, + inputProperties, + ); + + expect(result).toMatchObject({ + autoCapitalize: 'off', + autoCorrect: 'on', + maxLength: 10, + minLength: 1, + value: '123', + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((result as any).foobar).toBeUndefined(); + }); + it('can remove unexpected properties', () => { const result = getNativeProps>( { diff --git a/packages/react-components/react-utilities/src/utils/properties.ts b/packages/react-components/react-utilities/src/utils/properties.ts index 2f434f8afc95b..60867cd652f8c 100644 --- a/packages/react-components/react-utilities/src/utils/properties.ts +++ b/packages/react-components/react-utilities/src/utils/properties.ts @@ -248,6 +248,7 @@ export const buttonProperties = toObjectMap(htmlElementProperties, [ export const inputProperties = toObjectMap(buttonProperties, [ 'accept', // input 'alt', // area, img, input + 'autoCorrect', // input, textarea 'autoCapitalize', // input, textarea 'autoComplete', // form, input 'checked', // input @@ -259,6 +260,7 @@ export const inputProperties = toObjectMap(buttonProperties, [ 'max', // input, meter 'maxLength', // input, textarea 'min', // input, meter + 'minLength', // input, textarea 'multiple', // input, select 'pattern', // input 'placeholder', // input, textarea From 83a61f47466ccd33dea3d4b83f566de7a151bdc7 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Tue, 14 Jan 2025 15:45:40 +0100 Subject: [PATCH 04/45] docs(react-carousel): normalize title creation within storybook menu (#33604) --- .../react-carousel/stories/src/Carousel/index.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/react-carousel/stories/src/Carousel/index.stories.tsx b/packages/react-components/react-carousel/stories/src/Carousel/index.stories.tsx index 29d2ccf2468ad..9d3be3b898bc6 100644 --- a/packages/react-components/react-carousel/stories/src/Carousel/index.stories.tsx +++ b/packages/react-components/react-carousel/stories/src/Carousel/index.stories.tsx @@ -24,7 +24,7 @@ export { FirstRunExperience } from './CarouselFirstRunExperience.stories'; export { Eventing } from './CarouselEventing.stories'; export default { - title: 'Components/Carousel', + title: 'Components/Carousel/Carousel', component: Carousel, subcomponents: { CarouselAutoplayButton, From 9ed5fce8dda03bb85d583a23478ed298e51d305d Mon Sep 17 00:00:00 2001 From: Valentyna Date: Tue, 14 Jan 2025 07:34:03 -0800 Subject: [PATCH 05/45] fix(react-color-picker): focus shifts to inputY when user clicks `tab` (#33620) --- ...icker-preview-c30cb79c-aed3-4e9b-ad55-b1f946c8c691.json | 7 +++++++ .../library/src/components/ColorArea/useColorArea.ts | 4 ++-- .../stories/src/ColorPicker/AlphaSliderDefault.stories.tsx | 3 +-- .../src/ColorPicker/ColorAndSwatchPicker.stories.tsx | 6 ++++-- .../stories/src/ColorPicker/ColorAreaDefault.stories.tsx | 2 +- .../stories/src/ColorPicker/ColorPickerDefault.stories.tsx | 2 +- .../stories/src/ColorPicker/ColorPickerPopup.stories.tsx | 2 +- .../stories/src/ColorPicker/ColorPickerShape.stories.tsx | 6 +++--- .../stories/src/ColorPicker/ColorSliderDefault.stories.tsx | 3 +-- 9 files changed, 21 insertions(+), 14 deletions(-) create mode 100644 change/@fluentui-react-color-picker-preview-c30cb79c-aed3-4e9b-ad55-b1f946c8c691.json diff --git a/change/@fluentui-react-color-picker-preview-c30cb79c-aed3-4e9b-ad55-b1f946c8c691.json b/change/@fluentui-react-color-picker-preview-c30cb79c-aed3-4e9b-ad55-b1f946c8c691.json new file mode 100644 index 0000000000000..659240a99245b --- /dev/null +++ b/change/@fluentui-react-color-picker-preview-c30cb79c-aed3-4e9b-ad55-b1f946c8c691.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: focus jumps to inputY instead of next element", + "packageName": "@fluentui/react-color-picker-preview", + "email": "vkozlova@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-color-picker-preview/library/src/components/ColorArea/useColorArea.ts b/packages/react-components/react-color-picker-preview/library/src/components/ColorArea/useColorArea.ts index e11b88384ff35..d3b814a6aac83 100644 --- a/packages/react-components/react-color-picker-preview/library/src/components/ColorArea/useColorArea.ts +++ b/packages/react-components/react-color-picker-preview/library/src/components/ColorArea/useColorArea.ts @@ -175,7 +175,7 @@ export const useColorArea_unstable = (props: ColorAreaProps, ref: React.Ref) => { const styles = useStyles(); diff --git a/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorAndSwatchPicker.stories.tsx b/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorAndSwatchPicker.stories.tsx index 4c702b95060c0..6a8a4daf86014 100644 --- a/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorAndSwatchPicker.stories.tsx +++ b/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorAndSwatchPicker.stories.tsx @@ -48,8 +48,9 @@ const useStyles = makeStyles({ const ITEMS_LIMIT = 8; const DEFAULT_SELECTED_VALUE = '2be700'; -const DEFAULT_SELECTED_COLOR = '#2be700'; -const DEFAULT_COLOR_HSV = tinycolor(DEFAULT_SELECTED_COLOR).toHsv(); + +const DEFAULT_COLOR_HSV = { h: 109, s: 1, v: 0.9, a: 1 }; +const DEFAULT_SELECTED_COLOR = tinycolor(DEFAULT_COLOR_HSV).toHex(); export const ColorAndSwatchPickerExample = () => { const styles = useStyles(); @@ -110,6 +111,7 @@ export const ColorAndSwatchPickerExample = () => { aria-label="SwatchPicker with empty swatches" selectedValue={selectedValue} onSelectionChange={handleSelect} + shape="rounded" > {items.map(item => ( diff --git a/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorAreaDefault.stories.tsx b/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorAreaDefault.stories.tsx index 53b06d3ee5294..c991d299a6d45 100644 --- a/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorAreaDefault.stories.tsx +++ b/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorAreaDefault.stories.tsx @@ -21,7 +21,7 @@ const useStyles = makeStyles({ }, }); -const DEFAULT_COLOR_HSV = tinycolor('#804066').toHsv(); +const DEFAULT_COLOR_HSV = { h: 324, s: 0.5, v: 0.5, a: 1 }; export const ColorAreaExample = () => { const styles = useStyles(); diff --git a/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorPickerDefault.stories.tsx b/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorPickerDefault.stories.tsx index 394822d70e830..97a1dd81ccfb2 100644 --- a/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorPickerDefault.stories.tsx +++ b/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorPickerDefault.stories.tsx @@ -56,7 +56,7 @@ const useStyles = makeStyles({ const HEX_COLOR_REGEX = /^#?([0-9A-Fa-f]{0,6})$/; const NUMBER_REGEX = /^\d+$/; -const DEFAULT_COLOR_HSV = tinycolor('#2be700').toHsv(); +const DEFAULT_COLOR_HSV = { h: 109, s: 1, v: 0.9, a: 1 }; type RgbKey = 'r' | 'g' | 'b'; diff --git a/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorPickerPopup.stories.tsx b/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorPickerPopup.stories.tsx index 3b1b43a2bd410..bfc248fa6032f 100644 --- a/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorPickerPopup.stories.tsx +++ b/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorPickerPopup.stories.tsx @@ -36,7 +36,7 @@ const useStyles = makeStyles({ }, }); -const DEFAULT_COLOR_HSV = tinycolor('#2be700').toHsv(); +const DEFAULT_COLOR_HSV = { h: 109, s: 1, v: 0.9, a: 1 }; export const ColorPickerPopup = () => { const styles = useStyles(); diff --git a/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorPickerShape.stories.tsx b/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorPickerShape.stories.tsx index 89349528ca0f0..ce1b4ab0ca56c 100644 --- a/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorPickerShape.stories.tsx +++ b/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorPickerShape.stories.tsx @@ -27,7 +27,7 @@ const useStyles = makeStyles({ }, }); -const DEFAULT_COLOR_HSV = tinycolor('#2be700').toHsv(); +const DEFAULT_COLOR_HSV = { h: 109, s: 1, v: 0.91, a: 1 }; export const ColorPickerShape = () => { const styles = useStyles(); @@ -39,15 +39,15 @@ export const ColorPickerShape = () => {

Rounded (default)

+ -

Square (default)

+ -
diff --git a/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorSliderDefault.stories.tsx b/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorSliderDefault.stories.tsx index 1062a371d2bb9..2c1cbdec1772d 100644 --- a/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorSliderDefault.stories.tsx +++ b/packages/react-components/react-color-picker-preview/stories/src/ColorPicker/ColorSliderDefault.stories.tsx @@ -20,8 +20,7 @@ const useStyles = makeStyles({ }, }, }); - -const DEFAULT_COLOR_HSV = tinycolor('#2be700').toHsv(); +const DEFAULT_COLOR_HSV = { h: 109, s: 1, v: 0.9, a: 1 }; export const ColorSliderExample = (props: Partial) => { const styles = useStyles(); From ab6a30284d152c9ecde274e0cfc0579d002a7bbb Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Tue, 14 Jan 2025 17:38:07 +0100 Subject: [PATCH 06/45] feat(motion): add extended support for reduced motion (#33353) --- ...-f4c1433b-f36e-4b2c-a9bd-9b16f37489fc.json | 7 + .../library/etc/react-motion.api.md | 6 +- .../src/hooks/useAnimateAtoms.test.tsx | 88 ++++++++++ .../library/src/hooks/useAnimateAtoms.ts | 32 +++- .../react-motion/library/src/types.ts | 13 +- .../CreateMotionComponentDefault.stories.tsx | 4 + .../CreateMotionComponentFactory.stories.tsx | 4 + ...eMotionComponentFunctionParams.stories.tsx | 4 + ...CreateMotionComponentFunctions.stories.tsx | 4 + ...omponentImperativeRefPlayState.stories.tsx | 4 + ...eateMotionComponentTokensUsage.stories.tsx | 4 + ...ePresenceComponentReducedMotion.stories.md | 24 +++ ...PresenceComponentReducedMotion.stories.tsx | 150 ++++++++++++++++++ .../CreatePresenceComponent/index.stories.ts | 1 + 14 files changed, 334 insertions(+), 11 deletions(-) create mode 100644 change/@fluentui-react-motion-f4c1433b-f36e-4b2c-a9bd-9b16f37489fc.json create mode 100644 packages/react-components/react-motion/library/src/hooks/useAnimateAtoms.test.tsx create mode 100644 packages/react-components/react-motion/stories/src/CreatePresenceComponent/CreatePresenceComponentReducedMotion.stories.md create mode 100644 packages/react-components/react-motion/stories/src/CreatePresenceComponent/CreatePresenceComponentReducedMotion.stories.tsx diff --git a/change/@fluentui-react-motion-f4c1433b-f36e-4b2c-a9bd-9b16f37489fc.json b/change/@fluentui-react-motion-f4c1433b-f36e-4b2c-a9bd-9b16f37489fc.json new file mode 100644 index 0000000000000..cba37e60ef3fd --- /dev/null +++ b/change/@fluentui-react-motion-f4c1433b-f36e-4b2c-a9bd-9b16f37489fc.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "feat: add extended support for reduced motion", + "packageName": "@fluentui/react-motion", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-motion/library/etc/react-motion.api.md b/packages/react-components/react-motion/library/etc/react-motion.api.md index e8fd8b1a9e8be..7f97978066768 100644 --- a/packages/react-components/react-motion/library/etc/react-motion.api.md +++ b/packages/react-components/react-motion/library/etc/react-motion.api.md @@ -9,9 +9,9 @@ import { SlotComponentType } from '@fluentui/react-utilities'; import { SlotRenderFunction } from '@fluentui/react-utilities'; // @public (undocumented) -export type AtomMotion = { - keyframes: Keyframe[]; -} & KeyframeEffectOptions; +export type AtomMotion = AtomCore & { + reducedMotion?: Partial; +}; // @public (undocumented) export type AtomMotionFn = {}> = (params: { diff --git a/packages/react-components/react-motion/library/src/hooks/useAnimateAtoms.test.tsx b/packages/react-components/react-motion/library/src/hooks/useAnimateAtoms.test.tsx new file mode 100644 index 0000000000000..3501931bcf306 --- /dev/null +++ b/packages/react-components/react-motion/library/src/hooks/useAnimateAtoms.test.tsx @@ -0,0 +1,88 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import type { AtomMotion } from '../types'; +import { DEFAULT_ANIMATION_OPTIONS, useAnimateAtoms } from './useAnimateAtoms'; + +function createElementMock() { + const animate = jest.fn().mockReturnValue({ + persist: jest.fn(), + }); + + return [{ animate } as unknown as HTMLElement, animate] as const; +} + +const DEFAULT_KEYFRAMES = [{ transform: 'rotate(0)' }, { transform: 'rotate(180deg)' }]; +const REDUCED_MOTION_KEYFRAMES = [{ opacity: 0 }, { opacity: 1 }]; + +describe('useAnimateAtoms', () => { + beforeEach(() => { + // We set production environment to avoid testing the mock implementation + process.env.NODE_ENV = 'production'; + }); + + it('should return a function', () => { + const { result } = renderHook(() => useAnimateAtoms()); + + expect(result.current).toBeInstanceOf(Function); + }); + + describe('reduce motion', () => { + it('calls ".animate()" with regular motion', () => { + const { result } = renderHook(() => useAnimateAtoms()); + + const [element, animateMock] = createElementMock(); + const motion: AtomMotion = { keyframes: DEFAULT_KEYFRAMES }; + + result.current(element, motion, { isReducedMotion: false }); + + expect(animateMock).toHaveBeenCalledTimes(1); + expect(animateMock).toHaveBeenCalledWith(DEFAULT_KEYFRAMES, { ...DEFAULT_ANIMATION_OPTIONS }); + }); + + it('calls ".animate()" with shortened duration (1ms) when reduced motion is enabled', () => { + const { result } = renderHook(() => useAnimateAtoms()); + + const [element, animateMock] = createElementMock(); + const motion: AtomMotion = { keyframes: DEFAULT_KEYFRAMES }; + + result.current(element, motion, { isReducedMotion: true }); + + expect(animateMock).toHaveBeenCalledTimes(1); + expect(animateMock).toHaveBeenCalledWith(DEFAULT_KEYFRAMES, { ...DEFAULT_ANIMATION_OPTIONS, duration: 1 }); + }); + + it('calls ".animate()" with specified reduced motion keyframes when reduced motion is enabled', () => { + const { result } = renderHook(() => useAnimateAtoms()); + + const [element, animateMock] = createElementMock(); + const motion: AtomMotion = { + keyframes: DEFAULT_KEYFRAMES, + reducedMotion: { keyframes: REDUCED_MOTION_KEYFRAMES }, + }; + + result.current(element, motion, { isReducedMotion: true }); + + expect(animateMock).toHaveBeenCalledTimes(1); + expect(animateMock).toHaveBeenCalledWith(REDUCED_MOTION_KEYFRAMES, { ...DEFAULT_ANIMATION_OPTIONS }); + }); + + it('calls ".animate()" with specified reduced motion params when reduced motion is enabled', () => { + const { result } = renderHook(() => useAnimateAtoms()); + + const [element, animateMock] = createElementMock(); + const motion: AtomMotion = { + keyframes: DEFAULT_KEYFRAMES, + reducedMotion: { duration: 100, easing: 'linear' }, + }; + + result.current(element, motion, { isReducedMotion: true }); + + expect(animateMock).toHaveBeenCalledTimes(1); + expect(animateMock).toHaveBeenCalledWith(DEFAULT_KEYFRAMES, { + ...DEFAULT_ANIMATION_OPTIONS, + easing: 'linear', + duration: 100, + }); + }); + }); +}); diff --git a/packages/react-components/react-motion/library/src/hooks/useAnimateAtoms.ts b/packages/react-components/react-motion/library/src/hooks/useAnimateAtoms.ts index 9bc31e0d2e9ce..59bfe38e32203 100644 --- a/packages/react-components/react-motion/library/src/hooks/useAnimateAtoms.ts +++ b/packages/react-components/react-motion/library/src/hooks/useAnimateAtoms.ts @@ -1,6 +1,16 @@ import * as React from 'react'; import type { AnimationHandle, AtomMotion } from '../types'; +export const DEFAULT_ANIMATION_OPTIONS: KeyframeEffectOptions = { + fill: 'forwards', +}; + +// A motion atom's default reduced motion is a simple 1 ms duration. +// But an atom can define a custom reduced motion, overriding keyframes and/or params like duration, easing, iterations, etc. +const DEFAULT_REDUCED_MOTION_ATOM: NonNullable = { + duration: 1, +}; + function useAnimateAtomsInSupportedEnvironment() { // eslint-disable-next-line @nx/workspace-no-restricted-globals const SUPPORTS_PERSIST = typeof window !== 'undefined' && typeof window.Animation?.prototype.persist === 'function'; @@ -17,18 +27,26 @@ function useAnimateAtomsInSupportedEnvironment() { const { isReducedMotion } = options; const animations = atoms.map(motion => { - const { keyframes, ...params } = motion; - const animation = element.animate(keyframes, { - fill: 'forwards', - + // Grab the custom reduced motion definition if it exists, or fall back to the default reduced motion. + const { keyframes: motionKeyframes, reducedMotion = DEFAULT_REDUCED_MOTION_ATOM, ...params } = motion; + // Grab the reduced motion keyframes if they exist, or fall back to the regular keyframes. + const { keyframes: reducedMotionKeyframes = motionKeyframes, ...reducedMotionParams } = reducedMotion; + + const animationKeyframes: Keyframe[] = isReducedMotion ? reducedMotionKeyframes : motionKeyframes; + const animationParams: KeyframeEffectOptions = { + ...DEFAULT_ANIMATION_OPTIONS, ...params, - ...(isReducedMotion && { duration: 1 }), - }); + + // Use reduced motion overrides (e.g. duration, easing) when reduced motion is enabled + ...(isReducedMotion && reducedMotionParams), + }; + + const animation = element.animate(animationKeyframes, animationParams); if (SUPPORTS_PERSIST) { animation.persist(); } else { - const resultKeyframe = keyframes[keyframes.length - 1]; + const resultKeyframe = animationKeyframes[animationKeyframes.length - 1]; Object.assign(element.style ?? {}, resultKeyframe); } diff --git a/packages/react-components/react-motion/library/src/types.ts b/packages/react-components/react-motion/library/src/types.ts index b989d7e890b18..6fdfd8e713024 100644 --- a/packages/react-components/react-motion/library/src/types.ts +++ b/packages/react-components/react-motion/library/src/types.ts @@ -1,4 +1,15 @@ -export type AtomMotion = { keyframes: Keyframe[] } & KeyframeEffectOptions; +type AtomCore = { keyframes: Keyframe[] } & KeyframeEffectOptions; + +export type AtomMotion = AtomCore & { + /** + * Allows to specify a reduced motion version of the animation. If provided, the settings will be used when the + * user has enabled the reduced motion setting in the operating system (i.e `prefers-reduced-motion` media query is + * active). If not provided, the duration of the animation will be overridden to be 1ms. + * + * Note, if `keyframes` are provided, they will be used instead of the regular `keyframes`. + */ + reducedMotion?: Partial; +}; export type PresenceDirection = 'enter' | 'exit'; diff --git a/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentDefault.stories.tsx b/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentDefault.stories.tsx index 07a0143b051de..d40b1fb87d986 100644 --- a/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentDefault.stories.tsx +++ b/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentDefault.stories.tsx @@ -41,6 +41,10 @@ const FadeEnter = createMotionComponent({ keyframes: [{ opacity: 0 }, { opacity: 1 }], duration: motionTokens.durationSlow, iterations: Infinity, + + reducedMotion: { + iterations: 1, + }, }); export const CreateMotionComponentDefault = (props: MotionComponentProps) => { diff --git a/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentFactory.stories.tsx b/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentFactory.stories.tsx index 928ffc9bb2b67..57b41d3f226ae 100644 --- a/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentFactory.stories.tsx +++ b/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentFactory.stories.tsx @@ -45,6 +45,10 @@ const DropIn = createMotionComponent({ ], duration: 4000, iterations: Infinity, + + reducedMotion: { + iterations: 1, + }, }); export const CreateMotionComponentFactory = () => { diff --git a/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentFunctionParams.stories.tsx b/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentFunctionParams.stories.tsx index bd0374ed4ce86..81ed56abc9ee3 100644 --- a/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentFunctionParams.stories.tsx +++ b/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentFunctionParams.stories.tsx @@ -81,6 +81,10 @@ const Scale = createMotionComponent<{ startFrom?: number }>(({ startFrom = 0.5 } ], duration: motionTokens.durationUltraSlow, iterations: Infinity, + + reducedMotion: { + iterations: 1, + }, }; }); diff --git a/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentFunctions.stories.tsx b/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentFunctions.stories.tsx index a62027d0707b6..422b33618f658 100644 --- a/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentFunctions.stories.tsx +++ b/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentFunctions.stories.tsx @@ -68,6 +68,10 @@ const Grow = createMotionComponent(({ element }) => ({ { opacity: 0, maxHeight: `${element.scrollHeight / 2}px` }, ], iterations: Infinity, + + reducedMotion: { + iterations: 1, + }, })); export const CreateMotionComponentFunctions = () => { diff --git a/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentImperativeRefPlayState.stories.tsx b/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentImperativeRefPlayState.stories.tsx index b06668fd66ac7..d0e19a07e790c 100644 --- a/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentImperativeRefPlayState.stories.tsx +++ b/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentImperativeRefPlayState.stories.tsx @@ -66,6 +66,10 @@ const FadeEnter = createMotionComponent({ keyframes: [{ opacity: 0 }, { opacity: 1 }], duration: motionTokens.durationSlow, iterations: Infinity, + + reducedMotion: { + iterations: 1, + }, }); export const CreateMotionComponentImperativeRefPlayState = () => { diff --git a/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentTokensUsage.stories.tsx b/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentTokensUsage.stories.tsx index 5ac6bed6773d9..07c8af66fe146 100644 --- a/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentTokensUsage.stories.tsx +++ b/packages/react-components/react-motion/stories/src/CreateMotionComponent/CreateMotionComponentTokensUsage.stories.tsx @@ -45,6 +45,10 @@ const BackgroundChange = createMotionComponent({ ], duration: 3000, iterations: Infinity, + + reducedMotion: { + iterations: 1, + }, }); export const CreateMotionComponentTokensUsage = () => { diff --git a/packages/react-components/react-motion/stories/src/CreatePresenceComponent/CreatePresenceComponentReducedMotion.stories.md b/packages/react-components/react-motion/stories/src/CreatePresenceComponent/CreatePresenceComponentReducedMotion.stories.md new file mode 100644 index 0000000000000..34997dc8b2df9 --- /dev/null +++ b/packages/react-components/react-motion/stories/src/CreatePresenceComponent/CreatePresenceComponentReducedMotion.stories.md @@ -0,0 +1,24 @@ +By default, when [reduced motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion) is enabled the duration of the animation is set to `1ms`. `reducedMotion` allows to customize a reduced motion version of the animation: + +```ts +const Motion = createPresenceComponent({ + enter: { + keyframes: [ + { opacity: 0, transform: 'scale(0)' }, + { opacity: 1, transform: 'scale(1)' }, + ], + duration: 300, + + /* 💡reduced motion will not have scale animation */ + reducedMotion: { + keyframes: [{ opacity: 0 }, { opacity: 1 }], + duration: 1000, + }, + }, + exit: { + /* ... */ + }, +}); +``` + +> 💡Note, if `keyframes` are provided, they will be used instead of the regular keyframes. diff --git a/packages/react-components/react-motion/stories/src/CreatePresenceComponent/CreatePresenceComponentReducedMotion.stories.tsx b/packages/react-components/react-motion/stories/src/CreatePresenceComponent/CreatePresenceComponentReducedMotion.stories.tsx new file mode 100644 index 0000000000000..b480d9587821b --- /dev/null +++ b/packages/react-components/react-motion/stories/src/CreatePresenceComponent/CreatePresenceComponentReducedMotion.stories.tsx @@ -0,0 +1,150 @@ +import { + createPresenceComponent, + Field, + makeStyles, + mergeClasses, + type MotionImperativeRef, + motionTokens, + Slider, + Switch, + tokens, +} from '@fluentui/react-components'; +import * as React from 'react'; + +import description from './CreatePresenceComponentReducedMotion.stories.md'; + +const useClasses = makeStyles({ + container: { + display: 'grid', + gridTemplate: `"card card" "controls ." / 1fr 1fr`, + gap: '20px 10px', + }, + card: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'end', + gridArea: 'card', + + border: `${tokens.strokeWidthThicker} solid ${tokens.colorNeutralForeground3}`, + borderRadius: tokens.borderRadiusMedium, + boxShadow: tokens.shadow16, + padding: '10px', + }, + controls: { + display: 'flex', + flexDirection: 'column', + gridArea: 'controls', + + border: `${tokens.strokeWidthThicker} solid ${tokens.colorNeutralForeground3}`, + borderRadius: tokens.borderRadiusMedium, + boxShadow: tokens.shadow16, + padding: '10px', + }, + field: { + flex: 1, + }, + sliderField: { + gridTemplateColumns: 'min-content 1fr', + }, + sliderLabel: { + textWrap: 'nowrap', + }, + + item: { + backgroundColor: tokens.colorBrandBackground, + border: `${tokens.strokeWidthThicker} solid ${tokens.colorTransparentStroke}`, + + width: '100px', + height: '100px', + }, +}); + +const FadeAndScale = createPresenceComponent({ + enter: { + keyframes: [ + { opacity: 0, transform: 'rotate(0)' }, + { transform: 'rotate(90deg) scale(1.5)' }, + { opacity: 1, transform: 'rotate(0)' }, + ], + duration: motionTokens.durationGentle, + + reducedMotion: { + keyframes: [{ opacity: 0 }, { opacity: 1 }], + duration: motionTokens.durationUltraSlow, + }, + }, + exit: { + keyframes: [ + { opacity: 1, transform: 'rotate(0)' }, + { transform: 'rotate(-90deg) scale(1.5)' }, + { opacity: 0, transform: 'rotate(0)' }, + ], + duration: motionTokens.durationGentle, + + reducedMotion: { + keyframes: [{ opacity: 1 }, { opacity: 0 }], + duration: motionTokens.durationUltraSlow, + }, + }, +}); + +export const CreatePresenceComponentReducedMotion = () => { + const classes = useClasses(); + const motionRef = React.useRef(); + + const [playbackRate, setPlaybackRate] = React.useState(30); + const [visible, setVisible] = React.useState(true); + + // Heads up! + // This is optional and is intended solely to slow down the animations, making motions more visible in the examples. + React.useEffect(() => { + motionRef.current?.setPlaybackRate(playbackRate / 100); + }, [playbackRate, visible]); + + return ( +
+
+ +
+ +
+ +
+ + setVisible(v => !v)} /> + + + playbackRate: {playbackRate}% + + ), + className: classes.sliderLabel, + }} + orientation="horizontal" + > + setPlaybackRate(data.value)} + min={0} + max={100} + step={5} + /> + +
+
+ ); +}; + +CreatePresenceComponentReducedMotion.parameters = { + docs: { + description: { + story: description, + }, + }, +}; diff --git a/packages/react-components/react-motion/stories/src/CreatePresenceComponent/index.stories.ts b/packages/react-components/react-motion/stories/src/CreatePresenceComponent/index.stories.ts index 190d3b12fc3ae..71a7375a799b0 100644 --- a/packages/react-components/react-motion/stories/src/CreatePresenceComponent/index.stories.ts +++ b/packages/react-components/react-motion/stories/src/CreatePresenceComponent/index.stories.ts @@ -8,6 +8,7 @@ export { CreatePresenceComponentDefault as Default } from './CreatePresenceCompo export { CreatePresenceComponentFactory as createPresenceComponent } from './CreatePresenceComponentFactory.stories'; export { CreatePresenceComponentAppear as appear } from './CreatePresenceComponentAppear.stories'; +export { CreatePresenceComponentReducedMotion as reducedMotion } from './CreatePresenceComponentReducedMotion.stories'; export { CreatePresenceComponentUnmountOnExit as unmountOnExit } from './CreatePresenceComponentUnmountOnExit.stories'; export { CreatePresenceComponentLifecycleCallbacks as LifecycleCallbacks } from './CreatePresenceComponentLifecycleCallbacks.stories'; From a8c0c467fdcdb1f721350b5cbd4ed6dd84e0715c Mon Sep 17 00:00:00 2001 From: Makoto Morimoto Date: Tue, 14 Jan 2025 10:37:27 -0800 Subject: [PATCH 07/45] chore(react-provider): Updating `FluentProvider` types to avoid implicit import in API definition (#33590) Co-authored-by: Humberto Makoto Morimoto Burgos Co-authored-by: Martin Hochel --- ...-9f506bc2-b69a-4140-b85e-67b97c87e955.json | 7 + .../library/etc/react-provider.api.md | 161 +----------------- .../FluentProvider/FluentProvider.tsx | 5 +- 3 files changed, 15 insertions(+), 158 deletions(-) create mode 100644 change/@fluentui-react-provider-9f506bc2-b69a-4140-b85e-67b97c87e955.json diff --git a/change/@fluentui-react-provider-9f506bc2-b69a-4140-b85e-67b97c87e955.json b/change/@fluentui-react-provider-9f506bc2-b69a-4140-b85e-67b97c87e955.json new file mode 100644 index 0000000000000..c7f52f2deb033 --- /dev/null +++ b/change/@fluentui-react-provider-9f506bc2-b69a-4140-b85e-67b97c87e955.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "fix: Updating FluentProvider types to avoid implicit import in API definition.", + "packageName": "@fluentui/react-provider", + "email": "makotom@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-provider/library/etc/react-provider.api.md b/packages/react-components/react-provider/library/etc/react-provider.api.md index e7e94b84cad4f..de54394ef0a81 100644 --- a/packages/react-components/react-provider/library/etc/react-provider.api.md +++ b/packages/react-components/react-provider/library/etc/react-provider.api.md @@ -4,17 +4,17 @@ ```ts -import { ComponentProps } from '@fluentui/react-utilities'; +import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; import type { CustomStyleHooksContextValue_unstable } from '@fluentui/react-shared-contexts'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; import type { IconDirectionContextValue } from '@fluentui/react-icons/lib/providers'; -import { OverridesContextValue_unstable } from '@fluentui/react-shared-contexts'; +import type { OverridesContextValue_unstable } from '@fluentui/react-shared-contexts'; import type { PartialTheme } from '@fluentui/react-theme'; import type { ProviderContextValue_unstable } from '@fluentui/react-shared-contexts'; import * as React_2 from 'react'; import type { Slot } from '@fluentui/react-utilities'; import { SlotClassNames } from '@fluentui/react-utilities'; -import { Theme } from '@fluentui/tokens'; import type { ThemeClassNameContextValue_unstable } from '@fluentui/react-shared-contexts'; import type { ThemeContextValue_unstable } from '@fluentui/react-shared-contexts'; import type { TooltipVisibilityContextValue_unstable } from '@fluentui/react-shared-contexts'; @@ -23,160 +23,7 @@ import type { TooltipVisibilityContextValue_unstable } from '@fluentui/react-sha export function createCSSRuleFromTheme(selector: string, theme: PartialTheme | undefined): string; // @public (undocumented) -export const FluentProvider: React_2.ForwardRefExoticComponent, "dir"> & { - applyStylesToPortals?: boolean | undefined; - customStyleHooks_unstable?: Partial<{ - useAccordionHeaderStyles_unstable: (state: unknown) => void; - useAccordionItemStyles_unstable: (state: unknown) => void; - useAccordionPanelStyles_unstable: (state: unknown) => void; - useAccordionStyles_unstable: (state: unknown) => void; - useAvatarStyles_unstable: (state: unknown) => void; - useAvatarGroupStyles_unstable: (state: unknown) => void; - useAvatarGroupItemStyles_unstable: (state: unknown) => void; - useAvatarGroupPopoverStyles_unstable: (state: unknown) => void; - useBadgeStyles_unstable: (state: unknown) => void; - useCounterBadgeStyles_unstable: (state: unknown) => void; - useCardHeaderStyles_unstable: (state: unknown) => void; - useCardStyles_unstable: (state: unknown) => void; - useCardFooterStyles_unstable: (state: unknown) => void; - useCardPreviewStyles_unstable: (state: unknown) => void; - usePresenceBadgeStyles_unstable: (state: unknown) => void; - useButtonStyles_unstable: (state: unknown) => void; - useCompoundButtonStyles_unstable: (state: unknown) => void; - useMenuButtonStyles_unstable: (state: unknown) => void; - useSplitButtonStyles_unstable: (state: unknown) => void; - useToggleButtonStyles_unstable: (state: unknown) => void; - useCheckboxStyles_unstable: (state: unknown) => void; - useComboboxStyles_unstable: (state: unknown) => void; - useDropdownStyles_unstable: (state: unknown) => void; - useListboxStyles_unstable: (state: unknown) => void; - useListStyles_unstable: (state: unknown) => void; - useListItemStyles_unstable: (state: unknown) => void; - useListItemButtonStyles_unstable: (state: unknown) => void; - useOptionStyles_unstable: (state: unknown) => void; - useOptionGroupStyles_unstable: (state: unknown) => void; - useDividerStyles_unstable: (state: unknown) => void; - useInputStyles_unstable: (state: unknown) => void; - useImageStyles_unstable: (state: unknown) => void; - useLabelStyles_unstable: (state: unknown) => void; - useLinkStyles_unstable: (state: unknown) => void; - useMenuDividerStyles_unstable: (state: unknown) => void; - useMenuGroupHeaderStyles_unstable: (state: unknown) => void; - useMenuGroupStyles_unstable: (state: unknown) => void; - useMenuItemCheckboxStyles_unstable: (state: unknown) => void; - useMenuItemSwitchStyles_unstable: (state: unknown) => void; - useMenuItemRadioStyles_unstable: (state: unknown) => void; - useMenuItemStyles_unstable: (state: unknown) => void; - useMenuItemLinkStyles_unstable: (state: unknown) => void; - useMenuListStyles_unstable: (state: unknown) => void; - useMenuPopoverStyles_unstable: (state: unknown) => void; - useMenuSplitGroupStyles_unstable: (state: unknown) => void; - usePersonaStyles_unstable: (state: unknown) => void; - usePopoverSurfaceStyles_unstable: (state: unknown) => void; - useRadioGroupStyles_unstable: (state: unknown) => void; - useRadioStyles_unstable: (state: unknown) => void; - useSelectStyles_unstable: (state: unknown) => void; - useSliderStyles_unstable: (state: unknown) => void; - useSpinButtonStyles_unstable: (state: unknown) => void; - useSpinnerStyles_unstable: (state: unknown) => void; - useSwitchStyles_unstable: (state: unknown) => void; - useTabStyles_unstable: (state: unknown) => void; - useTabListStyles_unstable: (state: unknown) => void; - useTextStyles_unstable: (state: unknown) => void; - useTextareaStyles_unstable: (state: unknown) => void; - useTooltipStyles_unstable: (state: unknown) => void; - useDialogTitleStyles_unstable: (state: unknown) => void; - useDialogBodyStyles_unstable: (state: unknown) => void; - useDialogActionsStyles_unstable: (state: unknown) => void; - useDialogSurfaceStyles_unstable: (state: unknown) => void; - useDialogContentStyles_unstable: (state: unknown) => void; - useProgressBarStyles_unstable: (state: unknown) => void; - useToolbarButtonStyles_unstable: (state: unknown) => void; - useToolbarRadioButtonStyles_unstable: (state: unknown) => void; - useToolbarGroupStyles_unstable: (state: unknown) => void; - useToolbarToggleButtonStyles_unstable: (state: unknown) => void; - useToolbarDividerStyles_unstable: (state: unknown) => void; - useToolbarStyles_unstable: (state: unknown) => void; - useTableCellStyles_unstable: (state: unknown) => void; - useTableRowStyles_unstable: (state: unknown) => void; - useTableBodyStyles_unstable: (state: unknown) => void; - useTableStyles_unstable: (state: unknown) => void; - useTableHeaderStyles_unstable: (state: unknown) => void; - useTableHeaderCellStyles_unstable: (state: unknown) => void; - useTableResizeHandleStyles_unstable: (state: unknown) => void; - useTableSelectionCellStyles_unstable: (state: unknown) => void; - useTableCellActionsStyles_unstable: (state: unknown) => void; - useTableCellLayoutStyles_unstable: (state: unknown) => void; - useDataGridCellStyles_unstable: (state: unknown) => void; - useDataGridRowStyles_unstable: (state: unknown) => void; - useDataGridBodyStyles_unstable: (state: unknown) => void; - useDataGridStyles_unstable: (state: unknown) => void; - useDataGridHeaderStyles_unstable: (state: unknown) => void; - useDataGridHeaderCellStyles_unstable: (state: unknown) => void; - useDataGridSelectionCellStyles_unstable: (state: unknown) => void; - useDrawerStyles_unstable: (state: unknown) => void; - useDrawerInlineStyles_unstable: (state: unknown) => void; - useDrawerOverlayStyles_unstable: (state: unknown) => void; - useInlineDrawerStyles_unstable: (state: unknown) => void; - useOverlayDrawerStyles_unstable: (state: unknown) => void; - useDrawerHeaderStyles_unstable: (state: unknown) => void; - useDrawerHeaderNavigationStyles_unstable: (state: unknown) => void; - useDrawerHeaderTitleStyles_unstable: (state: unknown) => void; - useDrawerBodyStyles_unstable: (state: unknown) => void; - useDrawerFooterStyles_unstable: (state: unknown) => void; - useInteractionTagStyles_unstable: (state: unknown) => void; - useInteractionTagPrimaryStyles_unstable: (state: unknown) => void; - useInteractionTagSecondaryStyles_unstable: (state: unknown) => void; - useTagStyles_unstable: (state: unknown) => void; - useTagGroupStyles_unstable: (state: unknown) => void; - useBreadcrumbStyles_unstable: (state: unknown) => void; - useBreadcrumbButtonStyles_unstable: (state: unknown) => void; - useBreadcrumbItemStyles_unstable: (state: unknown) => void; - useBreadcrumbDividerStyles_unstable: (state: unknown) => void; - useMessageBarStyles_unstable: (state: unknown) => void; - useMessageBarBodyStyles_unstable: (state: unknown) => void; - useMessageBarTitleStyles_unstable: (state: unknown) => void; - useMessageBarActionsStyles_unstable: (state: unknown) => void; - useMessageBarGroupStyles_unstable: (state: unknown) => void; - useToasterStyles_unstable: (state: unknown) => void; - useTeachingPopoverStyles_unstable: (state: unknown) => void; - useTeachingPopoverActionsStyles_unstable: (state: unknown) => void; - useTeachingPopoverBodyStyles_unstable: (state: unknown) => void; - useTeachingPopoverButtonStyles_unstable: (state: unknown) => void; - useTeachingPopoverCarouselStyles_unstable: (state: unknown) => void; - useTeachingPopoverHeaderStyles_unstable: (state: unknown) => void; - useTeachingPopoverPageCountStyles_unstable: (state: unknown) => void; - useTeachingPopoverSurfaceStyles_unstable: (state: unknown) => void; - useTeachingPopoverTitleStyles_unstable: (state: unknown) => void; - useTimePickerCompatStyles_unstable: (state: unknown) => void; - useTagPickerInputStyles_unstable: (state: unknown) => void; - useTagPickerButtonStyles_unstable: (state: unknown) => void; - useTagPickerControlStyles_unstable: (state: unknown) => void; - useTagPickerGroupStyles_unstable: (state: unknown) => void; - useTagPickerListStyles_unstable: (state: unknown) => void; - useTagPickerOptionStyles_unstable: (state: unknown) => void; - useTagPickerOptionGroupStyles_unstable: (state: unknown) => void; - useColorSwatchStyles_unstable: (state: unknown) => void; - useImageSwatchStyles_unstable: (state: unknown) => void; - useEmptySwatchStyles_unstable: (state: unknown) => void; - useSwatchPickerRowStyles_unstable: (state: unknown) => void; - useSwatchPickerStyles_unstable: (state: unknown) => void; - useCarouselViewportStyles_unstable: (state: unknown) => void; - useCarouselSliderStyles_unstable: (state: unknown) => void; - useCarouselStyles_unstable: (state: unknown) => void; - useCarouselAutoplayButtonStyles_unstable: (state: unknown) => void; - useCarouselButtonStyles_unstable: (state: unknown) => void; - useCarouselCardStyles_unstable: (state: unknown) => void; - useCarouselNavStyles_unstable: (state: unknown) => void; - useCarouselNavButtonStyles_unstable: (state: unknown) => void; - useCarouselNavContainerStyles_unstable: (state: unknown) => void; - useCarouselNavImageButtonStyles_unstable: (state: unknown) => void; - }> | undefined; - dir?: "ltr" | "rtl" | undefined; - targetDocument?: Document | undefined; - theme?: Partial | undefined; - overrides_unstable?: OverridesContextValue_unstable | undefined; -} & React_2.RefAttributes>; +export const FluentProvider: ForwardRefComponent; // @public (undocumented) export const fluentProviderClassNames: SlotClassNames; diff --git a/packages/react-components/react-provider/library/src/components/FluentProvider/FluentProvider.tsx b/packages/react-components/react-provider/library/src/components/FluentProvider/FluentProvider.tsx index 6fbcb494a3c1a..e732fb42d1ec5 100644 --- a/packages/react-components/react-provider/library/src/components/FluentProvider/FluentProvider.tsx +++ b/packages/react-components/react-provider/library/src/components/FluentProvider/FluentProvider.tsx @@ -1,11 +1,14 @@ import * as React from 'react'; + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + import { renderFluentProvider_unstable } from './renderFluentProvider'; import { useFluentProvider_unstable } from './useFluentProvider'; import { useFluentProviderStyles_unstable } from './useFluentProviderStyles.styles'; import { useFluentProviderContextValues_unstable } from './useFluentProviderContextValues'; import type { FluentProviderProps } from './FluentProvider.types'; -export const FluentProvider = React.forwardRef((props, ref) => { +export const FluentProvider: ForwardRefComponent = React.forwardRef((props, ref) => { const state = useFluentProvider_unstable(props, ref); useFluentProviderStyles_unstable(state); From 614e8b594da0eec4104f9fcaa034a4352fee5c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Mat=C4=9Bjka?= Date: Tue, 14 Jan 2025 20:14:56 +0100 Subject: [PATCH 08/45] fix(TagItem v8): add missing data-id attribute (#31956) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kryštof Matějka Co-authored-by: Esteban Munoz Facusse --- ...-aeee29c3-70b1-47de-a1a6-ee1ec2c0597d.json | 7 + packages/react/etc/react.api.md | 6 + .../pickers/TagPicker/TagItem.test.tsx | 10 + .../components/pickers/TagPicker/TagItem.tsx | 2 + .../pickers/TagPicker/TagPicker.types.ts | 13 + .../__snapshots__/TagItem.test.tsx.snap | 255 ++++++++++++++++++ packages/react/src/index.ts | 1 + 7 files changed, 294 insertions(+) create mode 100644 change/@fluentui-react-aeee29c3-70b1-47de-a1a6-ee1ec2c0597d.json diff --git a/change/@fluentui-react-aeee29c3-70b1-47de-a1a6-ee1ec2c0597d.json b/change/@fluentui-react-aeee29c3-70b1-47de-a1a6-ee1ec2c0597d.json new file mode 100644 index 0000000000000..a14b5ba95b418 --- /dev/null +++ b/change/@fluentui-react-aeee29c3-70b1-47de-a1a6-ee1ec2c0597d.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "add missing data-id attribute to close button in TagItem", + "packageName": "@fluentui/react", + "email": "kmatejka@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react/etc/react.api.md b/packages/react/etc/react.api.md index 9fe14cb8bfbb2..6ad1107c0ee84 100644 --- a/packages/react/etc/react.api.md +++ b/packages/react/etc/react.api.md @@ -9205,11 +9205,17 @@ export interface ITag { export interface ITagItemProps extends IPickerItemProps { className?: string; enableTagFocusInDisabledPicker?: boolean; + removeButtonProps?: ITagItemRemoveButtonProps; styles?: IStyleFunctionOrObject; theme?: ITheme; title?: string; } +// @public +export interface ITagItemRemoveButtonProps extends IButtonProps { + 'data-id'?: string; +} + // @public export type ITagItemStyleProps = Required> & Pick & {}; diff --git a/packages/react/src/components/pickers/TagPicker/TagItem.test.tsx b/packages/react/src/components/pickers/TagPicker/TagItem.test.tsx index b2a28e459fb5e..8174ed6428b60 100644 --- a/packages/react/src/components/pickers/TagPicker/TagItem.test.tsx +++ b/packages/react/src/components/pickers/TagPicker/TagItem.test.tsx @@ -39,4 +39,14 @@ describe('TagItem', () => { const tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); + + it('accepts remove-button-data-id', () => { + const component = renderer.create( + + Red color + , + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); }); diff --git a/packages/react/src/components/pickers/TagPicker/TagItem.tsx b/packages/react/src/components/pickers/TagPicker/TagItem.tsx index 5bdb2ddb4a910..cae157e3af2b8 100644 --- a/packages/react/src/components/pickers/TagPicker/TagItem.tsx +++ b/packages/react/src/components/pickers/TagPicker/TagItem.tsx @@ -25,6 +25,7 @@ export const TagItemBase = (props: ITagItemProps) => { removeButtonAriaLabel, title = typeof props.children === 'string' ? props.children : props.item.name, removeButtonIconProps, + removeButtonProps, } = props; const buttonRef = React.createRef(); @@ -65,6 +66,7 @@ export const TagItemBase = (props: ITagItemProps) => { styles={{ icon: { fontSize: '12px' } }} className={classNames.close} aria-labelledby={`${itemId}-removeLabel ${itemId}-text`} + {...removeButtonProps} />