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;