diff --git a/packages/@adobe/react-spectrum/src/color/ColorSwatchPicker.tsx b/packages/@adobe/react-spectrum/src/color/ColorSwatchPicker.tsx index 8f1841b307e..d3165b9f338 100644 --- a/packages/@adobe/react-spectrum/src/color/ColorSwatchPicker.tsx +++ b/packages/@adobe/react-spectrum/src/color/ColorSwatchPicker.tsx @@ -24,7 +24,7 @@ import {useDOMRef} from '../utils/useDOMRef'; import {useStyleProps} from '../utils/styleProps'; export interface SpectrumColorSwatchPickerProps - extends ValueBase, StyleProps { + extends ValueBase, StyleProps { /** The ColorSwatches within the ColorSwatchPicker. */ children: ReactNode; /** diff --git a/packages/@react-spectrum/s2/src/ColorSwatchPicker.tsx b/packages/@react-spectrum/s2/src/ColorSwatchPicker.tsx index cf96d46c72d..5f4dfce532e 100644 --- a/packages/@react-spectrum/s2/src/ColorSwatchPicker.tsx +++ b/packages/@react-spectrum/s2/src/ColorSwatchPicker.tsx @@ -26,7 +26,7 @@ import {useDOMRef} from './useDOMRef'; import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface ColorSwatchPickerProps - extends ValueBase, StyleProps, SlotProps { + extends ValueBase, StyleProps, SlotProps { /** The ColorSwatches within the ColorSwatchPicker. */ children: ReactNode; /** diff --git a/packages/dev/s2-docs/pages/react-aria/ColorSwatchPicker.mdx b/packages/dev/s2-docs/pages/react-aria/ColorSwatchPicker.mdx index 7d296a4dab4..d16e878681b 100644 --- a/packages/dev/s2-docs/pages/react-aria/ColorSwatchPicker.mdx +++ b/packages/dev/s2-docs/pages/react-aria/ColorSwatchPicker.mdx @@ -51,11 +51,11 @@ Use the `value` or `defaultValue` prop to set the selected color, and `onChange` ```tsx render "use client"; import {ColorSwatchPicker, ColorSwatchPickerItem} from 'vanilla-starter/ColorSwatchPicker'; -import {parseColor} from 'react-aria-components/ColorSwatchPicker'; +import {parseColor, type Color} from 'react-aria-components/ColorSwatchPicker'; import {useState} from 'react'; function Example() { - let [value, setValue] = useState(parseColor('#A00')); + let [value, setValue] = useState(parseColor('#A00')); return ( <> @@ -69,7 +69,7 @@ function Example() { -
Selected color: {value.toString('rgb')}
+
Selected color: {value ? value.toString('rgb') : 'none'}
); } diff --git a/packages/dev/s2-docs/pages/s2/ColorSwatchPicker.mdx b/packages/dev/s2-docs/pages/s2/ColorSwatchPicker.mdx index 645e0aed011..fd3ed6252e1 100644 --- a/packages/dev/s2-docs/pages/s2/ColorSwatchPicker.mdx +++ b/packages/dev/s2-docs/pages/s2/ColorSwatchPicker.mdx @@ -31,12 +31,12 @@ Use the `value` or `defaultValue` prop to set the selected color, and `onChange` ```tsx render "use client"; -import {ColorSwatchPicker, ColorSwatch, parseColor} from '@react-spectrum/s2/ColorSwatchPicker'; +import {ColorSwatchPicker, ColorSwatch, parseColor, type Color} from '@react-spectrum/s2/ColorSwatchPicker'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; import {useState} from 'react'; function Example() { - let [value, setValue] = useState(parseColor('#e11d48')); + let [value, setValue] = useState(parseColor('#e11d48')); return ( <> @@ -51,7 +51,7 @@ function Example() { -
Selected color: {value.toString('rgb')}
+
Selected color: {value ? value.toString('rgb') : 'none'}
); } diff --git a/packages/react-aria-components/src/ColorSwatchPicker.tsx b/packages/react-aria-components/src/ColorSwatchPicker.tsx index 85efcdc9d02..e64a712db88 100644 --- a/packages/react-aria-components/src/ColorSwatchPicker.tsx +++ b/packages/react-aria-components/src/ColorSwatchPicker.tsx @@ -28,14 +28,14 @@ import React, { useEffect, useMemo } from 'react'; -import {useColorPickerState} from 'react-stately/useColorPickerState'; +import {useColorSwatchPickerState} from 'react-stately/useColorSwatchPickerState'; import {useLocale} from 'react-aria/I18nProvider'; import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; export interface ColorSwatchPickerRenderProps extends Omit {} export interface ColorSwatchPickerProps extends - ValueBase, + ValueBase, AriaLabelingProps, StyleRenderProps, GlobalDOMAttributes { @@ -68,7 +68,7 @@ export const ColorSwatchPicker = forwardRef(function ColorSwatchPicker( ref: ForwardedRef ) { [props, ref] = useContextProps(props, ref, ColorSwatchPickerContext); - let state = useColorPickerState(props); + let state = useColorSwatchPickerState(props); let colorMap = useMemo(() => new Map(), []); let formatter = useLocalizedStringFormatter(intlMessages, 'react-aria-components'); @@ -84,7 +84,7 @@ export const ColorSwatchPicker = forwardRef(function ColorSwatchPicker( } layout={props.layout || 'grid'} selectionMode="single" - selectedKeys={[state.color.toString('hexa')]} + selectedKeys={state.color ? [state.color.toString('hexa')] : []} onSelectionChange={keys => { // single select, 'all' cannot occur. appease typescript. if (keys !== 'all') { diff --git a/packages/react-aria-components/test/ColorSwatchPicker.test.js b/packages/react-aria-components/test/ColorSwatchPicker.test.js index 0d7aab5abdb..d111f40c803 100644 --- a/packages/react-aria-components/test/ColorSwatchPicker.test.js +++ b/packages/react-aria-components/test/ColorSwatchPicker.test.js @@ -143,6 +143,83 @@ describe('ColorSwatchPicker', function () { expect(options[2]).toHaveAttribute('aria-selected', 'true'); }); + it('has no selection by default', async function () { + let {getByRole} = render( + + + + + + + + + ); + + let listbox = getByRole('listbox'); + let options = within(listbox).getAllByRole('option'); + expect(options[0]).toHaveAttribute('aria-selected', 'false'); + expect(options[1]).toHaveAttribute('aria-selected', 'false'); + }); + + it('supports selecting black, and does not allow deselecting via click', async function () { + let onChange = jest.fn(); + let {getByRole} = render( + + + + + + + + + ); + + let listbox = getByRole('listbox'); + let options = within(listbox).getAllByRole('option'); + + await user.click(options[0]); + expect(onChange).toHaveBeenLastCalledWith(parseColor('#000000')); + expect(options[0]).toHaveAttribute('aria-selected', 'true'); + + // Clicking the already-selected swatch again should not deselect it, + // matching how selection normally works elsewhere. + await user.click(options[0]); + expect(onChange).toHaveBeenCalledTimes(1); + expect(options[0]).toHaveAttribute('aria-selected', 'true'); + }); + + it('supports clearing the selection via a controlled null value', async function () { + let {getByRole, rerender} = render( + + + + + + + + + ); + + let listbox = getByRole('listbox'); + let options = within(listbox).getAllByRole('option'); + expect(options[0]).toHaveAttribute('aria-selected', 'true'); + + rerender( + + + + + + + + + ); + + options = within(listbox).getAllByRole('option'); + expect(options[0]).toHaveAttribute('aria-selected', 'false'); + expect(options[1]).toHaveAttribute('aria-selected', 'false'); + }); + it('handles keyboard input', async function () { let onChange = jest.fn(); let {getByRole} = render( diff --git a/packages/react-stately/exports/index.ts b/packages/react-stately/exports/index.ts index e6a7f818cd6..6b56a9e615e 100644 --- a/packages/react-stately/exports/index.ts +++ b/packages/react-stately/exports/index.ts @@ -34,6 +34,10 @@ export type { } from '../src/color/useColorChannelFieldState'; export type {ColorFieldProps, ColorFieldState} from '../src/color/useColorFieldState'; export type {ColorPickerProps, ColorPickerState} from '../src/color/useColorPickerState'; +export type { + ColorSwatchPickerStateProps, + ColorSwatchPickerState +} from '../src/color/useColorSwatchPickerState'; export type { ColorSliderState, ColorSliderStateOptions, @@ -179,6 +183,7 @@ export {useColorAreaState} from '../src/color/useColorAreaState'; export {useColorChannelFieldState} from '../src/color/useColorChannelFieldState'; export {useColorFieldState} from '../src/color/useColorFieldState'; export {useColorPickerState} from '../src/color/useColorPickerState'; +export {useColorSwatchPickerState} from '../src/color/useColorSwatchPickerState'; export {useColorSliderState} from '../src/color/useColorSliderState'; export {useColorWheelState} from '../src/color/useColorWheelState'; export {useComboBoxState} from '../src/combobox/useComboBoxState'; diff --git a/packages/react-stately/exports/useColorSwatchPickerState.ts b/packages/react-stately/exports/useColorSwatchPickerState.ts new file mode 100644 index 00000000000..1f28cd8ac88 --- /dev/null +++ b/packages/react-stately/exports/useColorSwatchPickerState.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +export type { + ColorSwatchPickerStateProps, + ColorSwatchPickerState +} from '../src/color/useColorSwatchPickerState'; + +export {useColorSwatchPickerState} from '../src/color/useColorSwatchPickerState'; diff --git a/packages/react-stately/src/color/useColorSwatchPickerState.ts b/packages/react-stately/src/color/useColorSwatchPickerState.ts new file mode 100644 index 00000000000..75ce5a0afd8 --- /dev/null +++ b/packages/react-stately/src/color/useColorSwatchPickerState.ts @@ -0,0 +1,31 @@ +import {Color} from './types'; +import {useColor} from './useColor'; +import {useControlledState} from '../utils/useControlledState'; +import {ValueBase} from '@react-types/shared'; + +export interface ColorSwatchPickerStateProps extends ValueBase< + string | Color | null, + Color | null +> {} + +export interface ColorSwatchPickerState { + /** The current color value of the color swatch picker. */ + color: Color | null; + /** Sets the current color value of the color swatch picker. */ + setColor(color: Color | null): void; +} + +export function useColorSwatchPickerState( + props: ColorSwatchPickerStateProps +): ColorSwatchPickerState { + let value = useColor(props.value); + let defaultValue = useColor(props.defaultValue) ?? null; + let [color, setColor] = useControlledState(value, defaultValue, props.onChange); + + return { + color, + setColor(color) { + setColor(color); + } + }; +}