Skip to content

Commit

Permalink
fix(select-rich): allow arrowLeft and arrowRight to change the value …
Browse files Browse the repository at this point in the history
…when navigateWithinInvoker is true and dropdown is closed
  • Loading branch information
gerjanvangeest committed Jan 29, 2025
1 parent b983379 commit 1d9a328
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/spotty-emus-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lion/ui': patch
---

[select-rich] allow arrowLeft and arrowRight to change the value when navigateWithinInvoker is true and dropdown is closed
18 changes: 17 additions & 1 deletion packages/ui/components/select-rich/src/LionSelectRich.js
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
* @param {KeyboardEvent} ev
* @protected
*/
// TODO: rename to _onKeyUp in v1
// TODO: rename to #invokerOnKeyUp() (and move event listener to the invoker) in v1
__onKeyUp(ev) {
if (this.disabled || this.readOnly) {
return;
Expand Down Expand Up @@ -529,6 +529,22 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
this.opened = true;
}
break;
case 'ArrowLeft':
ev.preventDefault();
if (this.navigateWithinInvoker) {
this.setCheckedIndex(
this._getPreviousEnabledOption(/** @type {number} */ (this.checkedIndex)),
);
}
break;
case 'ArrowRight':
ev.preventDefault();
if (this.navigateWithinInvoker) {
this.setCheckedIndex(
this._getNextEnabledOption(/** @type {number} */ (this.checkedIndex)),
);
}
break;
default:
if (!this._noTypeAhead) {
this._handleTypeAhead(ev, { setAsChecked: true });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ function mimicKeyPress(el, key, code = '') {
el.dispatchEvent(new KeyboardEvent('keyup', { key, code }));
}

/**
* @param {LionOption[]} options
* @param {number} selectedIndex
*/
function expectOnlyGivenOneOptionToBeChecked(options, selectedIndex) {
/**
* @param {{ checked: any; }} option
* @param {any} i
*/
options.forEach((option, i) => {
if (i === selectedIndex) {
expect(option.checked).to.be.true;
} else {
expect(option.checked).to.be.false;
}
});
}

describe('lion-select-rich interactions', () => {
describe('Interaction mode', () => {
it('autodetects interactionMode if not defined', async () => {
Expand Down Expand Up @@ -86,26 +104,91 @@ describe('lion-select-rich interactions', () => {
});
});

describe('Invoker Keyboard navigation Windows', () => {
it('navigates through list with [ArrowDown] [ArrowUp] keys checks the option while listbox unopened', async () => {
/**
* @param {LionOption[]} options
* @param {number} selectedIndex
*/
function expectOnlyGivenOneOptionToBeChecked(options, selectedIndex) {
/**
* @param {{ checked: any; }} option
* @param {any} i
*/
options.forEach((option, i) => {
if (i === selectedIndex) {
expect(option.checked).to.be.true;
} else {
expect(option.checked).to.be.false;
}
});
}
describe('Invoker Keyboard navigation Mac', () => {
it('opens dropdown with [ArrowDown] [ArrowUp] keys or navigates through the listbox options', async () => {
const el = /** @type {LionSelectRich} */ (
await fixture(html`
<lion-select-rich interaction-mode="mac">
<lion-options slot="input">
<lion-option .choiceValue=${10}>Item 1</lion-option>
<lion-option .choiceValue=${20}>Item 2</lion-option>
<lion-option .choiceValue=${30}>Item 3</lion-option>
</lion-options>
</lion-select-rich>
`)
);

const options = el.formElements;
expect(el.checkedIndex).to.equal(0);
expectOnlyGivenOneOptionToBeChecked(options, 0);

el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));

expect(el.opened).to.be.true;
expect(el.checkedIndex).to.equal(0);
expectOnlyGivenOneOptionToBeChecked(options, 0);

el.opened = false;

el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' }));
expect(el.opened).to.be.true;
expect(el.checkedIndex).to.equal(0);
expectOnlyGivenOneOptionToBeChecked(options, 0);
});

it('does not open dropdown with [ArrowLeft] [ArrowRight] keys or navigates through the listbox options', async () => {
const el = /** @type {LionSelectRich} */ (
await fixture(html`
<lion-select-rich interaction-mode="mac">
<lion-options slot="input">
<lion-option .choiceValue=${10}>Item 1</lion-option>
<lion-option .choiceValue=${20}>Item 2</lion-option>
<lion-option .choiceValue=${30}>Item 3</lion-option>
</lion-options>
</lion-select-rich>
`)
);

const options = el.formElements;
expect(el.checkedIndex).to.equal(0);
expectOnlyGivenOneOptionToBeChecked(options, 0);

el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' }));

expect(el.opened).to.be.false;
expect(el.checkedIndex).to.equal(0);
expectOnlyGivenOneOptionToBeChecked(options, 0);

el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' }));
expect(el.opened).to.be.false;
expect(el.checkedIndex).to.equal(0);
expectOnlyGivenOneOptionToBeChecked(options, 0);
});

it('checks a value with [character] keys while listbox unopened', async () => {
const el = /** @type {LionSelectRich} */ (
await fixture(html`
<lion-select-rich interaction-mode="mac">
<lion-options slot="input">
<lion-option .choiceValue=${'red'}>Red</lion-option>
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
<lion-option .choiceValue=${'turquoise'}>Turquoise</lion-option>
</lion-options>
</lion-select-rich>
`)
);

// @ts-ignore [allow-private] in test
mimicKeyPress(el, 't', 'KeyT');
expect(el.checkedIndex).to.equal(1);

mimicKeyPress(el, 'u', 'KeyU');
expect(el.checkedIndex).to.equal(2);
});
});

describe('Invoker Keyboard navigation Windows/Linux', () => {
it('navigates through list with [ArrowDown] [ArrowUp] keys checks the option while listbox unopened', async () => {
let isTriggeredByUser;
const el = /** @type {LionSelectRich} */ (
await fixture(html`
Expand Down Expand Up @@ -141,7 +224,43 @@ describe('lion-select-rich interactions', () => {
expect(isTriggeredByUser).to.be.true;
});

it('checkes a value with [character] keys while listbox unopened', async () => {
it('navigates through list with [ArrowLeft] [ArrowRight] keys checks the option while listbox unopened', async () => {
let isTriggeredByUser;
const el = /** @type {LionSelectRich} */ (
await fixture(html`
<lion-select-rich
interaction-mode="windows/linux"
@model-value-changed="${(/** @type {CustomEvent} */ event) => {
isTriggeredByUser = event.detail.isTriggeredByUser;
}}"
>
<lion-options slot="input">
<lion-option .choiceValue=${10}>Item 1</lion-option>
<lion-option .choiceValue=${20}>Item 2</lion-option>
<lion-option .choiceValue=${30}>Item 3</lion-option>
</lion-options>
</lion-select-rich>
`)
);

const options = el.formElements;
expect(el.checkedIndex).to.equal(0);
expectOnlyGivenOneOptionToBeChecked(options, 0);

el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' }));
expect(el.checkedIndex).to.equal(1);
expectOnlyGivenOneOptionToBeChecked(options, 1);
expect(isTriggeredByUser).to.be.true;

isTriggeredByUser = false;

el.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' }));
expect(el.checkedIndex).to.equal(0);
expectOnlyGivenOneOptionToBeChecked(options, 0);
expect(isTriggeredByUser).to.be.true;
});

it('checks a value with [character] keys while listbox unopened', async () => {
const el = /** @type {LionSelectRich} */ (
await fixture(html`
<lion-select-rich interaction-mode="windows/linux">
Expand Down

0 comments on commit 1d9a328

Please sign in to comment.