Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/@adobe/react-spectrum/stories/listbox/ListBox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,26 @@ export const WithDisabledOptions: ListBoxStory = {
name: 'with disabled options'
};

export const DisabledListBox: ListBoxStory = {
render: () => (
<ListBox flexGrow={1} aria-labelledby="label" items={withSection} isDisabled>
{item => (
<Section key={item.name} items={item.children} title={item.name}>
{item => <Item key={item.name}>{item.name}</Item>}
</Section>
)}
</ListBox>
),
decorators: [
Story => (
<StoryDecorator>
<Story />
</StoryDecorator>
)
],
name: 'disabled ListBox'
};

export const StaticWithDisabledOptions: ListBoxStory = {
render: () => (
<ListBox flexGrow={1} aria-labelledby="label" disabledKeys={['3', '5']}>
Expand Down
21 changes: 21 additions & 0 deletions packages/@adobe/react-spectrum/test/listbox/ListBox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
25 changes: 25 additions & 0 deletions packages/react-aria-components/test/ListBox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ListBox
aria-label="Test"
selectionMode="multiple"
isDisabled
onSelectionChange={onSelectionChange}>
<ListBoxItem id="cat">Cat</ListBoxItem>
<ListBoxItem id="dog">Dog</ListBoxItem>
<ListBoxItem id="kangaroo">Kangaroo</ListBoxItem>
</ListBox>
);

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'}
Expand Down
6 changes: 5 additions & 1 deletion packages/react-aria/src/listbox/useListBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {useLabel} from '../label/useLabel';
import {useSelectableList} from '../selection/useSelectableList';

export interface ListBoxProps<T> extends CollectionBase<T>, 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. */
Expand Down Expand Up @@ -150,7 +152,7 @@ export function useListBox<T>(
ref,
selectionManager: state.selectionManager,
collection: state.collection,
disabledKeys: state.disabledKeys,
disabledKeys: props.isDisabled ? new Set(state.collection.getKeys()) : state.disabledKeys,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't love this extra full collection iteration, at the very least we should memo it.

otherwise, we'll want to consider expanding the api to "all" or a new isDisabled prop meaning the entire selectable list. That may end up adding the new partial behaviour to every selectable list though, so would need to be careful with that.

linkBehavior
});

Expand All @@ -164,6 +166,7 @@ export function useListBox<T>(
let id = useId(props.id);
listData.set(state, {
id,
isDisabled: props.isDisabled,
shouldUseVirtualFocus: props.shouldUseVirtualFocus,
shouldSelectOnPressUp: props.shouldSelectOnPressUp,
shouldFocusOnHover: props.shouldFocusOnHover,
Expand Down Expand Up @@ -194,6 +197,7 @@ export function useListBox<T>(
: {},
{
role: 'listbox',
'aria-disabled': props.isDisabled || undefined,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think aria-disabled should be be applied for disabledBehavior: 'selection'
I think we've forgotten to include it in the props for ListBox, but it's used in RAC ListBox.

'aria-orientation': orientation,
...mergeProps(fieldProps, listProps)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/react-aria/src/listbox/useOption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export function useOption<T>(

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;
Expand Down
1 change: 1 addition & 0 deletions packages/react-aria/src/listbox/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {ListState} from 'react-stately/useListState';

interface ListData {
id?: string;
isDisabled?: boolean;
shouldSelectOnPressUp?: boolean;
shouldFocusOnHover?: boolean;
shouldUseVirtualFocus?: boolean;
Expand Down
Loading