Merge pull request #1573 from jrobind/fix/disabledTabs
fix(tabs): prevent selection of disabled tabs.
This commit is contained in:
commit
1340c1bd61
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
|
||||
|
||||
You can include tabs within tabs
|
||||
|
|
|
|||
|
|
@ -164,6 +164,21 @@ export class LionTabs extends LitElement {
|
|||
firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
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 */
|
||||
|
|
@ -186,22 +201,16 @@ export class LionTabs extends LitElement {
|
|||
__setupStore() {
|
||||
/** @type {StoreEntry[]} */
|
||||
this.__store = [];
|
||||
const buttons = /** @type {HTMLElement[]} */ (Array.from(this.children)).filter(
|
||||
child => child.slot === 'tab',
|
||||
);
|
||||
const panels = /** @type {HTMLElement[]} */ (Array.from(this.children)).filter(
|
||||
child => child.slot === 'panel',
|
||||
);
|
||||
if (buttons.length !== panels.length) {
|
||||
if (this.tabs.length !== this.panels.length) {
|
||||
// eslint-disable-next-line no-console
|
||||
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 panel = panels[index];
|
||||
const panel = this.panels[index];
|
||||
|
||||
/** @type {StoreEntry} */
|
||||
const entry = {
|
||||
|
|
@ -235,6 +244,56 @@ export class LionTabs extends LitElement {
|
|||
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
|
||||
* @returns {(event: Event) => unknown}
|
||||
|
|
@ -257,24 +316,32 @@ export class LionTabs extends LitElement {
|
|||
case 'ArrowDown':
|
||||
case 'ArrowRight':
|
||||
if (this.selectedIndex + 1 >= this._pairCount) {
|
||||
this._setSelectedIndexWithFocus(0);
|
||||
this._setSelectedIndexWithFocus(this.__getNextAvailableIndex(0, _ev.key));
|
||||
} else {
|
||||
this._setSelectedIndexWithFocus(this.selectedIndex + 1);
|
||||
this._setSelectedIndexWithFocus(
|
||||
this.__getNextAvailableIndex(this.selectedIndex + 1, _ev.key),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'ArrowLeft':
|
||||
if (this.selectedIndex <= 0) {
|
||||
this._setSelectedIndexWithFocus(this._pairCount - 1);
|
||||
this._setSelectedIndexWithFocus(
|
||||
this.__getNextAvailableIndex(this._pairCount - 1, _ev.key),
|
||||
);
|
||||
} else {
|
||||
this._setSelectedIndexWithFocus(this.selectedIndex - 1);
|
||||
this._setSelectedIndexWithFocus(
|
||||
this.__getNextAvailableIndex(this.selectedIndex - 1, _ev.key),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
this._setSelectedIndexWithFocus(0);
|
||||
this._setSelectedIndexWithFocus(this.__getNextAvailableIndex(0, _ev.key));
|
||||
break;
|
||||
case 'End':
|
||||
this._setSelectedIndexWithFocus(this._pairCount - 1);
|
||||
this._setSelectedIndexWithFocus(
|
||||
this.__getNextAvailableIndex(this._pairCount - 1, _ev.key),
|
||||
);
|
||||
break;
|
||||
/* no default */
|
||||
}
|
||||
|
|
@ -297,6 +364,9 @@ export class LionTabs extends LitElement {
|
|||
* @protected
|
||||
*/
|
||||
_setSelectedIndexWithFocus(value) {
|
||||
if (value === -1) {
|
||||
return;
|
||||
}
|
||||
const stale = this.__selectedIndex;
|
||||
this.__selectedIndex = value;
|
||||
this.__updateSelected(true);
|
||||
|
|
@ -323,16 +393,8 @@ export class LionTabs extends LitElement {
|
|||
) {
|
||||
return;
|
||||
}
|
||||
const previousButton = /** @type {HTMLElement} */ (
|
||||
Array.from(this.children).find(
|
||||
child => child.slot === 'tab' && child.hasAttribute('selected'),
|
||||
)
|
||||
);
|
||||
const previousPanel = /** @type {HTMLElement} */ (
|
||||
Array.from(this.children).find(
|
||||
child => child.slot === 'panel' && child.hasAttribute('selected'),
|
||||
)
|
||||
);
|
||||
const previousButton = this.tabs.find(child => child.hasAttribute('selected'));
|
||||
const previousPanel = this.panels.find(child => child.hasAttribute('selected'));
|
||||
if (previousButton) {
|
||||
deselectButton(previousButton);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ const basicTabs = html`
|
|||
<div slot="panel">panel 2</div>
|
||||
<button slot="tab">tab 3</button>
|
||||
<div slot="panel">panel 3</div>
|
||||
<button slot="tab">tab 4</button>
|
||||
<div slot="panel">panel 4</div>
|
||||
</lion-tabs>
|
||||
`;
|
||||
|
||||
|
|
@ -216,7 +218,7 @@ describe('<lion-tabs>', () => {
|
|||
const el = /** @type {LionTabs} */ (await fixture(basicTabs));
|
||||
const tabs = el.querySelectorAll('[slot=tab]');
|
||||
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 () => {
|
||||
|
|
@ -254,6 +256,90 @@ describe('<lion-tabs>', () => {
|
|||
tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' }));
|
||||
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', () => {
|
||||
|
|
@ -276,12 +362,12 @@ describe('<lion-tabs>', () => {
|
|||
const selectedTab = Array.from(el.children).find(
|
||||
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(
|
||||
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