Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update focus states for Dropdown, Combobox & Multiselect #18230

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 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
16 changes: 16 additions & 0 deletions e2e/components/ComboBox/ComboBox-test.avt.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ test.describe('@avt ComboBox', () => {
const clearButton = page.getByRole('button', {
name: 'Clear selected item',
});
const exampleOption = page.getByRole('option', {
name: 'An example option that is really long to show what should be done to handle long text',
});
const optionOne = page.getByRole('option', {
name: 'An example option that is really long to show what should be done to handle long text',
});
Expand All @@ -71,6 +74,11 @@ test.describe('@avt ComboBox', () => {
await expect(combobox).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(menu).toBeVisible();
// Expect focus to be on 1st item in menu after Arrow Down
// when there is no initial selected item
await expect(exampleOption).toHaveClass(
'cds--list-box__menu-item cds--list-box__menu-item--highlighted'
);
// Close with Escape, retain focus, and open with Spacebar
await page.keyboard.press('Escape');
await expect(menu).toBeHidden();
Expand All @@ -84,6 +92,8 @@ test.describe('@avt ComboBox', () => {
await expect(combobox).toBeFocused();
await page.keyboard.press('Enter');
await expect(menu).toBeVisible();
// Expect focus to be retained when no initial selected item after Enter
await expect(combobox).toBeFocused();
await page.keyboard.press('ArrowDown');
// Navigation inside the menu
// move to first option
Expand All @@ -101,8 +111,14 @@ test.describe('@avt ComboBox', () => {
await expect(combobox).toBeFocused();
await expect(menu).toBeHidden();
await expect(clearButton).toBeVisible();
// Expect focus to be on selected item when opening with Arrow Down
await page.keyboard.press('ArrowDown');
await expect(exampleOption).toHaveClass(
'cds--list-box__menu-item cds--list-box__menu-item--active cds--list-box__menu-item--highlighted'
);
// should only clear selection when escape is pressed when the menu is closed
await page.keyboard.press('Escape');
await page.keyboard.press('Escape');
await expect(clearButton).toBeHidden();
await expect(combobox).toHaveValue('');
// should highlight menu items based on text input
Expand Down
28 changes: 28 additions & 0 deletions e2e/components/Dropdown/Dropdown-test.avt.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ test.describe('@avt Dropdown', () => {
await expect(toggleButton).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(menu).toBeVisible();
// Expect focus to be on 1st item in menu after Arrow Down
// when there is no initial selected item
await expect(
page.getByRole('option', {
name: 'Lorem, ipsum dolor sit amet consectetur adipisicing elit.',
})
).toHaveClass(
'cds--list-box__menu-item cds--list-box__menu-item--highlighted'
);
// Close with Escape, retain focus, and open with Space
await page.keyboard.press('Escape');
await page.keyboard.press('Space');
Expand All @@ -71,6 +80,15 @@ test.describe('@avt Dropdown', () => {
await expect(menu).toBeHidden();
await expect(toggleButton).toBeFocused();
await page.keyboard.press('Enter');
// Expect focus to be retained when no initial selected item after Enter
await expect(toggleButton).toBeFocused();
await expect(menu).toBeVisible();
// Select item from menu
await page.keyboard.press('ArrowDown');
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
// Open with Enter after item has been selected
await page.keyboard.press('Enter');
// Should focus on selected item by default
await expect(
page.getByRole('option', {
Expand All @@ -79,6 +97,16 @@ test.describe('@avt Dropdown', () => {
).toHaveClass(
'cds--list-box__menu-item cds--list-box__menu-item--active cds--list-box__menu-item--highlighted'
);
// Should focus on selected item by default on Arrow Down as well
await page.keyboard.press('Escape');
await page.keyboard.press('ArrowDown');
await expect(
page.getByRole('option', {
name: 'Option 1',
})
).toHaveClass(
'cds--list-box__menu-item cds--list-box__menu-item--active cds--list-box__menu-item--highlighted'
);
// Navigation inside the menu
await page.keyboard.press('ArrowDown');
await expect(
Expand Down
10 changes: 10 additions & 0 deletions e2e/components/FluidComboBox/FluidComboBox-test.avt.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ test.describe('@avt FluidComboBox', () => {
const clearButton = page.getByRole('button', {
name: 'Clear selected item',
});
const exampleOption = page.getByRole('option', {
name: 'Lorem, ipsum dolor sit amet consectetur adipisicing elit.',
});
const optionOne = page.getByRole('option', {
name: 'Lorem, ipsum dolor sit amet consectetur adipisicing elit.',
});
Expand All @@ -71,6 +74,11 @@ test.describe('@avt FluidComboBox', () => {
await expect(combobox).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(menu).toBeVisible();
// Expect focus to be on 1st item in menu after Arrow Down
// when there is no initial selected item
await expect(exampleOption).toHaveClass(
'cds--list-box__menu-item cds--list-box__menu-item--highlighted'
);
// Close with Escape, retain focus, and open with Spacebar
await page.keyboard.press('Escape');
await expect(menu).toBeHidden();
Expand All @@ -84,6 +92,8 @@ test.describe('@avt FluidComboBox', () => {
await expect(combobox).toBeFocused();
await page.keyboard.press('Enter');
await expect(menu).toBeVisible();
// Expect focus to be retained when no initial selected item after Enter
await expect(combobox).toBeFocused();
await page.keyboard.press('ArrowDown');
// Navigation inside the menu
// move to first option
Expand Down
29 changes: 29 additions & 0 deletions e2e/components/FluidDropdown/FluidDropdown-test.avt.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ test.describe('@avt FluidDropdown', () => {
await expect(toggleButton).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(menu).toBeVisible();
// Expect focus to be on 1st item in menu after Arrow Down
// when there is no initial selected item
await expect(
page.getByRole('option', {
name: 'Lorem, ipsum dolor sit amet consectetur adipisicing elit.',
})
).toHaveClass(
'cds--list-box__menu-item cds--list-box__menu-item--highlighted'
);
// Close with Escape, retain focus, and open with Space
await page.keyboard.press('Escape');
await page.keyboard.press('Space');
Expand All @@ -70,6 +79,16 @@ test.describe('@avt FluidDropdown', () => {
await expect(menu).toBeHidden();
await expect(toggleButton).toBeFocused();
await page.keyboard.press('Enter');
// Expect focus to be retained when no initial selected item after Enter
await expect(toggleButton).toBeFocused();
await expect(menu).toBeVisible();
// Select item from menu
await page.keyboard.press('ArrowDown');
await page.keyboard.press('ArrowDown');
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
// Open with Enter after item has been selected
await page.keyboard.press('Enter');
// Should focus on selected item by default
await expect(
page.getByRole('option', {
Expand All @@ -78,6 +97,16 @@ test.describe('@avt FluidDropdown', () => {
).toHaveClass(
'cds--list-box__menu-item cds--list-box__menu-item--active cds--list-box__menu-item--highlighted'
);
// Should focus on selected item by default on Arrow Down as well
await page.keyboard.press('Escape');
await page.keyboard.press('ArrowDown');
await expect(
page.getByRole('option', {
name: 'Option 2',
})
).toHaveClass(
'cds--list-box__menu-item cds--list-box__menu-item--active cds--list-box__menu-item--highlighted'
);
// Navigation inside the menu
await page.keyboard.press('ArrowDown');
await expect(
Expand Down
32 changes: 31 additions & 1 deletion e2e/components/MultiSelect/MultiSelect-test.avt.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ test.describe('@avt MultiSelect', () => {
const toggleButton = page.getByRole('combobox', {
expanded: false,
});
const toggleButtonExpanded = page.getByRole('combobox', {
expanded: true,
});
const selection = page.getByRole('button', {
name: 'Clear all selected items',
});
Expand All @@ -100,11 +103,22 @@ test.describe('@avt MultiSelect', () => {
await expect(toggleButton).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(menu).toBeVisible();
// Expect focus to be on 1st item in menu after Arrow Down
// when there is no initial selected item
await expect(
page.getByRole('option', {
name: 'An example option that is really long to show what should be done to handle long text',
})
).toHaveClass(
'cds--list-box__menu-item cds--list-box__menu-item--highlighted'
);
// Close with Escape, retain focus, and open with Enter
await page.keyboard.press('Escape');
await expect(menu).toBeHidden();
await expect(toggleButton).toBeFocused();
await page.keyboard.press('Enter');
// Expect focus to be retained when no initial selected item after Enter
await expect(toggleButtonExpanded).toBeFocused();
await expect(menu).toBeVisible();
// Close with Escape, retain focus, and open with Spacebar
await page.keyboard.press('Escape');
Expand Down Expand Up @@ -143,7 +157,23 @@ test.describe('@avt MultiSelect', () => {
name: 'An example option that is really long to show what should be done to handle long text',
selected: true,
})
).toBeVisible();
).toHaveClass(
'cds--list-box__menu-item cds--list-box__menu-item--active cds--list-box__menu-item--highlighted'
);
// Close with Escape, retain focus, and open with Arrow Down
await page.keyboard.press('Escape');
await expect(menu).toBeHidden();
await expect(toggleButton).toBeFocused();
await page.keyboard.press('ArrowDown');
// On Arrow Down, selected item should be focused
await expect(
page.getByRole('option', {
name: 'An example option that is really long to show what should be done to handle long text',
selected: true,
})
).toHaveClass(
'cds--list-box__menu-item cds--list-box__menu-item--active cds--list-box__menu-item--highlighted'
);
// move to second option
await page.keyboard.press('ArrowDown');
await expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,7 @@ export const Default = () => (
id="default"
titleText="Dropdown label"
helperText="This is some helper text"
initialSelectedItem={items[1]}
label="Option 1"
label="Choose an option"
items={items}
itemToString={(item) => (item ? item.text : '')}
/>
Expand Down
9 changes: 6 additions & 3 deletions packages/react/src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@
[`${prefix}--dropdown--invalid`]: invalid,
[`${prefix}--dropdown--warning`]: showWarning,
[`${prefix}--dropdown--open`]: isOpen,
[`${prefix}--dropdown--focus`]: isFocused,
[`${prefix}--dropdown--inline`]: inline,
[`${prefix}--dropdown--disabled`]: disabled,
[`${prefix}--dropdown--light`]: light,
Expand Down Expand Up @@ -489,8 +490,7 @@
[`${prefix}--dropdown__wrapper--inline--invalid`]: inline && invalid,
[`${prefix}--list-box__wrapper--inline--invalid`]: inline && invalid,
[`${prefix}--list-box__wrapper--fluid--invalid`]: isFluid && invalid,
[`${prefix}--list-box__wrapper--fluid--focus`]:
isFluid && isFocused && !isOpen,
[`${prefix}--list-box__wrapper--fluid--focus`]: isFluid && isFocused,
[`${prefix}--list-box__wrapper--slug`]: slug,
[`${prefix}--list-box__wrapper--decorator`]: decorator,
}
Expand Down Expand Up @@ -518,7 +518,7 @@
) : null;

const handleFocus = (evt: FocusEvent<HTMLDivElement>) => {
setIsFocused(evt.type === 'focus' ? true : false);
setIsFocused(evt.type === 'focus' && !selectedItem ? true : false);
};

const mergedRef = mergeRefs(toggleButtonProps.ref, ref);
Expand Down Expand Up @@ -548,6 +548,9 @@
}, 3000)
);
}
if (['ArrowDown'].includes(evt.key)) {
setIsFocused(false);

Check warning on line 552 in packages/react/src/components/Dropdown/Dropdown.tsx

View check run for this annotation

Codecov / codecov/patch

packages/react/src/components/Dropdown/Dropdown.tsx#L552

Added line #L552 was not covered by tests
}
if (toggleButtonProps.onKeyDown) {
toggleButtonProps.onKeyDown(evt);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ const ToggleTip = (
export const Default = () => (
<div style={{ width: '400px' }}>
<FluidDropdown
initialSelectedItem={items[2]}
id="default"
titleText="Label"
label="Choose an option"
Expand Down
14 changes: 12 additions & 2 deletions packages/react/src/components/MultiSelect/MultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -503,9 +503,20 @@ const MultiSelect = React.forwardRef(
setItemsCleared(false);
setIsOpenWrapper(true);
}
if (match(e, keys.ArrowDown) && selectedItems.length === 0) {
setInputFocused(false);
setIsFocused(false);
}
if (match(e, keys.Escape) && isOpen) {
setInputFocused(true);
}
if (match(e, keys.Enter) && isOpen) {
setInputFocused(true);
}
}
},
});

const mergedRef = mergeRefs(toggleButtonProps.ref, ref);

const selectedItems = selectedItem as ItemType[];
Expand Down Expand Up @@ -542,8 +553,7 @@ const MultiSelect = React.forwardRef(
inline && invalid,
[`${prefix}--list-box__wrapper--inline--invalid`]: inline && invalid,
[`${prefix}--list-box__wrapper--fluid--invalid`]: isFluid && invalid,
[`${prefix}--list-box__wrapper--fluid--focus`]:
!isOpen && isFluid && isFocused,
[`${prefix}--list-box__wrapper--fluid--focus`]: isFluid && isFocused,
[`${prefix}--list-box__wrapper--slug`]: slug,
[`${prefix}--list-box__wrapper--decorator`]: decorator,
}
Expand Down
4 changes: 4 additions & 0 deletions packages/styles/scss/components/dropdown/_dropdown.scss
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@
outline: none;
}

.#{$prefix}--dropdown--focus .#{$prefix}--list-box__field {
@include focus-outline('outline');
}

.#{$prefix}--dropdown--invalid {
@include focus-outline('invalid');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
.#{$prefix}--list-box__wrapper--fluid
.#{$prefix}--combo-box
.#{$prefix}--text-input {
z-index: 9101;
overflow: hidden;
padding-block: convert.to-rem(33px) convert.to-rem(13px);
padding-inline: $spacing-05 $spacing-10;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,19 +121,21 @@
}

.#{$prefix}--list-box__wrapper--fluid.#{$prefix}--list-box__wrapper--fluid--focus:has(
.#{$prefix}--list-box--expanded
.#{$prefix}--list-box--expanded.#{$prefix}--multi-select--selected
) {
outline-width: convert.to-rem(1px);
}

.#{$prefix}--list-box__wrapper--fluid .#{$prefix}--list-box__field:focus {
.#{$prefix}--list-box__wrapper--fluid--focus
.#{$prefix}--list-box__field:focus {
outline: none;
outline-offset: 0;
}

.#{$prefix}--list-box__wrapper--fluid:not(.#{$prefix}--list-box--up)
.#{$prefix}--list-box__menu {
inset-block-start: calc(100%);
margin-block-start: convert.to-rem(2px);
}

// Invalid / Warning styles
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,6 @@ export const ExplainabilityPopover = {
render: (args) => {
const { alignment, showActions } = args ?? {};

console.log('showActions', showActions);
return html`
<style>
${styles}
Expand Down
Loading