fix(tabs): prevent selection of disabled tabs.
This commit is contained in:
parent
eea05a5175
commit
eafa7d03d7
5 changed files with 212 additions and 29 deletions
5
.changeset/clever-tables-bow.md
Normal file
5
.changeset/clever-tables-bow.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/tabs': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Select first not-disabled tab if the first one is disabled.
|
||||||
5
.changeset/fair-adults-carry.md
Normal file
5
.changeset/fair-adults-carry.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/tabs': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Ensures that disabled tab elements are skipped when navigating a tab list with the keyboard.
|
||||||
|
|
@ -38,6 +38,31 @@ export const slotsOrder = () => html`
|
||||||
`;
|
`;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Disabled tabs
|
||||||
|
|
||||||
|
You can disable specific tabs by adding the `disabled` attribute to a tab button.
|
||||||
|
|
||||||
|
When using the arrow keys to switch tabs, it will skip disabled tabs, but it will not cycle from last to first or vice versa.
|
||||||
|
|
||||||
|
```js preview-story
|
||||||
|
export const tabsDisabled = () => html`
|
||||||
|
<lion-tabs>
|
||||||
|
<button slot="tab">tab 1</button>
|
||||||
|
<div slot="panel">panel 1</div>
|
||||||
|
<button slot="tab" disabled>tab 2</button>
|
||||||
|
<div slot="panel">panel 2</div>
|
||||||
|
<button slot="tab" disabled>tab 3</button>
|
||||||
|
<div slot="panel">panel 3</div>
|
||||||
|
<button slot="tab">tab 4</button>
|
||||||
|
<div slot="panel">panel 4</div>
|
||||||
|
<button slot="tab">tab 5</button>
|
||||||
|
<div slot="panel">panel 5</div>
|
||||||
|
<button slot="tab" disabled>tab 6</button>
|
||||||
|
<div slot="panel">panel 6</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
## Nesting tabs
|
## Nesting tabs
|
||||||
|
|
||||||
You can include tabs within tabs
|
You can include tabs within tabs
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,21 @@ export class LionTabs extends LitElement {
|
||||||
firstUpdated(changedProperties) {
|
firstUpdated(changedProperties) {
|
||||||
super.firstUpdated(changedProperties);
|
super.firstUpdated(changedProperties);
|
||||||
this.__setupSlots();
|
this.__setupSlots();
|
||||||
|
if (this.tabs[0].disabled) {
|
||||||
|
this.selectedIndex = this.tabs.findIndex(tab => !tab.disabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get tabs() {
|
||||||
|
return /** @type {HTMLButtonElement[]} */ (Array.from(this.children)).filter(
|
||||||
|
child => child.slot === 'tab',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get panels() {
|
||||||
|
return /** @type {HTMLElement[]} */ (Array.from(this.children)).filter(
|
||||||
|
child => child.slot === 'panel',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
|
|
@ -186,22 +201,16 @@ export class LionTabs extends LitElement {
|
||||||
__setupStore() {
|
__setupStore() {
|
||||||
/** @type {StoreEntry[]} */
|
/** @type {StoreEntry[]} */
|
||||||
this.__store = [];
|
this.__store = [];
|
||||||
const buttons = /** @type {HTMLElement[]} */ (Array.from(this.children)).filter(
|
if (this.tabs.length !== this.panels.length) {
|
||||||
child => child.slot === 'tab',
|
|
||||||
);
|
|
||||||
const panels = /** @type {HTMLElement[]} */ (Array.from(this.children)).filter(
|
|
||||||
child => child.slot === 'panel',
|
|
||||||
);
|
|
||||||
if (buttons.length !== panels.length) {
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn(
|
console.warn(
|
||||||
`The amount of tabs (${buttons.length}) doesn't match the amount of panels (${panels.length}).`,
|
`The amount of tabs (${this.tabs.length}) doesn't match the amount of panels (${this.panels.length}).`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
buttons.forEach((button, index) => {
|
this.tabs.forEach((button, index) => {
|
||||||
const uid = uuid();
|
const uid = uuid();
|
||||||
const panel = panels[index];
|
const panel = this.panels[index];
|
||||||
|
|
||||||
/** @type {StoreEntry} */
|
/** @type {StoreEntry} */
|
||||||
const entry = {
|
const entry = {
|
||||||
|
|
@ -235,6 +244,56 @@ export class LionTabs extends LitElement {
|
||||||
this.__store = [];
|
this.__store = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLButtonElement[]} tabs
|
||||||
|
* @param {HTMLButtonElement} currentTab
|
||||||
|
* @param {'right' | 'left'} dir
|
||||||
|
* @returns {HTMLButtonElement|undefined}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
__getNextNotDisabledTab(tabs, currentTab, dir) {
|
||||||
|
let orderedNotDisabledTabs = /** @type {HTMLButtonElement[]} */ ([]);
|
||||||
|
const nextNotDisabledTabs = tabs.filter((tab, i) => !tab.disabled && i > this.selectedIndex);
|
||||||
|
const prevNotDisabledTabs = tabs.filter((tab, i) => !tab.disabled && i < this.selectedIndex);
|
||||||
|
if (dir === 'right') {
|
||||||
|
orderedNotDisabledTabs = [...nextNotDisabledTabs, ...prevNotDisabledTabs];
|
||||||
|
} else {
|
||||||
|
orderedNotDisabledTabs = [...prevNotDisabledTabs.reverse(), ...nextNotDisabledTabs.reverse()];
|
||||||
|
}
|
||||||
|
return orderedNotDisabledTabs[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} newIndex
|
||||||
|
* @param {string} direction
|
||||||
|
* @returns {number}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
__getNextAvailableIndex(newIndex, direction) {
|
||||||
|
const currentTab = this.tabs[this.selectedIndex];
|
||||||
|
if (this.tabs.every(tab => !tab.disabled)) {
|
||||||
|
return newIndex;
|
||||||
|
}
|
||||||
|
if (direction === 'ArrowRight' || direction === 'ArrowDown') {
|
||||||
|
const nextNotDisabledTab = this.__getNextNotDisabledTab(this.tabs, currentTab, 'right');
|
||||||
|
return this.tabs.findIndex(tab => nextNotDisabledTab === tab);
|
||||||
|
}
|
||||||
|
if (direction === 'ArrowLeft' || direction === 'ArrowUp') {
|
||||||
|
const nextNotDisabledTab = this.__getNextNotDisabledTab(this.tabs, currentTab, 'left');
|
||||||
|
return this.tabs.findIndex(tab => nextNotDisabledTab === tab);
|
||||||
|
}
|
||||||
|
if (direction === 'Home') {
|
||||||
|
return this.tabs.findIndex(tab => !tab.disabled);
|
||||||
|
}
|
||||||
|
if (direction === 'End') {
|
||||||
|
const notDisabledTabs = this.tabs
|
||||||
|
.map((tab, i) => ({ disabled: tab.disabled, index: i }))
|
||||||
|
.filter(tab => !tab.disabled);
|
||||||
|
return notDisabledTabs[notDisabledTabs.length - 1].index;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} index
|
* @param {number} index
|
||||||
* @returns {(event: Event) => unknown}
|
* @returns {(event: Event) => unknown}
|
||||||
|
|
@ -257,24 +316,32 @@ export class LionTabs extends LitElement {
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
if (this.selectedIndex + 1 >= this._pairCount) {
|
if (this.selectedIndex + 1 >= this._pairCount) {
|
||||||
this._setSelectedIndexWithFocus(0);
|
this._setSelectedIndexWithFocus(this.__getNextAvailableIndex(0, _ev.key));
|
||||||
} else {
|
} else {
|
||||||
this._setSelectedIndexWithFocus(this.selectedIndex + 1);
|
this._setSelectedIndexWithFocus(
|
||||||
|
this.__getNextAvailableIndex(this.selectedIndex + 1, _ev.key),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
if (this.selectedIndex <= 0) {
|
if (this.selectedIndex <= 0) {
|
||||||
this._setSelectedIndexWithFocus(this._pairCount - 1);
|
this._setSelectedIndexWithFocus(
|
||||||
|
this.__getNextAvailableIndex(this._pairCount - 1, _ev.key),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this._setSelectedIndexWithFocus(this.selectedIndex - 1);
|
this._setSelectedIndexWithFocus(
|
||||||
|
this.__getNextAvailableIndex(this.selectedIndex - 1, _ev.key),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'Home':
|
case 'Home':
|
||||||
this._setSelectedIndexWithFocus(0);
|
this._setSelectedIndexWithFocus(this.__getNextAvailableIndex(0, _ev.key));
|
||||||
break;
|
break;
|
||||||
case 'End':
|
case 'End':
|
||||||
this._setSelectedIndexWithFocus(this._pairCount - 1);
|
this._setSelectedIndexWithFocus(
|
||||||
|
this.__getNextAvailableIndex(this._pairCount - 1, _ev.key),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
/* no default */
|
/* no default */
|
||||||
}
|
}
|
||||||
|
|
@ -297,6 +364,9 @@ export class LionTabs extends LitElement {
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_setSelectedIndexWithFocus(value) {
|
_setSelectedIndexWithFocus(value) {
|
||||||
|
if (value === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const stale = this.__selectedIndex;
|
const stale = this.__selectedIndex;
|
||||||
this.__selectedIndex = value;
|
this.__selectedIndex = value;
|
||||||
this.__updateSelected(true);
|
this.__updateSelected(true);
|
||||||
|
|
@ -323,16 +393,8 @@ export class LionTabs extends LitElement {
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const previousButton = /** @type {HTMLElement} */ (
|
const previousButton = this.tabs.find(child => child.hasAttribute('selected'));
|
||||||
Array.from(this.children).find(
|
const previousPanel = this.panels.find(child => child.hasAttribute('selected'));
|
||||||
child => child.slot === 'tab' && child.hasAttribute('selected'),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const previousPanel = /** @type {HTMLElement} */ (
|
|
||||||
Array.from(this.children).find(
|
|
||||||
child => child.slot === 'panel' && child.hasAttribute('selected'),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (previousButton) {
|
if (previousButton) {
|
||||||
deselectButton(previousButton);
|
deselectButton(previousButton);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ const basicTabs = html`
|
||||||
<div slot="panel">panel 2</div>
|
<div slot="panel">panel 2</div>
|
||||||
<button slot="tab">tab 3</button>
|
<button slot="tab">tab 3</button>
|
||||||
<div slot="panel">panel 3</div>
|
<div slot="panel">panel 3</div>
|
||||||
|
<button slot="tab">tab 4</button>
|
||||||
|
<div slot="panel">panel 4</div>
|
||||||
</lion-tabs>
|
</lion-tabs>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -216,7 +218,7 @@ describe('<lion-tabs>', () => {
|
||||||
const el = /** @type {LionTabs} */ (await fixture(basicTabs));
|
const el = /** @type {LionTabs} */ (await fixture(basicTabs));
|
||||||
const tabs = el.querySelectorAll('[slot=tab]');
|
const tabs = el.querySelectorAll('[slot=tab]');
|
||||||
tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'End' }));
|
tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'End' }));
|
||||||
expect(el.selectedIndex).to.equal(2);
|
expect(el.selectedIndex).to.equal(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('selects first tab on [arrow-right] if on last tab', async () => {
|
it('selects first tab on [arrow-right] if on last tab', async () => {
|
||||||
|
|
@ -254,6 +256,90 @@ describe('<lion-tabs>', () => {
|
||||||
tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' }));
|
tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' }));
|
||||||
expect(el.selectedIndex).to.equal(2);
|
expect(el.selectedIndex).to.equal(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('selects next available not disabled tab if first tab is disabled', async () => {
|
||||||
|
const el = /** @type {LionTabs} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<lion-tabs>
|
||||||
|
<button slot="tab" disabled>tab 1</button>
|
||||||
|
<div slot="panel">panel 1</div>
|
||||||
|
<button slot="tab">tab 2</button>
|
||||||
|
<div slot="panel">panel 2</div>
|
||||||
|
<button slot="tab">tab 3</button>
|
||||||
|
<div slot="panel">panel 3</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
expect(el.selectedIndex).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects next available not disabled tab on [arrow-right] and [arrow-down]', async () => {
|
||||||
|
const el = /** @type {LionTabs} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<lion-tabs>
|
||||||
|
<button slot="tab">tab 1</button>
|
||||||
|
<div slot="panel">panel 1</div>
|
||||||
|
<button slot="tab" disabled>tab 2</button>
|
||||||
|
<div slot="panel">panel 2</div>
|
||||||
|
<button slot="tab">tab 3</button>
|
||||||
|
<div slot="panel">panel 3</div>
|
||||||
|
<button slot="tab" disabled>tab 3</button>
|
||||||
|
<div slot="panel">panel 3</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
const tabs = el.querySelectorAll('[slot=tab]');
|
||||||
|
tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' }));
|
||||||
|
expect(el.selectedIndex).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects next available not disabled tab on [arrow-left] and [arrow-up]', async () => {
|
||||||
|
const el = /** @type {LionTabs} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<lion-tabs>
|
||||||
|
<button slot="tab">tab 1</button>
|
||||||
|
<div slot="panel">panel 1</div>
|
||||||
|
<button slot="tab" disabled>tab 2</button>
|
||||||
|
<div slot="panel">panel 2</div>
|
||||||
|
<button slot="tab">tab 3</button>
|
||||||
|
<div slot="panel">panel 3</div>
|
||||||
|
<button slot="tab" disabled>tab 3</button>
|
||||||
|
<div slot="panel">panel 3</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
el.selectedIndex = 2;
|
||||||
|
const tabs = el.querySelectorAll('[slot=tab]');
|
||||||
|
tabs[2].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' }));
|
||||||
|
expect(el.selectedIndex).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cycles through tabs even if the last or first tabs are disabled', async () => {
|
||||||
|
const el = /** @type {LionTabs} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<lion-tabs>
|
||||||
|
<button slot="tab" disabled>tab 0</button>
|
||||||
|
<div slot="panel">panel 0</div>
|
||||||
|
<button slot="tab">tab 1</button>
|
||||||
|
<div slot="panel">panel 1</div>
|
||||||
|
<button slot="tab">tab 2</button>
|
||||||
|
<div slot="panel">panel 2</div>
|
||||||
|
<button slot="tab" disabled>tab 3</button>
|
||||||
|
<div slot="panel">panel 3</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
const tabs = el.querySelectorAll('[slot=tab]');
|
||||||
|
el.selectedIndex = 1;
|
||||||
|
|
||||||
|
// 0 is disabled, same for 3, so should go to 2
|
||||||
|
tabs[1].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' }));
|
||||||
|
expect(el.selectedIndex).to.equal(2);
|
||||||
|
|
||||||
|
// 3 is disabled, same for 0, so should go to 1
|
||||||
|
tabs[2].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' }));
|
||||||
|
expect(el.selectedIndex).to.equal(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Content distribution', () => {
|
describe('Content distribution', () => {
|
||||||
|
|
@ -276,12 +362,12 @@ describe('<lion-tabs>', () => {
|
||||||
const selectedTab = Array.from(el.children).find(
|
const selectedTab = Array.from(el.children).find(
|
||||||
child => child.slot === 'tab' && child.hasAttribute('selected'),
|
child => child.slot === 'tab' && child.hasAttribute('selected'),
|
||||||
);
|
);
|
||||||
expect(selectedTab && selectedTab.textContent).to.equal('tab 5');
|
expect(selectedTab && selectedTab.textContent).to.equal('tab 6');
|
||||||
|
|
||||||
const selectedPanel = Array.from(el.children).find(
|
const selectedPanel = Array.from(el.children).find(
|
||||||
child => child.slot === 'panel' && child.hasAttribute('selected'),
|
child => child.slot === 'panel' && child.hasAttribute('selected'),
|
||||||
);
|
);
|
||||||
expect(selectedPanel && selectedPanel.textContent).to.equal('panel 5');
|
expect(selectedPanel && selectedPanel.textContent).to.equal('panel 6');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue