chore: moved lion-drawer to correct directories for new lion/ui setup

This commit is contained in:
Danny Moerkerke 2022-11-07 14:48:26 +01:00
parent 3cc3d2960f
commit c7ea03577c
20 changed files with 1255 additions and 5 deletions

View 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

View file

@ -0,0 +1,5 @@
---
'@lion/ui/drawer': patch
---
implemented lion-drawer

View file

@ -0,0 +1,3 @@
# Drawer ||20
-> go to Overview

View 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';
```

View 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;
}
`;

View 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
View file

@ -2880,6 +2880,28 @@
"resolved": "packages/ajax",
"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": {
"resolved": "packages/ui",
"link": true
@ -23199,7 +23221,7 @@
"license": "MIT"
},
"packages-node/providence-analytics": {
"version": "0.13.0",
"version": "0.14.0",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.10.1",
@ -23358,7 +23380,7 @@
},
"packages/ajax": {
"name": "@lion/ajax",
"version": "1.0.0",
"version": "1.1.0",
"license": "MIT"
},
"packages/button": {
@ -23449,6 +23471,14 @@
"@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": {
"name": "@lion/fieldset",
"version": "0.22.1",
@ -23738,7 +23768,7 @@
}
},
"packages/singleton-manager": {
"version": "1.5.0",
"version": "1.6.0",
"license": "MIT"
},
"packages/steps": {
@ -23803,7 +23833,7 @@
"awesome-phonenumber": "^3.0.1",
"ibantools": "^2.2.0",
"lit": "^2.4.0",
"singleton-manager": "^1.5.0"
"singleton-manager": "^1.6.0"
}
},
"packages/validate-messages": {
@ -25903,6 +25933,31 @@
"@lion/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": {
"version": "file:packages/ui",
"requires": {
@ -25913,7 +25968,7 @@
"awesome-phonenumber": "^3.0.1",
"ibantools": "^2.2.0",
"lit": "^2.4.0",
"singleton-manager": "^1.5.0"
"singleton-manager": "^1.6.0"
}
},
"@lit/reactive-element": {

View file

@ -0,0 +1,3 @@
# Lion Drawer Overview
[=> See Source <=](../../../docs/components/drawer/overview.md)

View file

@ -0,0 +1,3 @@
# Lion Drawer Use Cases
[=> See Source <=](../../../docs/components/drawer/use-cases.md)

1
packages/drawer/index.js Normal file
View file

@ -0,0 +1 @@
export { LionDrawer } from './src/LionDrawer.js';

View file

@ -0,0 +1,3 @@
import { LionDrawer } from './src/LionDrawer.js';
customElements.define('lion-drawer', LionDrawer);

View 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"
}

View 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>
`;
}
}

View 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);
}
`;

View 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);
});
});
});
});

View 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>
`;
}
}

View 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);
}
`;

View 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);
});
});
});
});

View file

@ -0,0 +1,3 @@
import { LionDrawer } from '../drawer.js';
customElements.define('lion-drawer', LionDrawer);

View file

@ -0,0 +1 @@
export { LionDrawer } from '../components/drawer/src/LionDrawer.js';