@lion/ui 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/drawer/src/LionDrawer.js +217 -0
- package/components/drawer/src/drawerStyle.js +59 -0
- package/components/drawer/test/lion-drawer.test.js +109 -0
- package/custom-elements.json +743 -0
- package/dist-types/components/drawer/src/LionDrawer.d.ts +67 -0
- package/dist-types/components/drawer/src/LionDrawer.d.ts.map +1 -0
- package/dist-types/components/drawer/src/drawerStyle.d.ts +2 -0
- package/dist-types/components/drawer/src/drawerStyle.d.ts.map +1 -0
- package/dist-types/components/drawer/test/lion-drawer.test.d.ts +3 -0
- package/dist-types/components/drawer/test/lion-drawer.test.d.ts.map +1 -0
- package/dist-types/exports/define/lion-drawer.d.ts +2 -0
- package/dist-types/exports/define/lion-drawer.d.ts.map +1 -0
- package/dist-types/exports/drawer.d.ts +2 -0
- package/dist-types/exports/drawer.d.ts.map +1 -0
- package/dist-types/tsconfig.tsbuildinfo +1 -1
- package/exports/define/lion-drawer.js +3 -0
- package/exports/drawer.js +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { LionCollapsible } from '@lion/ui/collapsible.js';
|
|
3
|
+
import { drawerStyle } from './drawerStyle.js';
|
|
4
|
+
|
|
5
|
+
const EVENT = {
|
|
6
|
+
TRANSITION_END: 'transitionend',
|
|
7
|
+
TRANSITION_START: 'transitionstart',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export class LionDrawer extends LionCollapsible {
|
|
11
|
+
static get properties() {
|
|
12
|
+
return {
|
|
13
|
+
transitioning: {
|
|
14
|
+
type: Boolean,
|
|
15
|
+
reflect: true,
|
|
16
|
+
},
|
|
17
|
+
opened: {
|
|
18
|
+
type: Boolean,
|
|
19
|
+
reflect: true,
|
|
20
|
+
},
|
|
21
|
+
position: {
|
|
22
|
+
type: String,
|
|
23
|
+
reflect: true,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
super();
|
|
30
|
+
|
|
31
|
+
/** @private */
|
|
32
|
+
this.__toggle = () => {
|
|
33
|
+
this.opened = !this.opened;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
connectedCallback() {
|
|
38
|
+
super.connectedCallback();
|
|
39
|
+
|
|
40
|
+
if (!this.hasAttribute('position')) {
|
|
41
|
+
this.position = 'left';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (this._contentNode) {
|
|
45
|
+
this._contentNode.style.setProperty('display', '');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.__setBoundaries();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Update aria labels on state change.
|
|
53
|
+
* @param {import('lit').PropertyValues } changedProperties
|
|
54
|
+
*/
|
|
55
|
+
updated(changedProperties) {
|
|
56
|
+
if (changedProperties.has('opened')) {
|
|
57
|
+
this._openedChanged();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static get styles() {
|
|
62
|
+
return [drawerStyle];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
__setBoundaries() {
|
|
66
|
+
const host = this.shadowRoot?.host;
|
|
67
|
+
|
|
68
|
+
if (this.position === 'top') {
|
|
69
|
+
this.minHeight = host ? getComputedStyle(host).getPropertyValue('--min-height') : '0px';
|
|
70
|
+
this.maxHeight = host ? getComputedStyle(host).getPropertyValue('--max-height') : '0px';
|
|
71
|
+
this.minWidth = '0px';
|
|
72
|
+
this.maxWidth = 'none';
|
|
73
|
+
} else {
|
|
74
|
+
this.minWidth = host ? getComputedStyle(host).getPropertyValue('--min-width') : '0px';
|
|
75
|
+
this.maxWidth = host ? getComputedStyle(host).getPropertyValue('--max-width') : '0px';
|
|
76
|
+
this.minHeight = 'auto';
|
|
77
|
+
this.maxHeight = 'fit-content';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
const prop = this.position === 'top' ? 'width' : 'height';
|
|
82
|
+
|
|
83
|
+
if (this.__contentNode) {
|
|
84
|
+
this.__contentNode.style.setProperty(prop, '');
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Setter for position property, available values are 'top', 'left' and 'right'
|
|
91
|
+
* @param {String} position
|
|
92
|
+
*/
|
|
93
|
+
set position(position) {
|
|
94
|
+
const stale = this.position;
|
|
95
|
+
this._position = position;
|
|
96
|
+
this.setAttribute('position', position);
|
|
97
|
+
|
|
98
|
+
this.__setBoundaries();
|
|
99
|
+
this.requestUpdate('position', stale);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get position() {
|
|
103
|
+
return this._position ?? 'left';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Trigger show animation and wait for transition to be finished.
|
|
108
|
+
* @param {Object} options - element node and its options
|
|
109
|
+
* @param {HTMLElement} options.contentNode
|
|
110
|
+
* @override
|
|
111
|
+
*/
|
|
112
|
+
async _showAnimation({ contentNode }) {
|
|
113
|
+
const min = this.position === 'top' ? this.minHeight : this.minWidth;
|
|
114
|
+
const max = this.position === 'top' ? this.maxHeight : this.maxWidth;
|
|
115
|
+
const prop = this.position === 'top' ? 'height' : 'width';
|
|
116
|
+
|
|
117
|
+
contentNode.style.setProperty(prop, /** @type {string} */ (min));
|
|
118
|
+
await new Promise(resolve => requestAnimationFrame(() => resolve(true)));
|
|
119
|
+
contentNode.style.setProperty(prop, /** @type {string} */ (max));
|
|
120
|
+
await this._waitForTransition({ contentNode });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Trigger hide animation and wait for transition to be finished.
|
|
125
|
+
* @param {Object} options - element node and its options
|
|
126
|
+
* @param {HTMLElement} options.contentNode
|
|
127
|
+
* @override
|
|
128
|
+
*/
|
|
129
|
+
async _hideAnimation({ contentNode }) {
|
|
130
|
+
if (
|
|
131
|
+
((this.position === 'left' || this.position === 'right') &&
|
|
132
|
+
this._contentWidth === this.minWidth) ||
|
|
133
|
+
(this.position === 'top' && this._contentHeight === this.minHeight)
|
|
134
|
+
) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const min = this.position === 'top' ? this.minHeight : this.minWidth;
|
|
139
|
+
const prop = this.position === 'top' ? 'height' : 'width';
|
|
140
|
+
|
|
141
|
+
contentNode.style.setProperty(prop, /** @type {string} */ (min));
|
|
142
|
+
await this._waitForTransition({ contentNode });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Wait until the transition event is finished.
|
|
147
|
+
* @param {Object} options - element node and its options
|
|
148
|
+
* @param {HTMLElement} options.contentNode
|
|
149
|
+
* @returns {Promise<void>} transition event
|
|
150
|
+
*/
|
|
151
|
+
_waitForTransition({ contentNode }) {
|
|
152
|
+
return new Promise(resolve => {
|
|
153
|
+
const transitionStarted = () => {
|
|
154
|
+
contentNode.removeEventListener(EVENT.TRANSITION_START, transitionStarted);
|
|
155
|
+
this.transitioning = true;
|
|
156
|
+
};
|
|
157
|
+
contentNode.addEventListener(EVENT.TRANSITION_START, transitionStarted);
|
|
158
|
+
|
|
159
|
+
const transitionEnded = () => {
|
|
160
|
+
contentNode.removeEventListener(EVENT.TRANSITION_END, transitionEnded);
|
|
161
|
+
this.transitioning = false;
|
|
162
|
+
resolve();
|
|
163
|
+
};
|
|
164
|
+
contentNode.addEventListener(EVENT.TRANSITION_END, transitionEnded);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @protected
|
|
170
|
+
*/
|
|
171
|
+
get __contentNode() {
|
|
172
|
+
return /** @type {HTMLElement} */ (this.shadowRoot?.querySelector('.container'));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
get _contentWidth() {
|
|
176
|
+
const size = this.__contentNode?.getBoundingClientRect().width || 0;
|
|
177
|
+
return `${size}px`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
get _contentHeight() {
|
|
181
|
+
const size = this.__contentNode?.getBoundingClientRect().height || 0;
|
|
182
|
+
return `${size}px`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_openedChanged() {
|
|
186
|
+
this._updateContentSize();
|
|
187
|
+
if (this._invokerNode) {
|
|
188
|
+
this._invokerNode.setAttribute('aria-expanded', `${this.opened}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.dispatchEvent(new CustomEvent('opened-changed'));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async _updateContentSize() {
|
|
195
|
+
if (this.__contentNode) {
|
|
196
|
+
if (this.opened) {
|
|
197
|
+
await this._showAnimation({ contentNode: this.__contentNode });
|
|
198
|
+
} else {
|
|
199
|
+
await this._hideAnimation({ contentNode: this.__contentNode });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
render() {
|
|
205
|
+
return html`
|
|
206
|
+
<div class="container">
|
|
207
|
+
<div class="headline-container">
|
|
208
|
+
<slot name="invoker"></slot>
|
|
209
|
+
<slot name="headline"></slot>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="content-container">
|
|
212
|
+
<slot name="content"></slot>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
|
|
3
|
+
export const drawerStyle = css`
|
|
4
|
+
:host {
|
|
5
|
+
display: block;
|
|
6
|
+
height: 100%;
|
|
7
|
+
--min-width: 72px;
|
|
8
|
+
--max-width: 320px;
|
|
9
|
+
--min-height: auto;
|
|
10
|
+
--max-height: fit-content;
|
|
11
|
+
--start-width: var(--min-width);
|
|
12
|
+
--start-height: 100%;
|
|
13
|
+
--transition-property: width;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
:host([position='top']) {
|
|
17
|
+
width: 100%;
|
|
18
|
+
--min-width: 0px;
|
|
19
|
+
--max-width: none;
|
|
20
|
+
--min-height: 50px;
|
|
21
|
+
--max-height: 200px;
|
|
22
|
+
--start-width: 100%;
|
|
23
|
+
--start-height: var(--min-height);
|
|
24
|
+
--transition-property: height;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.container {
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
width: var(--start-width);
|
|
31
|
+
height: var(--start-height);
|
|
32
|
+
min-width: var(--min-width);
|
|
33
|
+
max-width: var(--max-width);
|
|
34
|
+
min-height: var(--min-height);
|
|
35
|
+
max-height: var(--max-height);
|
|
36
|
+
overflow: hidden;
|
|
37
|
+
box-sizing: border-box;
|
|
38
|
+
transition: var(--transition-property) 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.headline-container {
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
flex-wrap: nowrap;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
:host([position='right']) .headline-container {
|
|
48
|
+
flex-direction: row-reverse;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.content-container {
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
flex-grow: 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
::slotted([slot='content']) {
|
|
57
|
+
width: var(--max-width);
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { expect, fixture as _fixture } from '@open-wc/testing';
|
|
2
|
+
import { html } from 'lit/static-html.js';
|
|
3
|
+
import '@lion/ui/define/lion-drawer.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {import('../src/LionDrawer.js').LionDrawer} LionDrawer
|
|
7
|
+
* @typedef {import('lit').TemplateResult} TemplateResult
|
|
8
|
+
*/
|
|
9
|
+
const fixture = /** @type {(arg: TemplateResult) => Promise<LionDrawer>} */ (_fixture);
|
|
10
|
+
|
|
11
|
+
const template = html`
|
|
12
|
+
<lion-drawer>
|
|
13
|
+
<button slot="invoker">Open</button>
|
|
14
|
+
<p slot="headline">Headline</p>
|
|
15
|
+
<div slot="content" class="drawer">This is the content of the drawer</div>
|
|
16
|
+
</lion-drawer>
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
describe('<lion-drawer>', () => {
|
|
20
|
+
describe('Drawer', () => {
|
|
21
|
+
it('sets position to "left" by default', async () => {
|
|
22
|
+
const drawer = await fixture(template);
|
|
23
|
+
expect(drawer.position).to.equal('left');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('has [position] attribute which serves as styling hook', async () => {
|
|
27
|
+
const drawer = await fixture(template);
|
|
28
|
+
expect(drawer).to.have.attribute('position').equal('left');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('sets the minimum and maximum width when position=left', async () => {
|
|
32
|
+
const drawer = await fixture(template);
|
|
33
|
+
const minWidth = getComputedStyle(drawer).getPropertyValue('--min-width');
|
|
34
|
+
const maxWidth = getComputedStyle(drawer).getPropertyValue('--max-width');
|
|
35
|
+
|
|
36
|
+
expect(drawer.minWidth).to.equal(minWidth);
|
|
37
|
+
expect(drawer.maxWidth).to.equal(maxWidth);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('sets the minimum and maximum width when position=right', async () => {
|
|
41
|
+
const drawer = await fixture(template);
|
|
42
|
+
drawer.position = 'right';
|
|
43
|
+
await drawer.updateComplete;
|
|
44
|
+
|
|
45
|
+
const minWidth = getComputedStyle(drawer).getPropertyValue('--min-width');
|
|
46
|
+
const maxWidth = getComputedStyle(drawer).getPropertyValue('--max-width');
|
|
47
|
+
|
|
48
|
+
expect(drawer.minWidth).to.equal(minWidth);
|
|
49
|
+
expect(drawer.maxWidth).to.equal(maxWidth);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('sets the minimum and maximum height when position=top', async () => {
|
|
53
|
+
const drawer = await fixture(template);
|
|
54
|
+
drawer.position = 'top';
|
|
55
|
+
await drawer.updateComplete;
|
|
56
|
+
|
|
57
|
+
const minHeight = getComputedStyle(drawer).getPropertyValue('--min-height');
|
|
58
|
+
const maxHeight = getComputedStyle(drawer).getPropertyValue('--max-height');
|
|
59
|
+
|
|
60
|
+
expect(drawer.minHeight).to.equal(minHeight);
|
|
61
|
+
expect(drawer.maxHeight).to.equal(maxHeight);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('Accessibility', () => {
|
|
66
|
+
it('[collapsed] is a11y AXE accessible', async () => {
|
|
67
|
+
const drawer = await fixture(template);
|
|
68
|
+
await expect(drawer).to.be.accessible();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('[opened] is a11y AXE accessible', async () => {
|
|
72
|
+
const drawer = await fixture(template);
|
|
73
|
+
drawer.opened = true;
|
|
74
|
+
await expect(drawer).to.be.accessible();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('Invoker', () => {
|
|
78
|
+
it('links id of content items to invoker via [aria-controls]', async () => {
|
|
79
|
+
const drawerElement = await fixture(template);
|
|
80
|
+
const invoker = drawerElement.querySelector('[slot=invoker]');
|
|
81
|
+
const content = drawerElement.querySelector('[slot=content]');
|
|
82
|
+
expect(invoker?.getAttribute('aria-controls')).to.equal(content?.id);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('adds aria-expanded="false" to invoker when its content is not expanded', async () => {
|
|
86
|
+
const drawerElement = await fixture(template);
|
|
87
|
+
const invoker = drawerElement.querySelector('[slot=invoker]');
|
|
88
|
+
expect(invoker).to.have.attribute('aria-expanded', 'false');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('adds aria-expanded="true" to invoker when its content is expanded', async () => {
|
|
92
|
+
const drawerElement = await fixture(template);
|
|
93
|
+
const invoker = drawerElement.querySelector('[slot=invoker]');
|
|
94
|
+
drawerElement.opened = true;
|
|
95
|
+
await drawerElement.updateComplete;
|
|
96
|
+
expect(invoker).to.have.attribute('aria-expanded', 'true');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('Contents', () => {
|
|
101
|
+
it('adds aria-labelledby referring to invoker id', async () => {
|
|
102
|
+
const drawerElement = await fixture(template);
|
|
103
|
+
const invoker = drawerElement.querySelector('[slot=invoker]');
|
|
104
|
+
const content = drawerElement.querySelector('[slot=content]');
|
|
105
|
+
expect(content).to.have.attribute('aria-labelledby', invoker?.id);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|