From c7ea03577c8000e118b4025fc150bdb126d88572 Mon Sep 17 00:00:00 2001 From: Danny Moerkerke Date: Mon, 7 Nov 2022 14:48:26 +0100 Subject: [PATCH 1/4] chore: moved lion-drawer to correct directories for new lion/ui setup --- .changeset/giant-scissors-grab.md | 10 + .changeset/hot-poets-shout.md | 5 + docs/components/drawer/index.md | 3 + docs/components/drawer/overview.md | 68 ++++++ docs/components/drawer/src/demoStyle.js | 71 ++++++ docs/components/drawer/use-cases.md | 194 ++++++++++++++++ package-lock.json | 65 +++++- packages/drawer/docs/overview.md | 3 + packages/drawer/docs/use-cases.md | 3 + packages/drawer/index.js | 1 + packages/drawer/lion-drawer.js | 3 + packages/drawer/package.json | 58 +++++ packages/drawer/src/LionDrawer.js | 217 ++++++++++++++++++ packages/drawer/src/drawerStyle.js | 60 +++++ packages/drawer/test/lion-drawer.test.js | 110 +++++++++ .../ui/components/drawer/src/LionDrawer.js | 217 ++++++++++++++++++ .../ui/components/drawer/src/drawerStyle.js | 59 +++++ .../drawer/test/lion-drawer.test.js | 109 +++++++++ packages/ui/exports/define/lion-drawer.js | 3 + packages/ui/exports/drawer.js | 1 + 20 files changed, 1255 insertions(+), 5 deletions(-) create mode 100644 .changeset/giant-scissors-grab.md create mode 100644 .changeset/hot-poets-shout.md create mode 100644 docs/components/drawer/index.md create mode 100644 docs/components/drawer/overview.md create mode 100644 docs/components/drawer/src/demoStyle.js create mode 100644 docs/components/drawer/use-cases.md create mode 100644 packages/drawer/docs/overview.md create mode 100644 packages/drawer/docs/use-cases.md create mode 100644 packages/drawer/index.js create mode 100644 packages/drawer/lion-drawer.js create mode 100644 packages/drawer/package.json create mode 100644 packages/drawer/src/LionDrawer.js create mode 100644 packages/drawer/src/drawerStyle.js create mode 100644 packages/drawer/test/lion-drawer.test.js create mode 100644 packages/ui/components/drawer/src/LionDrawer.js create mode 100644 packages/ui/components/drawer/src/drawerStyle.js create mode 100644 packages/ui/components/drawer/test/lion-drawer.test.js create mode 100644 packages/ui/exports/define/lion-drawer.js create mode 100644 packages/ui/exports/drawer.js diff --git a/.changeset/giant-scissors-grab.md b/.changeset/giant-scissors-grab.md new file mode 100644 index 000000000..c51520580 --- /dev/null +++ b/.changeset/giant-scissors-grab.md @@ -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 diff --git a/.changeset/hot-poets-shout.md b/.changeset/hot-poets-shout.md new file mode 100644 index 000000000..eb26bdbee --- /dev/null +++ b/.changeset/hot-poets-shout.md @@ -0,0 +1,5 @@ +--- +'@lion/ui/drawer': patch +--- + +implemented lion-drawer diff --git a/docs/components/drawer/index.md b/docs/components/drawer/index.md new file mode 100644 index 000000000..a00890323 --- /dev/null +++ b/docs/components/drawer/index.md @@ -0,0 +1,3 @@ +# Drawer ||20 + +-> go to Overview diff --git a/docs/components/drawer/overview.md b/docs/components/drawer/overview.md new file mode 100644 index 000000000..75c31f73e --- /dev/null +++ b/docs/components/drawer/overview.md @@ -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` + +
+ + +

Headline

