@boostdev/design-system-components 1.2.0 → 1.2.1
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/AGENTS.md +25 -4
- package/dist/client.cjs +58 -54
- package/dist/client.css +503 -525
- package/dist/client.js +58 -54
- package/dist/index.cjs +58 -54
- package/dist/index.css +503 -525
- package/dist/index.js +58 -54
- package/dist/web-components/{chunk-6MH5UWUD.js → chunk-2FGATTGT.js} +5 -0
- package/dist/web-components/{chunk-5IPHEONG.js → chunk-6I2DBFQ7.js} +1 -0
- package/dist/web-components/{chunk-DI46Q2EA.js → chunk-OCODKRVZ.js} +57 -82
- package/dist/web-components/index.d.ts +571 -1
- package/dist/web-components/index.js +1501 -4
- package/dist/web-components/interaction/bds-collapsible.js +1 -1
- package/dist/web-components/interaction/bds-drawer.js +1 -1
- package/dist/web-components/interaction/form/bds-segmented-control.js +1 -1
- package/package.json +1 -1
- package/src/components/interaction/Drawer/Drawer.module.css +37 -62
- package/src/components/interaction/Drawer/Drawer.tsx +21 -17
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +8 -4
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +13 -6
- package/src/stories/Introduction.mdx +2 -1
- package/src/web-components/index.ts +12 -0
- package/src/web-components/interaction/BdsAccordion.mdx +80 -28
- package/src/web-components/interaction/BdsAccordion.stories.tsx +67 -58
- package/src/web-components/interaction/BdsCollapsible.mdx +93 -23
- package/src/web-components/interaction/BdsCollapsible.stories.tsx +29 -48
- package/src/web-components/interaction/BdsDialog.mdx +88 -27
- package/src/web-components/interaction/BdsDialog.stories.tsx +129 -47
- package/src/web-components/interaction/BdsDrawer.mdx +85 -27
- package/src/web-components/interaction/BdsDrawer.stories.tsx +161 -31
- package/src/web-components/interaction/BdsDropdownMenu.mdx +108 -0
- package/src/web-components/interaction/BdsDropdownMenu.stories.tsx +91 -0
- package/src/web-components/interaction/BdsSkipLink.mdx +72 -16
- package/src/web-components/interaction/BdsSkipLink.stories.tsx +47 -34
- package/src/web-components/interaction/BdsTabs.mdx +97 -27
- package/src/web-components/interaction/BdsTabs.stories.tsx +69 -55
- package/src/web-components/interaction/BdsTooltip.mdx +84 -18
- package/src/web-components/interaction/BdsTooltip.stories.tsx +49 -30
- package/src/web-components/interaction/bds-collapsible.ts +1 -0
- package/src/web-components/interaction/bds-drawer.ts +59 -82
- package/src/web-components/interaction/bds-dropdown-menu-item.ts +124 -0
- package/src/web-components/interaction/bds-dropdown-menu.spec.ts +102 -0
- package/src/web-components/interaction/bds-dropdown-menu.ts +200 -0
- package/src/web-components/interaction/form/BdsCheckbox.mdx +57 -20
- package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +35 -36
- package/src/web-components/interaction/form/BdsCheckboxGroup.mdx +70 -0
- package/src/web-components/interaction/form/BdsCheckboxGroup.stories.tsx +81 -0
- package/src/web-components/interaction/form/BdsCombobox.mdx +52 -33
- package/src/web-components/interaction/form/BdsCombobox.stories.tsx +55 -53
- package/src/web-components/interaction/form/BdsFileInput.mdx +59 -24
- package/src/web-components/interaction/form/BdsFileInput.stories.tsx +38 -53
- package/src/web-components/interaction/form/BdsFormInput.mdx +98 -0
- package/src/web-components/interaction/form/BdsFormInput.stories.tsx +139 -0
- package/src/web-components/interaction/form/BdsNumberInput.mdx +51 -22
- package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +24 -46
- package/src/web-components/interaction/form/BdsRadio.mdx +53 -22
- package/src/web-components/interaction/form/BdsRadio.stories.tsx +19 -51
- package/src/web-components/interaction/form/BdsRadioGroup.mdx +73 -0
- package/src/web-components/interaction/form/BdsRadioGroup.stories.tsx +88 -0
- package/src/web-components/interaction/form/BdsSegmentedControl.mdx +76 -23
- package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +64 -54
- package/src/web-components/interaction/form/BdsSelect.mdx +59 -21
- package/src/web-components/interaction/form/BdsSelect.stories.tsx +47 -56
- package/src/web-components/interaction/form/BdsSlider.mdx +55 -27
- package/src/web-components/interaction/form/BdsSlider.stories.tsx +23 -44
- package/src/web-components/interaction/form/BdsSwitch.mdx +53 -20
- package/src/web-components/interaction/form/BdsSwitch.stories.tsx +22 -37
- package/src/web-components/interaction/form/BdsTextarea.mdx +54 -22
- package/src/web-components/interaction/form/BdsTextarea.stories.tsx +35 -44
- package/src/web-components/interaction/form/bds-checkbox-group.spec.ts +55 -0
- package/src/web-components/interaction/form/bds-checkbox-group.ts +117 -0
- package/src/web-components/interaction/form/bds-form-input.spec.ts +83 -0
- package/src/web-components/interaction/form/bds-form-input.ts +268 -0
- package/src/web-components/interaction/form/bds-radio-group.spec.ts +62 -0
- package/src/web-components/interaction/form/bds-radio-group.ts +142 -0
- package/src/web-components/interaction/form/bds-segmented-control.ts +5 -0
- package/src/web-components/ui/BdsAvatar.mdx +29 -20
- package/src/web-components/ui/BdsAvatar.stories.tsx +14 -28
- package/src/web-components/ui/BdsBreadcrumb.mdx +22 -19
- package/src/web-components/ui/BdsBreadcrumb.stories.tsx +11 -21
- package/src/web-components/ui/BdsButtonGroup.mdx +69 -0
- package/src/web-components/ui/BdsButtonGroup.stories.tsx +66 -0
- package/src/web-components/ui/BdsCalendar.mdx +93 -0
- package/src/web-components/ui/BdsCalendar.stories.tsx +63 -0
- package/src/web-components/ui/BdsCard.mdx +30 -25
- package/src/web-components/ui/BdsCard.stories.tsx +19 -42
- package/src/web-components/ui/BdsCarousel.mdx +83 -0
- package/src/web-components/ui/BdsCarousel.stories.tsx +96 -0
- package/src/web-components/ui/BdsDescriptionList.mdx +13 -16
- package/src/web-components/ui/BdsDescriptionList.stories.tsx +34 -49
- package/src/web-components/ui/BdsIconWrapper.mdx +20 -20
- package/src/web-components/ui/BdsIconWrapper.stories.tsx +46 -41
- package/src/web-components/ui/BdsLink.mdx +21 -17
- package/src/web-components/ui/BdsLink.stories.tsx +19 -20
- package/src/web-components/ui/BdsLoading.mdx +9 -13
- package/src/web-components/ui/BdsLoading.stories.tsx +8 -10
- package/src/web-components/ui/BdsNotificationBanner.mdx +32 -37
- package/src/web-components/ui/BdsNotificationBanner.stories.tsx +38 -47
- package/src/web-components/ui/BdsPagination.mdx +74 -0
- package/src/web-components/ui/BdsPagination.stories.tsx +36 -0
- package/src/web-components/ui/BdsProgress.mdx +24 -17
- package/src/web-components/ui/BdsProgress.stories.tsx +55 -29
- package/src/web-components/ui/BdsProgressCircle.mdx +24 -18
- package/src/web-components/ui/BdsProgressCircle.stories.tsx +17 -29
- package/src/web-components/ui/BdsRating.mdx +22 -22
- package/src/web-components/ui/BdsRating.stories.tsx +19 -13
- package/src/web-components/ui/BdsSectionHeader.mdx +31 -28
- package/src/web-components/ui/BdsSectionHeader.stories.tsx +21 -23
- package/src/web-components/ui/BdsSeparator.mdx +22 -8
- package/src/web-components/ui/BdsSeparator.stories.tsx +27 -19
- package/src/web-components/ui/BdsSkeleton.mdx +20 -26
- package/src/web-components/ui/BdsSkeleton.stories.tsx +23 -23
- package/src/web-components/ui/BdsTable.mdx +81 -0
- package/src/web-components/ui/BdsTable.stories.tsx +83 -0
- package/src/web-components/ui/BdsTypography.mdx +20 -29
- package/src/web-components/ui/BdsTypography.stories.tsx +19 -18
- package/src/web-components/ui/bds-button-group.spec.ts +40 -0
- package/src/web-components/ui/bds-button-group.ts +78 -0
- package/src/web-components/ui/bds-calendar.spec.ts +91 -0
- package/src/web-components/ui/bds-calendar.ts +427 -0
- package/src/web-components/ui/bds-carousel.spec.ts +64 -0
- package/src/web-components/ui/bds-carousel.ts +296 -0
- package/src/web-components/ui/bds-pagination.spec.ts +67 -0
- package/src/web-components/ui/bds-pagination.ts +197 -0
- package/src/web-components/ui/bds-table.spec.ts +45 -0
- package/src/web-components/ui/bds-table.ts +96 -0
|
@@ -37,85 +37,65 @@ export class BdsDrawer extends LitElement {
|
|
|
37
37
|
|
|
38
38
|
.drawer {
|
|
39
39
|
position: fixed;
|
|
40
|
-
inset: 0;
|
|
40
|
+
inset-block: 0;
|
|
41
|
+
inset-inline: auto 0;
|
|
42
|
+
inline-size: min(28rem, 90vw);
|
|
43
|
+
max-inline-size: 100vw;
|
|
44
|
+
block-size: 100dvh;
|
|
45
|
+
max-block-size: 100dvh;
|
|
41
46
|
margin: 0;
|
|
42
47
|
padding: 0;
|
|
43
|
-
inline-size: 100%;
|
|
44
|
-
max-inline-size: 100%;
|
|
45
|
-
block-size: 100%;
|
|
46
|
-
max-block-size: 100%;
|
|
47
|
-
border: none;
|
|
48
|
-
background: transparent;
|
|
49
|
-
overflow: hidden;
|
|
50
|
-
transition:
|
|
51
|
-
display var(--bds-animation_transition-duration) allow-discrete,
|
|
52
|
-
overlay var(--bds-animation_transition-duration) allow-discrete;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.drawer::backdrop {
|
|
56
|
-
background-color: var(--color_backdrop, rgb(0 0 0 / 50%));
|
|
57
|
-
backdrop-filter: blur(3px);
|
|
58
|
-
transition:
|
|
59
|
-
display var(--bds-animation_transition-duration) allow-discrete,
|
|
60
|
-
overlay var(--bds-animation_transition-duration) allow-discrete,
|
|
61
|
-
background-color var(--bds-animation_transition-duration) var(--bds-animation_easing),
|
|
62
|
-
backdrop-filter var(--bds-animation_transition-duration) var(--bds-animation_easing);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
@starting-style {
|
|
66
|
-
.drawer[open]::backdrop {
|
|
67
|
-
background-color: transparent;
|
|
68
|
-
backdrop-filter: blur(0);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.panel {
|
|
73
|
-
position: absolute;
|
|
74
|
-
inset-block: 0;
|
|
75
48
|
display: flex;
|
|
76
49
|
flex-direction: column;
|
|
77
|
-
inline-size: min(28rem, 90vw);
|
|
78
|
-
block-size: 100%;
|
|
79
50
|
background-color: var(--drawer_color, var(--bds-color_bg));
|
|
80
51
|
color: var(--drawer_on-color, var(--bds-color_on-bg));
|
|
52
|
+
border: none;
|
|
81
53
|
box-shadow: var(--bds-shadow_xl);
|
|
82
54
|
overflow: hidden;
|
|
83
55
|
translate: 100% 0;
|
|
84
|
-
transition:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
inset-inline-end: 0;
|
|
89
|
-
translate: 100% 0;
|
|
56
|
+
transition:
|
|
57
|
+
translate var(--bds-animation_transition-duration) var(--bds-animation_easing),
|
|
58
|
+
display var(--bds-animation_transition-duration) var(--bds-animation_easing) allow-discrete,
|
|
59
|
+
overlay var(--bds-animation_transition-duration) var(--bds-animation_easing) allow-discrete;
|
|
90
60
|
}
|
|
91
61
|
|
|
92
|
-
:host([side='left']) .
|
|
93
|
-
|
|
94
|
-
inset-inline-start: 0;
|
|
62
|
+
:host([side='left']) .drawer {
|
|
63
|
+
inset-inline: 0 auto;
|
|
95
64
|
translate: -100% 0;
|
|
96
65
|
}
|
|
97
66
|
|
|
98
|
-
.drawer[open]
|
|
67
|
+
.drawer[open] {
|
|
99
68
|
translate: 0 0;
|
|
100
69
|
}
|
|
101
70
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
71
|
+
@starting-style {
|
|
72
|
+
.drawer[open] {
|
|
73
|
+
translate: 100% 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
:host([side='left']) .drawer[open] {
|
|
77
|
+
translate: -100% 0;
|
|
78
|
+
}
|
|
105
79
|
}
|
|
106
80
|
|
|
107
|
-
|
|
108
|
-
|
|
81
|
+
.drawer::backdrop {
|
|
82
|
+
background-color: var(--color_backdrop, rgb(0 0 0 / 50%));
|
|
83
|
+
backdrop-filter: blur(3px);
|
|
84
|
+
opacity: 0;
|
|
85
|
+
transition:
|
|
86
|
+
opacity var(--bds-animation_transition-duration) var(--bds-animation_easing),
|
|
87
|
+
display var(--bds-animation_transition-duration) allow-discrete,
|
|
88
|
+
overlay var(--bds-animation_transition-duration) allow-discrete;
|
|
109
89
|
}
|
|
110
90
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
to { translate: 0 0; }
|
|
91
|
+
.drawer[open]::backdrop {
|
|
92
|
+
opacity: 1;
|
|
114
93
|
}
|
|
115
94
|
|
|
116
|
-
@
|
|
117
|
-
|
|
118
|
-
|
|
95
|
+
@starting-style {
|
|
96
|
+
.drawer[open]::backdrop {
|
|
97
|
+
opacity: 0;
|
|
98
|
+
}
|
|
119
99
|
}
|
|
120
100
|
|
|
121
101
|
.header {
|
|
@@ -170,16 +150,9 @@ export class BdsDrawer extends LitElement {
|
|
|
170
150
|
|
|
171
151
|
@media (prefers-reduced-motion: reduce) {
|
|
172
152
|
.drawer,
|
|
173
|
-
.drawer::backdrop
|
|
174
|
-
.panel {
|
|
153
|
+
.drawer::backdrop {
|
|
175
154
|
transition: none;
|
|
176
155
|
}
|
|
177
|
-
|
|
178
|
-
:host([side='right']) .drawer[open] .panel,
|
|
179
|
-
:host(:not([side])) .drawer[open] .panel,
|
|
180
|
-
:host([side='left']) .drawer[open] .panel {
|
|
181
|
-
animation: none;
|
|
182
|
-
}
|
|
183
156
|
}
|
|
184
157
|
`;
|
|
185
158
|
|
|
@@ -251,7 +224,13 @@ export class BdsDrawer extends LitElement {
|
|
|
251
224
|
}
|
|
252
225
|
|
|
253
226
|
private _handleBackdropClick(e: MouseEvent) {
|
|
254
|
-
|
|
227
|
+
const dialog = this._dialog;
|
|
228
|
+
if (!dialog) return;
|
|
229
|
+
const rect = dialog.getBoundingClientRect();
|
|
230
|
+
const outside =
|
|
231
|
+
e.clientX < rect.left || e.clientX > rect.right ||
|
|
232
|
+
e.clientY < rect.top || e.clientY > rect.bottom;
|
|
233
|
+
if (outside) this._closeDrawer();
|
|
255
234
|
}
|
|
256
235
|
|
|
257
236
|
private _handleCancel(e: Event) {
|
|
@@ -283,23 +262,21 @@ export class BdsDrawer extends LitElement {
|
|
|
283
262
|
@cancel=${this._handleCancel}
|
|
284
263
|
@keydown=${this._handleKeydown}
|
|
285
264
|
>
|
|
286
|
-
<div class="
|
|
287
|
-
<
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
>
|
|
295
|
-
<
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
<
|
|
301
|
-
<slot></slot>
|
|
302
|
-
</div>
|
|
265
|
+
<div class="header">
|
|
266
|
+
<span class="title"><slot name="title"></slot></span>
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
class="close-button"
|
|
270
|
+
aria-label="Close drawer"
|
|
271
|
+
@click=${this._closeDrawer}
|
|
272
|
+
>
|
|
273
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
274
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M18 6L6 18M6 6l12 12" />
|
|
275
|
+
</svg>
|
|
276
|
+
</button>
|
|
277
|
+
</div>
|
|
278
|
+
<div class="body">
|
|
279
|
+
<slot></slot>
|
|
303
280
|
</div>
|
|
304
281
|
</dialog>
|
|
305
282
|
`;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { LitElement, css, html } from 'lit';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `<bds-dropdown-menu-item>` — a single item inside a `<bds-dropdown-menu>`.
|
|
5
|
+
*
|
|
6
|
+
* Attributes:
|
|
7
|
+
* value — value associated with this item
|
|
8
|
+
* disabled — disables this item
|
|
9
|
+
* destructive — renders the item in a destructive (red) style
|
|
10
|
+
*
|
|
11
|
+
* Slots:
|
|
12
|
+
* (default) — item label text
|
|
13
|
+
*
|
|
14
|
+
* Events:
|
|
15
|
+
* bds-select — fired when the item is clicked or activated; `detail: { value: string }`
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* <bds-dropdown-menu-item value="delete" destructive>Delete</bds-dropdown-menu-item>
|
|
19
|
+
*/
|
|
20
|
+
export class BdsDropdownMenuItem extends LitElement {
|
|
21
|
+
static styles = css`
|
|
22
|
+
:host {
|
|
23
|
+
display: block;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
:host([disabled]) {
|
|
27
|
+
opacity: 0.5;
|
|
28
|
+
pointer-events: none;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.item {
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
gap: var(--bds-space_xs);
|
|
35
|
+
inline-size: 100%;
|
|
36
|
+
padding: var(--bds-space_xs) var(--bds-space_s);
|
|
37
|
+
font-size: var(--bds-font_size--body);
|
|
38
|
+
font-family: var(--bds-font_family--body);
|
|
39
|
+
color: var(--bds-color_on-bg);
|
|
40
|
+
background: none;
|
|
41
|
+
border: none;
|
|
42
|
+
text-align: start;
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
border-radius: var(--bds-border_radius--xs);
|
|
45
|
+
transition: var(--bds-animation_transition);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
:host([destructive]) .item {
|
|
49
|
+
color: var(--bds-color_error);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@media (hover: hover) and (pointer: fine) {
|
|
53
|
+
.item:hover {
|
|
54
|
+
background-color: var(--bds-color_bg--subtle);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
:host([destructive]) .item:hover {
|
|
58
|
+
background-color: color-mix(in srgb, var(--bds-color_error) 10%, transparent);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.item:focus-visible {
|
|
63
|
+
outline: var(--bds-outline_default);
|
|
64
|
+
outline-offset: var(--bds-outline_offset);
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
static properties = {
|
|
69
|
+
value: { type: String, reflect: true },
|
|
70
|
+
disabled: { type: Boolean, reflect: true },
|
|
71
|
+
destructive: { type: Boolean, reflect: true },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
declare value: string;
|
|
75
|
+
declare disabled: boolean;
|
|
76
|
+
declare destructive: boolean;
|
|
77
|
+
|
|
78
|
+
constructor() {
|
|
79
|
+
super();
|
|
80
|
+
this.value = '';
|
|
81
|
+
this.disabled = false;
|
|
82
|
+
this.destructive = false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private _handleClick() {
|
|
86
|
+
if (this.disabled) return;
|
|
87
|
+
this.dispatchEvent(new CustomEvent('bds-select', {
|
|
88
|
+
detail: { value: this.value },
|
|
89
|
+
bubbles: true,
|
|
90
|
+
composed: true,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private _handleKeydown(e: KeyboardEvent) {
|
|
95
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
this._handleClick();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
render() {
|
|
102
|
+
return html`
|
|
103
|
+
<button
|
|
104
|
+
class="item"
|
|
105
|
+
type="button"
|
|
106
|
+
role="menuitem"
|
|
107
|
+
tabindex=${this.disabled ? '-1' : '0'}
|
|
108
|
+
?disabled=${this.disabled}
|
|
109
|
+
@click=${this._handleClick}
|
|
110
|
+
@keydown=${this._handleKeydown}
|
|
111
|
+
>
|
|
112
|
+
<slot></slot>
|
|
113
|
+
</button>
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
customElements.define('bds-dropdown-menu-item', BdsDropdownMenuItem);
|
|
119
|
+
|
|
120
|
+
declare global {
|
|
121
|
+
interface HTMLElementTagNameMap {
|
|
122
|
+
'bds-dropdown-menu-item': BdsDropdownMenuItem;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { fixture, cleanup } from '../test/helpers';
|
|
2
|
+
import { BdsDropdownMenu } from './bds-dropdown-menu';
|
|
3
|
+
import { BdsDropdownMenuItem } from './bds-dropdown-menu-item';
|
|
4
|
+
|
|
5
|
+
describe('bds-dropdown-menu', () => {
|
|
6
|
+
it('is registered as a custom element', () => {
|
|
7
|
+
expect(customElements.get('bds-dropdown-menu')).toBe(BdsDropdownMenu);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('is closed by default', async () => {
|
|
11
|
+
const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
|
|
12
|
+
expect(el.open).toBe(false);
|
|
13
|
+
cleanup(el);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('open attribute is absent when closed', async () => {
|
|
17
|
+
const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
|
|
18
|
+
expect(el.open).toBe(false);
|
|
19
|
+
expect(el.hasAttribute('open')).toBe(false);
|
|
20
|
+
cleanup(el);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('fires bds-open when _open() is called', async () => {
|
|
24
|
+
const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
|
|
25
|
+
const events: string[] = [];
|
|
26
|
+
el.addEventListener('bds-open', () => events.push('open'));
|
|
27
|
+
el._open();
|
|
28
|
+
expect(events).toContain('open');
|
|
29
|
+
cleanup(el);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('fires bds-close when _close() is called', async () => {
|
|
33
|
+
const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
|
|
34
|
+
el._open();
|
|
35
|
+
const events: string[] = [];
|
|
36
|
+
el.addEventListener('bds-close', () => events.push('close'));
|
|
37
|
+
el._close();
|
|
38
|
+
expect(events).toContain('close');
|
|
39
|
+
cleanup(el);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not fire bds-open if already open (state guard)', async () => {
|
|
43
|
+
const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
|
|
44
|
+
el._open();
|
|
45
|
+
const events: string[] = [];
|
|
46
|
+
el.addEventListener('bds-open', () => events.push('open'));
|
|
47
|
+
el._open(); // call again
|
|
48
|
+
expect(events.length).toBe(0);
|
|
49
|
+
cleanup(el);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('does not fire bds-close if already closed (state guard)', async () => {
|
|
53
|
+
const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
|
|
54
|
+
const events: string[] = [];
|
|
55
|
+
el.addEventListener('bds-close', () => events.push('close'));
|
|
56
|
+
el._close(); // already closed
|
|
57
|
+
expect(events.length).toBe(0);
|
|
58
|
+
cleanup(el);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('closes on Escape key', async () => {
|
|
62
|
+
const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
|
|
63
|
+
el._open();
|
|
64
|
+
expect(el.open).toBe(true);
|
|
65
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
66
|
+
expect(el.open).toBe(false);
|
|
67
|
+
cleanup(el);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('bds-dropdown-menu-item', () => {
|
|
72
|
+
it('is registered as a custom element', () => {
|
|
73
|
+
expect(customElements.get('bds-dropdown-menu-item')).toBe(BdsDropdownMenuItem);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('renders a button with role="menuitem"', async () => {
|
|
77
|
+
const el = await fixture('<bds-dropdown-menu-item value="edit">Edit</bds-dropdown-menu-item>');
|
|
78
|
+
const btn = el.shadowRoot!.querySelector('button')!;
|
|
79
|
+
expect(btn.getAttribute('role')).toBe('menuitem');
|
|
80
|
+
cleanup(el);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('fires bds-select on click', async () => {
|
|
84
|
+
const el = await fixture('<bds-dropdown-menu-item value="edit">Edit</bds-dropdown-menu-item>');
|
|
85
|
+
const events: string[] = [];
|
|
86
|
+
el.addEventListener('bds-select', (e: Event) => {
|
|
87
|
+
events.push((e as CustomEvent).detail.value);
|
|
88
|
+
});
|
|
89
|
+
el.shadowRoot!.querySelector<HTMLButtonElement>('button')!.click();
|
|
90
|
+
expect(events).toEqual(['edit']);
|
|
91
|
+
cleanup(el);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('does not fire bds-select when disabled', async () => {
|
|
95
|
+
const el = await fixture('<bds-dropdown-menu-item value="edit" disabled>Edit</bds-dropdown-menu-item>');
|
|
96
|
+
const events: string[] = [];
|
|
97
|
+
el.addEventListener('bds-select', () => events.push('selected'));
|
|
98
|
+
el.shadowRoot!.querySelector<HTMLButtonElement>('button')!.click();
|
|
99
|
+
expect(events.length).toBe(0);
|
|
100
|
+
cleanup(el);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { LitElement, css, html } from 'lit';
|
|
2
|
+
import type { BdsDropdownMenuItem } from './bds-dropdown-menu-item';
|
|
3
|
+
|
|
4
|
+
export type DropdownMenuPlacement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `<bds-dropdown-menu>` — anchored dropdown menu with keyboard navigation.
|
|
8
|
+
*
|
|
9
|
+
* Uses the native Popover API for the floating panel (light-dismiss, top-layer).
|
|
10
|
+
* The trigger slot element controls open/close. The component handles keyboard
|
|
11
|
+
* navigation (Arrow keys, Escape, Tab) and focus management.
|
|
12
|
+
*
|
|
13
|
+
* Attributes:
|
|
14
|
+
* open — controls open/closed state (reflected)
|
|
15
|
+
* placement — menu position relative to trigger (default: `"bottom-start"`)
|
|
16
|
+
*
|
|
17
|
+
* Slots:
|
|
18
|
+
* trigger — the activating element (button or similar)
|
|
19
|
+
* (default) — `<bds-dropdown-menu-item>` elements
|
|
20
|
+
*
|
|
21
|
+
* Events:
|
|
22
|
+
* bds-open — fired when the menu opens
|
|
23
|
+
* bds-close — fired when the menu closes
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* <bds-dropdown-menu>
|
|
27
|
+
* <button slot="trigger">Options</button>
|
|
28
|
+
* <bds-dropdown-menu-item value="edit">Edit</bds-dropdown-menu-item>
|
|
29
|
+
* <bds-dropdown-menu-item value="delete" destructive>Delete</bds-dropdown-menu-item>
|
|
30
|
+
* </bds-dropdown-menu>
|
|
31
|
+
*/
|
|
32
|
+
export class BdsDropdownMenu extends LitElement {
|
|
33
|
+
static styles = css`
|
|
34
|
+
:host {
|
|
35
|
+
display: inline-block;
|
|
36
|
+
position: relative;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.panel {
|
|
40
|
+
position: absolute;
|
|
41
|
+
inset-block-start: calc(100% + var(--bds-space_xxs));
|
|
42
|
+
inset-inline-start: 0;
|
|
43
|
+
min-inline-size: 10rem;
|
|
44
|
+
background-color: var(--bds-color_bg);
|
|
45
|
+
border-radius: var(--bds-border_radius--s);
|
|
46
|
+
box-shadow: var(--bds-shadow_m);
|
|
47
|
+
padding: var(--bds-space_xxs);
|
|
48
|
+
z-index: var(--bds-z-index_dropdown, 200);
|
|
49
|
+
display: none;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
:host([placement='bottom-end']) .panel {
|
|
54
|
+
inset-inline-start: auto;
|
|
55
|
+
inset-inline-end: 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
:host([placement='top-start']) .panel {
|
|
59
|
+
inset-block-start: auto;
|
|
60
|
+
inset-block-end: calc(100% + var(--bds-space_xxs));
|
|
61
|
+
inset-inline-start: 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
:host([placement='top-end']) .panel {
|
|
65
|
+
inset-block-start: auto;
|
|
66
|
+
inset-block-end: calc(100% + var(--bds-space_xxs));
|
|
67
|
+
inset-inline-start: auto;
|
|
68
|
+
inset-inline-end: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
:host([open]) .panel {
|
|
72
|
+
display: flex;
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
static properties = {
|
|
77
|
+
open: { type: Boolean, reflect: true },
|
|
78
|
+
placement: { type: String, reflect: true },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
declare open: boolean;
|
|
82
|
+
declare placement: DropdownMenuPlacement;
|
|
83
|
+
|
|
84
|
+
constructor() {
|
|
85
|
+
super();
|
|
86
|
+
this.open = false;
|
|
87
|
+
this.placement = 'bottom-start';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
connectedCallback() {
|
|
91
|
+
super.connectedCallback();
|
|
92
|
+
document.addEventListener('click', this._handleOutsideClick);
|
|
93
|
+
this.addEventListener('keydown', this._handleKeydown);
|
|
94
|
+
this.addEventListener('bds-select', this._handleItemSelect);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
disconnectedCallback() {
|
|
98
|
+
super.disconnectedCallback();
|
|
99
|
+
document.removeEventListener('click', this._handleOutsideClick);
|
|
100
|
+
this.removeEventListener('keydown', this._handleKeydown);
|
|
101
|
+
this.removeEventListener('bds-select', this._handleItemSelect);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private get _items(): BdsDropdownMenuItem[] {
|
|
105
|
+
return [...this.querySelectorAll<BdsDropdownMenuItem>('bds-dropdown-menu-item')].filter(
|
|
106
|
+
item => !item.disabled,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private get _trigger(): HTMLElement | null {
|
|
111
|
+
const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name="trigger"]');
|
|
112
|
+
return (slot?.assignedElements()[0] as HTMLElement) ?? null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_open() {
|
|
116
|
+
if (this.open) return;
|
|
117
|
+
this.open = true;
|
|
118
|
+
this.dispatchEvent(new CustomEvent('bds-open', { bubbles: true, composed: true }));
|
|
119
|
+
// Focus first item after render
|
|
120
|
+
this.updateComplete.then(() => {
|
|
121
|
+
this._items[0]?.shadowRoot?.querySelector<HTMLElement>('button')?.focus();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_close() {
|
|
126
|
+
if (!this.open) return;
|
|
127
|
+
this.open = false;
|
|
128
|
+
this.dispatchEvent(new CustomEvent('bds-close', { bubbles: true, composed: true }));
|
|
129
|
+
this._trigger?.focus();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private _handleTriggerClick = (e: Event) => {
|
|
133
|
+
e.stopPropagation();
|
|
134
|
+
if (this.open) { this._close(); } else { this._open(); }
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
private _handleOutsideClick = (e: Event) => {
|
|
138
|
+
if (this.open && !this.contains(e.target as Node)) {
|
|
139
|
+
this._close();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
private _handleKeydown = (e: KeyboardEvent) => {
|
|
144
|
+
if (!this.open) return;
|
|
145
|
+
|
|
146
|
+
const items = this._items;
|
|
147
|
+
const focused = items.findIndex(item =>
|
|
148
|
+
item.shadowRoot?.activeElement !== null &&
|
|
149
|
+
item.shadowRoot?.querySelector('button') === item.shadowRoot?.activeElement,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (e.key === 'Escape') {
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
this._close();
|
|
155
|
+
} else if (e.key === 'ArrowDown') {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
const next = (focused + 1) % items.length;
|
|
158
|
+
items[next]?.shadowRoot?.querySelector<HTMLElement>('button')?.focus();
|
|
159
|
+
} else if (e.key === 'ArrowUp') {
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
const prev = (focused - 1 + items.length) % items.length;
|
|
162
|
+
items[prev]?.shadowRoot?.querySelector<HTMLElement>('button')?.focus();
|
|
163
|
+
} else if (e.key === 'Tab') {
|
|
164
|
+
this._close();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
private _handleItemSelect = () => {
|
|
169
|
+
this._close();
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
private _onTriggerSlotChange() {
|
|
173
|
+
const trigger = this._trigger;
|
|
174
|
+
if (trigger) {
|
|
175
|
+
trigger.removeEventListener('click', this._handleTriggerClick);
|
|
176
|
+
trigger.addEventListener('click', this._handleTriggerClick);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
render() {
|
|
181
|
+
return html`
|
|
182
|
+
<slot name="trigger" @slotchange=${this._onTriggerSlotChange}></slot>
|
|
183
|
+
<div
|
|
184
|
+
class="panel"
|
|
185
|
+
role="menu"
|
|
186
|
+
aria-hidden=${!this.open ? 'true' : 'false'}
|
|
187
|
+
>
|
|
188
|
+
<slot></slot>
|
|
189
|
+
</div>
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
customElements.define('bds-dropdown-menu', BdsDropdownMenu);
|
|
195
|
+
|
|
196
|
+
declare global {
|
|
197
|
+
interface HTMLElementTagNameMap {
|
|
198
|
+
'bds-dropdown-menu': BdsDropdownMenu;
|
|
199
|
+
}
|
|
200
|
+
}
|