diff --git a/packages/@react-spectrum/s2/src/ComboBox.tsx b/packages/@react-spectrum/s2/src/ComboBox.tsx index 9f5ea1cfb9d..34644b644c6 100644 --- a/packages/@react-spectrum/s2/src/ComboBox.tsx +++ b/packages/@react-spectrum/s2/src/ComboBox.tsx @@ -78,7 +78,7 @@ export interface ComboboxStyleProps { size?: 'S' | 'M' | 'L' | 'XL' } export interface ComboBoxProps extends - Omit, 'children' | 'style' | 'className' | 'defaultFilter' | 'allowsEmptyCollection' | keyof GlobalDOMAttributes>, + Omit, 'children' | 'style' | 'className' | 'defaultFilter' | 'allowsEmptyCollection' | 'isTriggerPressedWhenOpen' | keyof GlobalDOMAttributes>, ComboboxStyleProps, StyleProps, SpectrumLabelableProps, @@ -353,6 +353,7 @@ export const ComboBox = /*#__PURE__*/ (forwardRef as forwardRefType)(function Co return ( pressScale(buttonRef)(renderProps)} className={renderProps => inputButton({ ...renderProps, diff --git a/packages/@react-spectrum/s2/src/DatePicker.tsx b/packages/@react-spectrum/s2/src/DatePicker.tsx index 7ffc20b7b9f..b742ab21a7d 100644 --- a/packages/@react-spectrum/s2/src/DatePicker.tsx +++ b/packages/@react-spectrum/s2/src/DatePicker.tsx @@ -40,7 +40,7 @@ import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface DatePickerProps extends - Omit, 'children' | 'className' | 'style' | keyof GlobalDOMAttributes>, + Omit, 'children' | 'className' | 'style' | 'isTriggerPressedWhenOpen' | keyof GlobalDOMAttributes>, Pick, 'createCalendar' | 'pageBehavior' | 'firstDayOfWeek' | 'isDateUnavailable'>, StyleProps, SpectrumLabelableProps, @@ -153,6 +153,7 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function ref={ref} isRequired={isRequired} {...dateFieldProps} + isTriggerPressedWhenOpen={false} style={UNSAFE_style} className={(UNSAFE_className || '') + style(field(), getAllowedOverrides())({ isInForm: !!formContext, @@ -274,9 +275,6 @@ export function CalendarButton(props: {isOpen: boolean, size: 'S' | 'M' | 'L' | return ( + }], + [InsideSelectValueContext, true] + ]}> + {defaultChildren} + + ); + }} + + + + extends Omit, HTMLDivElement>>(null); @@ -123,7 +128,8 @@ function ComboBoxInner({props, collection, comboBoxRef: ref}: let { name, formValue = 'key', - allowsCustomValue + allowsCustomValue, + isTriggerPressedWhenOpen = true } = props; if (allowsCustomValue) { formValue = 'text'; @@ -207,7 +213,7 @@ function ComboBoxInner({props, collection, comboBoxRef: ref}: values={[ [ComboBoxStateContext, state], [LabelContext, {...labelProps, ref: labelRef}], - [ButtonContext, {...buttonProps, ref: buttonRef, isPressed: state.isOpen}], + [ButtonContext, {...buttonProps, ref: buttonRef, isPressed: isTriggerPressedWhenOpen && state.isOpen}], [InputContext, {...inputProps, ref: inputRef}], [OverlayTriggerStateContext, state], [PopoverContext, { diff --git a/packages/react-aria-components/src/DatePicker.tsx b/packages/react-aria-components/src/DatePicker.tsx index fe02d5e2b17..f5208583ef5 100644 --- a/packages/react-aria-components/src/DatePicker.tsx +++ b/packages/react-aria-components/src/DatePicker.tsx @@ -87,14 +87,24 @@ export interface DatePickerProps extends Omit + className?: ClassNameOrFunction, + /** + * Whether the trigger remains pressed when the overlay is open. + * @default true + */ + isTriggerPressedWhenOpen?: boolean } export interface DateRangePickerProps extends Omit, 'label' | 'description' | 'errorMessage' | 'validationState' | 'validationBehavior'>, Pick, 'shouldCloseOnSelect'>, RACValidation, RenderProps, SlotProps, GlobalDOMAttributes { /** * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. A function may be provided to compute the class based on component state. * @default 'react-aria-DateRangePicker' */ - className?: ClassNameOrFunction + className?: ClassNameOrFunction, + /** + * Whether the trigger remains pressed when the overlay is open. + * @default true + */ + isTriggerPressedWhenOpen?: boolean } export const DatePickerContext = createContext, HTMLDivElement>>(null); @@ -112,6 +122,7 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function [props, ref] = useContextProps(props, ref, DatePickerContext); let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {}; let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native'; + let {isTriggerPressedWhenOpen = true} = props; let state = useDatePickerState({ ...props, validationBehavior @@ -174,7 +185,7 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function [DatePickerStateContext, state], [GroupContext, {...groupProps, ref: groupRef, isInvalid: state.isInvalid}], [DateFieldContext, fieldProps], - [ButtonContext, {...buttonProps, isPressed: state.isOpen}], + [ButtonContext, {...buttonProps, isPressed: isTriggerPressedWhenOpen && state.isOpen}], [LabelContext, {...labelProps, ref: labelRef, elementType: 'span'}], [CalendarContext, calendarProps], [OverlayTriggerStateContext, state], @@ -221,6 +232,7 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func [props, ref] = useContextProps(props, ref, DateRangePickerContext); let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {}; let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native'; + let {isTriggerPressedWhenOpen = true} = props; let state = useDateRangePickerState({ ...props, validationBehavior @@ -283,7 +295,7 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func values={[ [DateRangePickerStateContext, state], [GroupContext, {...groupProps, ref: groupRef, isInvalid: state.isInvalid}], - [ButtonContext, {...buttonProps, isPressed: state.isOpen}], + [ButtonContext, {...buttonProps, isPressed: isTriggerPressedWhenOpen && state.isOpen}], [LabelContext, {...labelProps, ref: labelRef, elementType: 'span'}], [RangeCalendarContext, calendarProps], [OverlayTriggerStateContext, state], diff --git a/packages/react-aria-components/src/Dialog.tsx b/packages/react-aria-components/src/Dialog.tsx index 5fc9a33cbe6..d2dac4218c3 100644 --- a/packages/react-aria-components/src/Dialog.tsx +++ b/packages/react-aria-components/src/Dialog.tsx @@ -22,6 +22,11 @@ import React, {createContext, ForwardedRef, forwardRef, JSX, ReactNode, useCallb import {RootMenuTriggerStateContext} from './Menu'; export interface DialogTriggerProps extends OverlayTriggerProps { + /** + * Whether the trigger remains pressed when the overlay is open. + * @default true + */ + isPressedWhenOpen?: boolean, children: ReactNode } @@ -52,6 +57,7 @@ export function DialogTrigger(props: DialogTriggerProps): JSX.Element { let buttonRef = useRef(null); let {triggerProps, overlayProps} = useOverlayTrigger({type: 'dialog'}, state, buttonRef); + let {isPressedWhenOpen = true} = props; // Allows popover width to match trigger element let [buttonWidth, setButtonWidth] = useState(null); @@ -86,7 +92,7 @@ export function DialogTrigger(props: DialogTriggerProps): JSX.Element { style: {'--trigger-width': buttonWidth} as React.CSSProperties }] ]}> - + {props.children} diff --git a/packages/react-aria-components/src/Menu.tsx b/packages/react-aria-components/src/Menu.tsx index 292b1815132..92d888aa878 100644 --- a/packages/react-aria-components/src/Menu.tsx +++ b/packages/react-aria-components/src/Menu.tsx @@ -61,11 +61,17 @@ export const RootMenuTriggerStateContext = createContext(null); export interface MenuTriggerProps extends BaseMenuTriggerProps { + /** + * Whether the trigger remains pressed when the overlay is open. + * @default true + */ + isPressedWhenOpen?: boolean, children: ReactNode } export function MenuTrigger(props: MenuTriggerProps): JSX.Element { let state = useMenuTriggerState(props); + let {isPressedWhenOpen = true} = props; let ref = useRef(null); let {menuTriggerProps, menuProps} = useMenuTrigger({ ...props, @@ -100,7 +106,7 @@ export function MenuTrigger(props: MenuTriggerProps): JSX.Element { 'aria-labelledby': menuProps['aria-labelledby'] }] ]}> - + {props.children} diff --git a/packages/react-aria-components/src/Select.tsx b/packages/react-aria-components/src/Select.tsx index b2efd1e15c9..f9124bb5883 100644 --- a/packages/react-aria-components/src/Select.tsx +++ b/packages/react-aria-components/src/Select.tsx @@ -86,7 +86,12 @@ export interface SelectProps, HTMLDivElement>>(null); @@ -131,6 +136,7 @@ interface SelectInnerProps { function SelectInner({props, selectRef: ref, collection}: SelectInnerProps) { let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {}; let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native'; + let {isTriggerPressedWhenOpen = true} = props; let state = useSelectState({ ...props, collection, @@ -201,7 +207,7 @@ function SelectInner({props, selectRef: ref, collection}: Sele [SelectStateContext, state], [SelectValueContext, valueProps], [LabelContext, {...labelProps, ref: labelRef, elementType: 'span'}], - [ButtonContext, {...triggerProps, ref: buttonRef, isPressed: state.isOpen, autoFocus: props.autoFocus}], + [ButtonContext, {...triggerProps, ref: buttonRef, isPressed: isTriggerPressedWhenOpen && state.isOpen, autoFocus: props.autoFocus}], [OverlayTriggerStateContext, state], [PopoverContext, { trigger: 'Select', diff --git a/packages/react-aria-components/stories/DatePicker.stories.tsx b/packages/react-aria-components/stories/DatePicker.stories.tsx index 4838bcffe59..4ed4b05b7d2 100644 --- a/packages/react-aria-components/stories/DatePicker.stories.tsx +++ b/packages/react-aria-components/stories/DatePicker.stories.tsx @@ -46,6 +46,9 @@ export default { validationBehavior: { control: 'select', options: ['native', 'aria'] + }, + isTriggerPressedWhenOpen: { + control: 'boolean' } } } as Meta; diff --git a/packages/react-aria-components/stories/Select.stories.tsx b/packages/react-aria-components/stories/Select.stories.tsx index c71374de62e..e3d49f2daa4 100644 --- a/packages/react-aria-components/stories/Select.stories.tsx +++ b/packages/react-aria-components/stories/Select.stories.tsx @@ -31,6 +31,9 @@ export default { selectionMode: { control: 'radio', options: ['single', 'multiple'] + }, + isTriggerPressedWhenOpen: { + control: 'boolean' } } } as Meta; diff --git a/packages/react-aria-components/test/ComboBox.test.js b/packages/react-aria-components/test/ComboBox.test.js index b786271a0da..a7a9e03a24b 100644 --- a/packages/react-aria-components/test/ComboBox.test.js +++ b/packages/react-aria-components/test/ComboBox.test.js @@ -109,6 +109,15 @@ describe('ComboBox', () => { expect(button).toHaveAttribute('data-pressed'); }); + it('should not apply isPressed state to button when expanded and isTriggerPressedWhenOpen is false', async () => { + let {getByRole} = render(); + let button = getByRole('button'); + + expect(button).not.toHaveAttribute('data-pressed'); + await user.click(button); + expect(button).not.toHaveAttribute('data-pressed'); + }); + it('should support filtering sections', async () => { let tree = render( diff --git a/packages/react-aria-components/test/DatePicker.test.js b/packages/react-aria-components/test/DatePicker.test.js index 5298849b1a5..14e06f89e11 100644 --- a/packages/react-aria-components/test/DatePicker.test.js +++ b/packages/react-aria-components/test/DatePicker.test.js @@ -107,6 +107,15 @@ describe('DatePicker', () => { expect(button).toHaveAttribute('data-pressed'); }); + it('should not apply isPressed state to button when expanded and isTriggerPressedWhenOpen is false', async () => { + let {getByRole} = render(); + let button = getByRole('button'); + + expect(button).not.toHaveAttribute('data-pressed'); + await user.click(button); + expect(button).not.toHaveAttribute('data-pressed'); + }); + it('should support data-open state', async () => { let {getByRole} = render(); let datePicker = document.querySelector('.react-aria-DatePicker'); diff --git a/packages/react-aria-components/test/DateRangePicker.test.js b/packages/react-aria-components/test/DateRangePicker.test.js index e6562776338..790dbbb3c28 100644 --- a/packages/react-aria-components/test/DateRangePicker.test.js +++ b/packages/react-aria-components/test/DateRangePicker.test.js @@ -128,6 +128,15 @@ describe('DateRangePicker', () => { await user.click(button); expect(button).toHaveAttribute('data-pressed'); }); + + it('should not apply isPressed state to button when expanded and isTriggerPressedWhenOpen is false', async () => { + let {getByRole} = render(); + let button = getByRole('button'); + + expect(button).not.toHaveAttribute('data-pressed'); + await user.click(button); + expect(button).not.toHaveAttribute('data-pressed'); + }); it('should support data-open state', async () => { let {getByRole} = render(); diff --git a/packages/react-aria-components/test/Dialog.test.js b/packages/react-aria-components/test/Dialog.test.js index c047f886513..14ebeee243e 100644 --- a/packages/react-aria-components/test/Dialog.test.js +++ b/packages/react-aria-components/test/Dialog.test.js @@ -43,6 +43,23 @@ describe('Dialog', () => { expect(dialog).toHaveAttribute('data-rac'); }); + it('should not apply isPressed state on trigger when expanded and isPressedWhenOpen is false', async () => { + let {getByRole} = render( + + + + Title + + + ); + + let button = getByRole('button'); + expect(button).not.toHaveAttribute('data-pressed'); + + await user.click(button); + expect(button).not.toHaveAttribute('data-pressed'); + }); + it('works with modal', async () => { let {getByRole} = render( diff --git a/packages/react-aria-components/test/Menu.test.tsx b/packages/react-aria-components/test/Menu.test.tsx index 4ff56edafbf..939ffd72c8d 100644 --- a/packages/react-aria-components/test/Menu.test.tsx +++ b/packages/react-aria-components/test/Menu.test.tsx @@ -489,6 +489,25 @@ describe('Menu', () => { expect(onAction).toHaveBeenLastCalledWith('rename'); }); + it('should not apply isPressed state on trigger when expanded and isPressedWhenOpen is false', async () => { + let {getByRole} = render( + + + + + Open + + + + ); + + let button = getByRole('button'); + expect(button).not.toHaveAttribute('data-pressed'); + + await user.click(button); + expect(button).not.toHaveAttribute('data-pressed'); + }); + it('should support onScroll', () => { let onScroll = jest.fn(); let {getByRole} = renderMenu({onScroll}); diff --git a/packages/react-aria-components/test/Select.test.js b/packages/react-aria-components/test/Select.test.js index 4328ecfc904..62e50fb5ac1 100644 --- a/packages/react-aria-components/test/Select.test.js +++ b/packages/react-aria-components/test/Select.test.js @@ -369,6 +369,15 @@ describe('Select', () => { expect(trigger).toHaveTextContent('Kangaroo'); }); + it('should not apply isPressed state to button when expanded and isTriggerPressedWhenOpen is false', async () => { + let {getByRole} = render(); + let button = getByRole('button'); + + expect(button).not.toHaveAttribute('data-pressed'); + await user.click(button); + expect(button).not.toHaveAttribute('data-pressed'); + }); + describe('typeahead', () => { beforeEach(() => { jest.useFakeTimers();