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

Merged
merged 20 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7b870d2
feat: update focus states
annawen1 Dec 9, 2024
51e55e6
chore: revert story change
annawen1 Dec 9, 2024
44428d4
Merge branch 'main' into chore/focus-states
tay1orjones Dec 10, 2024
9b94af8
test(avt): add avt test for fluid dropdown and dropdown
annawen1 Dec 10, 2024
5e3d511
Merge branch 'chore/focus-states' of https://github.com/annawen1/carb…
annawen1 Dec 10, 2024
b155a5b
test(avt): fluid combobox and combobox tests
annawen1 Dec 10, 2024
ca01d83
test(avt): multiselect avt test
annawen1 Dec 10, 2024
f10c92b
Merge remote-tracking branch 'upstream/main' into chore/focus-states
annawen1 Dec 17, 2024
63fa9da
chore: adjust styles for focus states in fluid
annawen1 Dec 17, 2024
b1e96e8
chore: resolve merge conflicts
annawen1 Jan 7, 2025
0157c59
fix: remove styles setting outline to none
annawen1 Jan 8, 2025
d63dd89
chore: remove initial selected item for fluid dropdown story
annawen1 Jan 9, 2025
524fecc
chore: make sure first option is focused for filterable multiselect
annawen1 Jan 9, 2025
c024b5c
chore: fix issue with no focus state dropdown
annawen1 Jan 10, 2025
9302de0
chore: resolve merge conflicts
annawen1 Jan 10, 2025
58634a9
chore: adjust focus stroke width
annawen1 Jan 10, 2025
f85e03a
chore: fix first selected item focus width
annawen1 Jan 13, 2025
233a717
Merge branch 'main' into chore/focus-states
annawen1 Jan 13, 2025
b717d58
chore: resolve merge conflict
annawen1 Jan 21, 2025
36e8e41
Merge branch 'main' into chore/focus-states
Kritvi-bhatia17 Jan 23, 2025
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
1 change: 0 additions & 1 deletion packages/react/src/components/ComboBox/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,6 @@ const ComboBox = forwardRef(
containerClassName,
{
[`${prefix}--list-box__wrapper--fluid--invalid`]: isFluid && invalid,
[`${prefix}--list-box__wrapper--fluid--focus`]: isFluid && isFocused,
[`${prefix}--list-box__wrapper--slug`]: slug,
[`${prefix}--list-box__wrapper--decorator`]: decorator,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,7 @@ export const Default = (args) => {
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 : '')}
{...args}
Expand Down
11 changes: 8 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,6 @@
[`${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--slug`]: slug,
[`${prefix}--list-box__wrapper--decorator`]: decorator,
}
Expand Down Expand Up @@ -518,7 +517,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 +547,12 @@
}, 3000)
);
}
if (['ArrowDown'].includes(evt.key)) {
setIsFocused(false);

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L551 was not covered by tests
}
if (['Enter'].includes(evt.key) && !selectedItem && !isOpen) {
setIsFocused(true);

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L554 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 @@ -105,7 +105,6 @@ const sharedArgTypes = {
export const Default = (args) => (
<div style={{ width: args.defaultWidth }}>
<FluidDropdown
initialSelectedItem={items[2]}
id="default"
titleText="Label"
label="Choose an option"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,6 @@
[`${prefix}--list-box__wrapper--inline--invalid`]: inline && invalid,
[`${prefix}--list-box--up`]: direction === 'top',
[`${prefix}--list-box__wrapper--fluid--invalid`]: isFluid && invalid,
[`${prefix}--list-box__wrapper--fluid--focus`]: isFluid && isFocused,
[`${prefix}--list-box__wrapper--slug`]: slug,
[`${prefix}--list-box__wrapper--decorator`]: decorator,
[`${prefix}--autoalign`]: autoAlign,
Expand Down Expand Up @@ -610,6 +609,10 @@
case InputKeyDownArrowDown:
if (InputKeyDownArrowDown === type && !isOpen) {
setIsOpen(true);
return {

Check warning on line 612 in packages/react/src/components/MultiSelect/FilterableMultiSelect.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L612 was not covered by tests
...changes,
highlightedIndex: 0,
};
}
if (highlightedIndex > -1) {
const itemArray = document.querySelectorAll(
Expand Down
13 changes: 11 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,6 @@ 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--slug`]: slug,
[`${prefix}--list-box__wrapper--decorator`]: decorator,
}
Expand Down
15 changes: 15 additions & 0 deletions packages/styles/scss/components/combo-box/_combo-box.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@
background-color: $field;
}

.#{$prefix}--combo-box
.#{$prefix}--list-box__menu-item:first-child.#{$prefix}--list-box__menu-item--highlighted::before {
position: absolute;
border: 2px solid $focus;
block-size: 100%;
border-block-start: 1px solid $focus;
content: '';
inline-size: 100%;
}

.#{$prefix}--combo-box
.#{$prefix}--list-box__menu-item:first-child.#{$prefix}--list-box__menu-item--highlighted {
@include focus-outline('reset');
}

// V11: Possibly deprecate
.#{$prefix}--combo-box.#{$prefix}--list-box--light:hover {
background-color: $field-02;
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 @@ -37,15 +37,6 @@
white-space: nowrap;
}

.#{$prefix}--list-box__wrapper--fluid.#{$prefix}--list-box__wrapper--fluid--focus
.#{$prefix}--combo-box.#{$prefix}--list-box--expanded:has(
input[aria-activedescendant]:not([aria-activedescendant=''])
)
.#{$prefix}--combo-box--input--focus.#{$prefix}--text-input {
outline-offset: convert.to-rem(-1px);
outline-width: convert.to-rem(1px);
}

.#{$prefix}--list-box__wrapper--fluid
.#{$prefix}--combo-box
.#{$prefix}--list-box__selection {
Expand Down
Loading
Loading