From 4ea0fa40b5e4964b9591070f03d4f9a2f9cf3dfd Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 30 Jan 2026 15:07:31 -0800 Subject: [PATCH 01/11] add isExpanded --- packages/@react-spectrum/s2/src/ComboBox.tsx | 3 +- .../@react-spectrum/s2/src/DatePicker.tsx | 3 +- .../s2/src/DateRangePicker.tsx | 3 +- .../@react-spectrum/s2/src/DialogTrigger.tsx | 4 +- packages/@react-spectrum/s2/src/Menu.tsx | 2 +- packages/@react-spectrum/s2/src/Picker.tsx | 3 +- .../@react-spectrum/s2/src/TabsPicker.tsx | 127 ++++++++---------- packages/react-aria-components/src/Button.tsx | 16 ++- .../react-aria-components/src/ComboBox.tsx | 6 +- .../react-aria-components/src/DatePicker.tsx | 12 +- packages/react-aria-components/src/Dialog.tsx | 7 +- packages/react-aria-components/src/Menu.tsx | 8 +- packages/react-aria-components/src/Select.tsx | 6 +- .../stories/DatePicker.stories.tsx | 3 - .../stories/Select.stories.tsx | 3 - .../test/ComboBox.test.js | 8 +- .../test/DatePicker.test.js | 8 +- .../test/DateRangePicker.test.js | 8 +- .../react-aria-components/test/Dialog.test.js | 8 +- .../react-aria-components/test/Menu.test.tsx | 8 +- .../react-aria-components/test/Select.test.js | 8 +- 21 files changed, 113 insertions(+), 141 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ComboBox.tsx b/packages/@react-spectrum/s2/src/ComboBox.tsx index d3cd25f882f..e1292ded0a5 100644 --- a/packages/@react-spectrum/s2/src/ComboBox.tsx +++ b/packages/@react-spectrum/s2/src/ComboBox.tsx @@ -79,7 +79,7 @@ export interface ComboboxStyleProps { size?: 'S' | 'M' | 'L' | 'XL' } export interface ComboBoxProps extends - Omit, 'children' | 'style' | 'className' | 'render' | 'defaultFilter' | 'allowsEmptyCollection' | 'isTriggerUpWhenOpen' | keyof GlobalDOMAttributes>, + Omit, 'children' | 'style' | 'className' | 'render' | 'defaultFilter' | 'allowsEmptyCollection' | keyof GlobalDOMAttributes>, ComboboxStyleProps, StyleProps, SpectrumLabelableProps, @@ -354,7 +354,6 @@ export const ComboBox = /*#__PURE__*/ (forwardRef as forwardRefType)(function Co return ( extends - Omit, 'children' | 'className' | 'style' | 'render' | 'isTriggerUpWhenOpen' | keyof GlobalDOMAttributes>, + Omit, 'children' | 'className' | 'style' | 'render' | keyof GlobalDOMAttributes>, Pick, 'createCalendar' | 'pageBehavior' | 'firstDayOfWeek' | 'isDateUnavailable'>, Pick, StyleProps, @@ -155,7 +155,6 @@ 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, diff --git a/packages/@react-spectrum/s2/src/DateRangePicker.tsx b/packages/@react-spectrum/s2/src/DateRangePicker.tsx index 6ed09210ce6..fbf54a71bf4 100644 --- a/packages/@react-spectrum/s2/src/DateRangePicker.tsx +++ b/packages/@react-spectrum/s2/src/DateRangePicker.tsx @@ -33,7 +33,7 @@ import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface DateRangePickerProps extends - Omit, 'children' | 'className' | 'style' | 'render' | 'isTriggerUpWhenOpen' | keyof GlobalDOMAttributes>, + Omit, 'children' | 'className' | 'style' | 'render' | keyof GlobalDOMAttributes>, Pick, 'createCalendar' | 'pageBehavior' | 'firstDayOfWeek' | 'isDateUnavailable'>, Pick, StyleProps, @@ -89,7 +89,6 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func ref={ref} isRequired={isRequired} {...dateFieldProps} - isTriggerUpWhenOpen style={UNSAFE_style} className={(UNSAFE_className || '') + style(field(), getAllowedOverrides())({ isInForm: !!formContext, diff --git a/packages/@react-spectrum/s2/src/DialogTrigger.tsx b/packages/@react-spectrum/s2/src/DialogTrigger.tsx index 7cfb487877b..257d5e61d20 100644 --- a/packages/@react-spectrum/s2/src/DialogTrigger.tsx +++ b/packages/@react-spectrum/s2/src/DialogTrigger.tsx @@ -13,7 +13,7 @@ import {DialogTrigger as AriaDialogTrigger, DialogTriggerProps as AriaDialogTriggerProps} from 'react-aria-components'; import {ReactNode} from 'react'; -export type DialogTriggerProps = Omit; +export interface DialogTriggerProps extends AriaDialogTriggerProps {} /** * DialogTrigger serves as a wrapper around a Dialog and its associated trigger, linking the Dialog's @@ -22,6 +22,6 @@ export type DialogTriggerProps = Omit + ); } diff --git a/packages/@react-spectrum/s2/src/Menu.tsx b/packages/@react-spectrum/s2/src/Menu.tsx index aaef84b9203..9aa196dc2e5 100644 --- a/packages/@react-spectrum/s2/src/Menu.tsx +++ b/packages/@react-spectrum/s2/src/Menu.tsx @@ -53,7 +53,7 @@ import {useSpectrumContextProps} from './useSpectrumContextProps'; // viewbox on LinkOut is super weird just because i copied the icon from designs... // need to strip id's from icons -export interface MenuTriggerProps extends Omit { +export interface MenuTriggerProps extends AriaMenuTriggerProps { /** * Alignment of the menu relative to the trigger. * diff --git a/packages/@react-spectrum/s2/src/Picker.tsx b/packages/@react-spectrum/s2/src/Picker.tsx index 766d4be7882..8c36df86761 100644 --- a/packages/@react-spectrum/s2/src/Picker.tsx +++ b/packages/@react-spectrum/s2/src/Picker.tsx @@ -98,7 +98,7 @@ export interface PickerStyleProps { type SelectionMode = 'single' | 'multiple'; export interface PickerProps extends - Omit, 'children' | 'style' | 'className' | 'render' | 'allowsEmptyCollection' | 'isTriggerUpWhenOpen' | keyof GlobalDOMAttributes>, + Omit, 'children' | 'style' | 'className' | 'render' | 'allowsEmptyCollection' | keyof GlobalDOMAttributes>, PickerStyleProps, StyleProps, SpectrumLabelableProps, @@ -351,7 +351,6 @@ export const Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(function Pick return ( (props: PickerProps, ref: FocusableRef { - if (e.pointerType !== 'mouse') { - return; - } - setPressed(true); - addGlobalListener(document, 'pointerup', () => { - setPressed(false); - }, {once: true, capture: true}); - }; - return (
{({isOpen}) => ( <> - - - + } + }], + [InsideSelectValueContext, true] + ]}> + {defaultChildren} + + ); + }} + + + , HoverEvents, SlotProps, RenderProps, Omit, 'onClick'> { @@ -82,7 +87,8 @@ export interface ButtonProps extends Omit>({}); @@ -107,7 +113,8 @@ export const Button = /*#__PURE__*/ createHideableComponent(function Button(prop isFocused, isFocusVisible, isDisabled: props.isDisabled || false, - isPending: isPending ?? false + isPending: isPending ?? false, + isExpanded: ctx.isExpanded || false }; let renderProps = useRenderProps({ @@ -160,7 +167,8 @@ export const Button = /*#__PURE__*/ createHideableComponent(function Button(prop data-hovered={isHovered || undefined} data-focused={isFocused || undefined} data-pending={isPending || undefined} - data-focus-visible={isFocusVisible || undefined}> + data-focus-visible={isFocusVisible || undefined} + data-expanded={renderValues.isExpanded || undefined}> {renderProps.children} diff --git a/packages/react-aria-components/src/ComboBox.tsx b/packages/react-aria-components/src/ComboBox.tsx index 42343816a6d..48a28044cee 100644 --- a/packages/react-aria-components/src/ComboBox.tsx +++ b/packages/react-aria-components/src/ComboBox.tsx @@ -78,9 +78,7 @@ export interface ComboBoxProps extends Omit, HTMLDivElement>>(null); @@ -210,7 +208,7 @@ function ComboBoxInner({props, collection, comboBoxRef: ref}: values={[ [ComboBoxStateContext, state], [LabelContext, {...labelProps, ref: labelRef}], - [ButtonContext, {...buttonProps, ref: buttonRef, isPressed: !props.isTriggerUpWhenOpen && state.isOpen}], + [ButtonContext, {...buttonProps, ref: buttonRef, isExpanded: 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 c1b8821612d..a92e56c12df 100644 --- a/packages/react-aria-components/src/DatePicker.tsx +++ b/packages/react-aria-components/src/DatePicker.tsx @@ -88,18 +88,14 @@ export interface DatePickerProps extends Omit, - /** Whether the trigger is up when the overlay is open. */ - isTriggerUpWhenOpen?: boolean + className?: ClassNameOrFunction } 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, - /** Whether the trigger is up when the overlay is open. */ - isTriggerUpWhenOpen?: boolean + className?: ClassNameOrFunction } export const DatePickerContext = createContext, HTMLDivElement>>(null); @@ -179,7 +175,7 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function [DatePickerStateContext, state], [GroupContext, {...groupProps, ref: groupRef, isInvalid: state.isInvalid}], [DateFieldContext, fieldProps], - [ButtonContext, {...buttonProps, isPressed: !props.isTriggerUpWhenOpen && state.isOpen}], + [ButtonContext, {...buttonProps, isExpanded: state.isOpen}], [LabelContext, {...labelProps, ref: labelRef, elementType: 'span'}], [CalendarContext, calendarProps], [OverlayTriggerStateContext, state], @@ -288,7 +284,7 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func values={[ [DateRangePickerStateContext, state], [GroupContext, {...groupProps, ref: groupRef, isInvalid: state.isInvalid}], - [ButtonContext, {...buttonProps, isPressed: !props.isTriggerUpWhenOpen && state.isOpen}], + [ButtonContext, {...buttonProps, isExpanded: 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 c95dce96e32..480cbd5cdf6 100644 --- a/packages/react-aria-components/src/Dialog.tsx +++ b/packages/react-aria-components/src/Dialog.tsx @@ -22,8 +22,6 @@ 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,9 +84,10 @@ export function DialogTrigger(props: DialogTriggerProps): JSX.Element { triggerRef: buttonRef, 'aria-labelledby': overlayProps['aria-labelledby'], style: {'--trigger-width': buttonWidth} as React.CSSProperties - }] + }], + [ButtonContext, {isExpanded: state.isOpen}] ]}> - + {props.children} diff --git a/packages/react-aria-components/src/Menu.tsx b/packages/react-aria-components/src/Menu.tsx index 994df7d3932..7e3c316e3c2 100644 --- a/packages/react-aria-components/src/Menu.tsx +++ b/packages/react-aria-components/src/Menu.tsx @@ -13,6 +13,7 @@ import {AriaMenuProps, FocusScope, mergeProps, useHover, useMenu, useMenuItem, useMenuSection, useMenuTrigger, useSubmenuTrigger} from 'react-aria'; import {BaseCollection, Collection, CollectionBuilder, CollectionNode, createBranchComponent, createLeafComponent, ItemNode, SectionNode} from '@react-aria/collections'; import {MenuTriggerProps as BaseMenuTriggerProps, Collection as ICollection, Node, RootMenuTriggerState, TreeState, useMenuTriggerState, useSubmenuTriggerState, useTreeState} from 'react-stately'; +import {ButtonContext} from './Button'; import { ClassNameOrFunction, ContextValue, @@ -64,8 +65,6 @@ export const RootMenuTriggerStateContext = createContext(null); export interface MenuTriggerProps extends BaseMenuTriggerProps { - /** Whether the trigger is up when the overlay is open. */ - isTriggerUpWhenOpen?: boolean, children: ReactNode } @@ -103,9 +102,10 @@ export function MenuTrigger(props: MenuTriggerProps): JSX.Element { placement: 'bottom start', style: {'--trigger-width': buttonWidth} as React.CSSProperties, 'aria-labelledby': menuProps['aria-labelledby'] - }] + }], + [ButtonContext, {isExpanded: state.isOpen}] ]}> - + {props.children} diff --git a/packages/react-aria-components/src/Select.tsx b/packages/react-aria-components/src/Select.tsx index ded3a340eee..82689dd2c26 100644 --- a/packages/react-aria-components/src/Select.tsx +++ b/packages/react-aria-components/src/Select.tsx @@ -87,9 +87,7 @@ export interface SelectProps, HTMLDivElement>>(null); @@ -204,7 +202,7 @@ function SelectInner({props, selectRef: ref, collection}: Sele [SelectStateContext, state], [SelectValueContext, valueProps], [LabelContext, {...labelProps, ref: labelRef, elementType: 'span'}], - [ButtonContext, {...triggerProps, ref: buttonRef, isPressed: !props.isTriggerUpWhenOpen && state.isOpen, autoFocus: props.autoFocus}], + [ButtonContext, {...triggerProps, ref: buttonRef, isExpanded: 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 bc9cde5a648..ae079959edf 100644 --- a/packages/react-aria-components/stories/DatePicker.stories.tsx +++ b/packages/react-aria-components/stories/DatePicker.stories.tsx @@ -47,9 +47,6 @@ 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 b7bf2204137..c71374de62e 100644 --- a/packages/react-aria-components/stories/Select.stories.tsx +++ b/packages/react-aria-components/stories/Select.stories.tsx @@ -31,9 +31,6 @@ 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 a9cf870563c..831b038d9d9 100644 --- a/packages/react-aria-components/test/ComboBox.test.js +++ b/packages/react-aria-components/test/ComboBox.test.js @@ -115,13 +115,13 @@ 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(); + it('should set data-expanded on button when popover is open', async () => { + let {getByRole} = render(); let button = getByRole('button'); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).not.toHaveAttribute('data-expanded'); await user.click(button); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).toHaveAttribute('data-expanded', 'true'); }); it('should support filtering sections', async () => { diff --git a/packages/react-aria-components/test/DatePicker.test.js b/packages/react-aria-components/test/DatePicker.test.js index e68afdb3be3..c2c2ec8cbec 100644 --- a/packages/react-aria-components/test/DatePicker.test.js +++ b/packages/react-aria-components/test/DatePicker.test.js @@ -113,13 +113,13 @@ 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(); + it('should set data-expanded on button when popover is open', async () => { + let {getByRole} = render(); let button = getByRole('button'); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).not.toHaveAttribute('data-expanded'); await user.click(button); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).toHaveAttribute('data-expanded', 'true'); }); it('should support data-open state', async () => { diff --git a/packages/react-aria-components/test/DateRangePicker.test.js b/packages/react-aria-components/test/DateRangePicker.test.js index f89cc8fee01..d0e467f3f48 100644 --- a/packages/react-aria-components/test/DateRangePicker.test.js +++ b/packages/react-aria-components/test/DateRangePicker.test.js @@ -135,13 +135,13 @@ describe('DateRangePicker', () => { expect(button).toHaveAttribute('data-pressed'); }); - it('should not apply isPressed state to button when expanded and isTriggerUpWhenOpen is true', async () => { - let {getByRole} = render(); + it('should set data-expanded on button when popover is open', async () => { + let {getByRole} = render(); let button = getByRole('button'); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).not.toHaveAttribute('data-expanded'); await user.click(button); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).toHaveAttribute('data-expanded', 'true'); }); it('should support data-open state', async () => { diff --git a/packages/react-aria-components/test/Dialog.test.js b/packages/react-aria-components/test/Dialog.test.js index fa5a043b8d8..e0f3336d092 100644 --- a/packages/react-aria-components/test/Dialog.test.js +++ b/packages/react-aria-components/test/Dialog.test.js @@ -56,9 +56,9 @@ describe('Dialog', () => { expect(dialog).toHaveAttribute('data-rac'); }); - it('should not apply isPressed state on trigger when expanded and isTriggerUpWhenOpen is true', async () => { + it('should set data-expanded on button when dialog is open', async () => { let {getByRole} = render( - + Title @@ -67,10 +67,10 @@ describe('Dialog', () => { ); let button = getByRole('button'); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).not.toHaveAttribute('data-expanded'); await user.click(button); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).toHaveAttribute('data-expanded', 'true'); }); it('works with modal', async () => { diff --git a/packages/react-aria-components/test/Menu.test.tsx b/packages/react-aria-components/test/Menu.test.tsx index aa3f48f1dbe..72251057704 100644 --- a/packages/react-aria-components/test/Menu.test.tsx +++ b/packages/react-aria-components/test/Menu.test.tsx @@ -536,9 +536,9 @@ describe('Menu', () => { expect(onAction).toHaveBeenLastCalledWith('rename'); }); - it('should not apply isPressed state on trigger when expanded and isTriggerUpWhenOpen is true', async () => { + it('should set data-expanded on button when menu is open', async () => { let {getByRole} = render( - + @@ -549,10 +549,10 @@ describe('Menu', () => { ); let button = getByRole('button'); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).not.toHaveAttribute('data-expanded'); await user.click(button); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).toHaveAttribute('data-expanded', 'true'); }); it('should support onScroll', () => { diff --git a/packages/react-aria-components/test/Select.test.js b/packages/react-aria-components/test/Select.test.js index 97d7cba1038..aa2954941b2 100644 --- a/packages/react-aria-components/test/Select.test.js +++ b/packages/react-aria-components/test/Select.test.js @@ -399,13 +399,13 @@ describe('Select', () => { expect(trigger).toHaveTextContent('Kangaroo'); }); - it('should not apply isPressed state to button when expanded and isTriggerUpWhenOpen is true', async () => { - let {getByRole} = render(); + it('should set data-expanded on button when popover is open', async () => { + let {getByRole} = render(); let button = getByRole('button'); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).not.toHaveAttribute('data-expanded'); await user.click(button); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).toHaveAttribute('data-expanded', 'true'); }); describe('typeahead', () => { From 3da0757eda167c987487ab1873c2c7214e2d94e5 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 30 Jan 2026 15:23:13 -0800 Subject: [PATCH 02/11] make RAC select handle ensuring press scaling still works even when the overlay is opened on mousedown --- packages/@react-spectrum/s2/src/Picker.tsx | 25 +++---------------- .../@react-spectrum/s2/src/TabsPicker.tsx | 5 +--- packages/react-aria-components/src/Select.tsx | 25 +++++++++++++++---- 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Picker.tsx b/packages/@react-spectrum/s2/src/Picker.tsx index 8c36df86761..36a7727d67b 100644 --- a/packages/@react-spectrum/s2/src/Picker.tsx +++ b/packages/@react-spectrum/s2/src/Picker.tsx @@ -32,7 +32,7 @@ import { SelectValue, Virtualizer } from 'react-aria-components'; -import {AsyncLoadable, FocusableRef, FocusableRefValue, GlobalDOMAttributes, HelpTextProps, LoadingState, PressEvent, RefObject, SpectrumLabelableProps} from '@react-types/shared'; +import {AsyncLoadable, FocusableRef, FocusableRefValue, GlobalDOMAttributes, HelpTextProps, LoadingState, RefObject, SpectrumLabelableProps} from '@react-types/shared'; import {AvatarContext} from './Avatar'; import {baseColor, focusRing, style} from '../style' with {type: 'macro'}; import {box, iconStyles as checkboxIconStyles} from './Checkbox'; @@ -72,13 +72,12 @@ import intlMessages from '../intl/*.json'; import {mergeStyles} from '../style/runtime'; import {Placement} from 'react-aria'; import {Popover} from './Popover'; -import {PressResponder} from '@react-aria/interactions'; import {pressScale} from './pressScale'; import {ProgressCircle} from './ProgressCircle'; import {raw} from '../style/style-macro' with {type: 'macro'}; import React, {createContext, forwardRef, ReactNode, useContext, useMemo, useRef, useState} from 'react'; import {useFocusableRef} from '@react-spectrum/utils'; -import {useGlobalListeners, useSlotId} from '@react-aria/utils'; +import {useSlotId} from '@react-aria/utils'; import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n'; import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -487,7 +486,6 @@ interface PickerButtonInnerProps extends PickerStyleProps, Omi buttonRef: RefObject } -// Needs to be hidable component or otherwise the PressResponder throws a warning when rendered in the fake DOM and tries to register const PickerButton = createHideableComponent(function PickerButton(props: PickerButtonInnerProps) { let { isOpen, @@ -502,24 +500,8 @@ const PickerButton = createHideableComponent(function PickerButton { - if (e.pointerType !== 'mouse') { - return; - } - setPressed(true); - addGlobalListener(document, 'pointerup', () => { - setPressed(false); - }, {once: true, capture: true}); - }; - return ( - - - ); }); diff --git a/packages/@react-spectrum/s2/src/TabsPicker.tsx b/packages/@react-spectrum/s2/src/TabsPicker.tsx index 082825b4a48..f9547a6af3b 100644 --- a/packages/@react-spectrum/s2/src/TabsPicker.tsx +++ b/packages/@react-spectrum/s2/src/TabsPicker.tsx @@ -56,7 +56,7 @@ import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface PickerStyleProps { } export interface PickerProps extends - Omit, 'children' | 'style' | 'className' | 'render' | 'placeholder' | 'isTriggerUpWhenOpen'>, + Omit, 'children' | 'style' | 'className' | 'render' | 'placeholder'>, PickerStyleProps, StyleProps, SpectrumLabelableProps, @@ -194,9 +194,6 @@ function Picker(props: PickerProps, ref: FocusableRef pressScale(domRef)(renderProps)} - // Prevent press scale from sticking while Picker is open. - // @ts-ignore - isPressed={false} className={renderProps => inputButton({ ...renderProps, size: 'M', diff --git a/packages/react-aria-components/src/Select.tsx b/packages/react-aria-components/src/Select.tsx index 82689dd2c26..06a3e59d99f 100644 --- a/packages/react-aria-components/src/Select.tsx +++ b/packages/react-aria-components/src/Select.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {AriaSelectProps, HiddenSelect, useFocusRing, useListFormatter, useLocalizedStringFormatter, useSelect} from 'react-aria'; +import {AriaSelectProps, HiddenSelect, PressEvent, useFocusRing, useListFormatter, useLocalizedStringFormatter, useSelect} from 'react-aria'; import {ButtonContext} from './Button'; import { ClassNameOrFunction, @@ -29,7 +29,7 @@ import { import {Collection, Node, SelectState, useSelectState} from 'react-stately'; import {CollectionBuilder, createHideableComponent} from '@react-aria/collections'; import {FieldErrorContext} from './FieldError'; -import {filterDOMProps, mergeProps, useResizeObserver} from '@react-aria/utils'; +import {filterDOMProps, mergeProps, useGlobalListeners, useResizeObserver} from '@react-aria/utils'; import {FormContext} from './Form'; import {forwardRefType, GlobalDOMAttributes} from '@react-types/shared'; // @ts-ignore @@ -174,6 +174,21 @@ function SelectInner({props, selectRef: ref, collection}: Sele onResize: onResize }); + // For mouse interactions, pickers open on press start. When the popover underlay appears + // it covers the trigger button, causing onPressEnd to fire immediately and no press scaling + // to occur. We override this by listening for pointerup on the document ourselves. + let [isPressed, setPressed] = useState(false); + let {addGlobalListener} = useGlobalListeners(); + let onPressStart = (e: PressEvent) => { + if (e.pointerType !== 'mouse') { + return; + } + setPressed(true); + addGlobalListener(document, 'pointerup', () => { + setPressed(false); + }, {once: true, capture: true}); + }; + // Only expose a subset of state to renderProps function to avoid infinite render loop let renderPropsState = useMemo(() => ({ isOpen: state.isOpen, @@ -202,7 +217,7 @@ function SelectInner({props, selectRef: ref, collection}: Sele [SelectStateContext, state], [SelectValueContext, valueProps], [LabelContext, {...labelProps, ref: labelRef, elementType: 'span'}], - [ButtonContext, {...triggerProps, ref: buttonRef, isExpanded: state.isOpen, autoFocus: props.autoFocus}], + [ButtonContext, {...mergeProps(triggerProps, {onPressStart}), ref: buttonRef, isExpanded: state.isOpen, autoFocus: props.autoFocus, isPressed}], [OverlayTriggerStateContext, state], [PopoverContext, { trigger: 'Select', @@ -303,8 +318,8 @@ export const SelectValue = /*#__PURE__*/ createHideableComponent(function Select let textValue = useMemo(() => state.selectedItems.map(item => item?.textValue), [state.selectedItems]); let selectionMode = state.selectionManager.selectionMode; let selectedText = useMemo(() => ( - selectionMode === 'single' - ? textValue[0] ?? '' + selectionMode === 'single' + ? textValue[0] ?? '' : formatter.format(textValue) ), [selectionMode, formatter, textValue]); From 71d9ef531aff9fc6916667bc090c105fa9120726 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 30 Jan 2026 15:59:06 -0800 Subject: [PATCH 03/11] fix lint and tests --- packages/@react-spectrum/s2/src/Picker.tsx | 152 +++++++++--------- packages/react-aria-components/src/Button.tsx | 2 +- .../test/ComboBox.test.js | 22 +-- .../test/DatePicker.test.js | 6 +- .../test/DateRangePicker.test.js | 8 +- .../react-aria-components/test/Dialog.test.js | 4 +- .../react-aria-components/test/Menu.test.tsx | 36 ++--- .../react-aria-components/test/Select.test.js | 6 +- 8 files changed, 118 insertions(+), 118 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Picker.tsx b/packages/@react-spectrum/s2/src/Picker.tsx index 36a7727d67b..c3043a8161a 100644 --- a/packages/@react-spectrum/s2/src/Picker.tsx +++ b/packages/@react-spectrum/s2/src/Picker.tsx @@ -75,11 +75,11 @@ import {Popover} from './Popover'; import {pressScale} from './pressScale'; import {ProgressCircle} from './ProgressCircle'; import {raw} from '../style/style-macro' with {type: 'macro'}; -import React, {createContext, forwardRef, ReactNode, useContext, useMemo, useRef, useState} from 'react'; +import React, {createContext, forwardRef, ReactNode, useContext, useMemo, useRef} from 'react'; import {useFocusableRef} from '@react-spectrum/utils'; -import {useSlotId} from '@react-aria/utils'; import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n'; import {useScale} from './utils'; +import {useSlotId} from '@react-aria/utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface PickerStyleProps { @@ -502,83 +502,83 @@ const PickerButton = createHideableComponent(function PickerButton pressScale(buttonRef)(renderProps)} - className={renderProps => inputButton({ - ...renderProps, - size: size, - isOpen, - isQuiet - })}> - {(renderProps) => ( - <> - :not([slot=icon], [slot=avatar], [slot=label], [data-slot=label]) {display: none;}')}> - {({selectedItems, defaultChildren}) => { - return ( - pressScale(buttonRef)(renderProps)} + className={renderProps => inputButton({ + ...renderProps, + size: size, + isOpen, + isQuiet + })}> + {(renderProps) => ( + <> + :not([slot=icon], [slot=avatar], [slot=label], [data-slot=label]) {display: none;}')}> + {({selectedItems, defaultChildren}) => { + return ( + - {selectedItems.length <= 1 - ? defaultChildren - : {stringFormatter.format('picker.selectedCount', {count: selectedItems.length})} - } - - ); - }} - - {isInvalid && } - {loadingState === 'loading' && !isOpen && loadingCircle} - - {isFocusVisible && isQuiet && } - {isInvalid && !isDisabled && !isQuiet && - // @ts-ignore known limitation detecting functions from the theme -
- } - - )} - + } + }], + [InsideSelectValueContext, true] + ]}> + {selectedItems.length <= 1 + ? defaultChildren + : {stringFormatter.format('picker.selectedCount', {count: selectedItems.length})} + } + + ); + }} + + {isInvalid && } + {loadingState === 'loading' && !isOpen && loadingCircle} + + {isFocusVisible && isQuiet && } + {isInvalid && !isDisabled && !isQuiet && + // @ts-ignore known limitation detecting functions from the theme +
+ } + + )} + ); }); diff --git a/packages/react-aria-components/src/Button.tsx b/packages/react-aria-components/src/Button.tsx index 5bc97aff034..66abac167f7 100644 --- a/packages/react-aria-components/src/Button.tsx +++ b/packages/react-aria-components/src/Button.tsx @@ -70,7 +70,7 @@ export interface ButtonRenderProps { * Whether an overlay triggered by the button is currently open. * @selector [data-expanded] */ - isExpanded: boolean + isExpanded?: boolean } export interface ButtonProps extends Omit, HoverEvents, SlotProps, RenderProps, Omit, 'onClick'> { diff --git a/packages/react-aria-components/test/ComboBox.test.js b/packages/react-aria-components/test/ComboBox.test.js index 831b038d9d9..3564cfa3c1e 100644 --- a/packages/react-aria-components/test/ComboBox.test.js +++ b/packages/react-aria-components/test/ComboBox.test.js @@ -106,13 +106,13 @@ describe('ComboBox', () => { expect(field).toHaveAttribute('data-custom', 'true'); }); - it('should apply isPressed state to button when expanded', async () => { + it('should apply isExpanded state to button when expanded', async () => { let {getByRole} = render(); let button = getByRole('button'); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).not.toHaveAttribute('data-expanded'); await user.click(button); - expect(button).toHaveAttribute('data-pressed'); + expect(button).toHaveAttribute('data-expanded'); }); it('should set data-expanded on button when popover is open', async () => { @@ -290,17 +290,17 @@ describe('ComboBox', () => { ); - + const comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container}); const combobox = comboboxTester.combobox; - + expect(combobox).toHaveValue('Dog'); await comboboxTester.open(); - + const options = comboboxTester.options(); await user.click(options[0]); expect(combobox).toHaveValue('Cat'); - + await user.click(document.querySelector('input[type="reset"]')); expect(combobox).toHaveValue('Dog'); expect(document.querySelector('input[name=combobox]')).toHaveValue('2'); @@ -596,7 +596,7 @@ describe('ComboBox', () => { let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container}); let button = tree.getByRole('button'); - + // Open the combobox await user.click(button); act(() => { @@ -611,7 +611,7 @@ describe('ComboBox', () => { // Find and click on a section header let fruitHeader = tree.getByText('Fruit'); expect(fruitHeader).toBeInTheDocument(); - + await user.click(fruitHeader); act(() => { jest.runAllTimers(); @@ -625,13 +625,13 @@ describe('ComboBox', () => { // Verify we can still interact with options let options = comboboxTester.options(); expect(options.length).toBeGreaterThan(0); - + // Click an option await user.click(options[0]); act(() => { jest.runAllTimers(); }); - + // Verify the combobox is closed and the value is updated expect(tree.queryByRole('listbox')).toBeNull(); expect(comboboxTester.combobox).toHaveValue('Apple'); diff --git a/packages/react-aria-components/test/DatePicker.test.js b/packages/react-aria-components/test/DatePicker.test.js index c2c2ec8cbec..4ce11560358 100644 --- a/packages/react-aria-components/test/DatePicker.test.js +++ b/packages/react-aria-components/test/DatePicker.test.js @@ -104,13 +104,13 @@ describe('DatePicker', () => { expect(group).toHaveAttribute('data-custom', 'true'); }); - it('should apply isPressed state to button when expanded', async () => { + it('should apply isExpanded state to button when expanded', async () => { let {getByRole} = render(); let button = getByRole('button'); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).not.toHaveAttribute('data-expanded'); await user.click(button); - expect(button).toHaveAttribute('data-pressed'); + expect(button).toHaveAttribute('data-expanded'); }); it('should set data-expanded on button when popover is open', async () => { diff --git a/packages/react-aria-components/test/DateRangePicker.test.js b/packages/react-aria-components/test/DateRangePicker.test.js index d0e467f3f48..91c52911a1d 100644 --- a/packages/react-aria-components/test/DateRangePicker.test.js +++ b/packages/react-aria-components/test/DateRangePicker.test.js @@ -126,15 +126,15 @@ describe('DateRangePicker', () => { expect(group).toHaveAttribute('data-custom', 'true'); }); - it('should apply isPressed state to button when expanded', async () => { + it('should apply isExpanded state to button when expanded', async () => { let {getByRole} = render(); let button = getByRole('button'); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).not.toHaveAttribute('data-expanded'); await user.click(button); - expect(button).toHaveAttribute('data-pressed'); + expect(button).toHaveAttribute('data-expanded'); }); - + it('should set data-expanded on button when popover is open', async () => { let {getByRole} = render(); let button = getByRole('button'); diff --git a/packages/react-aria-components/test/Dialog.test.js b/packages/react-aria-components/test/Dialog.test.js index e0f3336d092..a5dfcce4437 100644 --- a/packages/react-aria-components/test/Dialog.test.js +++ b/packages/react-aria-components/test/Dialog.test.js @@ -189,11 +189,11 @@ describe('Dialog', () => { ); let button = getByRole('button'); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).not.toHaveAttribute('data-expanded'); let dialogTester = testUtilUser.createTester('Dialog', {root: button, overlayType: 'popover'}); await dialogTester.open(); - expect(button).toHaveAttribute('data-pressed'); + expect(button).toHaveAttribute('data-expanded'); let dialog = dialogTester.dialog; let heading = getByRole('heading'); diff --git a/packages/react-aria-components/test/Menu.test.tsx b/packages/react-aria-components/test/Menu.test.tsx index 72251057704..cfa52efa2d2 100644 --- a/packages/react-aria-components/test/Menu.test.tsx +++ b/packages/react-aria-components/test/Menu.test.tsx @@ -520,10 +520,10 @@ describe('Menu', () => { ); let button = getByRole('button'); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).not.toHaveAttribute('data-expanded'); await user.click(button); - expect(button).toHaveAttribute('data-pressed'); + expect(button).toHaveAttribute('data-expanded'); let menu = getByRole('menu'); expect(getAllByRole('menuitem')).toHaveLength(5); @@ -757,10 +757,10 @@ describe('Menu', () => { ); let button = getByRole('button'); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).not.toHaveAttribute('data-expanded'); await user.click(button); - expect(button).toHaveAttribute('data-pressed'); + expect(button).toHaveAttribute('data-expanded'); let menu = getAllByRole('menu')[0]; expect(getAllByRole('menuitem')).toHaveLength(5); @@ -830,10 +830,10 @@ describe('Menu', () => { ); let button = getByRole('button'); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).not.toHaveAttribute('data-expanded'); await user.click(button); - expect(button).toHaveAttribute('data-pressed'); + expect(button).toHaveAttribute('data-expanded'); let menu = getAllByRole('menu')[0]; expect(getAllByRole('menuitem')).toHaveLength(5); @@ -923,10 +923,10 @@ describe('Menu', () => { ); let button = getByRole('button'); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).not.toHaveAttribute('data-expanded'); await user.click(button); - expect(button).toHaveAttribute('data-pressed'); + expect(button).toHaveAttribute('data-expanded'); let menu = getAllByRole('menu')[0]; expect(getAllByRole('menuitem')).toHaveLength(5); @@ -1010,10 +1010,10 @@ describe('Menu', () => { ); let button = getByRole('button'); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).not.toHaveAttribute('data-expanded'); await user.click(button); - expect(button).toHaveAttribute('data-pressed'); + expect(button).toHaveAttribute('data-expanded'); let menu = getAllByRole('menu')[0]; expect(getAllByRole('menuitem')).toHaveLength(5); @@ -1088,9 +1088,9 @@ describe('Menu', () => { let menuTester = testUtilUser.createTester('Menu', {root: getByRole('button'), interactionType: 'keyboard'}); - expect(menuTester.trigger).not.toHaveAttribute('data-pressed'); + expect(menuTester.trigger).not.toHaveAttribute('data-expanded'); await menuTester.open(); - expect(menuTester.trigger).toHaveAttribute('data-pressed'); + expect(menuTester.trigger).toHaveAttribute('data-expanded'); expect(menuTester.options()).toHaveLength(5); expect(menuTester.menu).toBeInTheDocument(); @@ -1165,10 +1165,10 @@ describe('Menu', () => { ); let button = getByRole('button'); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).not.toHaveAttribute('data-expanded'); await user.click(button); - expect(button).toHaveAttribute('data-pressed'); + expect(button).toHaveAttribute('data-expanded'); let menu = getAllByRole('menu')[0]; expect(getAllByRole('menuitem')).toHaveLength(5); @@ -1244,10 +1244,10 @@ describe('Menu', () => { ); let button = getByRole('button'); - expect(button).not.toHaveAttribute('data-pressed'); + expect(button).not.toHaveAttribute('data-expanded'); let menuTester = testUtilUser.createTester('Menu', {user, root: button}); await menuTester.open(); - expect(button).toHaveAttribute('data-pressed'); + expect(button).toHaveAttribute('data-expanded'); let groups = menuTester.sections; expect(groups).toHaveLength(2); @@ -1335,10 +1335,10 @@ describe('Menu', () => { ); let menuTester = testUtilUser.createTester('Menu', {root: getByRole('button')}); - expect(menuTester.trigger).not.toHaveAttribute('data-pressed'); + expect(menuTester.trigger).not.toHaveAttribute('data-expanded'); await menuTester.open(); - expect(menuTester.trigger).toHaveAttribute('data-pressed'); + expect(menuTester.trigger).toHaveAttribute('data-expanded'); expect(menuTester.options()).toHaveLength(5); let popover = menuTester.menu?.closest('.react-aria-Popover'); diff --git a/packages/react-aria-components/test/Select.test.js b/packages/react-aria-components/test/Select.test.js index aa2954941b2..b21714e91cd 100644 --- a/packages/react-aria-components/test/Select.test.js +++ b/packages/react-aria-components/test/Select.test.js @@ -49,7 +49,7 @@ describe('Select', () => { let trigger = selectTester.trigger; expect(trigger).toHaveTextContent('Select an item'); - expect(trigger).not.toHaveAttribute('data-pressed'); + expect(trigger).not.toHaveAttribute('data-expanded'); expect(wrapper).toHaveAttribute('data-foo', 'bar'); @@ -67,7 +67,7 @@ describe('Select', () => { await selectTester.open(); - expect(trigger).toHaveAttribute('data-pressed', 'true'); + expect(trigger).toHaveAttribute('data-expanded', 'true'); let listbox = selectTester.listbox; expect(listbox).toHaveAttribute('class', 'react-aria-ListBox'); expect(listbox.closest('.react-aria-Popover')).toBeInTheDocument(); @@ -393,7 +393,7 @@ describe('Select', () => { let selectTester = testUtilUser.createTester('Select', {root: wrapper, interactionType: 'keyboard'}); let trigger = selectTester.trigger; expect(trigger).toHaveTextContent('Select an item'); - expect(trigger).not.toHaveAttribute('data-pressed'); + expect(trigger).not.toHaveAttribute('data-expanded'); await selectTester.selectOption({option: 'Kangaroo'}); expect(trigger).toHaveTextContent('Kangaroo'); From f0d97721607f0f048cf78cb11a50de6a9aef75de Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 30 Jan 2026 16:03:27 -0800 Subject: [PATCH 04/11] rename col index data attribute --- packages/react-aria-components/src/Table.tsx | 2 +- .../react-aria-components/test/Table.test.js | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index e0f5fcf67a8..1b491def4ae 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -1405,7 +1405,7 @@ export const Cell = /*#__PURE__*/ createLeafComponent(TableCellNode, (props: Cel data-focus-visible={isFocusVisible || undefined} data-pressed={isPressed || undefined} data-selected={isSelected || undefined} - data-col-index={colIndex}> + data-column-index={colIndex}> {renderProps.children} diff --git a/packages/react-aria-components/test/Table.test.js b/packages/react-aria-components/test/Table.test.js index c5e5d900662..f5def712a01 100644 --- a/packages/react-aria-components/test/Table.test.js +++ b/packages/react-aria-components/test/Table.test.js @@ -925,10 +925,10 @@ describe('Table', () => { expect(cells[1]).toHaveTextContent('cell index: 1'); expect(cells[2]).toHaveTextContent('cell index: 2'); expect(cells[3]).toHaveTextContent('cell index: 3'); - expect(cells[0]).toHaveAttribute('data-col-index', '0'); - expect(cells[1]).toHaveAttribute('data-col-index', '1'); - expect(cells[2]).toHaveAttribute('data-col-index', '2'); - expect(cells[3]).toHaveAttribute('data-col-index', '3'); + expect(cells[0]).toHaveAttribute('data-column-index', '0'); + expect(cells[1]).toHaveAttribute('data-column-index', '1'); + expect(cells[2]).toHaveAttribute('data-column-index', '2'); + expect(cells[3]).toHaveAttribute('data-column-index', '3'); }); it('should support colspan with cell index', () => { @@ -975,19 +975,19 @@ describe('Table', () => { expect(cells[0]).toHaveTextContent('cell index: 0'); expect(cells[1]).toHaveTextContent('cell index: 2'); expect(cells[2]).toHaveTextContent('cell index: 3'); - expect(cells[0]).toHaveAttribute('data-col-index', '0'); - expect(cells[1]).toHaveAttribute('data-col-index', '2'); - expect(cells[2]).toHaveAttribute('data-col-index', '3'); + expect(cells[0]).toHaveAttribute('data-column-index', '0'); + expect(cells[1]).toHaveAttribute('data-column-index', '2'); + expect(cells[2]).toHaveAttribute('data-column-index', '3'); // second row expect(cells[3]).toHaveTextContent('cell index: 0'); expect(cells[4]).toHaveTextContent('cell index: 1'); expect(cells[5]).toHaveTextContent('cell index: 2'); expect(cells[6]).toHaveTextContent('cell index: 3'); - expect(cells[3]).toHaveAttribute('data-col-index', '0'); - expect(cells[4]).toHaveAttribute('data-col-index', '1'); - expect(cells[5]).toHaveAttribute('data-col-index', '2'); - expect(cells[6]).toHaveAttribute('data-col-index', '3'); + expect(cells[3]).toHaveAttribute('data-column-index', '0'); + expect(cells[4]).toHaveAttribute('data-column-index', '1'); + expect(cells[5]).toHaveAttribute('data-column-index', '2'); + expect(cells[6]).toHaveAttribute('data-column-index', '3'); }); it('should support columnHeader typeahead', async () => { From 9052e47220256efee96d9df98c0f510629b267f8 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 2 Feb 2026 09:43:06 -0800 Subject: [PATCH 05/11] add checks for data-pressed --- packages/react-aria-components/test/ComboBox.test.js | 2 ++ packages/react-aria-components/test/DatePicker.test.js | 2 ++ packages/react-aria-components/test/DateRangePicker.test.js | 2 ++ packages/react-aria-components/test/Dialog.test.js | 2 ++ packages/react-aria-components/test/Menu.test.tsx | 2 ++ packages/react-aria-components/test/Select.test.js | 2 ++ 6 files changed, 12 insertions(+) diff --git a/packages/react-aria-components/test/ComboBox.test.js b/packages/react-aria-components/test/ComboBox.test.js index 3564cfa3c1e..b1f301144ff 100644 --- a/packages/react-aria-components/test/ComboBox.test.js +++ b/packages/react-aria-components/test/ComboBox.test.js @@ -111,8 +111,10 @@ describe('ComboBox', () => { let button = getByRole('button'); expect(button).not.toHaveAttribute('data-expanded'); + expect(button).not.toHaveAttribute('data-pressed'); await user.click(button); expect(button).toHaveAttribute('data-expanded'); + expect(button).not.toHaveAttribute('data-pressed'); }); it('should set data-expanded on button when popover is open', async () => { diff --git a/packages/react-aria-components/test/DatePicker.test.js b/packages/react-aria-components/test/DatePicker.test.js index 4ce11560358..b600dad1f4a 100644 --- a/packages/react-aria-components/test/DatePicker.test.js +++ b/packages/react-aria-components/test/DatePicker.test.js @@ -109,8 +109,10 @@ describe('DatePicker', () => { let button = getByRole('button'); expect(button).not.toHaveAttribute('data-expanded'); + expect(button).not.toHaveAttribute('data-pressed'); await user.click(button); expect(button).toHaveAttribute('data-expanded'); + expect(button).not.toHaveAttribute('data-pressed'); }); it('should set data-expanded on button when popover is open', async () => { diff --git a/packages/react-aria-components/test/DateRangePicker.test.js b/packages/react-aria-components/test/DateRangePicker.test.js index 91c52911a1d..1401b7ca9a8 100644 --- a/packages/react-aria-components/test/DateRangePicker.test.js +++ b/packages/react-aria-components/test/DateRangePicker.test.js @@ -131,8 +131,10 @@ describe('DateRangePicker', () => { let button = getByRole('button'); expect(button).not.toHaveAttribute('data-expanded'); + expect(button).not.toHaveAttribute('data-pressed'); await user.click(button); expect(button).toHaveAttribute('data-expanded'); + expect(button).not.toHaveAttribute('data-pressed'); }); it('should set data-expanded on button when popover is open', async () => { diff --git a/packages/react-aria-components/test/Dialog.test.js b/packages/react-aria-components/test/Dialog.test.js index a5dfcce4437..99cbb98e569 100644 --- a/packages/react-aria-components/test/Dialog.test.js +++ b/packages/react-aria-components/test/Dialog.test.js @@ -68,9 +68,11 @@ describe('Dialog', () => { let button = getByRole('button'); expect(button).not.toHaveAttribute('data-expanded'); + expect(button).not.toHaveAttribute('data-pressed'); await user.click(button); expect(button).toHaveAttribute('data-expanded', 'true'); + expect(button).not.toHaveAttribute('data-pressed'); }); it('works with modal', async () => { diff --git a/packages/react-aria-components/test/Menu.test.tsx b/packages/react-aria-components/test/Menu.test.tsx index cfa52efa2d2..042352a2fc6 100644 --- a/packages/react-aria-components/test/Menu.test.tsx +++ b/packages/react-aria-components/test/Menu.test.tsx @@ -521,9 +521,11 @@ describe('Menu', () => { let button = getByRole('button'); expect(button).not.toHaveAttribute('data-expanded'); + expect(button).not.toHaveAttribute('data-pressed'); await user.click(button); expect(button).toHaveAttribute('data-expanded'); + expect(button).not.toHaveAttribute('data-pressed'); let menu = getByRole('menu'); expect(getAllByRole('menuitem')).toHaveLength(5); diff --git a/packages/react-aria-components/test/Select.test.js b/packages/react-aria-components/test/Select.test.js index b21714e91cd..8d9af7d4f53 100644 --- a/packages/react-aria-components/test/Select.test.js +++ b/packages/react-aria-components/test/Select.test.js @@ -50,6 +50,7 @@ describe('Select', () => { let trigger = selectTester.trigger; expect(trigger).toHaveTextContent('Select an item'); expect(trigger).not.toHaveAttribute('data-expanded'); + expect(trigger).not.toHaveAttribute('data-pressed'); expect(wrapper).toHaveAttribute('data-foo', 'bar'); @@ -68,6 +69,7 @@ describe('Select', () => { await selectTester.open(); expect(trigger).toHaveAttribute('data-expanded', 'true'); + expect(trigger).not.toHaveAttribute('data-pressed'); let listbox = selectTester.listbox; expect(listbox).toHaveAttribute('class', 'react-aria-ListBox'); expect(listbox.closest('.react-aria-Popover')).toBeInTheDocument(); From 108781c37e2edc05e955c9f0b012a51a60df18af Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 2 Feb 2026 11:22:07 -0800 Subject: [PATCH 06/11] Revert "rename col index data attribute" This reverts commit f0d97721607f0f048cf78cb11a50de6a9aef75de. --- packages/react-aria-components/src/Table.tsx | 2 +- .../react-aria-components/test/Table.test.js | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index 1b491def4ae..e0f5fcf67a8 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -1405,7 +1405,7 @@ export const Cell = /*#__PURE__*/ createLeafComponent(TableCellNode, (props: Cel data-focus-visible={isFocusVisible || undefined} data-pressed={isPressed || undefined} data-selected={isSelected || undefined} - data-column-index={colIndex}> + data-col-index={colIndex}> {renderProps.children} diff --git a/packages/react-aria-components/test/Table.test.js b/packages/react-aria-components/test/Table.test.js index f5def712a01..c5e5d900662 100644 --- a/packages/react-aria-components/test/Table.test.js +++ b/packages/react-aria-components/test/Table.test.js @@ -925,10 +925,10 @@ describe('Table', () => { expect(cells[1]).toHaveTextContent('cell index: 1'); expect(cells[2]).toHaveTextContent('cell index: 2'); expect(cells[3]).toHaveTextContent('cell index: 3'); - expect(cells[0]).toHaveAttribute('data-column-index', '0'); - expect(cells[1]).toHaveAttribute('data-column-index', '1'); - expect(cells[2]).toHaveAttribute('data-column-index', '2'); - expect(cells[3]).toHaveAttribute('data-column-index', '3'); + expect(cells[0]).toHaveAttribute('data-col-index', '0'); + expect(cells[1]).toHaveAttribute('data-col-index', '1'); + expect(cells[2]).toHaveAttribute('data-col-index', '2'); + expect(cells[3]).toHaveAttribute('data-col-index', '3'); }); it('should support colspan with cell index', () => { @@ -975,19 +975,19 @@ describe('Table', () => { expect(cells[0]).toHaveTextContent('cell index: 0'); expect(cells[1]).toHaveTextContent('cell index: 2'); expect(cells[2]).toHaveTextContent('cell index: 3'); - expect(cells[0]).toHaveAttribute('data-column-index', '0'); - expect(cells[1]).toHaveAttribute('data-column-index', '2'); - expect(cells[2]).toHaveAttribute('data-column-index', '3'); + expect(cells[0]).toHaveAttribute('data-col-index', '0'); + expect(cells[1]).toHaveAttribute('data-col-index', '2'); + expect(cells[2]).toHaveAttribute('data-col-index', '3'); // second row expect(cells[3]).toHaveTextContent('cell index: 0'); expect(cells[4]).toHaveTextContent('cell index: 1'); expect(cells[5]).toHaveTextContent('cell index: 2'); expect(cells[6]).toHaveTextContent('cell index: 3'); - expect(cells[3]).toHaveAttribute('data-column-index', '0'); - expect(cells[4]).toHaveAttribute('data-column-index', '1'); - expect(cells[5]).toHaveAttribute('data-column-index', '2'); - expect(cells[6]).toHaveAttribute('data-column-index', '3'); + expect(cells[3]).toHaveAttribute('data-col-index', '0'); + expect(cells[4]).toHaveAttribute('data-col-index', '1'); + expect(cells[5]).toHaveAttribute('data-col-index', '2'); + expect(cells[6]).toHaveAttribute('data-col-index', '3'); }); it('should support columnHeader typeahead', async () => { From 7c79211a009720b3e7609d071301c89a9062117f Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 2 Feb 2026 11:35:27 -0800 Subject: [PATCH 07/11] review comments --- .../@react-spectrum/s2/src/ActionButton.tsx | 3 +-- packages/@react-spectrum/s2/src/Button.tsx | 3 +-- packages/@react-spectrum/s2/src/Menu.tsx | 25 +++---------------- packages/react-aria-components/src/Menu.tsx | 22 +++++++++++++--- starters/docs/src/Menu.css | 2 +- starters/docs/src/Select.css | 2 +- 6 files changed, 26 insertions(+), 31 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ActionButton.tsx b/packages/@react-spectrum/s2/src/ActionButton.tsx index 4753def4f4a..27bd794d2a8 100644 --- a/packages/@react-spectrum/s2/src/ActionButton.tsx +++ b/packages/@react-spectrum/s2/src/ActionButton.tsx @@ -282,7 +282,6 @@ export const ActionButton = forwardRef(function ActionButton(props: ActionButton let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); let {isPending = false} = props; let domRef = useFocusableRef(ref); - let overlayTriggerState = useContext(OverlayTriggerStateContext); let ctx = useSlottedContext(ActionButtonGroupContext); let isInGroup = !!ctx; let { @@ -306,7 +305,7 @@ export const ActionButton = forwardRef(function ActionButton(props: ActionButton className={renderProps => (props.UNSAFE_className || '') + btnStyles({ ...renderProps, // Retain hover styles when an overlay is open. - isHovered: renderProps.isHovered || overlayTriggerState?.isOpen || false, + isHovered: renderProps.isHovered || renderProps.isExpanded || false, isDisabled: renderProps.isDisabled || isProgressVisible, staticColor, isStaticColor: !!staticColor, diff --git a/packages/@react-spectrum/s2/src/Button.tsx b/packages/@react-spectrum/s2/src/Button.tsx index 412f4ee66a0..4b6e33f56f8 100644 --- a/packages/@react-spectrum/s2/src/Button.tsx +++ b/packages/@react-spectrum/s2/src/Button.tsx @@ -328,7 +328,6 @@ export const Button = forwardRef(function Button(props: ButtonProps, ref: Focusa staticColor } = props; let domRef = useFocusableRef(ref); - let overlayTriggerState = useContext(OverlayTriggerStateContext); let {isProgressVisible} = usePendingState(isPending); @@ -340,7 +339,7 @@ export const Button = forwardRef(function Button(props: ButtonProps, ref: Focusa className={renderProps => (props.UNSAFE_className || '') + button({ ...renderProps, // Retain hover styles when an overlay is open. - isHovered: renderProps.isHovered || overlayTriggerState?.isOpen || false, + isHovered: renderProps.isHovered || renderProps.isExpanded || false, isDisabled: renderProps.isDisabled || isProgressVisible, variant, fillStyle, diff --git a/packages/@react-spectrum/s2/src/Menu.tsx b/packages/@react-spectrum/s2/src/Menu.tsx index 9aa196dc2e5..f0429b2318e 100644 --- a/packages/@react-spectrum/s2/src/Menu.tsx +++ b/packages/@react-spectrum/s2/src/Menu.tsx @@ -34,9 +34,9 @@ import {centerBaseline} from './CenterBaseline'; import {centerPadding, control, controlFont, controlSize, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import CheckmarkIcon from '../ui-icons/Checkmark'; import ChevronRightIcon from '../ui-icons/Chevron'; -import {createContext, forwardRef, JSX, ReactNode, useContext, useRef, useState} from 'react'; +import {createContext, forwardRef, JSX, ReactNode, useContext, useRef} from 'react'; import {divider} from './Divider'; -import {DOMRef, DOMRefValue, GlobalDOMAttributes, PressEvent} from '@react-types/shared'; +import {DOMRef, DOMRefValue, GlobalDOMAttributes} from '@react-types/shared'; import {edgeToText} from '../style/spectrum-theme' with {type: 'macro'}; import {forwardRefType} from './types'; import {HeaderContext, HeadingContext, KeyboardContext, Text, TextContext} from './Content'; @@ -46,9 +46,7 @@ import {InPopoverContext, Popover, PopoverContext} from './Popover'; import LinkOutIcon from '../ui-icons/LinkOut'; import {mergeStyles} from '../style/runtime'; import {Placement, useLocale} from 'react-aria'; -import {PressResponder} from '@react-aria/interactions'; import {pressScale} from './pressScale'; -import {useGlobalListeners} from '@react-aria/utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; // viewbox on LinkOut is super weird just because i copied the icon from designs... // need to strip id's from icons @@ -543,21 +541,6 @@ export function MenuItem(props: MenuItemProps): ReactNode { * linking the Menu's open state with the trigger's press state. */ function MenuTrigger(props: MenuTriggerProps): ReactNode { - // For mouse interactions, menus open on press start. When the popover underlay appears - // it covers the trigger button, causing onPressEnd to fire immediately and no press scaling - // to occur. We override this by listening for pointerup on the document ourselves. - let [isPressed, setPressed] = useState(false); - let {addGlobalListener} = useGlobalListeners(); - let onPressStart = (e: PressEvent) => { - if (e.pointerType !== 'mouse') { - return; - } - setPressed(true); - addGlobalListener(document, 'pointerup', () => { - setPressed(false); - }, {once: true, capture: true}); - }; - let {align = 'start', direction = 'bottom', shouldFlip} = props; let placement: Placement; switch (direction) { @@ -583,9 +566,7 @@ function MenuTrigger(props: MenuTriggerProps): ReactNode { - - {props.children} - + {props.children} diff --git a/packages/react-aria-components/src/Menu.tsx b/packages/react-aria-components/src/Menu.tsx index 7e3c316e3c2..f450623a8e7 100644 --- a/packages/react-aria-components/src/Menu.tsx +++ b/packages/react-aria-components/src/Menu.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {AriaMenuProps, FocusScope, mergeProps, useHover, useMenu, useMenuItem, useMenuSection, useMenuTrigger, useSubmenuTrigger} from 'react-aria'; +import {AriaMenuProps, FocusScope, mergeProps, PressEvent, useHover, useMenu, useMenuItem, useMenuSection, useMenuTrigger, useSubmenuTrigger} from 'react-aria'; import {BaseCollection, Collection, CollectionBuilder, CollectionNode, createBranchComponent, createLeafComponent, ItemNode, SectionNode} from '@react-aria/collections'; import {MenuTriggerProps as BaseMenuTriggerProps, Collection as ICollection, Node, RootMenuTriggerState, TreeState, useMenuTriggerState, useSubmenuTriggerState, useTreeState} from 'react-stately'; import {ButtonContext} from './Button'; @@ -32,7 +32,7 @@ import { } from './utils'; import {CollectionProps, CollectionRendererContext, ItemRenderProps, SectionContext, SectionProps, usePersistedKeys} from './Collection'; import {FieldInputContext, SelectableCollectionContext, SelectableCollectionContextValue} from './RSPContexts'; -import {filterDOMProps, useObjectRef, useResizeObserver} from '@react-aria/utils'; +import {filterDOMProps, useGlobalListeners, useObjectRef, useResizeObserver} from '@react-aria/utils'; import {FocusEvents, FocusStrategy, forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, MultipleSelection, PressEvents} from '@react-types/shared'; import {HeaderContext} from './Header'; import {KeyboardContext} from './Keyboard'; @@ -75,6 +75,22 @@ export function MenuTrigger(props: MenuTriggerProps): JSX.Element { ...props, type: 'menu' }, state, ref); + + // For mouse interactions, menus open on press start. When the popover underlay appears + // it covers the trigger button, causing onPressEnd to fire immediately and no press scaling + // to occur. We override this by listening for pointerup on the document ourselves. + let [isPressed, setPressed] = useState(false); + let {addGlobalListener} = useGlobalListeners(); + let onPressStart = (e: PressEvent) => { + if (e.pointerType !== 'mouse') { + return; + } + setPressed(true); + addGlobalListener(document, 'pointerup', () => { + setPressed(false); + }, {once: true, capture: true}); + }; + // Allows menu width to match button let [buttonWidth, setButtonWidth] = useState(null); let onResize = useCallback(() => { @@ -105,7 +121,7 @@ export function MenuTrigger(props: MenuTriggerProps): JSX.Element { }], [ButtonContext, {isExpanded: state.isOpen}] ]}> - + {props.children} diff --git a/starters/docs/src/Menu.css b/starters/docs/src/Menu.css index e2c867663fa..c18861515a3 100644 --- a/starters/docs/src/Menu.css +++ b/starters/docs/src/Menu.css @@ -63,7 +63,7 @@ } &[data-open], - &[data-pressed] { + &[data-expanded] { background: var(--highlight-hover); } diff --git a/starters/docs/src/Select.css b/starters/docs/src/Select.css index 8b4c45f3eb7..1295ae0e6b6 100644 --- a/starters/docs/src/Select.css +++ b/starters/docs/src/Select.css @@ -11,7 +11,7 @@ width: 100%; min-width: 0; - &[data-pressed] { + &[data-expanded] { scale: 1; } } From 67cd1ef892438efe2dd8070d941b23d1cc595979 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 2 Feb 2026 11:46:45 -0800 Subject: [PATCH 08/11] fix lint --- packages/@react-spectrum/s2/src/ActionButton.tsx | 4 ++-- packages/@react-spectrum/s2/src/Button.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ActionButton.tsx b/packages/@react-spectrum/s2/src/ActionButton.tsx index 27bd794d2a8..3c945de849f 100644 --- a/packages/@react-spectrum/s2/src/ActionButton.tsx +++ b/packages/@react-spectrum/s2/src/ActionButton.tsx @@ -13,10 +13,10 @@ import {ActionButtonGroupContext} from './ActionButtonGroup'; import {AvatarContext} from './Avatar'; import {baseColor, focusRing, fontRelative, lightDark, style} from '../style' with { type: 'macro' }; -import {ButtonProps, ButtonRenderProps, ContextValue, OverlayTriggerStateContext, Provider, Button as RACButton, useSlottedContext} from 'react-aria-components'; +import {ButtonProps, ButtonRenderProps, ContextValue, Provider, Button as RACButton, useSlottedContext} from 'react-aria-components'; import {centerBaseline} from './CenterBaseline'; import {control, getAllowedOverrides, staticColor, StyleProps} from './style-utils' with { type: 'macro' }; -import {createContext, forwardRef, ReactNode, useContext} from 'react'; +import {createContext, forwardRef, ReactNode} from 'react'; import {FocusableRef, FocusableRefValue, GlobalDOMAttributes} from '@react-types/shared'; import {IconContext} from './Icon'; import {ImageContext} from './Image'; diff --git a/packages/@react-spectrum/s2/src/Button.tsx b/packages/@react-spectrum/s2/src/Button.tsx index 4b6e33f56f8..2d2b3ba1ac2 100644 --- a/packages/@react-spectrum/s2/src/Button.tsx +++ b/packages/@react-spectrum/s2/src/Button.tsx @@ -354,7 +354,7 @@ export const Button = forwardRef(function Button(props: ButtonProps, ref: Focusa className={gradient({ ...renderProps, // Retain hover styles when an overlay is open. - isHovered: renderProps.isHovered || overlayTriggerState?.isOpen || false, + isHovered: renderProps.isHovered || renderProps?.isExpanded || false, isDisabled: renderProps.isDisabled || isProgressVisible, variant })} /> From 1edfef2e5aef0ad0a059fb1e9f7a497fe31fd897 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 2 Feb 2026 12:53:05 -0800 Subject: [PATCH 09/11] remove isExpanded from ToggleButton --- packages/react-aria-components/src/ToggleButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-aria-components/src/ToggleButton.tsx b/packages/react-aria-components/src/ToggleButton.tsx index 2f3f2fcfbb0..903bc9a45b6 100644 --- a/packages/react-aria-components/src/ToggleButton.tsx +++ b/packages/react-aria-components/src/ToggleButton.tsx @@ -28,7 +28,7 @@ import {SelectionIndicatorContext} from './SelectionIndicator'; import {ToggleGroupStateContext} from './ToggleButtonGroup'; import {ToggleState, useToggleState} from 'react-stately'; -export interface ToggleButtonRenderProps extends Omit { +export interface ToggleButtonRenderProps extends Omit { /** * Whether the button is currently selected. * @selector [data-selected] From 1589ff9720656b1f50c15f319d08b4ff7e0c0623 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 3 Feb 2026 13:52:15 -0800 Subject: [PATCH 10/11] fix styling for ComboBox, DatePicker, Menu, Select for isExpanded --- starters/docs/src/Form.css | 7 ++++--- starters/docs/src/Menu.css | 2 +- starters/docs/src/Select.css | 2 +- starters/docs/src/utilities.css | 2 +- starters/tailwind/src/Button.tsx | 8 ++++---- starters/tailwind/src/FieldButton.tsx | 2 +- starters/tailwind/src/Select.tsx | 2 +- 7 files changed, 13 insertions(+), 12 deletions(-) diff --git a/starters/docs/src/Form.css b/starters/docs/src/Form.css index ede03dabd4d..33272554892 100644 --- a/starters/docs/src/Form.css +++ b/starters/docs/src/Form.css @@ -81,7 +81,7 @@ height: var(--spacing-4); } - &:where([data-hovered], [data-pressed]) { + &:where([data-hovered], [data-pressed], [data-expanded]) { background: var(--tint-300); color: var(--tint-1200); } @@ -91,7 +91,8 @@ color: HighlightText; } - &[data-pressed] { + &[data-pressed], + &[data-expanded] { scale: 0.9; } @@ -104,4 +105,4 @@ background: none; color: var(--text-color-disabled); } -} \ No newline at end of file +} diff --git a/starters/docs/src/Menu.css b/starters/docs/src/Menu.css index c18861515a3..e2c867663fa 100644 --- a/starters/docs/src/Menu.css +++ b/starters/docs/src/Menu.css @@ -63,7 +63,7 @@ } &[data-open], - &[data-expanded] { + &[data-pressed] { background: var(--highlight-hover); } diff --git a/starters/docs/src/Select.css b/starters/docs/src/Select.css index 1295ae0e6b6..8b4c45f3eb7 100644 --- a/starters/docs/src/Select.css +++ b/starters/docs/src/Select.css @@ -11,7 +11,7 @@ width: 100%; min-width: 0; - &[data-expanded] { + &[data-pressed] { scale: 1; } } diff --git a/starters/docs/src/utilities.css b/starters/docs/src/utilities.css index 0eeacb73b03..2dfa037b51b 100644 --- a/starters/docs/src/utilities.css +++ b/starters/docs/src/utilities.css @@ -35,7 +35,7 @@ inset 0 var(--button-gradient-size) var(--button-gradient-size) -2px var(--button-gradient); /* inner gradient */ } - &:where([data-pressed]) { + &:where([data-pressed], [data-expanded]) { --button-background: oklch(from var(--button-color) var(--lightness-200) var(--chroma-200) h); } diff --git a/starters/tailwind/src/Button.tsx b/starters/tailwind/src/Button.tsx index d7cda47d091..24833cb4e89 100644 --- a/starters/tailwind/src/Button.tsx +++ b/starters/tailwind/src/Button.tsx @@ -14,10 +14,10 @@ let button = tv({ base: 'relative inline-flex items-center justify-center gap-2 border border-transparent dark:border-white/10 h-9 box-border px-3.5 py-0 [&:has(>svg:only-child)]:px-0 [&:has(>svg:only-child)]:h-8 [&:has(>svg:only-child)]:w-8 font-sans text-sm text-center transition rounded-lg cursor-default [-webkit-tap-highlight-color:transparent]', variants: { variant: { - primary: 'bg-blue-600 hover:bg-blue-700 pressed:bg-blue-800 text-white', - secondary: 'border-black/10 bg-neutral-50 hover:bg-neutral-100 pressed:bg-neutral-200 text-neutral-800 dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:pressed:bg-neutral-500 dark:text-neutral-100', - destructive: 'bg-red-700 hover:bg-red-800 pressed:bg-red-900 text-white', - quiet: 'border-0 bg-transparent hover:bg-neutral-200 pressed:bg-neutral-300 text-neutral-800 dark:hover:bg-neutral-700 dark:pressed:bg-neutral-600 dark:text-neutral-100' + primary: 'bg-blue-600 hover:bg-blue-700 pressed:bg-blue-800 text-white expanded:bg-blue-800', + secondary: 'border-black/10 bg-neutral-50 hover:bg-neutral-100 pressed:bg-neutral-200 text-neutral-800 dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:pressed:bg-neutral-500 dark:text-neutral-100 expanded:bg-neutral-200 dark:expanded:bg-neutral-500', + destructive: 'bg-red-700 hover:bg-red-800 pressed:bg-red-900 text-white expanded:bg-red-900', + quiet: 'border-0 bg-transparent hover:bg-neutral-200 pressed:bg-neutral-300 text-neutral-800 dark:hover:bg-neutral-700 dark:pressed:bg-neutral-600 dark:text-neutral-100 expanded:bg-neutral-300 dark:expanded:bg-neutral-600' }, isDisabled: { true: 'border-transparent dark:border-transparent bg-neutral-100 dark:bg-neutral-800 text-neutral-300 dark:text-neutral-600 forced-colors:text-[GrayText]' diff --git a/starters/tailwind/src/FieldButton.tsx b/starters/tailwind/src/FieldButton.tsx index 6b3d91611f6..257a3b2f81c 100644 --- a/starters/tailwind/src/FieldButton.tsx +++ b/starters/tailwind/src/FieldButton.tsx @@ -11,7 +11,7 @@ export interface ButtonProps extends RACButtonProps { let button = tv({ extend: focusRing, - base: 'relative inline-flex items-center border-0 font-sans text-sm text-center transition rounded-md cursor-default p-1 flex items-center justify-center text-neutral-600 bg-transparent hover:bg-black/[5%] pressed:bg-black/10 dark:text-neutral-400 dark:hover:bg-white/10 dark:pressed:bg-white/20 disabled:bg-transparent [-webkit-tap-highlight-color:transparent]', + base: 'relative inline-flex items-center border-0 font-sans text-sm text-center transition rounded-md cursor-default p-1 flex items-center justify-center text-neutral-600 bg-transparent hover:bg-black/[5%] pressed:bg-black/10 dark:text-neutral-400 dark:hover:bg-white/10 dark:pressed:bg-white/20 disabled:bg-transparent [-webkit-tap-highlight-color:transparent] expanded:bg-black/10 dark:expanded:bg-white/20', variants: { isDisabled: { true: 'bg-neutral-100 dark:bg-neutral-800 text-neutral-300 dark:text-neutral-600 forced-colors:text-[GrayText] border-black/5 dark:border-white/5' diff --git a/starters/tailwind/src/Select.tsx b/starters/tailwind/src/Select.tsx index 94e6b9efb32..36e0aa4de14 100644 --- a/starters/tailwind/src/Select.tsx +++ b/starters/tailwind/src/Select.tsx @@ -21,7 +21,7 @@ const styles = tv({ base: 'flex items-center text-start gap-4 w-full font-sans border border-black/10 dark:border-white/10 cursor-default rounded-lg pl-3 pr-2 h-9 min-w-[180px] transition bg-neutral-50 dark:bg-neutral-700 [-webkit-tap-highlight-color:transparent]', variants: { isDisabled: { - false: 'text-neutral-800 dark:text-neutral-300 hover:bg-neutral-100 pressed:bg-neutral-200 dark:hover:bg-neutral-600 dark:pressed:bg-neutral-500 group-invalid:outline group-invalid:outline-red-600 forced-colors:group-invalid:outline-[Mark]', + false: 'text-neutral-800 dark:text-neutral-300 hover:bg-neutral-100 pressed:bg-neutral-200 dark:hover:bg-neutral-600 dark:pressed:bg-neutral-500 group-invalid:outline group-invalid:outline-red-600 forced-colors:group-invalid:outline-[Mark] expanded:bg-neutral-200 dark:expanded:bg-neutral-500', true: 'border-transparent dark:border-transparent text-neutral-200 dark:text-neutral-600 forced-colors:text-[GrayText] bg-neutral-100 dark:bg-neutral-800' } } From 3844e2fbd4f53480ef055608ff7f7270990b1306 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 6 Feb 2026 11:25:05 -0800 Subject: [PATCH 11/11] remove persistent scaling from form component buttons when expanded --- starters/docs/src/Form.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/starters/docs/src/Form.css b/starters/docs/src/Form.css index 33272554892..a039bf06a11 100644 --- a/starters/docs/src/Form.css +++ b/starters/docs/src/Form.css @@ -91,8 +91,7 @@ color: HighlightText; } - &[data-pressed], - &[data-expanded] { + &[data-pressed] { scale: 0.9; }