@inglorious/ui 8.6.0 → 9.0.0
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/package.json +1 -1
- package/src/controls/button/template.js +4 -4
- package/src/controls/combobox/style.css +14 -7
- package/src/controls/combobox/template.js +12 -2
- package/src/controls/rating/style.css +26 -4
- package/src/controls/rating/template.js +29 -12
- package/src/controls/switch/template.js +3 -1
- package/src/data-display/tooltip/style.css +22 -14
- package/src/examples/dashboard/primitive-content.js +6 -6
- package/src/examples/dashboard/style.css +11 -5
- package/src/feedback/dialog/dialog.stories.js +9 -0
- package/src/feedback/dialog/style.css +13 -12
- package/src/feedback/dialog/template.js +63 -18
- package/src/feedback/progress/style.css +47 -23
- package/src/feedback/progress/template.js +18 -11
- package/src/feedback/snackbar/style.css +13 -0
- package/src/feedback/snackbar/template.js +7 -0
- package/src/navigation/drawer/drawer.stories.js +3 -7
- package/src/navigation/drawer/style.css +59 -18
- package/src/navigation/drawer/template.js +137 -80
- package/src/navigation/menu/menu.stories.js +15 -0
- package/src/navigation/menu/style.css +19 -0
- package/src/navigation/menu/template.js +59 -6
- package/src/navigation/speed-dial/style.css +32 -5
- package/src/navigation/speed-dial/template.js +41 -12
- package/src/navigation/tabs/template.js +52 -4
- package/src/shared/anchor.js +11 -0
- package/src/shared/top-layer.js +83 -0
- package/src/surfaces/accordion/style.css +14 -7
- package/src/surfaces/accordion/template.js +27 -20
- package/types/controls/rating.d.ts +4 -0
- package/types/feedback/dialog.d.ts +3 -1
- package/types/feedback/progress.d.ts +2 -0
- package/types/navigation/drawer.d.ts +8 -3
- package/types/navigation/menu.d.ts +17 -0
- package/types/navigation/speed-dial.d.ts +5 -0
- package/types/surfaces/accordion.d.ts +2 -0
|
@@ -24,41 +24,65 @@
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/* A native <progress>, restyled. Browsers draw its value through their own
|
|
28
|
+
pseudo-elements, so the look is set once here: the bar takes `color`, like
|
|
29
|
+
the color variants above, and an app adjusts the rest through
|
|
30
|
+
--iw-progress-height, --iw-progress-radius and --iw-progress-track-color. */
|
|
27
31
|
.iw-progress-linear {
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.iw-progress-linear-track {
|
|
32
|
-
width: 100%;
|
|
33
|
-
height: 8px;
|
|
34
|
-
background: color-mix(in srgb, currentColor 24%, transparent);
|
|
35
|
-
border-radius: var(--iw-radius-sm);
|
|
36
|
-
overflow: hidden;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.iw-progress-linear-bar {
|
|
40
|
-
display: block;
|
|
41
|
-
height: 100%;
|
|
42
|
-
background: linear-gradient(
|
|
32
|
+
--iw-progress-bar: linear-gradient(
|
|
43
33
|
90deg,
|
|
44
34
|
color-mix(in srgb, currentColor 84%, white),
|
|
45
35
|
currentColor
|
|
46
36
|
);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
37
|
+
--iw-progress-track: var(
|
|
38
|
+
--iw-progress-track-color,
|
|
39
|
+
color-mix(in srgb, currentColor 24%, transparent)
|
|
40
|
+
);
|
|
50
41
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
42
|
+
appearance: none;
|
|
43
|
+
display: block;
|
|
44
|
+
width: 100%;
|
|
45
|
+
height: var(--iw-progress-height, 8px);
|
|
46
|
+
border: 0;
|
|
47
|
+
border-radius: var(--iw-progress-radius, var(--iw-radius-sm));
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
background: var(--iw-progress-track);
|
|
50
|
+
|
|
51
|
+
&::-webkit-progress-bar {
|
|
52
|
+
background: transparent;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
&::-webkit-progress-value {
|
|
56
|
+
background: var(--iw-progress-bar);
|
|
57
|
+
border-radius: var(--iw-progress-radius, var(--iw-radius-sm));
|
|
58
|
+
transition: width 0.2s ease;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
&::-moz-progress-bar {
|
|
62
|
+
background: var(--iw-progress-bar);
|
|
63
|
+
border-radius: var(--iw-progress-radius, var(--iw-radius-sm));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Indeterminate: no value to draw, so a sliver of the bar sweeps across
|
|
67
|
+
the track, painted on the element itself. */
|
|
68
|
+
&:indeterminate {
|
|
69
|
+
background:
|
|
70
|
+
var(--iw-progress-bar) no-repeat 0 0 / 30% 100%,
|
|
71
|
+
var(--iw-progress-track);
|
|
72
|
+
animation: iw-progress-linear 1.4s ease-in-out infinite;
|
|
73
|
+
|
|
74
|
+
&::-moz-progress-bar {
|
|
75
|
+
background: transparent;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
54
78
|
}
|
|
55
79
|
|
|
56
80
|
@keyframes iw-progress-linear {
|
|
57
81
|
0% {
|
|
58
|
-
|
|
82
|
+
background-position-x: -50%;
|
|
59
83
|
}
|
|
60
84
|
100% {
|
|
61
|
-
|
|
85
|
+
background-position-x: 150%;
|
|
62
86
|
}
|
|
63
87
|
}
|
|
64
88
|
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
* @typedef {import('@inglorious/web').TemplateResult} TemplateResult
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { html } from "@inglorious/web"
|
|
6
|
+
import { html, nothing } from "@inglorious/web"
|
|
7
7
|
import { classMap } from "@inglorious/web/directives/class-map"
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Renders a linear or circular progress indicator.
|
|
11
11
|
* Indeterminate mode is used when no explicit value is provided.
|
|
12
|
+
* The linear bar is a native `<progress>`; the circle, which no native element
|
|
13
|
+
* draws, is an SVG with the same `progressbar` semantics.
|
|
12
14
|
* @param {ProgressProps} props
|
|
13
15
|
* @returns {TemplateResult}
|
|
14
16
|
*/
|
|
@@ -19,6 +21,7 @@ export function render(props) {
|
|
|
19
21
|
size = 40,
|
|
20
22
|
thickness = 4,
|
|
21
23
|
color = "default",
|
|
24
|
+
ariaLabel,
|
|
22
25
|
className = "",
|
|
23
26
|
} = props
|
|
24
27
|
|
|
@@ -45,8 +48,13 @@ export function render(props) {
|
|
|
45
48
|
...extraClasses,
|
|
46
49
|
})}
|
|
47
50
|
style=${`width: ${size}px; height: ${size}px;`}
|
|
51
|
+
role="progressbar"
|
|
52
|
+
aria-label=${ariaLabel || nothing}
|
|
53
|
+
aria-valuemin="0"
|
|
54
|
+
aria-valuemax="100"
|
|
55
|
+
aria-valuenow=${progress ?? nothing}
|
|
48
56
|
>
|
|
49
|
-
<svg viewBox="0 0 ${size} ${size}">
|
|
57
|
+
<svg aria-hidden="true" viewBox="0 0 ${size} ${size}">
|
|
50
58
|
<circle
|
|
51
59
|
class="iw-progress-track"
|
|
52
60
|
cx=${size / 2}
|
|
@@ -69,7 +77,10 @@ export function render(props) {
|
|
|
69
77
|
|
|
70
78
|
const progress = value == null ? null : clamp(value)
|
|
71
79
|
|
|
72
|
-
|
|
80
|
+
// An attribute, not a property: removing it is the only way back to
|
|
81
|
+
// indeterminate, which `:indeterminate` styles and assistive tech announces
|
|
82
|
+
// as busy.
|
|
83
|
+
return html`<progress
|
|
73
84
|
class=${classMap({
|
|
74
85
|
"iw-progress": true,
|
|
75
86
|
"iw-progress-linear": true,
|
|
@@ -77,14 +88,10 @@ export function render(props) {
|
|
|
77
88
|
"iw-progress-indeterminate": progress == null,
|
|
78
89
|
...extraClasses,
|
|
79
90
|
})}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
style=${progress == null ? "" : `width: ${progress}%;`}
|
|
85
|
-
></div>
|
|
86
|
-
</div>
|
|
87
|
-
</div>`
|
|
91
|
+
max="100"
|
|
92
|
+
value=${progress ?? nothing}
|
|
93
|
+
aria-label=${ariaLabel || nothing}
|
|
94
|
+
></progress>`
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
function clamp(value) {
|
|
@@ -10,6 +10,19 @@
|
|
|
10
10
|
max-width: calc(100vw - 2 * var(--iw-snackbar-gap));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/* A manual popover in the top layer. The browser centres a popover in the
|
|
14
|
+
viewport as a bordered box, so its insets and box are reset, with no
|
|
15
|
+
specificity so the positions below still set their own sides. */
|
|
16
|
+
:where(.iw-snackbar) {
|
|
17
|
+
inset: auto;
|
|
18
|
+
margin: 0;
|
|
19
|
+
padding: 0;
|
|
20
|
+
border: 0;
|
|
21
|
+
overflow: visible;
|
|
22
|
+
color: inherit;
|
|
23
|
+
background: transparent;
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
.iw-snackbar-bottom-left,
|
|
14
27
|
.iw-snackbar-bottom-center,
|
|
15
28
|
.iw-snackbar-bottom-right {
|
|
@@ -5,10 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
import { html } from "@inglorious/web"
|
|
7
7
|
import { classMap } from "@inglorious/web/directives/class-map"
|
|
8
|
+
import { ref } from "@inglorious/web/directives/ref"
|
|
9
|
+
|
|
10
|
+
import { syncPopover } from "../../shared/top-layer.js"
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Renders a transient snackbar message with optional action and dismiss control.
|
|
11
14
|
* Position is configurable (for example bottom-left or top-right).
|
|
15
|
+
* A manual popover, so it shows in the top layer, above an open modal dialog,
|
|
16
|
+
* and only `isOpen` or its close button take it down.
|
|
12
17
|
* @param {SnackbarProps} props
|
|
13
18
|
* @returns {TemplateResult | null}
|
|
14
19
|
*/
|
|
@@ -39,6 +44,8 @@ export function render(props) {
|
|
|
39
44
|
})}
|
|
40
45
|
role="status"
|
|
41
46
|
aria-live="polite"
|
|
47
|
+
popover="manual"
|
|
48
|
+
${ref((element) => syncPopover(element, { isOpen: true }))}
|
|
42
49
|
>
|
|
43
50
|
<div class="iw-snackbar-content">
|
|
44
51
|
<span class="iw-snackbar-message">${message}</span>
|
|
@@ -19,13 +19,9 @@ export default {
|
|
|
19
19
|
},
|
|
20
20
|
variant: {
|
|
21
21
|
control: "select",
|
|
22
|
-
options: ["
|
|
23
|
-
description:
|
|
24
|
-
|
|
25
|
-
breakpoint: {
|
|
26
|
-
control: "select",
|
|
27
|
-
options: ["sm", "md", "lg", "xl"],
|
|
28
|
-
description: "Desktop breakpoint for responsive drawers.",
|
|
22
|
+
options: ["responsive", "temporary", "permanent"],
|
|
23
|
+
description:
|
|
24
|
+
"When the drawer overlays the page: below 64rem, always, or never.",
|
|
29
25
|
},
|
|
30
26
|
isCollapsed: {
|
|
31
27
|
control: "boolean",
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
/* While it overlays the page the drawer is a popover in the top layer. The
|
|
2
|
+
browser styles every popover as a centred, bordered box hidden until it
|
|
3
|
+
opens, so those defaults are reset here: the drawer stays displayed while
|
|
4
|
+
closed, off-screen or docked, and slides rather than appears. */
|
|
1
5
|
.iw-drawer {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
2
8
|
width: 16rem;
|
|
9
|
+
height: auto;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
border: 0;
|
|
13
|
+
overflow: visible;
|
|
3
14
|
background: var(--iw-drawer-bg, var(--iw-color-surface));
|
|
4
15
|
color: var(--iw-color-text);
|
|
5
16
|
transition:
|
|
@@ -10,8 +21,7 @@
|
|
|
10
21
|
max-width var(--iw-transition-fast);
|
|
11
22
|
|
|
12
23
|
position: fixed;
|
|
13
|
-
|
|
14
|
-
bottom: 0;
|
|
24
|
+
inset-block: 0;
|
|
15
25
|
z-index: var(--iw-z-modal, 1050);
|
|
16
26
|
|
|
17
27
|
/* As tall as an app bar, so the line under a drawer and the app bar beside
|
|
@@ -105,33 +115,68 @@
|
|
|
105
115
|
/* `left` and `right` are the start and end of the line: the left drawer
|
|
106
116
|
sits on the right in a right-to-left page, and slides off that way. */
|
|
107
117
|
.iw-drawer-left {
|
|
108
|
-
inset-inline
|
|
118
|
+
inset-inline: 0 auto;
|
|
109
119
|
border-inline-end: 1px solid var(--iw-color-border);
|
|
110
120
|
transform: translateX(calc(-100% * var(--iw-dir-sign, 1)));
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
.iw-drawer-right {
|
|
114
|
-
inset-inline
|
|
124
|
+
inset-inline: auto 0;
|
|
115
125
|
border-inline-start: 1px solid var(--iw-color-border);
|
|
116
126
|
transform: translateX(calc(100% * var(--iw-dir-sign, 1)));
|
|
117
127
|
}
|
|
118
128
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
129
|
+
.iw-drawer::backdrop {
|
|
130
|
+
background: color-mix(in srgb, var(--iw-color-black) 65%, transparent);
|
|
131
|
+
backdrop-filter: blur(3px);
|
|
132
|
+
}
|
|
122
133
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
.iw-drawer-no-backdrop::backdrop {
|
|
135
|
+
background: transparent;
|
|
136
|
+
backdrop-filter: none;
|
|
137
|
+
}
|
|
126
138
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
139
|
+
/* Overlaying: a temporary drawer at every width, a responsive one below
|
|
140
|
+
64rem. Off-screen until open, raised, closed from its header. The collapse
|
|
141
|
+
toggle only makes sense for a rail, so the footer goes. */
|
|
142
|
+
.iw-drawer {
|
|
143
|
+
box-shadow: var(--iw-shadow-lg);
|
|
144
|
+
|
|
145
|
+
&.iw-drawer-open {
|
|
146
|
+
transform: translateX(0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.iw-drawer-footer {
|
|
150
|
+
display: none;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Docked: a permanent drawer at every width, a responsive one from 64rem
|
|
155
|
+
(keep in step with DOCKED_QUERY in template.js). In place, flat, and
|
|
156
|
+
hidden only on request. */
|
|
157
|
+
.iw-drawer-permanent {
|
|
158
|
+
transform: none;
|
|
159
|
+
box-shadow: none;
|
|
160
|
+
|
|
161
|
+
&.iw-drawer-left.iw-drawer-hidden {
|
|
162
|
+
transform: translateX(calc(-100% * var(--iw-dir-sign, 1)));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
&.iw-drawer-right.iw-drawer-hidden {
|
|
166
|
+
transform: translateX(calc(100% * var(--iw-dir-sign, 1)));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.iw-drawer-close {
|
|
170
|
+
display: none;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.iw-drawer-footer {
|
|
174
|
+
display: flex;
|
|
130
175
|
}
|
|
131
176
|
}
|
|
132
177
|
|
|
133
178
|
@media (min-width: 64rem) {
|
|
134
|
-
.iw-drawer {
|
|
179
|
+
.iw-drawer-responsive {
|
|
135
180
|
transform: none;
|
|
136
181
|
box-shadow: none;
|
|
137
182
|
|
|
@@ -143,10 +188,6 @@
|
|
|
143
188
|
transform: translateX(calc(100% * var(--iw-dir-sign, 1)));
|
|
144
189
|
}
|
|
145
190
|
|
|
146
|
-
& ~ .iw-drawer-backdrop {
|
|
147
|
-
display: none;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
191
|
.iw-drawer-close {
|
|
151
192
|
display: none;
|
|
152
193
|
}
|
|
@@ -3,18 +3,38 @@
|
|
|
3
3
|
* @typedef {import('@inglorious/web').TemplateResult} TemplateResult
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { html } from "@inglorious/web"
|
|
6
|
+
import { html, nothing } from "@inglorious/web"
|
|
7
|
+
import { classMap } from "@inglorious/web/directives/class-map"
|
|
8
|
+
import { ref } from "@inglorious/web/directives/ref"
|
|
7
9
|
import { when } from "@inglorious/web/directives/when"
|
|
8
10
|
|
|
9
11
|
import { Button } from "../../controls/button/index.js"
|
|
10
12
|
import { Icon } from "../../data-display/icon/index.js"
|
|
11
|
-
import { Backdrop } from "../../feedback/backdrop/index.js"
|
|
12
13
|
import { Flex } from "../../layout/flex/index.js"
|
|
14
|
+
import { applyElementProps } from "../../shared/apply-element-props.js"
|
|
13
15
|
import { close, leftPanelClose } from "../../shared/icons.js"
|
|
16
|
+
import { syncPopover } from "../../shared/top-layer.js"
|
|
17
|
+
|
|
18
|
+
/** Where a responsive drawer docks. Keep in step with style.css. */
|
|
19
|
+
const DOCKED_QUERY = "(min-width: 64rem)"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Latest props of each responsive drawer, so they can be re-synced when the
|
|
23
|
+
* viewport crosses the breakpoint without anything re-rendering.
|
|
24
|
+
* @type {Map<HTMLElement, DrawerProps>}
|
|
25
|
+
*/
|
|
26
|
+
const responsiveDrawers = new Map()
|
|
27
|
+
|
|
28
|
+
/** @type {MediaQueryList | undefined} */
|
|
29
|
+
let dockedMedia
|
|
14
30
|
|
|
15
31
|
/**
|
|
16
32
|
* Renders a side drawer panel with optional header, body, and collapse controls.
|
|
17
|
-
*
|
|
33
|
+
* While it overlays the page it is a native popover: it sits in the top layer
|
|
34
|
+
* over a `::backdrop`, and Esc or a press outside it (the backdrop included)
|
|
35
|
+
* closes it.
|
|
36
|
+
* `variant` sets when it overlays: `responsive` below 64rem and docked above,
|
|
37
|
+
* `temporary` at every width, `permanent` never.
|
|
18
38
|
* @param {DrawerProps} props
|
|
19
39
|
* @returns {TemplateResult}
|
|
20
40
|
*/
|
|
@@ -25,6 +45,7 @@ export function render(props) {
|
|
|
25
45
|
isOpen = false,
|
|
26
46
|
isHidden = false,
|
|
27
47
|
anchor = "left",
|
|
48
|
+
variant = "responsive",
|
|
28
49
|
isCollapsed = false,
|
|
29
50
|
hasBackdrop = true,
|
|
30
51
|
title,
|
|
@@ -42,89 +63,125 @@ export function render(props) {
|
|
|
42
63
|
.map((name) => [name, true]),
|
|
43
64
|
)
|
|
44
65
|
|
|
45
|
-
const
|
|
46
|
-
"iw-drawer",
|
|
47
|
-
`iw-drawer-${anchor}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return html`
|
|
57
|
-
${Flex.render({
|
|
58
|
-
element: "aside",
|
|
59
|
-
direction: "column",
|
|
60
|
-
className: drawerClassName,
|
|
61
|
-
children: [
|
|
62
|
-
when(
|
|
63
|
-
title || onClose,
|
|
64
|
-
() => html`
|
|
65
|
-
${Flex.render({
|
|
66
|
-
align: "center",
|
|
67
|
-
justify: "between",
|
|
68
|
-
gap: "sm",
|
|
69
|
-
padding: "sm",
|
|
70
|
-
className: "iw-drawer-header",
|
|
71
|
-
children: [
|
|
72
|
-
when(
|
|
73
|
-
title,
|
|
74
|
-
() => html`<div class="iw-drawer-title">${title}</div>`,
|
|
75
|
-
),
|
|
76
|
-
when(onClose, () =>
|
|
77
|
-
Button.render({
|
|
78
|
-
color: "secondary",
|
|
79
|
-
variant: "ghost",
|
|
80
|
-
shape: "square",
|
|
81
|
-
className: "iw-drawer-close",
|
|
82
|
-
"aria-label": "Close",
|
|
83
|
-
onClick: onClose,
|
|
84
|
-
children: Icon.render({ icon: close, size: "lg" }),
|
|
85
|
-
}),
|
|
86
|
-
),
|
|
87
|
-
],
|
|
88
|
-
})}
|
|
89
|
-
`,
|
|
90
|
-
),
|
|
66
|
+
const classes = {
|
|
67
|
+
"iw-drawer": true,
|
|
68
|
+
[`iw-drawer-${anchor}`]: true,
|
|
69
|
+
[`iw-drawer-${variant}`]: true,
|
|
70
|
+
"iw-drawer-open": isOpen,
|
|
71
|
+
"iw-drawer-hidden": isHidden,
|
|
72
|
+
"iw-drawer-collapsed": isCollapsed,
|
|
73
|
+
"iw-drawer-no-backdrop": !hasBackdrop,
|
|
74
|
+
...extraClasses,
|
|
75
|
+
}
|
|
91
76
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}),
|
|
77
|
+
// Without onClose nothing would take isOpen down, so the browser is not
|
|
78
|
+
// allowed to dismiss it: a manual popover ignores Esc and outside presses.
|
|
79
|
+
const popover =
|
|
80
|
+
variant === "permanent" ? nothing : onClose ? "auto" : "manual"
|
|
97
81
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
82
|
+
return html`<aside
|
|
83
|
+
class=${classMap(classes)}
|
|
84
|
+
popover=${popover}
|
|
85
|
+
${ref((element) => {
|
|
86
|
+
if (!element) return
|
|
87
|
+
applyElementProps(element, rest)
|
|
88
|
+
sync(element, { isOpen, variant, onClose })
|
|
89
|
+
})}
|
|
90
|
+
>
|
|
91
|
+
${when(
|
|
92
|
+
title || onClose,
|
|
93
|
+
() => html`
|
|
94
|
+
${Flex.render({
|
|
95
|
+
align: "center",
|
|
96
|
+
justify: "between",
|
|
97
|
+
gap: "sm",
|
|
98
|
+
padding: "sm",
|
|
99
|
+
className: "iw-drawer-header",
|
|
100
|
+
children: [
|
|
101
|
+
when(
|
|
102
|
+
title,
|
|
103
|
+
() => html`<div class="iw-drawer-title">${title}</div>`,
|
|
104
|
+
),
|
|
105
|
+
when(onClose, () =>
|
|
106
|
+
Button.render({
|
|
106
107
|
color: "secondary",
|
|
107
108
|
variant: "ghost",
|
|
108
109
|
shape: "square",
|
|
109
|
-
className: "iw-drawer-
|
|
110
|
-
"aria-label":
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
onClick: onCollapseToggle,
|
|
114
|
-
children: Icon.render({ icon: leftPanelClose, size: "lg" }),
|
|
110
|
+
className: "iw-drawer-close",
|
|
111
|
+
"aria-label": "Close",
|
|
112
|
+
onClick: onClose,
|
|
113
|
+
children: Icon.render({ icon: close, size: "lg" }),
|
|
115
114
|
}),
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
),
|
|
116
|
+
],
|
|
117
|
+
})}
|
|
118
|
+
`,
|
|
119
|
+
)}
|
|
120
|
+
${Flex.render({
|
|
121
|
+
direction: "column",
|
|
122
|
+
className: "iw-drawer-body",
|
|
123
|
+
children,
|
|
121
124
|
})}
|
|
122
|
-
${when(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
${when(
|
|
126
|
+
onCollapseToggle,
|
|
127
|
+
() => html`
|
|
128
|
+
${Flex.render({
|
|
129
|
+
justify: "end",
|
|
130
|
+
padding: "sm",
|
|
131
|
+
className: "iw-drawer-footer",
|
|
132
|
+
children: Button.render({
|
|
133
|
+
color: "secondary",
|
|
134
|
+
variant: "ghost",
|
|
135
|
+
shape: "square",
|
|
136
|
+
className: "iw-drawer-toggler",
|
|
137
|
+
"aria-label": isCollapsed ? "Expand sidebar" : "Collapse sidebar",
|
|
138
|
+
onClick: onCollapseToggle,
|
|
139
|
+
children: Icon.render({ icon: leftPanelClose, size: "lg" }),
|
|
140
|
+
}),
|
|
141
|
+
})}
|
|
142
|
+
`,
|
|
128
143
|
)}
|
|
129
|
-
|
|
144
|
+
</aside>`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Shows the popover only while the drawer overlays the page, and remembers a
|
|
149
|
+
* responsive drawer so a breakpoint crossing can re-sync it.
|
|
150
|
+
* @param {HTMLElement} element
|
|
151
|
+
* @param {Pick<DrawerProps, "isOpen" | "variant" | "onClose">} props
|
|
152
|
+
*/
|
|
153
|
+
function sync(element, props) {
|
|
154
|
+
if (props.variant === "responsive") {
|
|
155
|
+
watchDockedMedia()
|
|
156
|
+
responsiveDrawers.set(element, props)
|
|
157
|
+
} else {
|
|
158
|
+
responsiveDrawers.delete(element)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
syncPopover(element, {
|
|
162
|
+
isOpen: props.isOpen && isOverlay(props.variant),
|
|
163
|
+
onClose: props.onClose,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isOverlay(variant) {
|
|
168
|
+
if (variant === "permanent") return false
|
|
169
|
+
if (variant === "temporary") return true
|
|
170
|
+
return !dockedMedia?.matches
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Listens once for the breakpoint. Widening the window docks an open
|
|
175
|
+
* responsive drawer, which then has to leave the top layer: a silent hide,
|
|
176
|
+
* since nothing asked the store to close it.
|
|
177
|
+
*/
|
|
178
|
+
function watchDockedMedia() {
|
|
179
|
+
if (dockedMedia || !globalThis.matchMedia) return
|
|
180
|
+
dockedMedia = globalThis.matchMedia(DOCKED_QUERY)
|
|
181
|
+
dockedMedia.addEventListener("change", () => {
|
|
182
|
+
for (const [element, props] of responsiveDrawers) {
|
|
183
|
+
if (element.isConnected) sync(element, props)
|
|
184
|
+
else responsiveDrawers.delete(element)
|
|
185
|
+
}
|
|
186
|
+
})
|
|
130
187
|
}
|
|
@@ -12,6 +12,11 @@ export default {
|
|
|
12
12
|
},
|
|
13
13
|
items: { control: "object", description: "Menu items and separators." },
|
|
14
14
|
isDense: { control: "boolean", description: "Compact item spacing." },
|
|
15
|
+
trigger: {
|
|
16
|
+
control: "text",
|
|
17
|
+
description: "Content of a button that opens the menu as a popover.",
|
|
18
|
+
},
|
|
19
|
+
onToggle: { action: "onToggle" },
|
|
15
20
|
onItemClick: { action: "onItemClick" },
|
|
16
21
|
},
|
|
17
22
|
parameters: {
|
|
@@ -34,3 +39,13 @@ export const Default = {
|
|
|
34
39
|
],
|
|
35
40
|
},
|
|
36
41
|
}
|
|
42
|
+
|
|
43
|
+
// Anchored to its trigger. Without onToggle wired to a store, the browser
|
|
44
|
+
// keeps the open state on its own.
|
|
45
|
+
export const WithTrigger = {
|
|
46
|
+
args: {
|
|
47
|
+
id: "story-menu",
|
|
48
|
+
trigger: "Actions",
|
|
49
|
+
items: Default.args.items,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
@@ -3,12 +3,31 @@
|
|
|
3
3
|
flex-direction: column;
|
|
4
4
|
min-width: 12rem;
|
|
5
5
|
padding: var(--iw-space-2);
|
|
6
|
+
color: var(--iw-color-text);
|
|
6
7
|
background: var(--iw-color-surface);
|
|
7
8
|
border: 1px solid var(--iw-color-border);
|
|
8
9
|
border-radius: var(--iw-radius-md);
|
|
9
10
|
box-shadow: var(--iw-shadow-lg);
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
/* Anchored to its trigger: below it and aligned with its start, flipping up
|
|
14
|
+
or across when there is no room. The browser's popover box is centred in
|
|
15
|
+
the viewport, so its insets and margins are reset; its hidden state has to
|
|
16
|
+
be restated, because the display above outranks it. */
|
|
17
|
+
.iw-menu[popover] {
|
|
18
|
+
inset: auto;
|
|
19
|
+
margin: var(--iw-space-1) 0 0;
|
|
20
|
+
position-area: block-end span-inline-end;
|
|
21
|
+
position-try-fallbacks:
|
|
22
|
+
flip-block,
|
|
23
|
+
flip-inline,
|
|
24
|
+
flip-block flip-inline;
|
|
25
|
+
|
|
26
|
+
&:not(:popover-open) {
|
|
27
|
+
display: none;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
.iw-menu-item {
|
|
13
32
|
border: 0 none;
|
|
14
33
|
background: transparent;
|