chore: moved lion-drawer to correct directories for new lion/ui setup
This commit is contained in:
parent
3cc3d2960f
commit
c7ea03577c
20 changed files with 1255 additions and 5 deletions
10
.changeset/giant-scissors-grab.md
Normal file
10
.changeset/giant-scissors-grab.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
'@lion/ui/checkbox-group': patch
|
||||||
|
'@lion/ui/core': patch
|
||||||
|
'@lion/ui/drawer': patch
|
||||||
|
'@lion/ui/form-core': patch
|
||||||
|
'@lion/ui/input-tel': patch
|
||||||
|
'@lion/ui/input-tel-dropdown': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
replaced import('lit-element') with import('lit') to fix tests, fixed test for SlotMixin
|
||||||
5
.changeset/hot-poets-shout.md
Normal file
5
.changeset/hot-poets-shout.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/ui/drawer': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
implemented lion-drawer
|
||||||
3
docs/components/drawer/index.md
Normal file
3
docs/components/drawer/index.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Drawer ||20
|
||||||
|
|
||||||
|
-> go to Overview
|
||||||
68
docs/components/drawer/overview.md
Normal file
68
docs/components/drawer/overview.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Drawer >> Overview ||10
|
||||||
|
|
||||||
|
A combination of a button (the invoker) and a chunk of 'extra content'. This web component can be extended with an
|
||||||
|
animation to disclose the extra content.
|
||||||
|
|
||||||
|
There are three slots available respectively; `invoker` to specify the
|
||||||
|
drawer's invoker, `headline` for the optional headline and `content` for the content of the drawer.
|
||||||
|
|
||||||
|
Through the `position` property, the drawer can be placed on the `top`, `left` or `right` of the viewport.
|
||||||
|
|
||||||
|
```js script
|
||||||
|
import { html } from '@mdjs/mdjs-preview';
|
||||||
|
import '@lion/ui/define/lion-drawer.js';
|
||||||
|
import '@lion/ui/define/lion-icon.js';
|
||||||
|
import { icons } from '@lion/ui/icon.js';
|
||||||
|
import { demoStyle } from './src/demoStyle.js';
|
||||||
|
|
||||||
|
icons.addIconResolver('lion', (iconset, name) => {
|
||||||
|
switch (iconset) {
|
||||||
|
case 'misc':
|
||||||
|
return import('../icon/assets/iconset-misc.js').then(module => module[name]);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown iconset ${iconset}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```js preview-story
|
||||||
|
export const main = () => html`
|
||||||
|
<style>
|
||||||
|
${demoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-container">
|
||||||
|
<lion-drawer>
|
||||||
|
<button slot="invoker">
|
||||||
|
<lion-icon icon-id="lion:misc:arrowLeft" style="width: 16px; height: 16px;"></lion-icon>
|
||||||
|
</button>
|
||||||
|
<p slot="headline">Headline</p>
|
||||||
|
<div slot="content" class="drawer">Hello! This is the content of the drawer</div>
|
||||||
|
<button slot="bottom-invoker">
|
||||||
|
<lion-icon icon-id="lion:misc:arrowLeft" style="width: 16px; height: 16px;"></lion-icon>
|
||||||
|
</button>
|
||||||
|
</lion-drawer>
|
||||||
|
<div>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum convallis, lorem sit amet
|
||||||
|
sollicitudin egestas, dui lectus sodales leo, quis luctus nulla metus vitae lacus. In at
|
||||||
|
imperdiet augue. Mauris mauris dolor, faucibus non nulla vel, vulputate hendrerit mauris.
|
||||||
|
Praesent dapibus leo nec libero scelerisque, ac venenatis ante tincidunt. Nulla maximus
|
||||||
|
vestibulum orci, ac viverra nisi molestie vel. Vivamus eget elit et turpis elementum tempor
|
||||||
|
ultricies at turpis. Ut pretium aliquet finibus. Duis ullamcorper ultrices velit id luctus.
|
||||||
|
Phasellus in ex luctus, interdum ex vel, eleifend dolor. Cras massa odio, sodales quis
|
||||||
|
consectetur a, blandit eu purus. Donec ut gravida libero, sed accumsan arcu.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i --save @lion/ui
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { LionDrawer } from '@lion/ui/drawer.js';
|
||||||
|
// or
|
||||||
|
import '@lion/ui/define/lion-drawer.js';
|
||||||
|
```
|
||||||
71
docs/components/drawer/src/demoStyle.js
Normal file
71
docs/components/drawer/src/demoStyle.js
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { css } from 'lit';
|
||||||
|
|
||||||
|
export const demoStyle = css`
|
||||||
|
.demo-container {
|
||||||
|
height: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-container > div {
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
lion-drawer {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
all: revert !important;
|
||||||
|
border: 2px solid #000000;
|
||||||
|
background-color: rgb(239, 239, 239);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-container-top {
|
||||||
|
height: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-container-top > div {
|
||||||
|
padding: 8px;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-container-top lion-drawer {
|
||||||
|
height: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-container-right {
|
||||||
|
height: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-container-right > div {
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-container-right lion-drawer {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-container-opened {
|
||||||
|
height: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-container-opened > div {
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-container-opened lion-drawer {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
194
docs/components/drawer/use-cases.md
Normal file
194
docs/components/drawer/use-cases.md
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
# Drawer >> Use Cases ||20
|
||||||
|
|
||||||
|
```js script
|
||||||
|
import { html } from '@mdjs/mdjs-preview';
|
||||||
|
import { icons } from '@lion/ui/icon.js';
|
||||||
|
import '@lion/ui/define/lion-drawer.js';
|
||||||
|
import '@lion/ui/define/lion-icon.js';
|
||||||
|
import { demoStyle } from './src/demoStyle.js';
|
||||||
|
|
||||||
|
icons.addIconResolver('lion', (iconset, name) => {
|
||||||
|
switch (iconset) {
|
||||||
|
case 'misc':
|
||||||
|
return import('../icon/assets/iconset-misc.js').then(module => module[name]);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown iconset ${iconset}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Positioning
|
||||||
|
|
||||||
|
### Default left
|
||||||
|
|
||||||
|
By default, the drawer is positioned on the left side of the viewport.
|
||||||
|
|
||||||
|
With the `position` property it can be positioned at the top or on the right of the screen.
|
||||||
|
|
||||||
|
### Top
|
||||||
|
|
||||||
|
```js preview-story
|
||||||
|
export const top = () => html`
|
||||||
|
<style>
|
||||||
|
${demoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-container-top">
|
||||||
|
<lion-drawer position="top">
|
||||||
|
<button slot="invoker">
|
||||||
|
<lion-icon icon-id="lion:misc:arrowLeft" style="width: 16px; height: 16px;"></lion-icon>
|
||||||
|
</button>
|
||||||
|
<p slot="headline">Headline</p>
|
||||||
|
<div slot="content" class="drawer">Hello! This is the content of the drawer</div>
|
||||||
|
</lion-drawer>
|
||||||
|
<div>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum convallis, lorem sit amet
|
||||||
|
sollicitudin egestas, dui lectus sodales leo, quis luctus nulla metus vitae lacus. In at
|
||||||
|
imperdiet augue. Mauris mauris dolor, faucibus non nulla vel, vulputate hendrerit mauris.
|
||||||
|
Praesent dapibus leo nec libero scelerisque, ac venenatis ante tincidunt. Nulla maximus
|
||||||
|
vestibulum orci, ac viverra nisi molestie vel. Vivamus eget elit et turpis elementum tempor
|
||||||
|
ultricies at turpis. Ut pretium aliquet finibus. Duis ullamcorper ultrices velit id luctus.
|
||||||
|
Phasellus in ex luctus, interdum ex vel, eleifend dolor. Cras massa odio, sodales quis
|
||||||
|
consectetur a, blandit eu purus. Donec ut gravida libero, sed accumsan arcu.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Right
|
||||||
|
|
||||||
|
```js preview-story
|
||||||
|
export const right = () => html`
|
||||||
|
<style>
|
||||||
|
${demoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-container-right">
|
||||||
|
<lion-drawer position="right">
|
||||||
|
<button slot="invoker">
|
||||||
|
<lion-icon icon-id="lion:misc:arrowLeft" style="width: 16px; height: 16px;"></lion-icon>
|
||||||
|
</button>
|
||||||
|
<p slot="headline">Headline</p>
|
||||||
|
<div slot="content" class="drawer">Hello! This is the content of the drawer</div>
|
||||||
|
</lion-drawer>
|
||||||
|
<div>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum convallis, lorem sit amet
|
||||||
|
sollicitudin egestas, dui lectus sodales leo, quis luctus nulla metus vitae lacus. In at
|
||||||
|
imperdiet augue. Mauris mauris dolor, faucibus non nulla vel, vulputate hendrerit mauris.
|
||||||
|
Praesent dapibus leo nec libero scelerisque, ac venenatis ante tincidunt. Nulla maximus
|
||||||
|
vestibulum orci, ac viverra nisi molestie vel. Vivamus eget elit et turpis elementum tempor
|
||||||
|
ultricies at turpis. Ut pretium aliquet finibus. Duis ullamcorper ultrices velit id luctus.
|
||||||
|
Phasellus in ex luctus, interdum ex vel, eleifend dolor. Cras massa odio, sodales quis
|
||||||
|
consectetur a, blandit eu purus. Donec ut gravida libero, sed accumsan arcu.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Opened
|
||||||
|
|
||||||
|
Add the `opened` attribute to display the drawer opened.
|
||||||
|
|
||||||
|
```js preview-story
|
||||||
|
export const opened = () => html`
|
||||||
|
<style>
|
||||||
|
${demoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-container">
|
||||||
|
<lion-drawer opened>
|
||||||
|
<p slot="headline">Headline</p>
|
||||||
|
<div slot="content" class="drawer">Hello! This is the content of the drawer</div>
|
||||||
|
</lion-drawer>
|
||||||
|
<div>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum convallis, lorem sit amet
|
||||||
|
sollicitudin egestas, dui lectus sodales leo, quis luctus nulla metus vitae lacus. In at
|
||||||
|
imperdiet augue. Mauris mauris dolor, faucibus non nulla vel, vulputate hendrerit mauris.
|
||||||
|
Praesent dapibus leo nec libero scelerisque, ac venenatis ante tincidunt. Nulla maximus
|
||||||
|
vestibulum orci, ac viverra nisi molestie vel. Vivamus eget elit et turpis elementum tempor
|
||||||
|
ultricies at turpis. Ut pretium aliquet finibus. Duis ullamcorper ultrices velit id luctus.
|
||||||
|
Phasellus in ex luctus, interdum ex vel, eleifend dolor. Cras massa odio, sodales quis
|
||||||
|
consectetur a, blandit eu purus. Donec ut gravida libero, sed accumsan arcu.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
There are the following methods available to control the extra content for the drawer.
|
||||||
|
|
||||||
|
- `toggle()`: toggle the extra content
|
||||||
|
- `show()`: show the extra content
|
||||||
|
- `hide()`: hide the extra content
|
||||||
|
|
||||||
|
```js preview-story
|
||||||
|
export const methods = ({ shadowRoot }) => html`
|
||||||
|
<style>
|
||||||
|
${demoStyle}
|
||||||
|
</style>
|
||||||
|
<section style="margin-top:16px">
|
||||||
|
<button @click=${() => shadowRoot.querySelector('#drawer').toggle()}>Toggle content</button>
|
||||||
|
<button @click=${() => shadowRoot.querySelector('#drawer').show()}>Show content</button>
|
||||||
|
<button @click=${() => shadowRoot.querySelector('#drawer').hide()}>Hide content</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="demo-container">
|
||||||
|
<lion-drawer id="drawer">
|
||||||
|
<button slot="invoker">
|
||||||
|
<lion-icon icon-id="lion:misc:arrowLeft" style="width: 16px; height: 16px;"></lion-icon>
|
||||||
|
</button>
|
||||||
|
<p slot="headline">Headline</p>
|
||||||
|
<div slot="content" class="drawer">Hello! This is the content of the drawer</div>
|
||||||
|
</lion-drawer>
|
||||||
|
<div>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum convallis, lorem sit amet
|
||||||
|
sollicitudin egestas, dui lectus sodales leo, quis luctus nulla metus vitae lacus. In at
|
||||||
|
imperdiet augue. Mauris mauris dolor, faucibus non nulla vel, vulputate hendrerit mauris.
|
||||||
|
Praesent dapibus leo nec libero scelerisque, ac venenatis ante tincidunt. Nulla maximus
|
||||||
|
vestibulum orci, ac viverra nisi molestie vel. Vivamus eget elit et turpis elementum tempor
|
||||||
|
ultricies at turpis. Ut pretium aliquet finibus. Duis ullamcorper ultrices velit id luctus.
|
||||||
|
Phasellus in ex luctus, interdum ex vel, eleifend dolor. Cras massa odio, sodales quis
|
||||||
|
consectetur a, blandit eu purus. Donec ut gravida libero, sed accumsan arcu.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
`lion-drawer` fires an event on `invoker` click to notify the component's current state. It is useful for analytics purposes or to perform some actions while expanding and collapsing the component.
|
||||||
|
|
||||||
|
- `@opened-changed`: triggers when drawer either gets opened or closed
|
||||||
|
|
||||||
|
```js preview-story
|
||||||
|
export const events = ({ shadowRoot }) => html`
|
||||||
|
<style>
|
||||||
|
${demoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-custom-collapsible-state-container">
|
||||||
|
<strong id="collapsible-state"></strong>
|
||||||
|
</div>
|
||||||
|
<div class="demo-container">
|
||||||
|
<lion-drawer
|
||||||
|
@opened-changed=${ev => {
|
||||||
|
const collapsibleState = shadowRoot.querySelector('#collapsible-state');
|
||||||
|
collapsibleState.innerText = `Opened: ${ev.target.opened}`;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button slot="invoker">
|
||||||
|
<lion-icon icon-id="lion:misc:arrowLeft" style="width: 16px; height: 16px;"></lion-icon>
|
||||||
|
</button>
|
||||||
|
<p slot="headline">Headline</p>
|
||||||
|
<div slot="content" class="drawer">Hello! This is the content of the drawer</div>
|
||||||
|
</lion-drawer>
|
||||||
|
<div>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum convallis, lorem sit amet
|
||||||
|
sollicitudin egestas, dui lectus sodales leo, quis luctus nulla metus vitae lacus. In at
|
||||||
|
imperdiet augue. Mauris mauris dolor, faucibus non nulla vel, vulputate hendrerit mauris.
|
||||||
|
Praesent dapibus leo nec libero scelerisque, ac venenatis ante tincidunt. Nulla maximus
|
||||||
|
vestibulum orci, ac viverra nisi molestie vel. Vivamus eget elit et turpis elementum tempor
|
||||||
|
ultricies at turpis. Ut pretium aliquet finibus. Duis ullamcorper ultrices velit id luctus.
|
||||||
|
Phasellus in ex luctus, interdum ex vel, eleifend dolor. Cras massa odio, sodales quis
|
||||||
|
consectetur a, blandit eu purus. Donec ut gravida libero, sed accumsan arcu.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
```
|
||||||
65
package-lock.json
generated
65
package-lock.json
generated
|
|
@ -2880,6 +2880,28 @@
|
||||||
"resolved": "packages/ajax",
|
"resolved": "packages/ajax",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@lion/collapsible": {
|
||||||
|
"version": "0.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lion/collapsible/-/collapsible-0.9.1.tgz",
|
||||||
|
"integrity": "sha512-M+62Un4HQMhTzdG885TBJL52Ji7TSNPYAg0+FQk6kb3QHdz255lWkRTp6G94w1iE1K6IsYO1g+KyibXvtEbbug==",
|
||||||
|
"dependencies": {
|
||||||
|
"@lion/core": "^0.24.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lion/core": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lion/core/-/core-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-hC5Fpi5U3PY0HOVycSev1jzoE8DYHFSN42s5gt6g6RlvvRYN5Pou0wtKnDOkOYf1UfjuL+T/4r8W99UFD1r/Eg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@open-wc/dedupe-mixin": "^1.3.0",
|
||||||
|
"@open-wc/scoped-elements": "^2.1.1",
|
||||||
|
"lit": "^2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lion/drawer": {
|
||||||
|
"resolved": "packages/drawer",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@lion/ui": {
|
"node_modules/@lion/ui": {
|
||||||
"resolved": "packages/ui",
|
"resolved": "packages/ui",
|
||||||
"link": true
|
"link": true
|
||||||
|
|
@ -23199,7 +23221,7 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"packages-node/providence-analytics": {
|
"packages-node/providence-analytics": {
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.10.1",
|
"@babel/core": "^7.10.1",
|
||||||
|
|
@ -23358,7 +23380,7 @@
|
||||||
},
|
},
|
||||||
"packages/ajax": {
|
"packages/ajax": {
|
||||||
"name": "@lion/ajax",
|
"name": "@lion/ajax",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"packages/button": {
|
"packages/button": {
|
||||||
|
|
@ -23449,6 +23471,14 @@
|
||||||
"@lion/overlays": "^0.33.2"
|
"@lion/overlays": "^0.33.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/drawer": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lion/collapsible": "^0.9.1",
|
||||||
|
"@lion/core": "^0.24.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/fieldset": {
|
"packages/fieldset": {
|
||||||
"name": "@lion/fieldset",
|
"name": "@lion/fieldset",
|
||||||
"version": "0.22.1",
|
"version": "0.22.1",
|
||||||
|
|
@ -23738,7 +23768,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/singleton-manager": {
|
"packages/singleton-manager": {
|
||||||
"version": "1.5.0",
|
"version": "1.6.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"packages/steps": {
|
"packages/steps": {
|
||||||
|
|
@ -23803,7 +23833,7 @@
|
||||||
"awesome-phonenumber": "^3.0.1",
|
"awesome-phonenumber": "^3.0.1",
|
||||||
"ibantools": "^2.2.0",
|
"ibantools": "^2.2.0",
|
||||||
"lit": "^2.4.0",
|
"lit": "^2.4.0",
|
||||||
"singleton-manager": "^1.5.0"
|
"singleton-manager": "^1.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/validate-messages": {
|
"packages/validate-messages": {
|
||||||
|
|
@ -25903,6 +25933,31 @@
|
||||||
"@lion/ajax": {
|
"@lion/ajax": {
|
||||||
"version": "file:packages/ajax"
|
"version": "file:packages/ajax"
|
||||||
},
|
},
|
||||||
|
"@lion/collapsible": {
|
||||||
|
"version": "0.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lion/collapsible/-/collapsible-0.9.1.tgz",
|
||||||
|
"integrity": "sha512-M+62Un4HQMhTzdG885TBJL52Ji7TSNPYAg0+FQk6kb3QHdz255lWkRTp6G94w1iE1K6IsYO1g+KyibXvtEbbug==",
|
||||||
|
"requires": {
|
||||||
|
"@lion/core": "^0.24.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@lion/core": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lion/core/-/core-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-hC5Fpi5U3PY0HOVycSev1jzoE8DYHFSN42s5gt6g6RlvvRYN5Pou0wtKnDOkOYf1UfjuL+T/4r8W99UFD1r/Eg==",
|
||||||
|
"requires": {
|
||||||
|
"@open-wc/dedupe-mixin": "^1.3.0",
|
||||||
|
"@open-wc/scoped-elements": "^2.1.1",
|
||||||
|
"lit": "^2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@lion/drawer": {
|
||||||
|
"version": "file:packages/drawer",
|
||||||
|
"requires": {
|
||||||
|
"@lion/collapsible": "^0.9.1",
|
||||||
|
"@lion/core": "^0.24.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@lion/ui": {
|
"@lion/ui": {
|
||||||
"version": "file:packages/ui",
|
"version": "file:packages/ui",
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|
@ -25913,7 +25968,7 @@
|
||||||
"awesome-phonenumber": "^3.0.1",
|
"awesome-phonenumber": "^3.0.1",
|
||||||
"ibantools": "^2.2.0",
|
"ibantools": "^2.2.0",
|
||||||
"lit": "^2.4.0",
|
"lit": "^2.4.0",
|
||||||
"singleton-manager": "^1.5.0"
|
"singleton-manager": "^1.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@lit/reactive-element": {
|
"@lit/reactive-element": {
|
||||||
|
|
|
||||||
3
packages/drawer/docs/overview.md
Normal file
3
packages/drawer/docs/overview.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Lion Drawer Overview
|
||||||
|
|
||||||
|
[=> See Source <=](../../../docs/components/drawer/overview.md)
|
||||||
3
packages/drawer/docs/use-cases.md
Normal file
3
packages/drawer/docs/use-cases.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Lion Drawer Use Cases
|
||||||
|
|
||||||
|
[=> See Source <=](../../../docs/components/drawer/use-cases.md)
|
||||||
1
packages/drawer/index.js
Normal file
1
packages/drawer/index.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { LionDrawer } from './src/LionDrawer.js';
|
||||||
3
packages/drawer/lion-drawer.js
Normal file
3
packages/drawer/lion-drawer.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { LionDrawer } from './src/LionDrawer.js';
|
||||||
|
|
||||||
|
customElements.define('lion-drawer', LionDrawer);
|
||||||
58
packages/drawer/package.json
Normal file
58
packages/drawer/package.json
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
{
|
||||||
|
"name": "@lion/drawer",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Drawer that can be expanded to reveal it contents",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "ing-bank",
|
||||||
|
"homepage": "https://github.com/ing-bank/lion/",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/ing-bank/lion.git",
|
||||||
|
"directory": "packages/drawer"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.js",
|
||||||
|
"./define": "./lion-drawer.js",
|
||||||
|
"./docs/*": "./docs/*"
|
||||||
|
},
|
||||||
|
"main": "index.js",
|
||||||
|
"module": "index.js",
|
||||||
|
"files": [
|
||||||
|
"*.d.ts",
|
||||||
|
"*.js",
|
||||||
|
"custom-elements.json",
|
||||||
|
"docs",
|
||||||
|
"src",
|
||||||
|
"test",
|
||||||
|
"test-helpers",
|
||||||
|
"translations",
|
||||||
|
"types"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"custom-elements-manifest": "custom-elements-manifest analyze --litelement --exclude \"docs/**/*\" \"test-helpers/**/*\"",
|
||||||
|
"debug": "cd ../../ && npm run debug -- --group drawer",
|
||||||
|
"debug:firefox": "cd ../../ && npm run debug:firefox -- --group drawer",
|
||||||
|
"debug:webkit": "cd ../../ && npm run debug:webkit -- --group drawer",
|
||||||
|
"publish-docs": "node ../../packages-node/publish-docs/src/cli.js --github-url https://github.com/ing-bank/lion/ --git-root-dir ../../",
|
||||||
|
"prepublishOnly": "npm run publish-docs && npm run custom-elements-manifest",
|
||||||
|
"test": "cd ../../ && npm run test:browser -- --group drawer"
|
||||||
|
},
|
||||||
|
"sideEffects": [
|
||||||
|
"lion-drawer.js",
|
||||||
|
"./docs/styled-drawer-content.js"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@lion/collapsible": "^0.9.1",
|
||||||
|
"@lion/core": "^0.24.0"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"drawer",
|
||||||
|
"lion",
|
||||||
|
"web-components"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"customElements": "custom-elements.json"
|
||||||
|
}
|
||||||
217
packages/drawer/src/LionDrawer.js
Normal file
217
packages/drawer/src/LionDrawer.js
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
import { html } from '@lion/core';
|
||||||
|
import { LionCollapsible } from '@lion/collapsible';
|
||||||
|
import { drawerStyle } from './drawerStyle.js';
|
||||||
|
|
||||||
|
const EVENT = {
|
||||||
|
TRANSITION_END: 'transitionend',
|
||||||
|
TRANSITION_START: 'transitionstart',
|
||||||
|
};
|
||||||
|
|
||||||
|
export class LionDrawer extends LionCollapsible {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
transitioning: {
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
},
|
||||||
|
opened: {
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
type: String,
|
||||||
|
reflect: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
/** @private */
|
||||||
|
this.__toggle = () => {
|
||||||
|
this.opened = !this.opened;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
|
||||||
|
if (!this.hasAttribute('position')) {
|
||||||
|
this.position = 'left';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._contentNode) {
|
||||||
|
this._contentNode.style.setProperty('display', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.__setBoundaries();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update aria labels on state change.
|
||||||
|
* @param {import('@lion/core').PropertyValues } changedProperties
|
||||||
|
*/
|
||||||
|
updated(changedProperties) {
|
||||||
|
if (changedProperties.has('opened')) {
|
||||||
|
this._openedChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return [drawerStyle];
|
||||||
|
}
|
||||||
|
|
||||||
|
__setBoundaries() {
|
||||||
|
const host = this.shadowRoot?.host;
|
||||||
|
|
||||||
|
if (this.position === 'top') {
|
||||||
|
this.minHeight = host ? getComputedStyle(host).getPropertyValue('--min-height') : '0px';
|
||||||
|
this.maxHeight = host ? getComputedStyle(host).getPropertyValue('--max-height') : '0px';
|
||||||
|
this.minWidth = '0px';
|
||||||
|
this.maxWidth = 'none';
|
||||||
|
} else {
|
||||||
|
this.minWidth = host ? getComputedStyle(host).getPropertyValue('--min-width') : '0px';
|
||||||
|
this.maxWidth = host ? getComputedStyle(host).getPropertyValue('--max-width') : '0px';
|
||||||
|
this.minHeight = 'auto';
|
||||||
|
this.maxHeight = 'fit-content';
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const prop = this.position === 'top' ? 'width' : 'height';
|
||||||
|
|
||||||
|
if (this.__contentNode) {
|
||||||
|
this.__contentNode.style.setProperty(prop, '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setter for position property, available values are 'top', 'left' and 'right'
|
||||||
|
* @param {String} position
|
||||||
|
*/
|
||||||
|
set position(position) {
|
||||||
|
const stale = this.position;
|
||||||
|
this._position = position;
|
||||||
|
this.setAttribute('position', position);
|
||||||
|
|
||||||
|
this.__setBoundaries();
|
||||||
|
this.requestUpdate('position', stale);
|
||||||
|
}
|
||||||
|
|
||||||
|
get position() {
|
||||||
|
return this._position ?? 'left';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger show animation and wait for transition to be finished.
|
||||||
|
* @param {Object} options - element node and its options
|
||||||
|
* @param {HTMLElement} options.contentNode
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
async _showAnimation({ contentNode }) {
|
||||||
|
const min = this.position === 'top' ? this.minHeight : this.minWidth;
|
||||||
|
const max = this.position === 'top' ? this.maxHeight : this.maxWidth;
|
||||||
|
const prop = this.position === 'top' ? 'height' : 'width';
|
||||||
|
|
||||||
|
contentNode.style.setProperty(prop, /** @type {string} */ (min));
|
||||||
|
await new Promise(resolve => requestAnimationFrame(() => resolve(true)));
|
||||||
|
contentNode.style.setProperty(prop, /** @type {string} */ (max));
|
||||||
|
await this._waitForTransition({ contentNode });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger hide animation and wait for transition to be finished.
|
||||||
|
* @param {Object} options - element node and its options
|
||||||
|
* @param {HTMLElement} options.contentNode
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
async _hideAnimation({ contentNode }) {
|
||||||
|
if (
|
||||||
|
((this.position === 'left' || this.position === 'right') &&
|
||||||
|
this._contentWidth === this.minWidth) ||
|
||||||
|
(this.position === 'top' && this._contentHeight === this.minHeight)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = this.position === 'top' ? this.minHeight : this.minWidth;
|
||||||
|
const prop = this.position === 'top' ? 'height' : 'width';
|
||||||
|
|
||||||
|
contentNode.style.setProperty(prop, /** @type {string} */ (min));
|
||||||
|
await this._waitForTransition({ contentNode });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait until the transition event is finished.
|
||||||
|
* @param {Object} options - element node and its options
|
||||||
|
* @param {HTMLElement} options.contentNode
|
||||||
|
* @returns {Promise<void>} transition event
|
||||||
|
*/
|
||||||
|
_waitForTransition({ contentNode }) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const transitionStarted = () => {
|
||||||
|
contentNode.removeEventListener(EVENT.TRANSITION_START, transitionStarted);
|
||||||
|
this.transitioning = true;
|
||||||
|
};
|
||||||
|
contentNode.addEventListener(EVENT.TRANSITION_START, transitionStarted);
|
||||||
|
|
||||||
|
const transitionEnded = () => {
|
||||||
|
contentNode.removeEventListener(EVENT.TRANSITION_END, transitionEnded);
|
||||||
|
this.transitioning = false;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
contentNode.addEventListener(EVENT.TRANSITION_END, transitionEnded);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
get __contentNode() {
|
||||||
|
return /** @type {HTMLElement} */ (this.shadowRoot?.querySelector('.container'));
|
||||||
|
}
|
||||||
|
|
||||||
|
get _contentWidth() {
|
||||||
|
const size = this.__contentNode?.getBoundingClientRect().width || 0;
|
||||||
|
return `${size}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get _contentHeight() {
|
||||||
|
const size = this.__contentNode?.getBoundingClientRect().height || 0;
|
||||||
|
return `${size}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_openedChanged() {
|
||||||
|
this._updateContentSize();
|
||||||
|
if (this._invokerNode) {
|
||||||
|
this._invokerNode.setAttribute('aria-expanded', `${this.opened}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatchEvent(new CustomEvent('opened-changed'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async _updateContentSize() {
|
||||||
|
if (this.__contentNode) {
|
||||||
|
if (this.opened) {
|
||||||
|
await this._showAnimation({ contentNode: this.__contentNode });
|
||||||
|
} else {
|
||||||
|
await this._hideAnimation({ contentNode: this.__contentNode });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
<div class="headline-container">
|
||||||
|
<slot name="invoker"></slot>
|
||||||
|
<slot name="headline"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="content-container">
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
packages/drawer/src/drawerStyle.js
Normal file
60
packages/drawer/src/drawerStyle.js
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { css } from '@lion/core';
|
||||||
|
|
||||||
|
export const drawerStyle = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
--min-width: 72px;
|
||||||
|
--max-width: 320px;
|
||||||
|
--min-height: auto;
|
||||||
|
--max-height: fit-content;
|
||||||
|
--start-width: var(--min-width);
|
||||||
|
--start-height: 100%;
|
||||||
|
--transition-property: width;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([position='top']) {
|
||||||
|
width: 100%;
|
||||||
|
--min-width: 0px;
|
||||||
|
--max-width: none;
|
||||||
|
--min-height: 50px;
|
||||||
|
--max-height: 200px;
|
||||||
|
--start-width: 100%;
|
||||||
|
--start-height: var(--min-height);
|
||||||
|
--transition-property: height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: var(--start-width);
|
||||||
|
height: var(--start-height);
|
||||||
|
min-width: var(--min-width);
|
||||||
|
max-width: var(--max-width);
|
||||||
|
min-height: var(--min-height);
|
||||||
|
max-height: var(--max-height);
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: var(--transition-property) 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.headline-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([position='right']) .headline-container {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
overflow: hidden;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
::slotted([slot='content']) {
|
||||||
|
width: var(--max-width);
|
||||||
|
}
|
||||||
|
`;
|
||||||
110
packages/drawer/test/lion-drawer.test.js
Normal file
110
packages/drawer/test/lion-drawer.test.js
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { expect, fixture as _fixture } from '@open-wc/testing';
|
||||||
|
import { html } from 'lit/static-html.js';
|
||||||
|
|
||||||
|
import '@lion/drawer/define';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../src/LionDrawer').LionDrawer} LionDrawer
|
||||||
|
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
||||||
|
*/
|
||||||
|
const fixture = /** @type {(arg: TemplateResult) => Promise<LionDrawer>} */ (_fixture);
|
||||||
|
|
||||||
|
const template = html`
|
||||||
|
<lion-drawer>
|
||||||
|
<button slot="invoker">Open</button>
|
||||||
|
<p slot="headline">Headline</p>
|
||||||
|
<div slot="content" class="drawer">This is the content of the drawer</div>
|
||||||
|
</lion-drawer>
|
||||||
|
`;
|
||||||
|
|
||||||
|
describe('<lion-drawer>', () => {
|
||||||
|
describe('Drawer', () => {
|
||||||
|
it('sets position to "left" by default', async () => {
|
||||||
|
const drawer = await fixture(template);
|
||||||
|
expect(drawer.position).to.equal('left');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has [position] attribute which serves as styling hook', async () => {
|
||||||
|
const drawer = await fixture(template);
|
||||||
|
expect(drawer).to.have.attribute('position').equal('left');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the minimum and maximum width when position=left', async () => {
|
||||||
|
const drawer = await fixture(template);
|
||||||
|
const minWidth = getComputedStyle(drawer).getPropertyValue('--min-width');
|
||||||
|
const maxWidth = getComputedStyle(drawer).getPropertyValue('--max-width');
|
||||||
|
|
||||||
|
expect(drawer.minWidth).to.equal(minWidth);
|
||||||
|
expect(drawer.maxWidth).to.equal(maxWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the minimum and maximum width when position=right', async () => {
|
||||||
|
const drawer = await fixture(template);
|
||||||
|
drawer.position = 'right';
|
||||||
|
await drawer.updateComplete;
|
||||||
|
|
||||||
|
const minWidth = getComputedStyle(drawer).getPropertyValue('--min-width');
|
||||||
|
const maxWidth = getComputedStyle(drawer).getPropertyValue('--max-width');
|
||||||
|
|
||||||
|
expect(drawer.minWidth).to.equal(minWidth);
|
||||||
|
expect(drawer.maxWidth).to.equal(maxWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the minimum and maximum height when position=top', async () => {
|
||||||
|
const drawer = await fixture(template);
|
||||||
|
drawer.position = 'top';
|
||||||
|
await drawer.updateComplete;
|
||||||
|
|
||||||
|
const minHeight = getComputedStyle(drawer).getPropertyValue('--min-height');
|
||||||
|
const maxHeight = getComputedStyle(drawer).getPropertyValue('--max-height');
|
||||||
|
|
||||||
|
expect(drawer.minHeight).to.equal(minHeight);
|
||||||
|
expect(drawer.maxHeight).to.equal(maxHeight);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Accessibility', () => {
|
||||||
|
it('[collapsed] is a11y AXE accessible', async () => {
|
||||||
|
const drawer = await fixture(template);
|
||||||
|
await expect(drawer).to.be.accessible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('[opened] is a11y AXE accessible', async () => {
|
||||||
|
const drawer = await fixture(template);
|
||||||
|
drawer.opened = true;
|
||||||
|
await expect(drawer).to.be.accessible();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Invoker', () => {
|
||||||
|
it('links id of content items to invoker via [aria-controls]', async () => {
|
||||||
|
const drawerElement = await fixture(template);
|
||||||
|
const invoker = drawerElement.querySelector('[slot=invoker]');
|
||||||
|
const content = drawerElement.querySelector('[slot=content]');
|
||||||
|
expect(invoker?.getAttribute('aria-controls')).to.equal(content?.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds aria-expanded="false" to invoker when its content is not expanded', async () => {
|
||||||
|
const drawerElement = await fixture(template);
|
||||||
|
const invoker = drawerElement.querySelector('[slot=invoker]');
|
||||||
|
expect(invoker).to.have.attribute('aria-expanded', 'false');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds aria-expanded="true" to invoker when its content is expanded', async () => {
|
||||||
|
const drawerElement = await fixture(template);
|
||||||
|
const invoker = drawerElement.querySelector('[slot=invoker]');
|
||||||
|
drawerElement.opened = true;
|
||||||
|
await drawerElement.updateComplete;
|
||||||
|
expect(invoker).to.have.attribute('aria-expanded', 'true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Contents', () => {
|
||||||
|
it('adds aria-labelledby referring to invoker id', async () => {
|
||||||
|
const drawerElement = await fixture(template);
|
||||||
|
const invoker = drawerElement.querySelector('[slot=invoker]');
|
||||||
|
const content = drawerElement.querySelector('[slot=content]');
|
||||||
|
expect(content).to.have.attribute('aria-labelledby', invoker?.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
217
packages/ui/components/drawer/src/LionDrawer.js
Normal file
217
packages/ui/components/drawer/src/LionDrawer.js
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
import { html } from 'lit';
|
||||||
|
import { LionCollapsible } from '@lion/ui/collapsible.js';
|
||||||
|
import { drawerStyle } from './drawerStyle.js';
|
||||||
|
|
||||||
|
const EVENT = {
|
||||||
|
TRANSITION_END: 'transitionend',
|
||||||
|
TRANSITION_START: 'transitionstart',
|
||||||
|
};
|
||||||
|
|
||||||
|
export class LionDrawer extends LionCollapsible {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
transitioning: {
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
},
|
||||||
|
opened: {
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
type: String,
|
||||||
|
reflect: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
/** @private */
|
||||||
|
this.__toggle = () => {
|
||||||
|
this.opened = !this.opened;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
|
||||||
|
if (!this.hasAttribute('position')) {
|
||||||
|
this.position = 'left';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._contentNode) {
|
||||||
|
this._contentNode.style.setProperty('display', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.__setBoundaries();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update aria labels on state change.
|
||||||
|
* @param {import('@lion/core').PropertyValues } changedProperties
|
||||||
|
*/
|
||||||
|
updated(changedProperties) {
|
||||||
|
if (changedProperties.has('opened')) {
|
||||||
|
this._openedChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return [drawerStyle];
|
||||||
|
}
|
||||||
|
|
||||||
|
__setBoundaries() {
|
||||||
|
const host = this.shadowRoot?.host;
|
||||||
|
|
||||||
|
if (this.position === 'top') {
|
||||||
|
this.minHeight = host ? getComputedStyle(host).getPropertyValue('--min-height') : '0px';
|
||||||
|
this.maxHeight = host ? getComputedStyle(host).getPropertyValue('--max-height') : '0px';
|
||||||
|
this.minWidth = '0px';
|
||||||
|
this.maxWidth = 'none';
|
||||||
|
} else {
|
||||||
|
this.minWidth = host ? getComputedStyle(host).getPropertyValue('--min-width') : '0px';
|
||||||
|
this.maxWidth = host ? getComputedStyle(host).getPropertyValue('--max-width') : '0px';
|
||||||
|
this.minHeight = 'auto';
|
||||||
|
this.maxHeight = 'fit-content';
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const prop = this.position === 'top' ? 'width' : 'height';
|
||||||
|
|
||||||
|
if (this.__contentNode) {
|
||||||
|
this.__contentNode.style.setProperty(prop, '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setter for position property, available values are 'top', 'left' and 'right'
|
||||||
|
* @param {String} position
|
||||||
|
*/
|
||||||
|
set position(position) {
|
||||||
|
const stale = this.position;
|
||||||
|
this._position = position;
|
||||||
|
this.setAttribute('position', position);
|
||||||
|
|
||||||
|
this.__setBoundaries();
|
||||||
|
this.requestUpdate('position', stale);
|
||||||
|
}
|
||||||
|
|
||||||
|
get position() {
|
||||||
|
return this._position ?? 'left';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger show animation and wait for transition to be finished.
|
||||||
|
* @param {Object} options - element node and its options
|
||||||
|
* @param {HTMLElement} options.contentNode
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
async _showAnimation({ contentNode }) {
|
||||||
|
const min = this.position === 'top' ? this.minHeight : this.minWidth;
|
||||||
|
const max = this.position === 'top' ? this.maxHeight : this.maxWidth;
|
||||||
|
const prop = this.position === 'top' ? 'height' : 'width';
|
||||||
|
|
||||||
|
contentNode.style.setProperty(prop, /** @type {string} */ (min));
|
||||||
|
await new Promise(resolve => requestAnimationFrame(() => resolve(true)));
|
||||||
|
contentNode.style.setProperty(prop, /** @type {string} */ (max));
|
||||||
|
await this._waitForTransition({ contentNode });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger hide animation and wait for transition to be finished.
|
||||||
|
* @param {Object} options - element node and its options
|
||||||
|
* @param {HTMLElement} options.contentNode
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
async _hideAnimation({ contentNode }) {
|
||||||
|
if (
|
||||||
|
((this.position === 'left' || this.position === 'right') &&
|
||||||
|
this._contentWidth === this.minWidth) ||
|
||||||
|
(this.position === 'top' && this._contentHeight === this.minHeight)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = this.position === 'top' ? this.minHeight : this.minWidth;
|
||||||
|
const prop = this.position === 'top' ? 'height' : 'width';
|
||||||
|
|
||||||
|
contentNode.style.setProperty(prop, /** @type {string} */ (min));
|
||||||
|
await this._waitForTransition({ contentNode });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait until the transition event is finished.
|
||||||
|
* @param {Object} options - element node and its options
|
||||||
|
* @param {HTMLElement} options.contentNode
|
||||||
|
* @returns {Promise<void>} transition event
|
||||||
|
*/
|
||||||
|
_waitForTransition({ contentNode }) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const transitionStarted = () => {
|
||||||
|
contentNode.removeEventListener(EVENT.TRANSITION_START, transitionStarted);
|
||||||
|
this.transitioning = true;
|
||||||
|
};
|
||||||
|
contentNode.addEventListener(EVENT.TRANSITION_START, transitionStarted);
|
||||||
|
|
||||||
|
const transitionEnded = () => {
|
||||||
|
contentNode.removeEventListener(EVENT.TRANSITION_END, transitionEnded);
|
||||||
|
this.transitioning = false;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
contentNode.addEventListener(EVENT.TRANSITION_END, transitionEnded);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
get __contentNode() {
|
||||||
|
return /** @type {HTMLElement} */ (this.shadowRoot?.querySelector('.container'));
|
||||||
|
}
|
||||||
|
|
||||||
|
get _contentWidth() {
|
||||||
|
const size = this.__contentNode?.getBoundingClientRect().width || 0;
|
||||||
|
return `${size}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get _contentHeight() {
|
||||||
|
const size = this.__contentNode?.getBoundingClientRect().height || 0;
|
||||||
|
return `${size}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_openedChanged() {
|
||||||
|
this._updateContentSize();
|
||||||
|
if (this._invokerNode) {
|
||||||
|
this._invokerNode.setAttribute('aria-expanded', `${this.opened}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatchEvent(new CustomEvent('opened-changed'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async _updateContentSize() {
|
||||||
|
if (this.__contentNode) {
|
||||||
|
if (this.opened) {
|
||||||
|
await this._showAnimation({ contentNode: this.__contentNode });
|
||||||
|
} else {
|
||||||
|
await this._hideAnimation({ contentNode: this.__contentNode });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
<div class="headline-container">
|
||||||
|
<slot name="invoker"></slot>
|
||||||
|
<slot name="headline"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="content-container">
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
packages/ui/components/drawer/src/drawerStyle.js
Normal file
59
packages/ui/components/drawer/src/drawerStyle.js
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { css } from 'lit';
|
||||||
|
|
||||||
|
export const drawerStyle = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
--min-width: 72px;
|
||||||
|
--max-width: 320px;
|
||||||
|
--min-height: auto;
|
||||||
|
--max-height: fit-content;
|
||||||
|
--start-width: var(--min-width);
|
||||||
|
--start-height: 100%;
|
||||||
|
--transition-property: width;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([position='top']) {
|
||||||
|
width: 100%;
|
||||||
|
--min-width: 0px;
|
||||||
|
--max-width: none;
|
||||||
|
--min-height: 50px;
|
||||||
|
--max-height: 200px;
|
||||||
|
--start-width: 100%;
|
||||||
|
--start-height: var(--min-height);
|
||||||
|
--transition-property: height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: var(--start-width);
|
||||||
|
height: var(--start-height);
|
||||||
|
min-width: var(--min-width);
|
||||||
|
max-width: var(--max-width);
|
||||||
|
min-height: var(--min-height);
|
||||||
|
max-height: var(--max-height);
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: var(--transition-property) 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.headline-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([position='right']) .headline-container {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
overflow: hidden;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
::slotted([slot='content']) {
|
||||||
|
width: var(--max-width);
|
||||||
|
}
|
||||||
|
`;
|
||||||
109
packages/ui/components/drawer/test/lion-drawer.test.js
Normal file
109
packages/ui/components/drawer/test/lion-drawer.test.js
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { expect, fixture as _fixture } from '@open-wc/testing';
|
||||||
|
import { html } from 'lit/static-html.js';
|
||||||
|
import '@lion/ui/define/lion-drawer.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../src/LionDrawer').LionDrawer} LionDrawer
|
||||||
|
* @typedef {import('lit').TemplateResult} TemplateResult
|
||||||
|
*/
|
||||||
|
const fixture = /** @type {(arg: TemplateResult) => Promise<LionDrawer>} */ (_fixture);
|
||||||
|
|
||||||
|
const template = html`
|
||||||
|
<lion-drawer>
|
||||||
|
<button slot="invoker">Open</button>
|
||||||
|
<p slot="headline">Headline</p>
|
||||||
|
<div slot="content" class="drawer">This is the content of the drawer</div>
|
||||||
|
</lion-drawer>
|
||||||
|
`;
|
||||||
|
|
||||||
|
describe('<lion-drawer>', () => {
|
||||||
|
describe('Drawer', () => {
|
||||||
|
it('sets position to "left" by default', async () => {
|
||||||
|
const drawer = await fixture(template);
|
||||||
|
expect(drawer.position).to.equal('left');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has [position] attribute which serves as styling hook', async () => {
|
||||||
|
const drawer = await fixture(template);
|
||||||
|
expect(drawer).to.have.attribute('position').equal('left');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the minimum and maximum width when position=left', async () => {
|
||||||
|
const drawer = await fixture(template);
|
||||||
|
const minWidth = getComputedStyle(drawer).getPropertyValue('--min-width');
|
||||||
|
const maxWidth = getComputedStyle(drawer).getPropertyValue('--max-width');
|
||||||
|
|
||||||
|
expect(drawer.minWidth).to.equal(minWidth);
|
||||||
|
expect(drawer.maxWidth).to.equal(maxWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the minimum and maximum width when position=right', async () => {
|
||||||
|
const drawer = await fixture(template);
|
||||||
|
drawer.position = 'right';
|
||||||
|
await drawer.updateComplete;
|
||||||
|
|
||||||
|
const minWidth = getComputedStyle(drawer).getPropertyValue('--min-width');
|
||||||
|
const maxWidth = getComputedStyle(drawer).getPropertyValue('--max-width');
|
||||||
|
|
||||||
|
expect(drawer.minWidth).to.equal(minWidth);
|
||||||
|
expect(drawer.maxWidth).to.equal(maxWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the minimum and maximum height when position=top', async () => {
|
||||||
|
const drawer = await fixture(template);
|
||||||
|
drawer.position = 'top';
|
||||||
|
await drawer.updateComplete;
|
||||||
|
|
||||||
|
const minHeight = getComputedStyle(drawer).getPropertyValue('--min-height');
|
||||||
|
const maxHeight = getComputedStyle(drawer).getPropertyValue('--max-height');
|
||||||
|
|
||||||
|
expect(drawer.minHeight).to.equal(minHeight);
|
||||||
|
expect(drawer.maxHeight).to.equal(maxHeight);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Accessibility', () => {
|
||||||
|
it('[collapsed] is a11y AXE accessible', async () => {
|
||||||
|
const drawer = await fixture(template);
|
||||||
|
await expect(drawer).to.be.accessible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('[opened] is a11y AXE accessible', async () => {
|
||||||
|
const drawer = await fixture(template);
|
||||||
|
drawer.opened = true;
|
||||||
|
await expect(drawer).to.be.accessible();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Invoker', () => {
|
||||||
|
it('links id of content items to invoker via [aria-controls]', async () => {
|
||||||
|
const drawerElement = await fixture(template);
|
||||||
|
const invoker = drawerElement.querySelector('[slot=invoker]');
|
||||||
|
const content = drawerElement.querySelector('[slot=content]');
|
||||||
|
expect(invoker?.getAttribute('aria-controls')).to.equal(content?.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds aria-expanded="false" to invoker when its content is not expanded', async () => {
|
||||||
|
const drawerElement = await fixture(template);
|
||||||
|
const invoker = drawerElement.querySelector('[slot=invoker]');
|
||||||
|
expect(invoker).to.have.attribute('aria-expanded', 'false');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds aria-expanded="true" to invoker when its content is expanded', async () => {
|
||||||
|
const drawerElement = await fixture(template);
|
||||||
|
const invoker = drawerElement.querySelector('[slot=invoker]');
|
||||||
|
drawerElement.opened = true;
|
||||||
|
await drawerElement.updateComplete;
|
||||||
|
expect(invoker).to.have.attribute('aria-expanded', 'true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Contents', () => {
|
||||||
|
it('adds aria-labelledby referring to invoker id', async () => {
|
||||||
|
const drawerElement = await fixture(template);
|
||||||
|
const invoker = drawerElement.querySelector('[slot=invoker]');
|
||||||
|
const content = drawerElement.querySelector('[slot=content]');
|
||||||
|
expect(content).to.have.attribute('aria-labelledby', invoker?.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
3
packages/ui/exports/define/lion-drawer.js
Normal file
3
packages/ui/exports/define/lion-drawer.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { LionDrawer } from '../drawer.js';
|
||||||
|
|
||||||
|
customElements.define('lion-drawer', LionDrawer);
|
||||||
1
packages/ui/exports/drawer.js
Normal file
1
packages/ui/exports/drawer.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { LionDrawer } from '../components/drawer/src/LionDrawer.js';
|
||||||
Loading…
Reference in a new issue