diff --git a/packages/@react-spectrum/s2/src/ComboBox.tsx b/packages/@react-spectrum/s2/src/ComboBox.tsx index 9f5ea1cfb9d..f4789b3df0a 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' | 'isTriggerUpWhenOpen' | 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..7aca683cb00 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' | 'isTriggerUpWhenOpen' | 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} + isTriggerUpWhenOpen 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); @@ -207,7 +209,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: !props.isTriggerUpWhenOpen && 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..6fd1af8fabd 100644 --- a/packages/react-aria-components/src/DatePicker.tsx +++ b/packages/react-aria-components/src/DatePicker.tsx @@ -87,14 +87,18 @@ export interface DatePickerProps extends Omit + className?: ClassNameOrFunction, + /** Whether the trigger is up when the overlay is open. */ + isTriggerUpWhenOpen?: 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 is up when the overlay is open. */ + isTriggerUpWhenOpen?: boolean } export const DatePickerContext = createContext, HTMLDivElement>>(null); @@ -174,7 +178,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: !props.isTriggerUpWhenOpen && state.isOpen}], [LabelContext, {...labelProps, ref: labelRef, elementType: 'span'}], [CalendarContext, calendarProps], [OverlayTriggerStateContext, state], @@ -283,7 +287,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: !props.isTriggerUpWhenOpen && 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..6cd79de255f 100644 --- a/packages/react-aria-components/src/Dialog.tsx +++ b/packages/react-aria-components/src/Dialog.tsx @@ -22,6 +22,8 @@ import React, {createContext, ForwardedRef, forwardRef, JSX, ReactNode, useCallb import {RootMenuTriggerStateContext} from './Menu'; export interface DialogTriggerProps extends OverlayTriggerProps { + /** Whether the trigger is up when the overlay is open. */ + isTriggerUpWhenOpen?: boolean, children: ReactNode } @@ -86,7 +88,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..f3bb9112ba3 100644 --- a/packages/react-aria-components/src/Menu.tsx +++ b/packages/react-aria-components/src/Menu.tsx @@ -61,6 +61,8 @@ export const RootMenuTriggerStateContext = createContext(null); export interface MenuTriggerProps extends BaseMenuTriggerProps { + /** Whether the trigger is up when the overlay is open. */ + isTriggerUpWhenOpen?: boolean, children: ReactNode } @@ -100,7 +102,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..e28181646fd 100644 --- a/packages/react-aria-components/src/Select.tsx +++ b/packages/react-aria-components/src/Select.tsx @@ -86,7 +86,9 @@ export interface SelectProps, HTMLDivElement>>(null); @@ -201,7 +203,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: !props.isTriggerUpWhenOpen && 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..d6068afa41b 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'] + }, + isTriggerUpWhenOpen: { + 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..b7bf2204137 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'] + }, + isTriggerUpWhenOpen: { + 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..c308461f3ea 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 isTriggerUpWhenOpen is true', 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..00f1cc3e402 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 isTriggerUpWhenOpen is true', 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..83d49783ff4 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 isTriggerUpWhenOpen is true', 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 f21edee1e65..8cad80bef59 100644 --- a/packages/react-aria-components/test/Dialog.test.js +++ b/packages/react-aria-components/test/Dialog.test.js @@ -52,6 +52,23 @@ describe('Dialog', () => { expect(dialog).toHaveAttribute('data-rac'); }); + it('should not apply isPressed state on trigger when expanded and isTriggerUpWhenOpen is true', 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..7aa633339b3 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 isTriggerUpWhenOpen is true', 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..9a3d4232b2d 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 isTriggerUpWhenOpen is true', 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();