diff --git a/packages/@react-spectrum/s2/src/ActionButton.tsx b/packages/@react-spectrum/s2/src/ActionButton.tsx
index 4753def4f4a..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';
@@ -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..2d2b3ba1ac2 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,
@@ -355,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
})} />
diff --git a/packages/@react-spectrum/s2/src/DialogTrigger.tsx b/packages/@react-spectrum/s2/src/DialogTrigger.tsx
index 339855f79c3..257d5e61d20 100644
--- a/packages/@react-spectrum/s2/src/DialogTrigger.tsx
+++ b/packages/@react-spectrum/s2/src/DialogTrigger.tsx
@@ -11,7 +11,6 @@
*/
import {DialogTrigger as AriaDialogTrigger, DialogTriggerProps as AriaDialogTriggerProps} from 'react-aria-components';
-import {PressResponder} from '@react-aria/interactions';
import {ReactNode} from 'react';
export interface DialogTriggerProps extends AriaDialogTriggerProps {}
@@ -23,12 +22,6 @@ export interface DialogTriggerProps extends AriaDialogTriggerProps {}
*/
export function DialogTrigger(props: DialogTriggerProps): ReactNode {
return (
-
- {/* RAC sets isPressed via PressResponder when the dialog is open.
- We don't want press scaling to appear to get "stuck", so override this. */}
-
- {props.children}
-
-
+
);
}
diff --git a/packages/@react-spectrum/s2/src/Menu.tsx b/packages/@react-spectrum/s2/src/Menu.tsx
index bb0d8a9fe25..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,23 +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 {
- // RAC sets isPressed via PressResponder when the menu is open.
- // We don't want press scaling to appear to get "stuck", so override this.
- // 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) {
@@ -585,9 +566,7 @@ function MenuTrigger(props: MenuTriggerProps): ReactNode {
-
- {props.children}
-
+ {props.children}
diff --git a/packages/@react-spectrum/s2/src/Picker.tsx b/packages/@react-spectrum/s2/src/Picker.tsx
index 5b7db71b366..c3043a8161a 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,15 +72,14 @@ 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 React, {createContext, forwardRef, ReactNode, useContext, useMemo, useRef} from 'react';
import {useFocusableRef} from '@react-spectrum/utils';
-import {useGlobalListeners, 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 {
@@ -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,105 +500,85 @@ const PickerButton = createHideableComponent(function PickerButton {
- if (e.pointerType !== 'mouse') {
- return;
- }
- setPressed(true);
- addGlobalListener(document, 'pointerup', () => {
- setPressed(false);
- }, {once: true, capture: true});
- };
-
return (
-
-
-
+ }
+ }],
+ [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-spectrum/s2/src/TabsPicker.tsx b/packages/@react-spectrum/s2/src/TabsPicker.tsx
index ac6119ee1e0..f9547a6af3b 100644
--- a/packages/@react-spectrum/s2/src/TabsPicker.tsx
+++ b/packages/@react-spectrum/s2/src/TabsPicker.tsx
@@ -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/Button.tsx b/packages/react-aria-components/src/Button.tsx
index d43d789db59..66abac167f7 100644
--- a/packages/react-aria-components/src/Button.tsx
+++ b/packages/react-aria-components/src/Button.tsx
@@ -65,7 +65,12 @@ export interface ButtonRenderProps {
* Whether the button is currently in a pending state.
* @selector [data-pending]
*/
- isPending: boolean
+ isPending: boolean,
+ /**
+ * Whether an overlay triggered by the button is currently open.
+ * @selector [data-expanded]
+ */
+ isExpanded?: boolean
}
export interface ButtonProps extends Omit, 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 91c5cfe1208..48a28044cee 100644
--- a/packages/react-aria-components/src/ComboBox.tsx
+++ b/packages/react-aria-components/src/ComboBox.tsx
@@ -208,7 +208,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, 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 61a50dc6ae0..a92e56c12df 100644
--- a/packages/react-aria-components/src/DatePicker.tsx
+++ b/packages/react-aria-components/src/DatePicker.tsx
@@ -175,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: state.isOpen}],
+ [ButtonContext, {...buttonProps, isExpanded: state.isOpen}],
[LabelContext, {...labelProps, ref: labelRef, elementType: 'span'}],
[CalendarContext, calendarProps],
[OverlayTriggerStateContext, state],
@@ -284,7 +284,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, 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 2091109092a..480cbd5cdf6 100644
--- a/packages/react-aria-components/src/Dialog.tsx
+++ b/packages/react-aria-components/src/Dialog.tsx
@@ -84,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 91cb0727e4a..f450623a8e7 100644
--- a/packages/react-aria-components/src/Menu.tsx
+++ b/packages/react-aria-components/src/Menu.tsx
@@ -10,9 +10,10 @@
* 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';
import {
ClassNameOrFunction,
ContextValue,
@@ -31,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';
@@ -74,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(() => {
@@ -101,9 +118,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 3d4e3e311bb..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, isPressed: 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]);
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]
diff --git a/packages/react-aria-components/test/ComboBox.test.js b/packages/react-aria-components/test/ComboBox.test.js
index bb17da5e894..b1f301144ff 100644
--- a/packages/react-aria-components/test/ComboBox.test.js
+++ b/packages/react-aria-components/test/ComboBox.test.js
@@ -106,13 +106,24 @@ 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-expanded');
expect(button).not.toHaveAttribute('data-pressed');
await user.click(button);
- expect(button).toHaveAttribute('data-pressed');
+ expect(button).toHaveAttribute('data-expanded');
+ expect(button).not.toHaveAttribute('data-pressed');
+ });
+
+ it('should set data-expanded on button when popover is open', async () => {
+ let {getByRole} = render();
+ let button = getByRole('button');
+
+ expect(button).not.toHaveAttribute('data-expanded');
+ await user.click(button);
+ expect(button).toHaveAttribute('data-expanded', 'true');
});
it('should support filtering sections', async () => {
@@ -281,17 +292,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');
@@ -587,7 +598,7 @@ describe('ComboBox', () => {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
let button = tree.getByRole('button');
-
+
// Open the combobox
await user.click(button);
act(() => {
@@ -602,7 +613,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();
@@ -616,13 +627,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 62d0ce8c462..b600dad1f4a 100644
--- a/packages/react-aria-components/test/DatePicker.test.js
+++ b/packages/react-aria-components/test/DatePicker.test.js
@@ -104,13 +104,24 @@ 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-expanded');
expect(button).not.toHaveAttribute('data-pressed');
await user.click(button);
- expect(button).toHaveAttribute('data-pressed');
+ expect(button).toHaveAttribute('data-expanded');
+ expect(button).not.toHaveAttribute('data-pressed');
+ });
+
+ it('should set data-expanded on button when popover is open', async () => {
+ let {getByRole} = render();
+ let button = getByRole('button');
+
+ expect(button).not.toHaveAttribute('data-expanded');
+ await user.click(button);
+ 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 2dda3884105..1401b7ca9a8 100644
--- a/packages/react-aria-components/test/DateRangePicker.test.js
+++ b/packages/react-aria-components/test/DateRangePicker.test.js
@@ -126,13 +126,24 @@ 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-expanded');
expect(button).not.toHaveAttribute('data-pressed');
await user.click(button);
- expect(button).toHaveAttribute('data-pressed');
+ expect(button).toHaveAttribute('data-expanded');
+ expect(button).not.toHaveAttribute('data-pressed');
+ });
+
+ it('should set data-expanded on button when popover is open', async () => {
+ let {getByRole} = render();
+ let button = getByRole('button');
+
+ expect(button).not.toHaveAttribute('data-expanded');
+ await user.click(button);
+ 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 b9c57dc9940..99cbb98e569 100644
--- a/packages/react-aria-components/test/Dialog.test.js
+++ b/packages/react-aria-components/test/Dialog.test.js
@@ -56,6 +56,25 @@ describe('Dialog', () => {
expect(dialog).toHaveAttribute('data-rac');
});
+ it('should set data-expanded on button when dialog is open', async () => {
+ let {getByRole} = render(
+
+
+
+
+ );
+
+ 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 () => {
let {getByRole} = render(
@@ -172,11 +191,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 b46ac184faf..042352a2fc6 100644
--- a/packages/react-aria-components/test/Menu.test.tsx
+++ b/packages/react-aria-components/test/Menu.test.tsx
@@ -520,10 +520,12 @@ 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-pressed');
+ expect(button).toHaveAttribute('data-expanded');
+ expect(button).not.toHaveAttribute('data-pressed');
let menu = getByRole('menu');
expect(getAllByRole('menuitem')).toHaveLength(5);
@@ -536,6 +538,25 @@ describe('Menu', () => {
expect(onAction).toHaveBeenLastCalledWith('rename');
});
+ it('should set data-expanded on button when menu is open', async () => {
+ let {getByRole} = render(
+
+
+
+
+
+
+ );
+
+ let button = getByRole('button');
+ expect(button).not.toHaveAttribute('data-expanded');
+
+ await user.click(button);
+ expect(button).toHaveAttribute('data-expanded', 'true');
+ });
+
it('should support onScroll', () => {
let onScroll = jest.fn();
let {getByRole} = renderMenu({onScroll});
@@ -738,10 +759,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);
@@ -811,10 +832,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);
@@ -904,10 +925,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);
@@ -991,10 +1012,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);
@@ -1069,9 +1090,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();
@@ -1146,10 +1167,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);
@@ -1225,10 +1246,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);
@@ -1316,10 +1337,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 78dea4fccc1..8d9af7d4f53 100644
--- a/packages/react-aria-components/test/Select.test.js
+++ b/packages/react-aria-components/test/Select.test.js
@@ -49,6 +49,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');
@@ -67,7 +68,8 @@ describe('Select', () => {
await selectTester.open();
- expect(trigger).toHaveAttribute('data-pressed', 'true');
+ 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();
@@ -393,12 +395,21 @@ 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');
});
+ it('should set data-expanded on button when popover is open', async () => {
+ let {getByRole} = render();
+ let button = getByRole('button');
+
+ expect(button).not.toHaveAttribute('data-expanded');
+ await user.click(button);
+ expect(button).toHaveAttribute('data-expanded', 'true');
+ });
+
describe('typeahead', () => {
beforeEach(() => {
jest.useFakeTimers();
diff --git a/starters/docs/src/Form.css b/starters/docs/src/Form.css
index ede03dabd4d..a039bf06a11 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);
}
@@ -104,4 +104,4 @@
background: none;
color: var(--text-color-disabled);
}
-}
\ No newline at end of file
+}
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'
}
}