+
Hello! This is the content of the drawer
+ +
+
+ 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. +
+
+`; +``` + +## Installation + +```bash +npm i --save @lion/ui +``` + +```js +import { LionDrawer } from '@lion/ui/drawer.js'; +// or +import '@lion/ui/define/lion-drawer.js'; +``` diff --git a/docs/components/drawer/src/demoStyle.js b/docs/components/drawer/src/demoStyle.js new file mode 100644 index 000000000..763a89c60 --- /dev/null +++ b/docs/components/drawer/src/demoStyle.js @@ -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; + } +`; diff --git a/docs/components/drawer/use-cases.md b/docs/components/drawer/use-cases.md new file mode 100644 index 000000000..4ca44531d --- /dev/null +++ b/docs/components/drawer/use-cases.md @@ -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` + +
+ + +

Headline

+
Hello! This is the content of the drawer
+
+
+ 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. +
+
+`; +``` + +### Right + +```js preview-story +export const right = () => html` + +
+ + +

Headline

+
Hello! This is the content of the drawer
+
+
+ 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. +
+
+`; +``` + +## Opened + +Add the `opened` attribute to display the drawer opened. + +```js preview-story +export const opened = () => html` + +
+ +

Headline

+
Hello! This is the content of the drawer
+
+
+ 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. +
+
+`; +``` + +## 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` + +
+ + + +
+ +
+ + +

Headline

+
Hello! This is the content of the drawer
+
+
+ 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. +
+
+`; +``` + +## 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` + +
+ +
+
+ { + const collapsibleState = shadowRoot.querySelector('#collapsible-state'); + collapsibleState.innerText = `Opened: ${ev.target.opened}`; + }} + > + +

Headline

+
Hello! This is the content of the drawer
+
+
+ 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. +
+
+`; +``` diff --git a/package-lock.json b/package-lock.json index 34c5b2914..9d680c4b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/packages/drawer/docs/overview.md b/packages/drawer/docs/overview.md new file mode 100644 index 000000000..2474c9525 --- /dev/null +++ b/packages/drawer/docs/overview.md @@ -0,0 +1,3 @@ +# Lion Drawer Overview + +[=> See Source <=](../../../docs/components/drawer/overview.md) diff --git a/packages/drawer/docs/use-cases.md b/packages/drawer/docs/use-cases.md new file mode 100644 index 000000000..b1a4b2f5f --- /dev/null +++ b/packages/drawer/docs/use-cases.md @@ -0,0 +1,3 @@ +# Lion Drawer Use Cases + +[=> See Source <=](../../../docs/components/drawer/use-cases.md) diff --git a/packages/drawer/index.js b/packages/drawer/index.js new file mode 100644 index 000000000..e2fc22533 --- /dev/null +++ b/packages/drawer/index.js @@ -0,0 +1 @@ +export { LionDrawer } from './src/LionDrawer.js'; diff --git a/packages/drawer/lion-drawer.js b/packages/drawer/lion-drawer.js new file mode 100644 index 000000000..ceca5c144 --- /dev/null +++ b/packages/drawer/lion-drawer.js @@ -0,0 +1,3 @@ +import { LionDrawer } from './src/LionDrawer.js'; + +customElements.define('lion-drawer', LionDrawer); diff --git a/packages/drawer/package.json b/packages/drawer/package.json new file mode 100644 index 000000000..e2e921b21 --- /dev/null +++ b/packages/drawer/package.json @@ -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" +} diff --git a/packages/drawer/src/LionDrawer.js b/packages/drawer/src/LionDrawer.js new file mode 100644 index 000000000..346954edd --- /dev/null +++ b/packages/drawer/src/LionDrawer.js @@ -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} 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` +
+
+ + +
+
+ +
+
+ `; + } +} diff --git a/packages/drawer/src/drawerStyle.js b/packages/drawer/src/drawerStyle.js new file mode 100644 index 000000000..c05779daf --- /dev/null +++ b/packages/drawer/src/drawerStyle.js @@ -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); + } +`; diff --git a/packages/drawer/test/lion-drawer.test.js b/packages/drawer/test/lion-drawer.test.js new file mode 100644 index 000000000..bc71f60ab --- /dev/null +++ b/packages/drawer/test/lion-drawer.test.js @@ -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} */ (_fixture); + +const template = html` + + +

Headline

+
This is the content of the drawer
+
+`; + +describe('', () => { + 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); + }); + }); + }); +}); diff --git a/packages/ui/components/drawer/src/LionDrawer.js b/packages/ui/components/drawer/src/LionDrawer.js new file mode 100644 index 000000000..dc6cdcdd4 --- /dev/null +++ b/packages/ui/components/drawer/src/LionDrawer.js @@ -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} 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` +
+
+ + +
+
+ +
+
+ `; + } +} diff --git a/packages/ui/components/drawer/src/drawerStyle.js b/packages/ui/components/drawer/src/drawerStyle.js new file mode 100644 index 000000000..d55547b60 --- /dev/null +++ b/packages/ui/components/drawer/src/drawerStyle.js @@ -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); + } +`; diff --git a/packages/ui/components/drawer/test/lion-drawer.test.js b/packages/ui/components/drawer/test/lion-drawer.test.js new file mode 100644 index 000000000..a57b4fde9 --- /dev/null +++ b/packages/ui/components/drawer/test/lion-drawer.test.js @@ -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} */ (_fixture); + +const template = html` + + +

