diff --git a/packages/@adobe/react-spectrum/stories/listbox/ListBox.stories.tsx b/packages/@adobe/react-spectrum/stories/listbox/ListBox.stories.tsx
index e7665965644..2b6375a0bd9 100644
--- a/packages/@adobe/react-spectrum/stories/listbox/ListBox.stories.tsx
+++ b/packages/@adobe/react-spectrum/stories/listbox/ListBox.stories.tsx
@@ -476,6 +476,26 @@ export const WithDisabledOptions: ListBoxStory = {
name: 'with disabled options'
};
+export const DisabledListBox: ListBoxStory = {
+ render: () => (
+
+ {item => (
+
+ {item => - {item.name}
}
+
+ )}
+
+ ),
+ decorators: [
+ Story => (
+
+
+
+ )
+ ],
+ name: 'disabled ListBox'
+};
+
export const StaticWithDisabledOptions: ListBoxStory = {
render: () => (
diff --git a/packages/@adobe/react-spectrum/test/listbox/ListBox.test.js b/packages/@adobe/react-spectrum/test/listbox/ListBox.test.js
index 6704bf6d6eb..354d0231093 100644
--- a/packages/@adobe/react-spectrum/test/listbox/ListBox.test.js
+++ b/packages/@adobe/react-spectrum/test/listbox/ListBox.test.js
@@ -358,6 +358,27 @@ describe('ListBox', function () {
fireEvent.keyDown(listbox, {key: 'ArrowUp', code: 38, charCode: 38});
expect(document.activeElement).toBe(options[0]);
});
+
+ it('supports disabling the entire listbox', async function () {
+ let user = userEvent.setup({delay: null, pointerMap});
+ let tree = renderComponent({
+ onSelectionChange,
+ selectionMode: 'single',
+ isDisabled: true,
+ autoFocus: 'first'
+ });
+ let listbox = tree.getByRole('listbox');
+ let options = within(listbox).getAllByRole('option');
+
+ expect(listbox).toHaveAttribute('aria-disabled', 'true');
+ for (let option of options) {
+ expect(option).toHaveAttribute('aria-disabled', 'true');
+ }
+
+ await user.click(options[1]);
+ expect(onSelectionChange).toHaveBeenCalledTimes(0);
+ expect(document.activeElement).toBe(listbox);
+ });
});
describe('supports multi selection', function () {
diff --git a/packages/react-aria-components/test/ListBox.test.js b/packages/react-aria-components/test/ListBox.test.js
index 80410f975de..5da388fada5 100644
--- a/packages/react-aria-components/test/ListBox.test.js
+++ b/packages/react-aria-components/test/ListBox.test.js
@@ -698,6 +698,31 @@ describe('ListBox', () => {
expect(document.activeElement).toBe(items[2]);
});
+ it('should support isDisabled prop on the ListBox', async () => {
+ let onSelectionChange = jest.fn();
+ let {getByRole, getAllByRole} = render(
+
+ Cat
+ Dog
+ Kangaroo
+
+ );
+
+ let listbox = getByRole('listbox');
+ let items = getAllByRole('option');
+ expect(listbox).toHaveAttribute('aria-disabled', 'true');
+ for (let item of items) {
+ expect(item).toHaveAttribute('aria-disabled', 'true');
+ }
+
+ await user.click(items[1]);
+ expect(onSelectionChange).not.toHaveBeenCalled();
+ });
+
it.each`
interactionType
${'mouse'}
diff --git a/packages/react-aria/src/listbox/useListBox.ts b/packages/react-aria/src/listbox/useListBox.ts
index 14185b14f5a..409ecb9cf29 100644
--- a/packages/react-aria/src/listbox/useListBox.ts
+++ b/packages/react-aria/src/listbox/useListBox.ts
@@ -36,6 +36,8 @@ import {useLabel} from '../label/useLabel';
import {useSelectableList} from '../selection/useSelectableList';
export interface ListBoxProps extends CollectionBase, MultipleSelection, FocusEvents {
+ /** Whether the listbox is disabled. */
+ isDisabled?: boolean;
/** Whether to auto focus the listbox or an option. */
autoFocus?: boolean | FocusStrategy;
/** Whether focus should wrap around when the end/start is reached. */
@@ -150,7 +152,7 @@ export function useListBox(
ref,
selectionManager: state.selectionManager,
collection: state.collection,
- disabledKeys: state.disabledKeys,
+ disabledKeys: props.isDisabled ? new Set(state.collection.getKeys()) : state.disabledKeys,
linkBehavior
});
@@ -164,6 +166,7 @@ export function useListBox(
let id = useId(props.id);
listData.set(state, {
id,
+ isDisabled: props.isDisabled,
shouldUseVirtualFocus: props.shouldUseVirtualFocus,
shouldSelectOnPressUp: props.shouldSelectOnPressUp,
shouldFocusOnHover: props.shouldFocusOnHover,
@@ -194,6 +197,7 @@ export function useListBox(
: {},
{
role: 'listbox',
+ 'aria-disabled': props.isDisabled || undefined,
'aria-orientation': orientation,
...mergeProps(fieldProps, listProps)
}
diff --git a/packages/react-aria/src/listbox/useOption.ts b/packages/react-aria/src/listbox/useOption.ts
index dbab6edf899..a9896fc1043 100644
--- a/packages/react-aria/src/listbox/useOption.ts
+++ b/packages/react-aria/src/listbox/useOption.ts
@@ -107,7 +107,7 @@ export function useOption(
let data = listData.get(state);
- let isDisabled = props.isDisabled ?? state.selectionManager.isDisabled(key);
+ let isDisabled = props.isDisabled ?? (data?.isDisabled || state.selectionManager.isDisabled(key));
let isSelected = props.isSelected ?? state.selectionManager.isSelected(key);
let shouldSelectOnPressUp = props.shouldSelectOnPressUp ?? data?.shouldSelectOnPressUp;
let shouldFocusOnHover = props.shouldFocusOnHover ?? data?.shouldFocusOnHover;
diff --git a/packages/react-aria/src/listbox/utils.ts b/packages/react-aria/src/listbox/utils.ts
index 022c74e5afb..f61c7751a4e 100644
--- a/packages/react-aria/src/listbox/utils.ts
+++ b/packages/react-aria/src/listbox/utils.ts
@@ -15,6 +15,7 @@ import {ListState} from 'react-stately/useListState';
interface ListData {
id?: string;
+ isDisabled?: boolean;
shouldSelectOnPressUp?: boolean;
shouldFocusOnHover?: boolean;
shouldUseVirtualFocus?: boolean;