@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "A design system for Inglorious Web with render-function components and customizable themes",
|
|
5
5
|
"author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,8 +35,8 @@ export function render(props) {
|
|
|
35
35
|
isPressed = false,
|
|
36
36
|
isFullWidth = false,
|
|
37
37
|
buttonType = "button",
|
|
38
|
-
ariaLabel
|
|
39
|
-
isAriaPressed
|
|
38
|
+
ariaLabel,
|
|
39
|
+
isAriaPressed,
|
|
40
40
|
className = "",
|
|
41
41
|
accept,
|
|
42
42
|
isMultiple = false,
|
|
@@ -74,8 +74,8 @@ export function render(props) {
|
|
|
74
74
|
return html`
|
|
75
75
|
<button
|
|
76
76
|
type=${buttonType}
|
|
77
|
-
aria-label=${ariaLabel}
|
|
78
|
-
aria-pressed=${isAriaPressed}
|
|
77
|
+
aria-label=${ariaLabel || nothing}
|
|
78
|
+
aria-pressed=${isAriaPressed ?? nothing}
|
|
79
79
|
class=${classMap(classes)}
|
|
80
80
|
?disabled=${isDisabled}
|
|
81
81
|
@click=${handleClick}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
flex-direction: column;
|
|
6
6
|
gap: var(--iw-space-1);
|
|
7
7
|
font-family: var(--iw-font-family-sans);
|
|
8
|
-
|
|
8
|
+
/* Each combobox anchors its own dropdown to its own frame. */
|
|
9
|
+
anchor-scope: --iw-combobox;
|
|
9
10
|
|
|
10
11
|
&.iw-combobox-full-width {
|
|
11
12
|
width: 100%;
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
.iw-combobox-frame {
|
|
42
|
+
anchor-name: --iw-combobox;
|
|
41
43
|
display: flex;
|
|
42
44
|
align-items: center;
|
|
43
45
|
/* The gap spaces the text from the arrow, as it does for Input's icons. */
|
|
@@ -182,12 +184,17 @@
|
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
186
|
|
|
187
|
+
/* A popover in the top layer, placed under the frame and as wide as it,
|
|
188
|
+
flipping above when there is no room below. The browser's popover box
|
|
189
|
+
is centred in the viewport, so its insets, margin and colours are reset. */
|
|
185
190
|
.iw-combobox-dropdown {
|
|
186
|
-
position:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
position-anchor: --iw-combobox;
|
|
192
|
+
position-area: block-end span-inline-end;
|
|
193
|
+
position-try-fallbacks: flip-block;
|
|
194
|
+
inset: auto;
|
|
195
|
+
width: anchor-size(width);
|
|
196
|
+
padding: 0;
|
|
197
|
+
color: var(--iw-color-text);
|
|
191
198
|
display: flex;
|
|
192
199
|
flex-direction: column;
|
|
193
200
|
max-height: 16rem;
|
|
@@ -198,7 +205,7 @@
|
|
|
198
205
|
border-start-end-radius: 0;
|
|
199
206
|
background: var(--iw-color-bg);
|
|
200
207
|
box-shadow: var(--iw-shadow-lg);
|
|
201
|
-
margin
|
|
208
|
+
margin: -1px 0 0;
|
|
202
209
|
|
|
203
210
|
.iw-combobox-dropdown-search {
|
|
204
211
|
width: 100%;
|
|
@@ -12,6 +12,7 @@ import { when } from "@inglorious/web/directives/when"
|
|
|
12
12
|
|
|
13
13
|
import { Chip } from "../../data-display/chip/index.js"
|
|
14
14
|
import { fallbackLabel } from "../../shared/fallback-label.js"
|
|
15
|
+
import { syncPopover } from "../../shared/top-layer.js"
|
|
15
16
|
import {
|
|
16
17
|
filterOptions,
|
|
17
18
|
formatOption,
|
|
@@ -263,7 +264,12 @@ export const Combobox = {
|
|
|
263
264
|
|
|
264
265
|
/**
|
|
265
266
|
* Renders the dropdown panel that contains search input, options, and empty/loading states.
|
|
266
|
-
* Shown only when the combobox is open
|
|
267
|
+
* Shown only when the combobox is open, as a popover in the top layer so no
|
|
268
|
+
* container can clip it, anchored under the control by CSS.
|
|
269
|
+
*
|
|
270
|
+
* A manual popover: the control that opens it is not a button, so it cannot
|
|
271
|
+
* be the popover's invoker, and a light dismiss would close the dropdown on
|
|
272
|
+
* the very press that toggles it. Outside presses and Esc are handled here.
|
|
267
273
|
* @param {ComboboxProps} props
|
|
268
274
|
* @returns {TemplateResult}
|
|
269
275
|
*/
|
|
@@ -276,7 +282,11 @@ export const Combobox = {
|
|
|
276
282
|
id=${id ? `${id}-control-listbox` : nothing}
|
|
277
283
|
class="iw-combobox-dropdown"
|
|
278
284
|
role="listbox"
|
|
279
|
-
|
|
285
|
+
popover="manual"
|
|
286
|
+
${ref((el) => {
|
|
287
|
+
syncPopover(el, { isOpen: true })
|
|
288
|
+
trackOutsideClick(props, el)
|
|
289
|
+
})}
|
|
280
290
|
>
|
|
281
291
|
${when(isSearchable, () => this.renderSearchInput?.(props))}
|
|
282
292
|
${when(isLoading, () => this.renderLoading?.(props))}
|
|
@@ -1,17 +1,39 @@
|
|
|
1
|
+
/* A fieldset of native radios: the inputs stay in the page for the keyboard
|
|
2
|
+
and assistive tech but out of sight, and each star is the label that
|
|
3
|
+
checks its radio. */
|
|
1
4
|
.iw-rating {
|
|
2
5
|
display: inline-flex;
|
|
3
6
|
align-items: center;
|
|
4
7
|
gap: var(--iw-space-1);
|
|
8
|
+
margin: 0;
|
|
9
|
+
padding: 0;
|
|
10
|
+
border: 0;
|
|
11
|
+
min-inline-size: 0;
|
|
5
12
|
|
|
6
13
|
.iw-rating-item {
|
|
7
|
-
|
|
8
|
-
background: transparent;
|
|
9
|
-
padding: 0;
|
|
10
|
-
margin: 0;
|
|
14
|
+
position: relative;
|
|
11
15
|
cursor: pointer;
|
|
12
16
|
color: var(--iw-color-warning);
|
|
13
17
|
font-size: 1.25rem;
|
|
14
18
|
line-height: 1;
|
|
19
|
+
border-radius: var(--iw-radius-sm);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.iw-rating-input {
|
|
23
|
+
position: absolute;
|
|
24
|
+
inset: 0;
|
|
25
|
+
margin: 0;
|
|
26
|
+
opacity: 0;
|
|
27
|
+
cursor: inherit;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.iw-rating-item:has(.iw-rating-input:focus-visible) {
|
|
31
|
+
outline: 2px solid var(--iw-color-primary);
|
|
32
|
+
outline-offset: 2px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&:disabled .iw-rating-item {
|
|
36
|
+
cursor: default;
|
|
15
37
|
}
|
|
16
38
|
|
|
17
39
|
&.iw-rating-sm .iw-rating-item {
|
|
@@ -3,7 +3,7 @@
|
|
|
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
|
const DEFAULT_VALUE = 0
|
|
@@ -12,12 +12,16 @@ const INDEX_TO_RATING = 1
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Renders a star (or custom symbol) rating control.
|
|
15
|
+
* Each star is a native radio, so the arrow keys move between them, the group
|
|
16
|
+
* is announced as one choice, and the value submits with a form under `name`.
|
|
15
17
|
* Supports max value, read-only/disabled states, and emits selection changes.
|
|
16
18
|
* @param {RatingProps} props
|
|
17
19
|
* @returns {TemplateResult}
|
|
18
20
|
*/
|
|
19
21
|
export function render(props) {
|
|
20
22
|
const {
|
|
23
|
+
id,
|
|
24
|
+
name,
|
|
21
25
|
value = DEFAULT_VALUE,
|
|
22
26
|
max = DEFAULT_MAX,
|
|
23
27
|
isDisabled = false,
|
|
@@ -25,9 +29,14 @@ export function render(props) {
|
|
|
25
29
|
symbol = "★",
|
|
26
30
|
emptySymbol = "☆",
|
|
27
31
|
size = "md",
|
|
32
|
+
ariaLabel = "Rating",
|
|
28
33
|
onChange,
|
|
29
34
|
} = props
|
|
30
35
|
|
|
36
|
+
// Radios are a group by sharing a name: without one, the arrow keys do not
|
|
37
|
+
// move between the stars.
|
|
38
|
+
const groupName = name || id
|
|
39
|
+
|
|
31
40
|
const classes = {
|
|
32
41
|
"iw-rating": true,
|
|
33
42
|
[`iw-rating-${size}`]: size !== "md",
|
|
@@ -36,22 +45,30 @@ export function render(props) {
|
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
return html`
|
|
39
|
-
<
|
|
48
|
+
<fieldset
|
|
49
|
+
id=${id || nothing}
|
|
50
|
+
class=${classMap(classes)}
|
|
51
|
+
aria-label=${ariaLabel}
|
|
52
|
+
?disabled=${isDisabled || isReadOnly}
|
|
53
|
+
>
|
|
40
54
|
${Array.from({ length: max }, (_, index) => {
|
|
41
55
|
const ratingValue = index + INDEX_TO_RATING
|
|
42
56
|
const isActive = ratingValue <= value
|
|
43
57
|
return html`
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
<label class="iw-rating-item">
|
|
59
|
+
<input
|
|
60
|
+
class="iw-rating-input"
|
|
61
|
+
type="radio"
|
|
62
|
+
name=${groupName || nothing}
|
|
63
|
+
value=${ratingValue}
|
|
64
|
+
aria-label=${`${ratingValue} of ${max}`}
|
|
65
|
+
.checked=${ratingValue === value}
|
|
66
|
+
@change=${() => onChange?.(ratingValue)}
|
|
67
|
+
/>
|
|
68
|
+
<span aria-hidden="true">${isActive ? symbol : emptySymbol}</span>
|
|
69
|
+
</label>
|
|
53
70
|
`
|
|
54
71
|
})}
|
|
55
|
-
</
|
|
72
|
+
</fieldset>
|
|
56
73
|
`
|
|
57
74
|
}
|
|
@@ -10,7 +10,8 @@ import { ref } from "@inglorious/web/directives/ref"
|
|
|
10
10
|
import { applyElementProps } from "../../shared/apply-element-props.js"
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Renders a toggle switch backed by a checkbox input
|
|
13
|
+
* Renders a toggle switch backed by a checkbox input, which `role="switch"`
|
|
14
|
+
* has assistive tech announce as on/off rather than checked.
|
|
14
15
|
* Use it for boolean settings with optional labels and disabled state.
|
|
15
16
|
* @param {SwitchProps} props
|
|
16
17
|
* @returns {TemplateResult}
|
|
@@ -46,6 +47,7 @@ export function render(props) {
|
|
|
46
47
|
id=${inputId || nothing}
|
|
47
48
|
class="iw-switch-input"
|
|
48
49
|
type="checkbox"
|
|
50
|
+
role="switch"
|
|
49
51
|
name=${name}
|
|
50
52
|
.checked=${isChecked}
|
|
51
53
|
?disabled=${isDisabled}
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
+
/* The bubble is anchored to the trigger by CSS: the browser places it on the
|
|
2
|
+
requested side and flips it to the opposite one when it would leave the
|
|
3
|
+
viewport. Fixed, so a container that clips its overflow does not cut it. */
|
|
1
4
|
.iw-tooltip {
|
|
2
|
-
|
|
5
|
+
anchor-scope: --iw-tooltip;
|
|
3
6
|
display: inline-flex;
|
|
4
7
|
align-items: center;
|
|
5
8
|
|
|
9
|
+
.iw-tooltip-trigger {
|
|
10
|
+
anchor-name: --iw-tooltip;
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
.iw-tooltip-bubble {
|
|
7
|
-
position:
|
|
14
|
+
position: fixed;
|
|
15
|
+
position-anchor: --iw-tooltip;
|
|
16
|
+
position-area: top;
|
|
17
|
+
position-try-fallbacks: flip-block;
|
|
18
|
+
margin-block-end: var(--iw-space-2);
|
|
19
|
+
width: max-content;
|
|
8
20
|
z-index: var(--iw-z-tooltip);
|
|
9
21
|
opacity: 0;
|
|
10
22
|
pointer-events: none;
|
|
@@ -15,9 +27,6 @@
|
|
|
15
27
|
padding: var(--iw-space-2) var(--iw-space-3);
|
|
16
28
|
box-shadow: var(--iw-shadow-md);
|
|
17
29
|
transition: opacity 150ms ease;
|
|
18
|
-
left: 50%;
|
|
19
|
-
bottom: calc(100% + var(--iw-space-2));
|
|
20
|
-
transform: translateX(-50%);
|
|
21
30
|
white-space: normal;
|
|
22
31
|
}
|
|
23
32
|
|
|
@@ -38,20 +47,19 @@
|
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
&.iw-tooltip-bottom .iw-tooltip-bubble {
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
position-area: bottom;
|
|
51
|
+
margin: var(--iw-space-2) 0 0;
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
&.iw-tooltip-left .iw-tooltip-bubble {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
transform: translateY(50%);
|
|
55
|
+
position-area: left;
|
|
56
|
+
position-try-fallbacks: flip-inline;
|
|
57
|
+
margin: 0 var(--iw-space-2) 0 0;
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
&.iw-tooltip-right .iw-tooltip-bubble {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
position-area: right;
|
|
62
|
+
position-try-fallbacks: flip-inline;
|
|
63
|
+
margin: 0 0 0 var(--iw-space-2);
|
|
56
64
|
}
|
|
57
65
|
}
|
|
@@ -1028,6 +1028,7 @@ Dialog.render({
|
|
|
1028
1028
|
preview: () =>
|
|
1029
1029
|
Dialog.render({
|
|
1030
1030
|
isOpen: true,
|
|
1031
|
+
isModal: false,
|
|
1031
1032
|
title: "Delete record",
|
|
1032
1033
|
description: "This action cannot be undone.",
|
|
1033
1034
|
actions: previewRow([
|
|
@@ -1173,8 +1174,7 @@ Drawer.render({
|
|
|
1173
1174
|
})`,
|
|
1174
1175
|
preview: () =>
|
|
1175
1176
|
Drawer.render({
|
|
1176
|
-
|
|
1177
|
-
hasBackdrop: false,
|
|
1177
|
+
variant: "permanent",
|
|
1178
1178
|
title: "Navigation",
|
|
1179
1179
|
children: List.render({
|
|
1180
1180
|
items: [
|
|
@@ -1225,7 +1225,8 @@ Link.render({
|
|
|
1225
1225
|
code: `import { Menu } from "@inglorious/ui/menu"
|
|
1226
1226
|
|
|
1227
1227
|
Menu.render({
|
|
1228
|
-
|
|
1228
|
+
id: "record-actions",
|
|
1229
|
+
trigger: "Actions",
|
|
1229
1230
|
items: [
|
|
1230
1231
|
{ label: "Edit" },
|
|
1231
1232
|
{ label: "Duplicate" },
|
|
@@ -1235,7 +1236,8 @@ Menu.render({
|
|
|
1235
1236
|
})`,
|
|
1236
1237
|
preview: () =>
|
|
1237
1238
|
Menu.render({
|
|
1238
|
-
|
|
1239
|
+
id: "record-actions",
|
|
1240
|
+
trigger: "Actions",
|
|
1239
1241
|
items: [
|
|
1240
1242
|
{ label: "Edit" },
|
|
1241
1243
|
{ label: "Duplicate" },
|
|
@@ -1268,7 +1270,6 @@ Pagination.render({
|
|
|
1268
1270
|
code: `import { SpeedDial } from "@inglorious/ui/speed-dial"
|
|
1269
1271
|
|
|
1270
1272
|
SpeedDial.render({
|
|
1271
|
-
isOpen: true,
|
|
1272
1273
|
actions: [
|
|
1273
1274
|
{ label: "Add", icon: "+" },
|
|
1274
1275
|
{ label: "Share", icon: "↗" }
|
|
@@ -1276,7 +1277,6 @@ SpeedDial.render({
|
|
|
1276
1277
|
})`,
|
|
1277
1278
|
preview: () =>
|
|
1278
1279
|
SpeedDial.render({
|
|
1279
|
-
isOpen: true,
|
|
1280
1280
|
actions: [
|
|
1281
1281
|
{ label: "Add", icon: "+" },
|
|
1282
1282
|
{ label: "Share", icon: "↗" },
|
|
@@ -251,17 +251,23 @@
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
.iw-primitive-preview .iw-backdrop,
|
|
254
|
-
.iw-primitive-preview .iw-dialog-backdrop,
|
|
255
254
|
.iw-primitive-preview .iw-snackbar,
|
|
256
255
|
.iw-primitive-preview .iw-drawer {
|
|
257
256
|
position: absolute;
|
|
258
257
|
}
|
|
259
258
|
|
|
260
|
-
.iw-primitive-preview .iw-backdrop
|
|
261
|
-
.iw-primitive-preview .iw-dialog-backdrop {
|
|
259
|
+
.iw-primitive-preview .iw-backdrop {
|
|
262
260
|
inset: 0;
|
|
263
261
|
}
|
|
264
262
|
|
|
263
|
+
/* Shown in place (isModal: false), so it can sit in the preview's flow
|
|
264
|
+
rather than where the browser positions a non-modal dialog. */
|
|
265
|
+
.iw-primitive-preview .iw-dialog {
|
|
266
|
+
position: static;
|
|
267
|
+
min-width: 0;
|
|
268
|
+
max-width: 100%;
|
|
269
|
+
}
|
|
270
|
+
|
|
265
271
|
.iw-primitive-preview .iw-drawer {
|
|
266
272
|
top: var(--iw-space-3);
|
|
267
273
|
bottom: var(--iw-space-3);
|
|
@@ -278,8 +284,8 @@
|
|
|
278
284
|
color: var(--iw-color-text-secondary);
|
|
279
285
|
}
|
|
280
286
|
|
|
281
|
-
.iw-dashboard-progress-list .iw-progress-linear
|
|
282
|
-
height: 0.25rem;
|
|
287
|
+
.iw-dashboard-progress-list .iw-progress-linear {
|
|
288
|
+
--iw-progress-height: 0.25rem;
|
|
283
289
|
}
|
|
284
290
|
|
|
285
291
|
.iw-dashboard-user-subtitle {
|
|
@@ -9,15 +9,18 @@ export default {
|
|
|
9
9
|
render: createRender(Dialog),
|
|
10
10
|
argTypes: {
|
|
11
11
|
isOpen: { control: "boolean" },
|
|
12
|
+
isModal: { control: "boolean" },
|
|
12
13
|
title: { control: "text" },
|
|
13
14
|
description: { control: "text" },
|
|
14
15
|
onClose: { action: "onClose" },
|
|
15
16
|
},
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
// In place, so the docs page that shows every story stays usable.
|
|
18
20
|
export const Open = {
|
|
19
21
|
args: {
|
|
20
22
|
isOpen: true,
|
|
23
|
+
isModal: false,
|
|
21
24
|
title: "Delete item",
|
|
22
25
|
description: "This action cannot be undone.",
|
|
23
26
|
children: html`<p>Are you sure you want to continue?</p>`,
|
|
@@ -25,3 +28,9 @@ export const Open = {
|
|
|
25
28
|
<button class="iw-button iw-button-primary">Confirm</button>`,
|
|
26
29
|
},
|
|
27
30
|
}
|
|
31
|
+
|
|
32
|
+
// Modal takes over the page, so it is left out of the docs page.
|
|
33
|
+
export const Modal = {
|
|
34
|
+
tags: ["!autodocs"],
|
|
35
|
+
args: { ...Open.args, isModal: true },
|
|
36
|
+
}
|
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
display: flex;
|
|
6
|
-
align-items: center;
|
|
7
|
-
justify-content: center;
|
|
8
|
-
z-index: var(--iw-z-modal, 1050);
|
|
9
|
-
padding: var(--iw-space-4);
|
|
10
|
-
backdrop-filter: blur(2px);
|
|
11
|
-
}
|
|
12
|
-
|
|
1
|
+
/* A native <dialog>, so the browser's own box is reset: the surface inside
|
|
2
|
+
carries the padding, which leaves the dialog element itself reachable only
|
|
3
|
+
through the backdrop. Modal dialogs sit in the top layer, above any
|
|
4
|
+
z-index, and the browser centres them. */
|
|
13
5
|
.iw-dialog {
|
|
14
6
|
background: var(--iw-color-surface);
|
|
15
7
|
color: var(--iw-color-text);
|
|
@@ -18,6 +10,15 @@
|
|
|
18
10
|
min-width: min(32rem, 90vw);
|
|
19
11
|
max-width: min(48rem, 90vw);
|
|
20
12
|
box-shadow: var(--iw-shadow-lg, 0 12px 40px rgba(0, 0, 0, 0.3));
|
|
13
|
+
padding: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.iw-dialog::backdrop {
|
|
17
|
+
background: color-mix(in srgb, var(--iw-color-black) 55%, transparent);
|
|
18
|
+
backdrop-filter: blur(2px);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.iw-dialog-surface {
|
|
21
22
|
padding: var(--iw-space-4);
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -3,18 +3,31 @@
|
|
|
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
|
+
import { ref } from "@inglorious/web/directives/ref"
|
|
9
|
+
|
|
10
|
+
import { syncDialog } from "../../shared/top-layer.js"
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
13
|
+
* Whether the last press on each dialog started on its backdrop.
|
|
14
|
+
* @type {WeakMap<HTMLDialogElement, boolean>}
|
|
15
|
+
*/
|
|
16
|
+
const backdropPresses = new WeakMap()
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Renders a native `<dialog>` with header, body, and actions.
|
|
20
|
+
* Modal by default: the browser traps focus, makes the page inert, closes on
|
|
21
|
+
* Esc and returns focus to the trigger. Clicking the backdrop can close the
|
|
22
|
+
* dialog when handlers are provided.
|
|
12
23
|
* @param {DialogProps} props
|
|
13
|
-
* @returns {TemplateResult
|
|
24
|
+
* @returns {TemplateResult}
|
|
14
25
|
*/
|
|
15
26
|
export function render(props) {
|
|
16
27
|
const {
|
|
28
|
+
id,
|
|
17
29
|
isOpen = false,
|
|
30
|
+
isModal = true,
|
|
18
31
|
title,
|
|
19
32
|
description,
|
|
20
33
|
children,
|
|
@@ -24,8 +37,6 @@ export function render(props) {
|
|
|
24
37
|
onBackdropClick,
|
|
25
38
|
} = props
|
|
26
39
|
|
|
27
|
-
if (!isOpen) return null
|
|
28
|
-
|
|
29
40
|
const extraClasses = Object.fromEntries(
|
|
30
41
|
className
|
|
31
42
|
.split(/\s+/)
|
|
@@ -33,20 +44,49 @@ export function render(props) {
|
|
|
33
44
|
.map((name) => [name, true]),
|
|
34
45
|
)
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
const titleId = id && title ? `${id}-title` : ""
|
|
48
|
+
const descriptionId = id && description ? `${id}-description` : ""
|
|
49
|
+
const onBackdrop = onBackdropClick ?? onClose
|
|
50
|
+
|
|
51
|
+
// The surface fills the dialog box, so a click that lands on the dialog
|
|
52
|
+
// itself can only have come from the backdrop around it. The press has to
|
|
53
|
+
// start there too: a drag from inside the surface that ends on the backdrop
|
|
54
|
+
// also clicks the dialog, and must not close it.
|
|
55
|
+
return html`<dialog
|
|
56
|
+
id=${id || nothing}
|
|
57
|
+
class=${classMap({ "iw-dialog": true, ...extraClasses })}
|
|
58
|
+
aria-labelledby=${titleId || nothing}
|
|
59
|
+
aria-describedby=${descriptionId || nothing}
|
|
60
|
+
@pointerdown=${(event) => {
|
|
61
|
+
backdropPresses.set(
|
|
62
|
+
event.currentTarget,
|
|
63
|
+
event.target === event.currentTarget,
|
|
64
|
+
)
|
|
65
|
+
}}
|
|
66
|
+
@click=${(event) => {
|
|
67
|
+
const isBackdropPress = backdropPresses.get(event.currentTarget)
|
|
68
|
+
backdropPresses.delete(event.currentTarget)
|
|
69
|
+
if (event.target === event.currentTarget && isBackdropPress) {
|
|
70
|
+
onBackdrop?.()
|
|
71
|
+
}
|
|
72
|
+
}}
|
|
73
|
+
@cancel=${(event) => {
|
|
74
|
+
// Without onClose nothing would take isOpen down, so Esc is refused.
|
|
75
|
+
if (!onClose) event.preventDefault()
|
|
76
|
+
}}
|
|
77
|
+
${ref((element) => syncDialog(element, { isOpen, isModal, onClose }))}
|
|
39
78
|
>
|
|
40
|
-
<div
|
|
41
|
-
class=${classMap({ "iw-dialog": true, ...extraClasses })}
|
|
42
|
-
role="dialog"
|
|
43
|
-
aria-modal="true"
|
|
44
|
-
@click=${(event) => event.stopPropagation()}
|
|
45
|
-
>
|
|
79
|
+
<div class="iw-dialog-surface">
|
|
46
80
|
${
|
|
47
81
|
(title || onClose) &&
|
|
48
82
|
html`<div class="iw-dialog-header">
|
|
49
|
-
${
|
|
83
|
+
${
|
|
84
|
+
title
|
|
85
|
+
? html`<div id=${titleId || nothing} class="iw-dialog-title">
|
|
86
|
+
${title}
|
|
87
|
+
</div>`
|
|
88
|
+
: null
|
|
89
|
+
}
|
|
50
90
|
${
|
|
51
91
|
onClose
|
|
52
92
|
? html`<button
|
|
@@ -63,11 +103,16 @@ export function render(props) {
|
|
|
63
103
|
}
|
|
64
104
|
${
|
|
65
105
|
description
|
|
66
|
-
? html`<div
|
|
106
|
+
? html`<div
|
|
107
|
+
id=${descriptionId || nothing}
|
|
108
|
+
class="iw-dialog-description"
|
|
109
|
+
>
|
|
110
|
+
${description}
|
|
111
|
+
</div>`
|
|
67
112
|
: null
|
|
68
113
|
}
|
|
69
114
|
${children ? html`<div class="iw-dialog-body">${children}</div>` : null}
|
|
70
115
|
${actions ? html`<div class="iw-dialog-actions">${actions}</div>` : null}
|
|
71
116
|
</div>
|
|
72
|
-
</
|
|
117
|
+
</dialog>`
|
|
73
118
|
}
|