Headline

+
This is the content of the drawer
+
+`; + +describe('', () => { + 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); + }); + }); + }); +}); diff --git a/packages/ui/exports/define/lion-drawer.js b/packages/ui/exports/define/lion-drawer.js new file mode 100644 index 000000000..5ec1d68ce --- /dev/null +++ b/packages/ui/exports/define/lion-drawer.js @@ -0,0 +1,3 @@ +import { LionDrawer } from '../drawer.js'; + +customElements.define('lion-drawer', LionDrawer); diff --git a/packages/ui/exports/drawer.js b/packages/ui/exports/drawer.js new file mode 100644 index 000000000..551219947 --- /dev/null +++ b/packages/ui/exports/drawer.js @@ -0,0 +1 @@ +export { LionDrawer } from '../components/drawer/src/LionDrawer.js'; From a1f1270671655553ef89d6eae829176a49322898 Mon Sep 17 00:00:00 2001 From: Danny Moerkerke Date: Mon, 7 Nov 2022 16:04:34 +0100 Subject: [PATCH 2/4] fix: fixed import in @typedef --- packages/ui/components/drawer/test/lion-drawer.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/components/drawer/test/lion-drawer.test.js b/packages/ui/components/drawer/test/lion-drawer.test.js index a57b4fde9..f194503c6 100644 --- a/packages/ui/components/drawer/test/lion-drawer.test.js +++ b/packages/ui/components/drawer/test/lion-drawer.test.js @@ -3,7 +3,7 @@ import { html } from 'lit/static-html.js'; import '@lion/ui/define/lion-drawer.js'; /** - * @typedef {import('../src/LionDrawer').LionDrawer} LionDrawer + * @typedef {import('../src/LionDrawer.js').LionDrawer} LionDrawer * @typedef {import('lit').TemplateResult} TemplateResult */ const fixture = /** @type {(arg: TemplateResult) => Promise} */ (_fixture); From 886151ba85ebc3849b9f2ec5575f543f1667483c Mon Sep 17 00:00:00 2001 From: Danny Moerkerke Date: Mon, 7 Nov 2022 16:19:22 +0100 Subject: [PATCH 3/4] chore: removed accidentally committed directory --- packages/drawer/docs/overview.md | 3 - packages/drawer/docs/use-cases.md | 3 - packages/drawer/index.js | 1 - packages/drawer/lion-drawer.js | 3 - packages/drawer/package.json | 58 ------ packages/drawer/src/LionDrawer.js | 217 ----------------------- packages/drawer/src/drawerStyle.js | 60 ------- packages/drawer/test/lion-drawer.test.js | 110 ------------ 8 files changed, 455 deletions(-) delete mode 100644 packages/drawer/docs/overview.md delete mode 100644 packages/drawer/docs/use-cases.md delete mode 100644 packages/drawer/index.js delete mode 100644 packages/drawer/lion-drawer.js delete mode 100644 packages/drawer/package.json delete mode 100644 packages/drawer/src/LionDrawer.js delete mode 100644 packages/drawer/src/drawerStyle.js delete mode 100644 packages/drawer/test/lion-drawer.test.js diff --git a/packages/drawer/docs/overview.md b/packages/drawer/docs/overview.md deleted file mode 100644 index 2474c9525..000000000 --- a/packages/drawer/docs/overview.md +++ /dev/null @@ -1,3 +0,0 @@ -# Lion Drawer Overview - -[=> See Source <=](../../../docs/components/drawer/overview.md) diff --git a/packages/drawer/docs/use-cases.md b/packages/drawer/docs/use-cases.md deleted file mode 100644 index b1a4b2f5f..000000000 --- a/packages/drawer/docs/use-cases.md +++ /dev/null @@ -1,3 +0,0 @@ -# Lion Drawer Use Cases - -[=> See Source <=](../../../docs/components/drawer/use-cases.md) diff --git a/packages/drawer/index.js b/packages/drawer/index.js deleted file mode 100644 index e2fc22533..000000000 --- a/packages/drawer/index.js +++ /dev/null @@ -1 +0,0 @@ -export { LionDrawer } from './src/LionDrawer.js'; diff --git a/packages/drawer/lion-drawer.js b/packages/drawer/lion-drawer.js deleted file mode 100644 index ceca5c144..000000000 --- a/packages/drawer/lion-drawer.js +++ /dev/null @@ -1,3 +0,0 @@ -import { LionDrawer } from './src/LionDrawer.js'; - -customElements.define('lion-drawer', LionDrawer); diff --git a/packages/drawer/package.json b/packages/drawer/package.json deleted file mode 100644 index e2e921b21..000000000 --- a/packages/drawer/package.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "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" -} diff --git a/packages/drawer/src/LionDrawer.js b/packages/drawer/src/LionDrawer.js deleted file mode 100644 index 346954edd..000000000 --- a/packages/drawer/src/LionDrawer.js +++ /dev/null @@ -1,217 +0,0 @@ -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} 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` -
-
- - -
-
- -
-
- `; - } -} diff --git a/packages/drawer/src/drawerStyle.js b/packages/drawer/src/drawerStyle.js deleted file mode 100644 index c05779daf..000000000 --- a/packages/drawer/src/drawerStyle.js +++ /dev/null @@ -1,60 +0,0 @@ -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); - } -`; diff --git a/packages/drawer/test/lion-drawer.test.js b/packages/drawer/test/lion-drawer.test.js deleted file mode 100644 index bc71f60ab..000000000 --- a/packages/drawer/test/lion-drawer.test.js +++ /dev/null @@ -1,110 +0,0 @@ -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} */ (_fixture); - -const template = html` - - -

Headline

-
This is the content of the drawer
-
-`; - -describe('', () => { - 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); - }); - }); - }); -}); From 45f48c7ccd9bb3b408fb7491d78a19158d91a4b0 Mon Sep 17 00:00:00 2001 From: Danny Moerkerke Date: Tue, 8 Nov 2022 07:58:17 +0100 Subject: [PATCH 4/4] fix: changed import in type definition from @lion/core to lit --- packages/ui/components/drawer/src/LionDrawer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/components/drawer/src/LionDrawer.js b/packages/ui/components/drawer/src/LionDrawer.js index dc6cdcdd4..e68571bac 100644 --- a/packages/ui/components/drawer/src/LionDrawer.js +++ b/packages/ui/components/drawer/src/LionDrawer.js @@ -50,7 +50,7 @@ export class LionDrawer extends LionCollapsible { /** * Update aria labels on state change. - * @param {import('@lion/core').PropertyValues } changedProperties + * @param {import('lit').PropertyValues } changedProperties */ updated(changedProperties) { if (changedProperties.has('opened')) {