feat(tabs): create tabs component
Co-authored-by: CubLion <Alex.Ghiu@ing.com> Co-authored-by: erikkroes <Erik.Kroes@ing.com>
This commit is contained in:
parent
b3b1abe200
commit
7a562a63a3
10 changed files with 871 additions and 31 deletions
63
README.md
63
README.md
|
|
@ -29,37 +29,38 @@ npm i @lion/<package-name>
|
||||||
|
|
||||||
The accessibility column indicates whether the functionality is accessible in its core. Aspects like styling and content determine actual accessibility in usage.
|
The accessibility column indicates whether the functionality is accessible in its core. Aspects like styling and content determine actual accessibility in usage.
|
||||||
|
|
||||||
| Package | Version | Description | Accessibility |
|
| Package | Version | Description | Accessibility |
|
||||||
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | -------------------------------------------------------------------- |
|
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ | -------------------------------------------------------------------- |
|
||||||
| [core](./packages/core) | [](https://www.npmjs.com/package/@lion/core) | Core System (exports LitElement, lit-html) | n/a |
|
| [core](./packages/core) | [](https://www.npmjs.com/package/@lion/core) | Core System (exports LitElement, lit-html) | n/a |
|
||||||
| [localize](./packages/localize) | [](https://www.npmjs.com/package/@lion/localize) | Localize and translate your application/components | n/a |
|
| [localize](./packages/localize) | [](https://www.npmjs.com/package/@lion/localize) | Localize and translate your application/components | n/a |
|
||||||
| [ajax](./packages/ajax) | [](https://www.npmjs.com/package/@lion/ajax) | Fetching data via ajax request | n/a |
|
| [ajax](./packages/ajax) | [](https://www.npmjs.com/package/@lion/ajax) | Fetching data via ajax request | n/a |
|
||||||
| [button](./packages/button) | [](https://www.npmjs.com/package/@lion/button) | Button | [#64][i64] |
|
| [button](./packages/button) | [](https://www.npmjs.com/package/@lion/button) | Button | [#64][i64] |
|
||||||
| [switch](./packages/switch) | [](https://www.npmjs.com/package/@lion/switch) | Switch | ✔️ |
|
| [switch](./packages/switch) | [](https://www.npmjs.com/package/@lion/switch) | Switch | ✔️ |
|
||||||
| [calendar](./packages/calendar) | [](https://www.npmjs.com/package/@lion/calendar) | Standalone calendar | [#195][i195], [#194][i194], [#193][i193], [#191][i191] |
|
| [calendar](./packages/calendar) | [](https://www.npmjs.com/package/@lion/calendar) | Standalone calendar | [#195][i195], [#194][i194], [#193][i193], [#191][i191] |
|
||||||
| [icon](./packages/icon) | [](https://www.npmjs.com/package/@lion/icon) | Display our svg icons | [#173][i173], [#172][i172] |
|
| [icon](./packages/icon) | [](https://www.npmjs.com/package/@lion/icon) | Display our svg icons | [#173][i173], [#172][i172] |
|
||||||
| [steps](./packages/steps) | [](https://www.npmjs.com/package/@lion/steps) | Multi Step System | n/a |
|
| [steps](./packages/steps) | [](https://www.npmjs.com/package/@lion/steps) | Multi Step System | n/a |
|
||||||
| **-- Forms --** | | |
|
| [tabs](./packages/tabs) | [](https://www.npmjs.com/package/@lion/tabs) | Move between a small number of equally important views | n/a |
|
||||||
| [form](./packages/form) | [](https://www.npmjs.com/package/@lion/form) | Wrapper for multiple form elements | ✔️ |
|
| **-- Forms --** | | |
|
||||||
| [field](./packages/field) | [](https://www.npmjs.com/package/@lion/field) | Base Class for all inputs | [#190][i190] |
|
| [form](./packages/form) | [](https://www.npmjs.com/package/@lion/form) | Wrapper for multiple form elements | ✔️ |
|
||||||
| [fieldset](./packages/fieldset) | [](https://www.npmjs.com/package/@lion/fieldset) | Group for form inputs | ✔️ |
|
| [field](./packages/field) | [](https://www.npmjs.com/package/@lion/field) | Base Class for all inputs | [#190][i190] |
|
||||||
| [validate](./packages/validate) | [](https://www.npmjs.com/package/@lion/validate) | Validation for form components | n/a |
|
| [fieldset](./packages/fieldset) | [](https://www.npmjs.com/package/@lion/fieldset) | Group for form inputs | ✔️ |
|
||||||
| [checkbox](./packages/checkbox) | [](https://www.npmjs.com/package/@lion/checkbox) | Checkbox form element | ✔️ |
|
| [validate](./packages/validate) | [](https://www.npmjs.com/package/@lion/validate) | Validation for form components | n/a |
|
||||||
| [checkbox-group](./packages/checkbox-group) | [](https://www.npmjs.com/package/@lion/checkbox-group) | Group of checkboxes | ✔️ |
|
| [checkbox](./packages/checkbox) | [](https://www.npmjs.com/package/@lion/checkbox) | Checkbox form element | ✔️ |
|
||||||
| [input](./packages/input) | [](https://www.npmjs.com/package/@lion/input) | Input element for strings | ✔️ |
|
| [checkbox-group](./packages/checkbox-group) | [](https://www.npmjs.com/package/@lion/checkbox-group) | Group of checkboxes | ✔️ |
|
||||||
| [input-amount](./packages/input-amount) | [](https://www.npmjs.com/package/@lion/input-amount) | Input element for amounts | [#166][i166] | ✔️ |
|
| [input](./packages/input) | [](https://www.npmjs.com/package/@lion/input) | Input element for strings | ✔️ |
|
||||||
| [input-date](./packages/input-date) | [](https://www.npmjs.com/package/@lion/input-date) | Input element for dates | ✔️ |
|
| [input-amount](./packages/input-amount) | [](https://www.npmjs.com/package/@lion/input-amount) | Input element for amounts | [#166][i166] | ✔️ |
|
||||||
| [input-datepicker](./packages/input-datepicker) | [](https://www.npmjs.com/package/@lion/input-datepicker) | Input element for dates with a datepicker | ✔️ |
|
| [input-date](./packages/input-date) | [](https://www.npmjs.com/package/@lion/input-date) | Input element for dates | ✔️ |
|
||||||
| [input-email](./packages/input-email) | [](https://www.npmjs.com/package/@lion/input-email) | Input element for e-mails | [#169][i169] |
|
| [input-datepicker](./packages/input-datepicker) | [](https://www.npmjs.com/package/@lion/input-datepicker) | Input element for dates with a datepicker | ✔️ |
|
||||||
| [input-iban](./packages/input-iban) | [](https://www.npmjs.com/package/@lion/input-iban) | Input element for IBANs | [#169][i169] |
|
| [input-email](./packages/input-email) | [](https://www.npmjs.com/package/@lion/input-email) | Input element for e-mails | [#169][i169] |
|
||||||
| [radio](./packages/radio) | [](https://www.npmjs.com/package/@lion/radio) | Radio from element | ✔️ |
|
| [input-iban](./packages/input-iban) | [](https://www.npmjs.com/package/@lion/input-iban) | Input element for IBANs | [#169][i169] |
|
||||||
| [radio-group](./packages/radio-group) | [](https://www.npmjs.com/package/@lion/radio-group) | Group of radios | ✔️ |
|
| [radio](./packages/radio) | [](https://www.npmjs.com/package/@lion/radio) | Radio from element | ✔️ |
|
||||||
| [select](./packages/select) | [](https://www.npmjs.com/package/@lion/select) | Simple native dropdown element | ✔️ |
|
| [radio-group](./packages/radio-group) | [](https://www.npmjs.com/package/@lion/radio-group) | Group of radios | ✔️ |
|
||||||
| [textarea](./packages/textarea) | [](https://www.npmjs.com/package/@lion/textarea) | Multiline text input | [#165][i165] |
|
| [select](./packages/select) | [](https://www.npmjs.com/package/@lion/select) | Simple native dropdown element | ✔️ |
|
||||||
| **-- Overlays --** | | | |
|
| [textarea](./packages/textarea) | [](https://www.npmjs.com/package/@lion/textarea) | Multiline text input | [#165][i165] |
|
||||||
| [overlays](./packages/overlays) | [](https://www.npmjs.com/package/@lion/overlays) | Overlays System using lit-html for rendering | ✔️ |
|
| **-- Overlays --** | | | |
|
||||||
| [popup](./packages/popup) | [](https://www.npmjs.com/package/@lion/popup) | Popup element | [#175][i175], [#174][i174] |
|
| [overlays](./packages/overlays) | [](https://www.npmjs.com/package/@lion/overlays) | Overlays System using lit-html for rendering | ✔️ |
|
||||||
| [tooltip](./packages/tooltip) | [](https://www.npmjs.com/package/@lion/tooltip) | Popup element | [#178][i178], [#177][i177], [#176][i176], [#175][i175], [#174][i174] |
|
| [popup](./packages/popup) | [](https://www.npmjs.com/package/@lion/popup) | Popup element | [#175][i175], [#174][i174] |
|
||||||
|
| [tooltip](./packages/tooltip) | [](https://www.npmjs.com/package/@lion/tooltip) | Popup element | [#178][i178], [#177][i177], [#176][i176], [#175][i175], [#174][i174] |
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
|
|
||||||
|
|
|
||||||
4
packages/tabs/CHANGELOG.md
Normal file
4
packages/tabs/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
51
packages/tabs/README.md
Normal file
51
packages/tabs/README.md
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Tabs
|
||||||
|
|
||||||
|
`lion-tabs` implements Tabs view to allow users to quickly move between a small number of equally important views
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i --save @lion/tabs;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```js
|
||||||
|
import '@lion/tabs/lion-tabs.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<lion-tabs>
|
||||||
|
<button slot="tab">Info</button>
|
||||||
|
<p slot="panel">
|
||||||
|
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Laboriosam sequi odit cumque, enim aut
|
||||||
|
assumenda itaque quis voluptas est quos fugiat unde labore reiciendis saepe, iure, optio
|
||||||
|
officiis obcaecati quibusdam.
|
||||||
|
</p>
|
||||||
|
<div slot="tab">About</div>
|
||||||
|
<p slot="panel">
|
||||||
|
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Laboriosam sequi odit cumque, enim aut
|
||||||
|
assumenda itaque quis voluptas est quos fugiat unde labore reiciendis saepe, iure, optio
|
||||||
|
officiis obcaecati quibusdam.
|
||||||
|
</p>
|
||||||
|
</lion-tabs>
|
||||||
|
```
|
||||||
|
|
||||||
|
Rationales:
|
||||||
|
|
||||||
|
- **No separate active/focus state when using keyboard**
|
||||||
|
|
||||||
|
We will immediately switch content as all our content comes from light dom (e.g. no latency)
|
||||||
|
|
||||||
|
See Note at <https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-19>
|
||||||
|
|
||||||
|
> It is recommended that tabs activate automatically when they receive focus as long as their
|
||||||
|
> associated tab panels are displayed without noticeable latency. This typically requires tab
|
||||||
|
> panel content to be preloaded.
|
||||||
|
|
||||||
|
- **Panels are not focusable**
|
||||||
|
|
||||||
|
Focusable elements should have a means to interact with them. Tab panels themselves do not offer any interactiveness.
|
||||||
|
If there is a button or a form inside the tab panel then these elements get focused directly.
|
||||||
1
packages/tabs/index.js
Normal file
1
packages/tabs/index.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { LionTabs } from './src/LionTabs.js';
|
||||||
3
packages/tabs/lion-tabs.js
Normal file
3
packages/tabs/lion-tabs.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { LionTabs } from './src/LionTabs.js';
|
||||||
|
|
||||||
|
customElements.define('lion-tabs', LionTabs);
|
||||||
42
packages/tabs/package.json
Normal file
42
packages/tabs/package.json
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"name": "@lion/tabs",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Allows users to quickly move between a small number of equally important views.",
|
||||||
|
"author": "ing-bank",
|
||||||
|
"homepage": "https://github.com/ing-bank/lion/",
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/ing-bank/lion.git",
|
||||||
|
"directory": "packages/tabs"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"prepublishOnly": "../../scripts/npm-prepublish.js"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"lion",
|
||||||
|
"web-components",
|
||||||
|
"tabs"
|
||||||
|
],
|
||||||
|
"main": "index.js",
|
||||||
|
"module": "index.js",
|
||||||
|
"files": [
|
||||||
|
"docs",
|
||||||
|
"src",
|
||||||
|
"stories",
|
||||||
|
"test",
|
||||||
|
"translations",
|
||||||
|
"*.js"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@lion/core": "^0.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@open-wc/demoing-storybook": "^0.2.0",
|
||||||
|
"@open-wc/testing": "^2.3.4",
|
||||||
|
"sinon": "^7.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
235
packages/tabs/src/LionTabs.js
Normal file
235
packages/tabs/src/LionTabs.js
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
import { LitElement, css, html } from '@lion/core';
|
||||||
|
|
||||||
|
const uuid = () =>
|
||||||
|
Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substr(2, 10);
|
||||||
|
|
||||||
|
const setupPanel = ({ element, uid }) => {
|
||||||
|
element.setAttribute('id', `panel-${uid}`);
|
||||||
|
element.setAttribute('role', 'tabpanel');
|
||||||
|
element.setAttribute('aria-labelledby', `button-${uid}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectPanel = element => {
|
||||||
|
element.setAttribute('selected', true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deselectPanel = element => {
|
||||||
|
element.removeAttribute('selected');
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupButton = ({ element, uid, clickHandler, keydownHandler }) => {
|
||||||
|
element.setAttribute('id', `button-${uid}`);
|
||||||
|
element.setAttribute('role', 'tab');
|
||||||
|
element.setAttribute('aria-controls', `panel-${uid}`);
|
||||||
|
element.addEventListener('click', clickHandler);
|
||||||
|
element.addEventListener('keyup', keydownHandler);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanButton = (element, clickHandler, keydownHandler) => {
|
||||||
|
element.removeAttribute('id');
|
||||||
|
element.removeAttribute('role');
|
||||||
|
element.removeAttribute('aria-controls');
|
||||||
|
element.removeEventListener('click', clickHandler);
|
||||||
|
element.removeEventListener('keyup', keydownHandler);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectButton = element => {
|
||||||
|
element.focus();
|
||||||
|
element.setAttribute('selected', true);
|
||||||
|
element.setAttribute('aria-selected', true);
|
||||||
|
element.setAttribute('tabindex', 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deselectButton = element => {
|
||||||
|
element.removeAttribute('selected');
|
||||||
|
element.setAttribute('aria-selected', false);
|
||||||
|
element.setAttribute('tabindex', -1);
|
||||||
|
};
|
||||||
|
|
||||||
|
export class LionTabs extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* index number of the selected tab
|
||||||
|
*/
|
||||||
|
selectedIndex: {
|
||||||
|
type: Number,
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return [
|
||||||
|
css`
|
||||||
|
.tabs__tab-group {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs__tab-group ::slotted([slot='tab'][selected]) {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs__panels ::slotted([slot='panel']) {
|
||||||
|
visibility: hidden;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs__panels ::slotted([slot='panel'][selected]) {
|
||||||
|
visibility: visible;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs__panels {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div class="tabs__tab-group" role="tablist">
|
||||||
|
<slot name="tab"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="tabs__panels">
|
||||||
|
<slot name="panel"></slot>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated() {
|
||||||
|
super.firstUpdated();
|
||||||
|
this.__setupSlots();
|
||||||
|
}
|
||||||
|
|
||||||
|
__setupSlots() {
|
||||||
|
const tabSlot = this.shadowRoot.querySelector('slot[name=tab]');
|
||||||
|
const handleSlotChange = () => {
|
||||||
|
this.__cleanStore();
|
||||||
|
this.__setupStore();
|
||||||
|
this.__updateSelected();
|
||||||
|
};
|
||||||
|
tabSlot.addEventListener('slotchange', handleSlotChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
__setupStore() {
|
||||||
|
this.__store = [];
|
||||||
|
const buttons = this.querySelectorAll('[slot="tab"]');
|
||||||
|
const panels = this.querySelectorAll('[slot="panel"]');
|
||||||
|
if (buttons.length !== 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}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.forEach((button, index) => {
|
||||||
|
const uid = uuid();
|
||||||
|
const panel = panels[index];
|
||||||
|
const entry = {
|
||||||
|
uid,
|
||||||
|
button,
|
||||||
|
panel,
|
||||||
|
clickHandler: this.__createButtonClickHandler(index),
|
||||||
|
keydownHandler: this.__handleButtonKeydown.bind(this),
|
||||||
|
};
|
||||||
|
setupPanel({ element: entry.panel, ...entry });
|
||||||
|
setupButton({ element: entry.button, ...entry });
|
||||||
|
deselectPanel(entry.panel);
|
||||||
|
deselectButton(entry.button);
|
||||||
|
this.__store.push(entry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
__cleanStore() {
|
||||||
|
if (!this.__store) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.__store.forEach(entry => {
|
||||||
|
cleanButton(entry.button, entry.clickHandler, entry.keydownHandler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
__createButtonClickHandler(index) {
|
||||||
|
return () => {
|
||||||
|
this.selectedIndex = index;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
__handleButtonKeydown(e) {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
case 'ArrowRight':
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.selectedIndex + 1 >= this._pairCount) {
|
||||||
|
this.selectedIndex = 0;
|
||||||
|
} else {
|
||||||
|
this.selectedIndex += 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
case 'ArrowLeft':
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.selectedIndex <= 0) {
|
||||||
|
this.selectedIndex = this._pairCount - 1;
|
||||||
|
} else {
|
||||||
|
this.selectedIndex -= 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Home':
|
||||||
|
e.preventDefault();
|
||||||
|
this.selectedIndex = 0;
|
||||||
|
break;
|
||||||
|
case 'End':
|
||||||
|
e.preventDefault();
|
||||||
|
this.selectedIndex = this._pairCount - 1;
|
||||||
|
break;
|
||||||
|
/* no default */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set selectedIndex(value) {
|
||||||
|
const stale = this.__selectedIndex;
|
||||||
|
this.__selectedIndex = value;
|
||||||
|
this.__updateSelected();
|
||||||
|
this.dispatchEvent(new Event('selected-changed'));
|
||||||
|
this.requestUpdate('selectedIndex', stale);
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedIndex() {
|
||||||
|
return this.__selectedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
get _pairCount() {
|
||||||
|
return this.__store.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
__updateSelected() {
|
||||||
|
if (!(this.__store && this.__store[this.selectedIndex])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previousButton = this.querySelector('[slot="tab"][selected]');
|
||||||
|
const previousPanel = this.querySelector('[slot="panel"][selected]');
|
||||||
|
if (previousButton) {
|
||||||
|
deselectButton(previousButton);
|
||||||
|
}
|
||||||
|
if (previousPanel) {
|
||||||
|
deselectPanel(previousPanel);
|
||||||
|
}
|
||||||
|
const { button: currentButton, panel: currentPanel } = this.__store[this.selectedIndex];
|
||||||
|
if (currentButton) {
|
||||||
|
selectButton(currentButton);
|
||||||
|
}
|
||||||
|
if (currentPanel) {
|
||||||
|
selectPanel(currentPanel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
168
packages/tabs/stories/index.stories.js
Normal file
168
packages/tabs/stories/index.stories.js
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
import { storiesOf, html } from '@open-wc/demoing-storybook';
|
||||||
|
import { LitElement, css } from '@lion/core';
|
||||||
|
import '../lion-tabs.js';
|
||||||
|
|
||||||
|
const tabsDemoStyle = css`
|
||||||
|
.demo-tabs__tab[selected] {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
storiesOf('Tabs', module)
|
||||||
|
.add(
|
||||||
|
'Default',
|
||||||
|
() => html`
|
||||||
|
<style>
|
||||||
|
${tabsDemoStyle}
|
||||||
|
</style>
|
||||||
|
<lion-tabs>
|
||||||
|
<button slot="tab" class="demo-tabs__tab">Info</button>
|
||||||
|
<div slot="panel">
|
||||||
|
<h2>Info</h2>
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Laboriosam sequi odit cumque,
|
||||||
|
enim aut assumenda itaque quis voluptas est quos fugiat unde labore reiciendis saepe,
|
||||||
|
iure, optio officiis obcaecati quibusdam.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button slot="tab" class="demo-tabs__tab">About</button>
|
||||||
|
<div slot="panel">
|
||||||
|
<h2>About</h2>
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Laboriosam sequi odit cumque,
|
||||||
|
enim aut assumenda itaque quis voluptas est quos fugiat unde labore reiciendis saepe,
|
||||||
|
iure, optio officiis obcaecati quibusdam.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
'selectedIndex',
|
||||||
|
() => html`
|
||||||
|
<lion-tabs .selectedIndex=${1}>
|
||||||
|
<button slot="tab">Info</button>
|
||||||
|
<button slot="tab">About</button>
|
||||||
|
<div slot="panel">
|
||||||
|
<h2>Info</h2>
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Laboriosam sequi odit cumque,
|
||||||
|
enim aut assumenda itaque quis voluptas est quos fugiat unde labore reiciendis saepe,
|
||||||
|
iure, optio officiis obcaecati quibusdam.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div slot="panel">
|
||||||
|
<h2>About</h2>
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Laboriosam sequi odit cumque,
|
||||||
|
enim aut assumenda itaque quis voluptas est quos fugiat unde labore reiciendis saepe,
|
||||||
|
iure, optio officiis obcaecati quibusdam.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
'Slots order',
|
||||||
|
() => html`
|
||||||
|
<lion-tabs>
|
||||||
|
<button slot="tab">Info</button>
|
||||||
|
<button slot="tab">About</button>
|
||||||
|
<div slot="panel">
|
||||||
|
<h2>Info</h2>
|
||||||
|
<p>This is exactly the same just in the code it's differently ordered.</p>
|
||||||
|
</div>
|
||||||
|
<div slot="panel">
|
||||||
|
<h2>About</h2>
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Laboriosam sequi odit cumque,
|
||||||
|
enim aut assumenda itaque quis voluptas est quos fugiat unde labore reiciendis saepe,
|
||||||
|
iure, optio officiis obcaecati quibusdam.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add('Distribute new elements', () => {
|
||||||
|
const tagName = 'lion-tabs-experimental';
|
||||||
|
if (!customElements.get(tagName)) {
|
||||||
|
customElements.define(
|
||||||
|
tagName,
|
||||||
|
class extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
__collection: { type: Array },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<h3>Append</h3>
|
||||||
|
<button @click="${this.__handleAppendClick}">
|
||||||
|
Append
|
||||||
|
</button>
|
||||||
|
<lion-tabs id="appendTabs">
|
||||||
|
<button slot="tab">tab 1</button>
|
||||||
|
<p slot="panel">panel 1</p>
|
||||||
|
<button slot="tab">tab 2</button>
|
||||||
|
<p slot="panel">panel 2</p>
|
||||||
|
</lion-tabs>
|
||||||
|
<hr />
|
||||||
|
<h3>Push</h3>
|
||||||
|
<button @click="${this.__handlePushClick}">
|
||||||
|
Push
|
||||||
|
</button>
|
||||||
|
<lion-tabs id="pushTabs">
|
||||||
|
<button slot="tab">tab 1</button>
|
||||||
|
<p slot="panel">panel 1</p>
|
||||||
|
<button slot="tab">tab 2</button>
|
||||||
|
<p slot="panel">panel 2</p>
|
||||||
|
${this.__collection.map(
|
||||||
|
item => html`
|
||||||
|
<button slot="tab">${item.button}</button>
|
||||||
|
<p slot="panel">${item.panel}</p>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</lion-tabs>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.__collection = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
__handleAppendClick() {
|
||||||
|
const tabsElement = this.shadowRoot.querySelector('#appendTabs');
|
||||||
|
const c = 2;
|
||||||
|
const n = Math.floor(tabsElement.children.length / 2);
|
||||||
|
for (let i = n + 1; i < n + c; i += 1) {
|
||||||
|
const tab = document.createElement('button');
|
||||||
|
tab.setAttribute('slot', 'tab');
|
||||||
|
tab.innerText = `tab ${i}`;
|
||||||
|
const panel = document.createElement('p');
|
||||||
|
panel.setAttribute('slot', 'panel');
|
||||||
|
panel.innerText = `panel ${i}`;
|
||||||
|
tabsElement.append(tab);
|
||||||
|
tabsElement.append(panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__handlePushClick() {
|
||||||
|
const tabsElement = this.shadowRoot.querySelector('#pushTabs');
|
||||||
|
const i = Math.floor(tabsElement.children.length / 2) + 1;
|
||||||
|
this.__collection = [
|
||||||
|
...this.__collection,
|
||||||
|
{
|
||||||
|
button: `tab ${i}`,
|
||||||
|
panel: `panel ${i}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<lion-tabs-experimental></lion-tabs-experimental>
|
||||||
|
`;
|
||||||
|
});
|
||||||
333
packages/tabs/test/lion-tabs.test.js
Normal file
333
packages/tabs/test/lion-tabs.test.js
Normal file
|
|
@ -0,0 +1,333 @@
|
||||||
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import '../lion-tabs.js';
|
||||||
|
|
||||||
|
const basicTabs = html`
|
||||||
|
<lion-tabs>
|
||||||
|
<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">tab 3</button>
|
||||||
|
<div slot="panel">panel 3</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`;
|
||||||
|
|
||||||
|
describe('<lion-tabs>', () => {
|
||||||
|
describe('Tabs', () => {
|
||||||
|
it('sets selectedIndex to 0 by default', async () => {
|
||||||
|
const el = await fixture(basicTabs);
|
||||||
|
expect(el.selectedIndex).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can programmatically set selectedIndex', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-tabs .selectedIndex=${1}>
|
||||||
|
<div slot="tab">tab 1</div>
|
||||||
|
<div slot="panel">panel 1</div>
|
||||||
|
<div slot="tab">tab 2</div>
|
||||||
|
<div slot="panel">panel 2</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`);
|
||||||
|
expect(el.selectedIndex).to.equal(1);
|
||||||
|
expect(el.querySelector('[slot=tab][selected]').textContent).to.equal('tab 2');
|
||||||
|
|
||||||
|
el.selectedIndex = 0;
|
||||||
|
expect(el.querySelector('[slot=tab][selected]').textContent).to.equal('tab 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has [selected] on current selected tab which serves as styling hook', async () => {
|
||||||
|
const el = await fixture(basicTabs);
|
||||||
|
const tabs = el.querySelectorAll('[slot=tab]');
|
||||||
|
el.selectedIndex = 0;
|
||||||
|
expect(tabs[0]).to.have.attribute('selected');
|
||||||
|
expect(tabs[1]).to.not.have.attribute('selected');
|
||||||
|
|
||||||
|
el.selectedIndex = 1;
|
||||||
|
expect(tabs[0]).to.not.have.attribute('selected');
|
||||||
|
expect(tabs[1]).to.have.attribute('selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends event "selected-changed" for every selected state change', async () => {
|
||||||
|
const el = await fixture(basicTabs);
|
||||||
|
const spy = sinon.spy();
|
||||||
|
el.addEventListener('selected-changed', spy);
|
||||||
|
el.selectedIndex = 1;
|
||||||
|
expect(spy).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws warning if unequal amount of tabs and panels', async () => {
|
||||||
|
const spy = sinon.spy(console, 'warn');
|
||||||
|
await fixture(html`
|
||||||
|
<lion-tabs>
|
||||||
|
<button slot="tab">tab</button>
|
||||||
|
<div slot="panel">panel 1</div>
|
||||||
|
<div slot="panel">panel 2</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`);
|
||||||
|
expect(spy.callCount).to.equal(1);
|
||||||
|
console.warn.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Tabs ([slot=tab])', () => {
|
||||||
|
it('adds role=tab', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-tabs>
|
||||||
|
<button slot="tab">tab</button>
|
||||||
|
<div slot="panel">panel</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`);
|
||||||
|
expect(el.querySelector('[slot=tab]')).to.have.attribute('role', 'tab');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not in scope:
|
||||||
|
* - has flexible html that allows animations like the material design underline
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Tab Panels (slot=panel)', () => {
|
||||||
|
it('are visible when corresponding tab is selected ', async () => {
|
||||||
|
const el = await fixture(basicTabs);
|
||||||
|
const panels = el.querySelectorAll('[slot=panel]');
|
||||||
|
el.selectedIndex = 0;
|
||||||
|
expect(panels[0]).to.be.visible;
|
||||||
|
expect(panels[1]).to.be.not.visible;
|
||||||
|
|
||||||
|
el.selectedIndex = 1;
|
||||||
|
expect(panels[0]).to.be.not.visible;
|
||||||
|
expect(panels[1]).to.be.visible;
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip('have a DOM structure that allows them to be animated ', async () => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We will immediately switch content as all our content comes from light dom.
|
||||||
|
*
|
||||||
|
* See Note at https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-19
|
||||||
|
* > It is recommended that tabs activate automatically when they receive focus as long as their
|
||||||
|
* > associated tab panels are displayed without noticeable latency. This typically requires tab
|
||||||
|
* > panel content to be preloaded.
|
||||||
|
*/
|
||||||
|
describe('User interaction', () => {
|
||||||
|
it('selects a tab on click', async () => {
|
||||||
|
const el = await fixture(basicTabs);
|
||||||
|
const tabs = el.querySelectorAll('[slot=tab]');
|
||||||
|
tabs[1].dispatchEvent(new Event('click'));
|
||||||
|
expect(el.selectedIndex).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects next tab on [arrow-right] and [arrow-down]', async () => {
|
||||||
|
const el = await fixture(basicTabs);
|
||||||
|
const tabs = el.querySelectorAll('[slot=tab]');
|
||||||
|
tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' }));
|
||||||
|
expect(el.selectedIndex).to.equal(1);
|
||||||
|
tabs[1].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
|
||||||
|
expect(el.selectedIndex).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects previous tab on [arrow-left] and [arrow-up]', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-tabs .selectedIndex=${1}>
|
||||||
|
<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">tab 3</button>
|
||||||
|
<div slot="panel">panel 3</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`);
|
||||||
|
const tabs = el.querySelectorAll('[slot=tab]');
|
||||||
|
el.selectedIndex = 2;
|
||||||
|
tabs[2].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' }));
|
||||||
|
expect(el.selectedIndex).to.equal(1);
|
||||||
|
tabs[1].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' }));
|
||||||
|
expect(el.selectedIndex).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects first tab on [home]', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-tabs .selectedIndex=${1}>
|
||||||
|
<button slot="tab">tab 1</button>
|
||||||
|
<div slot="panel">panel 1</div>
|
||||||
|
<button slot="tab">tab 2</button>
|
||||||
|
<div slot="panel">panel 2</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`);
|
||||||
|
const tabs = el.querySelectorAll('[slot=tab]');
|
||||||
|
tabs[1].dispatchEvent(new KeyboardEvent('keyup', { key: 'Home' }));
|
||||||
|
expect(el.selectedIndex).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects last tab on [end]', async () => {
|
||||||
|
const el = await fixture(basicTabs);
|
||||||
|
const tabs = el.querySelectorAll('[slot=tab]');
|
||||||
|
tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'End' }));
|
||||||
|
expect(el.selectedIndex).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects first tab on [arrow-right] if on last tab', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-tabs selectedIndex="2">
|
||||||
|
<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">tab 3</button>
|
||||||
|
<div slot="panel">panel 3</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`);
|
||||||
|
const tabs = el.querySelectorAll('[slot=tab]');
|
||||||
|
tabs[2].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' }));
|
||||||
|
expect(el.selectedIndex).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects last tab on [arrow-left] if on first tab', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-tabs>
|
||||||
|
<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">tab 3</button>
|
||||||
|
<div slot="panel">panel 3</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`);
|
||||||
|
const tabs = el.querySelectorAll('[slot=tab]');
|
||||||
|
tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' }));
|
||||||
|
expect(el.selectedIndex).to.equal(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Content distribution', () => {
|
||||||
|
it('should work with append children', async () => {
|
||||||
|
const el = await fixture(basicTabs);
|
||||||
|
const c = 2;
|
||||||
|
const n = el.children.length / 2;
|
||||||
|
for (let i = n + 1; i < n + c + 1; i += 1) {
|
||||||
|
const tab = document.createElement('button');
|
||||||
|
tab.setAttribute('slot', 'tab');
|
||||||
|
tab.innerText = `tab ${i}`;
|
||||||
|
const panel = document.createElement('panel');
|
||||||
|
panel.setAttribute('slot', 'panel');
|
||||||
|
panel.innerText = `panel ${i}`;
|
||||||
|
el.append(tab);
|
||||||
|
el.append(panel);
|
||||||
|
}
|
||||||
|
el.selectedIndex = el.children.length / 2 - 1;
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.querySelector('[slot=tab][selected]').textContent).to.equal('tab 5');
|
||||||
|
expect(el.querySelector('[slot=panel][selected]').textContent).to.equal('panel 5');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Accessibility', () => {
|
||||||
|
it('does not make panels focusable', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-tabs>
|
||||||
|
<button slot="tab">tab 1</button>
|
||||||
|
<div slot="panel">panel 1</div>
|
||||||
|
<button slot="tab">tab 2</button>
|
||||||
|
<div slot="panel">panel 2</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`);
|
||||||
|
expect(el.querySelector('[slot=panel]')).to.not.have.attribute('tabindex');
|
||||||
|
expect(el.querySelector('[slot=panel]')).to.not.have.attribute('tabindex');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('makes selected tab focusable (other tabs are unfocusable)', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-tabs>
|
||||||
|
<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">tab 3</button>
|
||||||
|
<div slot="panel">panel 3</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`);
|
||||||
|
const tabs = el.querySelectorAll('[slot=tab]');
|
||||||
|
expect(tabs[0]).to.have.attribute('tabindex', '0');
|
||||||
|
expect(tabs[1]).to.have.attribute('tabindex', '-1');
|
||||||
|
expect(tabs[2]).to.have.attribute('tabindex', '-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Tabs', () => {
|
||||||
|
it('links ids of content items to tab via [aria-controls]', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-tabs>
|
||||||
|
<button id="t1" slot="tab">tab 1</button>
|
||||||
|
<div id="p1" slot="panel">panel 1</div>
|
||||||
|
<button id="t2" slot="tab">tab 2</button>
|
||||||
|
<div id="p2" slot="panel">panel 2</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`);
|
||||||
|
const tabs = el.querySelectorAll('[slot=tab]');
|
||||||
|
const panels = el.querySelectorAll('[slot=panel]');
|
||||||
|
expect(tabs[0].getAttribute('aria-controls')).to.equal(panels[0].id);
|
||||||
|
expect(tabs[1].getAttribute('aria-controls')).to.equal(panels[1].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds aria-selected=“true” to selected tab', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-tabs>
|
||||||
|
<button id="t1" slot="tab">tab 1</button>
|
||||||
|
<div id="p1" slot="panel">panel 1</div>
|
||||||
|
<button id="t2" slot="tab">tab 2</button>
|
||||||
|
<div id="p2" slot="panel">panel 2</div>
|
||||||
|
<button id="t3" slot="tab">tab 3</button>
|
||||||
|
<div id="p3" slot="panel">panel 3</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const tabs = el.querySelectorAll('[slot=tab]');
|
||||||
|
expect(tabs[0].getAttribute('aria-selected')).to.equal('true');
|
||||||
|
expect(tabs[1].getAttribute('aria-selected')).to.equal('false');
|
||||||
|
expect(tabs[2].getAttribute('aria-selected')).to.equal('false');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('panels', () => {
|
||||||
|
it('adds role="tabpanel" to panels', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-tabs>
|
||||||
|
<button slot="tab">tab 1</button>
|
||||||
|
<div slot="panel">panel 1</div>
|
||||||
|
<button slot="tab">tab 2</button>
|
||||||
|
<div slot="panel">panel 2</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`);
|
||||||
|
const panels = el.querySelectorAll('[slot=panel]');
|
||||||
|
expect(panels[0]).to.have.attribute('role', 'tabpanel');
|
||||||
|
expect(panels[1]).to.have.attribute('role', 'tabpanel');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds aria-labelledby referring to tab ids', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-tabs>
|
||||||
|
<button slot="tab">tab 1</button>
|
||||||
|
<div slot="panel">panel 1</div>
|
||||||
|
<button slot="tab">tab 2</button>
|
||||||
|
<div slot="panel">panel 2</div>
|
||||||
|
</lion-tabs>
|
||||||
|
`);
|
||||||
|
const panels = el.querySelectorAll('[slot=panel]');
|
||||||
|
const tabs = el.querySelectorAll('[slot=tab]');
|
||||||
|
expect(panels[0]).to.have.attribute('aria-labelledby', tabs[0].id);
|
||||||
|
expect(panels[1]).to.have.attribute('aria-labelledby', tabs[1].id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not in scope:
|
||||||
|
* - allow to delete tabs
|
||||||
|
*
|
||||||
|
* For extending layer with Design system:
|
||||||
|
* - add options for alignment (justify, right, left,center) of tabs
|
||||||
|
* - add option for large tabs
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import '../packages/tabs/stories/index.stories.js';
|
||||||
|
|
||||||
import '../packages/button/stories/index.stories.js';
|
import '../packages/button/stories/index.stories.js';
|
||||||
|
|
||||||
import '../packages/input/stories/index.stories.js';
|
import '../packages/input/stories/index.stories.js';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue