diff --git a/examples/s2-next-macros/src/app/Lazy.js b/examples/s2-next-macros/src/app/Lazy.js
index cd4d1202b2a..b4c10079ddb 100644
--- a/examples/s2-next-macros/src/app/Lazy.js
+++ b/examples/s2-next-macros/src/app/Lazy.js
@@ -109,7 +109,7 @@ export default function Lazy() {
return (
<>
-
+
@@ -153,16 +153,16 @@ export default function Lazy() {
Baseball
Basketball
-
+
Dogs
Cats
Low power mode
-
-
-
+
+
+
@@ -359,7 +359,7 @@ export default function Lazy() {
-
+
Red Panda
Cat
Dog
diff --git a/examples/s2-parcel-example/src/Lazy.js b/examples/s2-parcel-example/src/Lazy.js
index 43376848858..68b3eb4bc7c 100644
--- a/examples/s2-parcel-example/src/Lazy.js
+++ b/examples/s2-parcel-example/src/Lazy.js
@@ -95,7 +95,7 @@ export default function Lazy() {
return (
<>
-
+
@@ -139,16 +139,16 @@ export default function Lazy() {
Baseball
Basketball
-
+
Dogs
Cats
Low power mode
-
-
-
+
+
+
@@ -345,7 +345,7 @@ export default function Lazy() {
-
+
Red Panda
Cat
Dog
diff --git a/examples/s2-webpack-5-example/src/Lazy.js b/examples/s2-webpack-5-example/src/Lazy.js
index 43376848858..68b3eb4bc7c 100644
--- a/examples/s2-webpack-5-example/src/Lazy.js
+++ b/examples/s2-webpack-5-example/src/Lazy.js
@@ -95,7 +95,7 @@ export default function Lazy() {
return (
<>
-
+
@@ -139,16 +139,16 @@ export default function Lazy() {
Baseball
Basketball
-
+
Dogs
Cats
Low power mode
-
-
-
+
+
+
@@ -345,7 +345,7 @@ export default function Lazy() {
-
+
Red Panda
Cat
Dog
diff --git a/packages/@adobe/spectrum-css-temp/components/tray/index.css b/packages/@adobe/spectrum-css-temp/components/tray/index.css
index 30f025ac6d2..0725c8790ca 100644
--- a/packages/@adobe/spectrum-css-temp/components/tray/index.css
+++ b/packages/@adobe/spectrum-css-temp/components/tray/index.css
@@ -55,7 +55,7 @@
/* Add padding at the bottom to account for the rest of the viewport height behind the keyboard.
* This is necessary so that there isn't a visible gap that appears while the keyboard is animating
* in and out. Fall back to the safe area inset to account for things like iOS home indicator.
- We also add an additional 100vh of padding (offset by the bottom position below) so the tray
+ We also add an additional 100vh of padding (offset by the bottom position below) so the tray
extends behind Safari's address bar and keyboard in iOS 26. */
padding-bottom: calc(max(calc(100dvh - var(--spectrum-visual-viewport-height)), env(safe-area-inset-bottom)) + 100vh);
position: absolute;
@@ -75,6 +75,8 @@
/* Exit animations */
transition: opacity var(--spectrum-dialog-exit-animation-duration) cubic-bezier(0.5, 0, 1, 1) var(--spectrum-dialog-exit-animation-delay),
transform var(--spectrum-dialog-exit-animation-duration) cubic-bezier(0.5, 0, 1, 1) var(--spectrum-dialog-exit-animation-delay);
+
+ box-sizing: content-box;
}
.is-open {
diff --git a/packages/@react-aria/combobox/src/useComboBox.ts b/packages/@react-aria/combobox/src/useComboBox.ts
index 1b42aea1daf..5fe460012f0 100644
--- a/packages/@react-aria/combobox/src/useComboBox.ts
+++ b/packages/@react-aria/combobox/src/useComboBox.ts
@@ -16,7 +16,7 @@ import {AriaComboBoxProps} from '@react-types/combobox';
import {ariaHideOutside} from '@react-aria/overlays';
import {AriaListBoxOptions, getItemId, listData} from '@react-aria/listbox';
import {BaseEvent, DOMAttributes, KeyboardDelegate, LayoutDelegate, PressEvent, RefObject, RouterOptions, ValidationResult} from '@react-types/shared';
-import {chain, getActiveElement, getOwnerDocument, isAppleDevice, mergeProps, useLabels, useRouter, useUpdateEffect} from '@react-aria/utils';
+import {chain, getActiveElement, getOwnerDocument, isAppleDevice, mergeProps, useEvent, useLabels, useRouter, useUpdateEffect} from '@react-aria/utils';
import {ComboBoxState} from '@react-stately/combobox';
import {dispatchVirtualFocus} from '@react-aria/focus';
import {FocusEvent, InputHTMLAttributes, KeyboardEvent, TouchEvent, useEffect, useMemo, useRef} from 'react';
@@ -354,6 +354,10 @@ export function useComboBox(props: AriaComboBoxOptions, state: ComboBoxSta
}
}, [focusedItem]);
+ useEvent(listBoxRef, 'react-aria-item-action', state.isOpen ? () => {
+ state.close();
+ } : undefined);
+
return {
labelProps,
buttonProps: {
@@ -383,7 +387,8 @@ export function useComboBox(props: AriaComboBoxOptions, state: ComboBoxSta
shouldUseVirtualFocus: true,
shouldSelectOnPressUp: true,
shouldFocusOnHover: true,
- linkBehavior: 'selection' as const
+ linkBehavior: 'selection' as const,
+ ['UNSTABLE_itemBehavior']: 'action'
}),
descriptionProps,
errorMessageProps,
diff --git a/packages/@react-aria/listbox/src/useListBox.ts b/packages/@react-aria/listbox/src/useListBox.ts
index 8774c71bab1..c5ac0984bbf 100644
--- a/packages/@react-aria/listbox/src/useListBox.ts
+++ b/packages/@react-aria/listbox/src/useListBox.ts
@@ -100,7 +100,9 @@ export function useListBox(props: AriaListBoxOptions, state: ListState,
shouldFocusOnHover: props.shouldFocusOnHover,
isVirtualized: props.isVirtualized,
onAction: props.onAction,
- linkBehavior
+ linkBehavior,
+ // @ts-ignore
+ UNSTABLE_itemBehavior: props['UNSTABLE_itemBehavior']
});
let {labelProps, fieldProps} = useLabel({
diff --git a/packages/@react-aria/listbox/src/useOption.ts b/packages/@react-aria/listbox/src/useOption.ts
index a8412d1acb6..a2856a8e9dd 100644
--- a/packages/@react-aria/listbox/src/useOption.ts
+++ b/packages/@react-aria/listbox/src/useOption.ts
@@ -137,6 +137,8 @@ export function useOption(props: AriaOptionProps, state: ListState, ref: R
isDisabled,
onAction: onAction || item?.props?.onAction ? chain(item?.props?.onAction, onAction) : undefined,
linkBehavior: data?.linkBehavior,
+ // @ts-ignore
+ UNSTABLE_itemBehavior: data?.['UNSTABLE_itemBehavior'],
id
});
diff --git a/packages/@react-aria/selection/src/useSelectableItem.ts b/packages/@react-aria/selection/src/useSelectableItem.ts
index 3a892960cf9..cd6107a769b 100644
--- a/packages/@react-aria/selection/src/useSelectableItem.ts
+++ b/packages/@react-aria/selection/src/useSelectableItem.ts
@@ -205,8 +205,9 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
// With highlight selection, onAction is secondary, and occurs on double click. Single click selects the row.
// With touch, onAction occurs on single tap, and long press enters selection mode.
let isLinkOverride = manager.isLink(key) && linkBehavior === 'override';
+ let isActionOverride = onAction && options['UNSTABLE_itemBehavior'] === 'action';
let hasLinkAction = manager.isLink(key) && linkBehavior !== 'selection' && linkBehavior !== 'none';
- let allowsSelection = !isDisabled && manager.canSelectItem(key) && !isLinkOverride;
+ let allowsSelection = !isDisabled && manager.canSelectItem(key) && !isLinkOverride && !isActionOverride;
let allowsActions = (onAction || hasLinkAction) && !isDisabled;
let hasPrimaryAction = allowsActions && (
manager.selectionBehavior === 'replace'
@@ -225,6 +226,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
let performAction = (e) => {
if (onAction) {
onAction();
+ ref.current?.dispatchEvent(new CustomEvent('react-aria-item-action', {bubbles: true}));
}
if (hasLinkAction && ref.current) {
diff --git a/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx b/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx
index b4132fab066..73dea75fd1c 100644
--- a/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx
+++ b/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx
@@ -318,6 +318,7 @@ export function WithCreateOption() {
return (
{inputValue.length > 0 && (
diff --git a/packages/react-aria-components/test/ComboBox.test.js b/packages/react-aria-components/test/ComboBox.test.js
index e9b1971a5da..b786271a0da 100644
--- a/packages/react-aria-components/test/ComboBox.test.js
+++ b/packages/react-aria-components/test/ComboBox.test.js
@@ -409,7 +409,7 @@ describe('ComboBox', () => {
expect(options[0]).toHaveTextContent('No results');
});
- it('should support onAction', async () => {
+ it.each(['keyboard', 'mouse'])('should support onAction with %s', async (interactionType) => {
let onAction = jest.fn();
function WithCreateOption() {
let [inputValue, setInputValue] = useState('');
@@ -457,8 +457,29 @@ describe('ComboBox', () => {
expect(options).toHaveLength(1);
expect(options[0]).toHaveTextContent('Create "L"');
- await user.keyboard('{ArrowDown}{Enter}');
+ if (interactionType === 'keyboard') {
+ await user.keyboard('{ArrowDown}{Enter}');
+ } else {
+ await user.click(options[0]);
+ }
expect(onAction).toHaveBeenCalledTimes(1);
expect(comboboxTester.combobox).toHaveValue('');
+
+ // Repeat with an option selected.
+ await comboboxTester.selectOption({option: 'Cat'});
+
+ await user.keyboard('s');
+
+ options = comboboxTester.options();
+ expect(options).toHaveLength(1);
+ expect(options[0]).toHaveTextContent('Create "Cats"');
+
+ if (interactionType === 'keyboard') {
+ await user.keyboard('{ArrowDown}{Enter}');
+ } else {
+ await user.click(options[0]);
+ }
+ expect(onAction).toHaveBeenCalledTimes(2);
+ expect(comboboxTester.combobox).toHaveValue('Cat');
});
});