Skip to content

Commit eee39f8

Browse files
annawen1tay1orjonesKritvi-bhatia17
authored
feat: update focus states for Dropdown, Combobox & Multiselect (#18230)
* feat: update focus states * chore: revert story change * test(avt): add avt test for fluid dropdown and dropdown * test(avt): fluid combobox and combobox tests * test(avt): multiselect avt test * chore: adjust styles for focus states in fluid * fix: remove styles setting outline to none * chore: remove initial selected item for fluid dropdown story * chore: make sure first option is focused for filterable multiselect * chore: fix issue with no focus state dropdown * chore: adjust focus stroke width * chore: fix first selected item focus width --------- Co-authored-by: Taylor Jones <tay1orjones@users.noreply.github.com> Co-authored-by: Kritvi <158570656+Kritvi-bhatia17@users.noreply.github.com>
1 parent 02d6f94 commit eee39f8

File tree

18 files changed

+179
-40
lines changed

18 files changed

+179
-40
lines changed

e2e/components/ComboBox/ComboBox-test.avt.e2e.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ test.describe('@avt ComboBox', () => {
5757
const clearButton = page.getByRole('button', {
5858
name: 'Clear selected item',
5959
});
60+
const exampleOption = page.getByRole('option', {
61+
name: 'An example option that is really long to show what should be done to handle long text',
62+
});
6063
const optionOne = page.getByRole('option', {
6164
name: 'An example option that is really long to show what should be done to handle long text',
6265
});
@@ -71,6 +74,11 @@ test.describe('@avt ComboBox', () => {
7174
await expect(combobox).toBeFocused();
7275
await page.keyboard.press('ArrowDown');
7376
await expect(menu).toBeVisible();
77+
// Expect focus to be on 1st item in menu after Arrow Down
78+
// when there is no initial selected item
79+
await expect(exampleOption).toHaveClass(
80+
'cds--list-box__menu-item cds--list-box__menu-item--highlighted'
81+
);
7482
// Close with Escape, retain focus, and open with Spacebar
7583
await page.keyboard.press('Escape');
7684
await expect(menu).toBeHidden();
@@ -84,6 +92,8 @@ test.describe('@avt ComboBox', () => {
8492
await expect(combobox).toBeFocused();
8593
await page.keyboard.press('Enter');
8694
await expect(menu).toBeVisible();
95+
// Expect focus to be retained when no initial selected item after Enter
96+
await expect(combobox).toBeFocused();
8797
await page.keyboard.press('ArrowDown');
8898
// Navigation inside the menu
8999
// move to first option
@@ -101,8 +111,14 @@ test.describe('@avt ComboBox', () => {
101111
await expect(combobox).toBeFocused();
102112
await expect(menu).toBeHidden();
103113
await expect(clearButton).toBeVisible();
114+
// Expect focus to be on selected item when opening with Arrow Down
115+
await page.keyboard.press('ArrowDown');
116+
await expect(exampleOption).toHaveClass(
117+
'cds--list-box__menu-item cds--list-box__menu-item--active cds--list-box__menu-item--highlighted'
118+
);
104119
// should only clear selection when escape is pressed when the menu is closed
105120
await page.keyboard.press('Escape');
121+
await page.keyboard.press('Escape');
106122
await expect(clearButton).toBeHidden();
107123
await expect(combobox).toHaveValue('');
108124
// should highlight menu items based on text input

e2e/components/Dropdown/Dropdown-test.avt.e2e.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ test.describe('@avt Dropdown', () => {
6262
await expect(toggleButton).toBeFocused();
6363
await page.keyboard.press('ArrowDown');
6464
await expect(menu).toBeVisible();
65+
// Expect focus to be on 1st item in menu after Arrow Down
66+
// when there is no initial selected item
67+
await expect(
68+
page.getByRole('option', {
69+
name: 'Lorem, ipsum dolor sit amet consectetur adipisicing elit.',
70+
})
71+
).toHaveClass(
72+
'cds--list-box__menu-item cds--list-box__menu-item--highlighted'
73+
);
6574
// Close with Escape, retain focus, and open with Space
6675
await page.keyboard.press('Escape');
6776
await page.keyboard.press('Space');
@@ -71,6 +80,15 @@ test.describe('@avt Dropdown', () => {
7180
await expect(menu).toBeHidden();
7281
await expect(toggleButton).toBeFocused();
7382
await page.keyboard.press('Enter');
83+
// Expect focus to be retained when no initial selected item after Enter
84+
await expect(toggleButton).toBeFocused();
85+
await expect(menu).toBeVisible();
86+
// Select item from menu
87+
await page.keyboard.press('ArrowDown');
88+
await page.keyboard.press('ArrowDown');
89+
await page.keyboard.press('Enter');
90+
// Open with Enter after item has been selected
91+
await page.keyboard.press('Enter');
7492
// Should focus on selected item by default
7593
await expect(
7694
page.getByRole('option', {
@@ -79,6 +97,16 @@ test.describe('@avt Dropdown', () => {
7997
).toHaveClass(
8098
'cds--list-box__menu-item cds--list-box__menu-item--active cds--list-box__menu-item--highlighted'
8199
);
100+
// Should focus on selected item by default on Arrow Down as well
101+
await page.keyboard.press('Escape');
102+
await page.keyboard.press('ArrowDown');
103+
await expect(
104+
page.getByRole('option', {
105+
name: 'Option 1',
106+
})
107+
).toHaveClass(
108+
'cds--list-box__menu-item cds--list-box__menu-item--active cds--list-box__menu-item--highlighted'
109+
);
82110
// Navigation inside the menu
83111
await page.keyboard.press('ArrowDown');
84112
await expect(

e2e/components/FluidComboBox/FluidComboBox-test.avt.e2e.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ test.describe('@avt FluidComboBox', () => {
5757
const clearButton = page.getByRole('button', {
5858
name: 'Clear selected item',
5959
});
60+
const exampleOption = page.getByRole('option', {
61+
name: 'Lorem, ipsum dolor sit amet consectetur adipisicing elit.',
62+
});
6063
const optionOne = page.getByRole('option', {
6164
name: 'Lorem, ipsum dolor sit amet consectetur adipisicing elit.',
6265
});
@@ -71,6 +74,11 @@ test.describe('@avt FluidComboBox', () => {
7174
await expect(combobox).toBeFocused();
7275
await page.keyboard.press('ArrowDown');
7376
await expect(menu).toBeVisible();
77+
// Expect focus to be on 1st item in menu after Arrow Down
78+
// when there is no initial selected item
79+
await expect(exampleOption).toHaveClass(
80+
'cds--list-box__menu-item cds--list-box__menu-item--highlighted'
81+
);
7482
// Close with Escape, retain focus, and open with Spacebar
7583
await page.keyboard.press('Escape');
7684
await expect(menu).toBeHidden();
@@ -84,6 +92,8 @@ test.describe('@avt FluidComboBox', () => {
8492
await expect(combobox).toBeFocused();
8593
await page.keyboard.press('Enter');
8694
await expect(menu).toBeVisible();
95+
// Expect focus to be retained when no initial selected item after Enter
96+
await expect(combobox).toBeFocused();
8797
await page.keyboard.press('ArrowDown');
8898
// Navigation inside the menu
8999
// move to first option

e2e/components/FluidDropdown/FluidDropdown-test.avt.e2e.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ test.describe('@avt FluidDropdown', () => {
6161
await expect(toggleButton).toBeFocused();
6262
await page.keyboard.press('ArrowDown');
6363
await expect(menu).toBeVisible();
64+
// Expect focus to be on 1st item in menu after Arrow Down
65+
// when there is no initial selected item
66+
await expect(
67+
page.getByRole('option', {
68+
name: 'Lorem, ipsum dolor sit amet consectetur adipisicing elit.',
69+
})
70+
).toHaveClass(
71+
'cds--list-box__menu-item cds--list-box__menu-item--highlighted'
72+
);
6473
// Close with Escape, retain focus, and open with Space
6574
await page.keyboard.press('Escape');
6675
await page.keyboard.press('Space');
@@ -70,6 +79,16 @@ test.describe('@avt FluidDropdown', () => {
7079
await expect(menu).toBeHidden();
7180
await expect(toggleButton).toBeFocused();
7281
await page.keyboard.press('Enter');
82+
// Expect focus to be retained when no initial selected item after Enter
83+
await expect(toggleButton).toBeFocused();
84+
await expect(menu).toBeVisible();
85+
// Select item from menu
86+
await page.keyboard.press('ArrowDown');
87+
await page.keyboard.press('ArrowDown');
88+
await page.keyboard.press('ArrowDown');
89+
await page.keyboard.press('Enter');
90+
// Open with Enter after item has been selected
91+
await page.keyboard.press('Enter');
7392
// Should focus on selected item by default
7493
await expect(
7594
page.getByRole('option', {
@@ -78,6 +97,16 @@ test.describe('@avt FluidDropdown', () => {
7897
).toHaveClass(
7998
'cds--list-box__menu-item cds--list-box__menu-item--active cds--list-box__menu-item--highlighted'
8099
);
100+
// Should focus on selected item by default on Arrow Down as well
101+
await page.keyboard.press('Escape');
102+
await page.keyboard.press('ArrowDown');
103+
await expect(
104+
page.getByRole('option', {
105+
name: 'Option 2',
106+
})
107+
).toHaveClass(
108+
'cds--list-box__menu-item cds--list-box__menu-item--active cds--list-box__menu-item--highlighted'
109+
);
81110
// Navigation inside the menu
82111
await page.keyboard.press('ArrowDown');
83112
await expect(

e2e/components/MultiSelect/MultiSelect-test.avt.e2e.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ test.describe('@avt MultiSelect', () => {
8888
const toggleButton = page.getByRole('combobox', {
8989
expanded: false,
9090
});
91+
const toggleButtonExpanded = page.getByRole('combobox', {
92+
expanded: true,
93+
});
9194
const selection = page.getByRole('button', {
9295
name: 'Clear all selected items',
9396
});
@@ -100,11 +103,22 @@ test.describe('@avt MultiSelect', () => {
100103
await expect(toggleButton).toBeFocused();
101104
await page.keyboard.press('ArrowDown');
102105
await expect(menu).toBeVisible();
106+
// Expect focus to be on 1st item in menu after Arrow Down
107+
// when there is no initial selected item
108+
await expect(
109+
page.getByRole('option', {
110+
name: 'An example option that is really long to show what should be done to handle long text',
111+
})
112+
).toHaveClass(
113+
'cds--list-box__menu-item cds--list-box__menu-item--highlighted'
114+
);
103115
// Close with Escape, retain focus, and open with Enter
104116
await page.keyboard.press('Escape');
105117
await expect(menu).toBeHidden();
106118
await expect(toggleButton).toBeFocused();
107119
await page.keyboard.press('Enter');
120+
// Expect focus to be retained when no initial selected item after Enter
121+
await expect(toggleButtonExpanded).toBeFocused();
108122
await expect(menu).toBeVisible();
109123
// Close with Escape, retain focus, and open with Spacebar
110124
await page.keyboard.press('Escape');
@@ -143,7 +157,23 @@ test.describe('@avt MultiSelect', () => {
143157
name: 'An example option that is really long to show what should be done to handle long text',
144158
selected: true,
145159
})
146-
).toBeVisible();
160+
).toHaveClass(
161+
'cds--list-box__menu-item cds--list-box__menu-item--active cds--list-box__menu-item--highlighted'
162+
);
163+
// Close with Escape, retain focus, and open with Arrow Down
164+
await page.keyboard.press('Escape');
165+
await expect(menu).toBeHidden();
166+
await expect(toggleButton).toBeFocused();
167+
await page.keyboard.press('ArrowDown');
168+
// On Arrow Down, selected item should be focused
169+
await expect(
170+
page.getByRole('option', {
171+
name: 'An example option that is really long to show what should be done to handle long text',
172+
selected: true,
173+
})
174+
).toHaveClass(
175+
'cds--list-box__menu-item cds--list-box__menu-item--active cds--list-box__menu-item--highlighted'
176+
);
147177
// move to second option
148178
await page.keyboard.press('ArrowDown');
149179
await expect(

packages/react/src/components/ComboBox/ComboBox.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -700,7 +700,6 @@ const ComboBox = forwardRef(
700700
containerClassName,
701701
{
702702
[`${prefix}--list-box__wrapper--fluid--invalid`]: isFluid && invalid,
703-
[`${prefix}--list-box__wrapper--fluid--focus`]: isFluid && isFocused,
704703
[`${prefix}--list-box__wrapper--slug`]: slug,
705704
[`${prefix}--list-box__wrapper--decorator`]: decorator,
706705
},

packages/react/src/components/Dropdown/Dropdown.stories.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,7 @@ export const Default = (args) => {
193193
id="default"
194194
titleText="Dropdown label"
195195
helperText="This is some helper text"
196-
initialSelectedItem={items[1]}
197-
label="Option 1"
196+
label="Choose an option"
198197
items={items}
199198
itemToString={(item) => (item ? item.text : '')}
200199
{...args}

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@ const Dropdown = React.forwardRef(
461461
[`${prefix}--dropdown--invalid`]: invalid,
462462
[`${prefix}--dropdown--warning`]: showWarning,
463463
[`${prefix}--dropdown--open`]: isOpen,
464+
[`${prefix}--dropdown--focus`]: isFocused,
464465
[`${prefix}--dropdown--inline`]: inline,
465466
[`${prefix}--dropdown--disabled`]: disabled,
466467
[`${prefix}--dropdown--light`]: light,
@@ -489,8 +490,6 @@ const Dropdown = React.forwardRef(
489490
[`${prefix}--dropdown__wrapper--inline--invalid`]: inline && invalid,
490491
[`${prefix}--list-box__wrapper--inline--invalid`]: inline && invalid,
491492
[`${prefix}--list-box__wrapper--fluid--invalid`]: isFluid && invalid,
492-
[`${prefix}--list-box__wrapper--fluid--focus`]:
493-
isFluid && isFocused && !isOpen,
494493
[`${prefix}--list-box__wrapper--slug`]: slug,
495494
[`${prefix}--list-box__wrapper--decorator`]: decorator,
496495
}
@@ -518,7 +517,7 @@ const Dropdown = React.forwardRef(
518517
) : null;
519518

520519
const handleFocus = (evt: FocusEvent<HTMLDivElement>) => {
521-
setIsFocused(evt.type === 'focus' ? true : false);
520+
setIsFocused(evt.type === 'focus' && !selectedItem ? true : false);
522521
};
523522

524523
const mergedRef = mergeRefs(toggleButtonProps.ref, ref);
@@ -548,6 +547,12 @@ const Dropdown = React.forwardRef(
548547
}, 3000)
549548
);
550549
}
550+
if (['ArrowDown'].includes(evt.key)) {
551+
setIsFocused(false);
552+
}
553+
if (['Enter'].includes(evt.key) && !selectedItem && !isOpen) {
554+
setIsFocused(true);
555+
}
551556
if (toggleButtonProps.onKeyDown) {
552557
toggleButtonProps.onKeyDown(evt);
553558
}

packages/react/src/components/FluidDropdown/FluidDropdown.stories.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ const sharedArgTypes = {
105105
export const Default = (args) => (
106106
<div style={{ width: args.defaultWidth }}>
107107
<FluidDropdown
108-
initialSelectedItem={items[2]}
109108
id="default"
110109
titleText="Label"
111110
label="Choose an option"

packages/react/src/components/MultiSelect/FilterableMultiSelect.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,6 @@ const FilterableMultiSelect = React.forwardRef(function FilterableMultiSelect<
472472
[`${prefix}--list-box__wrapper--inline--invalid`]: inline && invalid,
473473
[`${prefix}--list-box--up`]: direction === 'top',
474474
[`${prefix}--list-box__wrapper--fluid--invalid`]: isFluid && invalid,
475-
[`${prefix}--list-box__wrapper--fluid--focus`]: isFluid && isFocused,
476475
[`${prefix}--list-box__wrapper--slug`]: slug,
477476
[`${prefix}--list-box__wrapper--decorator`]: decorator,
478477
[`${prefix}--autoalign`]: autoAlign,
@@ -610,6 +609,10 @@ const FilterableMultiSelect = React.forwardRef(function FilterableMultiSelect<
610609
case InputKeyDownArrowDown:
611610
if (InputKeyDownArrowDown === type && !isOpen) {
612611
setIsOpen(true);
612+
return {
613+
...changes,
614+
highlightedIndex: 0,
615+
};
613616
}
614617
if (highlightedIndex > -1) {
615618
const itemArray = document.querySelectorAll(

0 commit comments

Comments
 (0)