@inglorious/ui 8.6.0 → 9.1.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.
Files changed (76) hide show
  1. package/package.json +2 -2
  2. package/src/controls/button/button.stories.js +7 -1
  3. package/src/controls/button/style.css +11 -0
  4. package/src/controls/button/template.js +4 -4
  5. package/src/controls/checkbox/checkbox.stories.js +12 -0
  6. package/src/controls/checkbox/style.css +11 -0
  7. package/src/controls/checkbox/template.js +9 -1
  8. package/src/controls/combobox/style.css +14 -7
  9. package/src/controls/combobox/template.js +12 -2
  10. package/src/controls/radio-group/radio-group.stories.js +21 -1
  11. package/src/controls/radio-group/style.css +11 -0
  12. package/src/controls/radio-group/template.js +11 -1
  13. package/src/controls/rating/rating.stories.js +8 -0
  14. package/src/controls/rating/style.css +26 -4
  15. package/src/controls/rating/template.js +29 -12
  16. package/src/controls/slider/slider.stories.js +28 -1
  17. package/src/controls/slider/style.css +102 -0
  18. package/src/controls/slider/template.js +114 -15
  19. package/src/controls/switch/style.css +11 -0
  20. package/src/controls/switch/switch.stories.js +10 -0
  21. package/src/controls/switch/template.js +14 -2
  22. package/src/data-display/chip/chip.stories.js +17 -0
  23. package/src/data-display/chip/style.css +5 -0
  24. package/src/data-display/chip/template.js +3 -1
  25. package/src/data-display/divider/divider.stories.js +9 -0
  26. package/src/data-display/divider/template.js +12 -4
  27. package/src/data-display/tooltip/style.css +22 -14
  28. package/src/examples/dashboard/primitive-content.js +6 -6
  29. package/src/examples/dashboard/style.css +11 -5
  30. package/src/feedback/alert/alert.stories.js +9 -1
  31. package/src/feedback/alert/style.css +5 -0
  32. package/src/feedback/alert/template.js +2 -1
  33. package/src/feedback/dialog/dialog.stories.js +31 -0
  34. package/src/feedback/dialog/style.css +27 -13
  35. package/src/feedback/dialog/template.js +68 -18
  36. package/src/feedback/progress/progress.stories.js +5 -0
  37. package/src/feedback/progress/style.css +47 -23
  38. package/src/feedback/progress/template.js +18 -11
  39. package/src/feedback/snackbar/style.css +13 -0
  40. package/src/feedback/snackbar/template.js +7 -0
  41. package/src/layout/flex/flex.stories.js +1 -1
  42. package/src/layout/flex/style.css +8 -0
  43. package/src/navigation/drawer/drawer.stories.js +3 -7
  44. package/src/navigation/drawer/style.css +59 -18
  45. package/src/navigation/drawer/template.js +137 -80
  46. package/src/navigation/menu/menu.stories.js +15 -0
  47. package/src/navigation/menu/style.css +19 -0
  48. package/src/navigation/menu/template.js +59 -6
  49. package/src/navigation/speed-dial/style.css +32 -5
  50. package/src/navigation/speed-dial/template.js +41 -12
  51. package/src/navigation/tabs/template.js +52 -4
  52. package/src/shared/anchor.js +11 -0
  53. package/src/shared/top-layer.js +83 -0
  54. package/src/surfaces/accordion/accordion.stories.js +9 -0
  55. package/src/surfaces/accordion/style.css +14 -7
  56. package/src/surfaces/accordion/template.js +31 -20
  57. package/src/surfaces/card/card.stories.js +24 -0
  58. package/src/surfaces/card/style.css +15 -0
  59. package/src/surfaces/card/template.js +2 -0
  60. package/types/controls/button.d.ts +1 -1
  61. package/types/controls/checkbox.d.ts +2 -0
  62. package/types/controls/radio-group.d.ts +2 -0
  63. package/types/controls/rating.d.ts +4 -0
  64. package/types/controls/slider.d.ts +6 -2
  65. package/types/controls/switch.d.ts +2 -0
  66. package/types/data-display/chip.d.ts +2 -0
  67. package/types/data-display/divider.d.ts +2 -0
  68. package/types/feedback/alert.d.ts +1 -1
  69. package/types/feedback/dialog.d.ts +5 -1
  70. package/types/feedback/progress.d.ts +2 -0
  71. package/types/layout/flex.d.ts +1 -1
  72. package/types/navigation/drawer.d.ts +8 -3
  73. package/types/navigation/menu.d.ts +17 -0
  74. package/types/navigation/speed-dial.d.ts +5 -0
  75. package/types/surfaces/accordion.d.ts +2 -0
  76. package/types/surfaces/card.d.ts +5 -0
@@ -3,18 +3,32 @@
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
- * Renders a modal dialog with header, body, and actions when open.
11
- * Clicking the backdrop can close the dialog when handlers are provided.
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 | null}
24
+ * @returns {TemplateResult}
14
25
  */
15
26
  export function render(props) {
16
27
  const {
28
+ id,
17
29
  isOpen = false,
30
+ isModal = true,
31
+ size = "md",
18
32
  title,
19
33
  description,
20
34
  children,
@@ -24,8 +38,6 @@ export function render(props) {
24
38
  onBackdropClick,
25
39
  } = props
26
40
 
27
- if (!isOpen) return null
28
-
29
41
  const extraClasses = Object.fromEntries(
30
42
  className
31
43
  .split(/\s+/)
@@ -33,20 +45,53 @@ export function render(props) {
33
45
  .map((name) => [name, true]),
34
46
  )
35
47
 
36
- return html`<div
37
- class=${classMap({ "iw-dialog-backdrop": true })}
38
- @click=${onBackdropClick ?? onClose}
48
+ const titleId = id && title ? `${id}-title` : ""
49
+ const descriptionId = id && description ? `${id}-description` : ""
50
+ const onBackdrop = onBackdropClick ?? onClose
51
+
52
+ // The surface fills the dialog box, so a click that lands on the dialog
53
+ // itself can only have come from the backdrop around it. The press has to
54
+ // start there too: a drag from inside the surface that ends on the backdrop
55
+ // also clicks the dialog, and must not close it.
56
+ return html`<dialog
57
+ id=${id || nothing}
58
+ class=${classMap({
59
+ "iw-dialog": true,
60
+ [`iw-dialog-${size}`]: true,
61
+ ...extraClasses,
62
+ })}
63
+ aria-labelledby=${titleId || nothing}
64
+ aria-describedby=${descriptionId || nothing}
65
+ @pointerdown=${(event) => {
66
+ backdropPresses.set(
67
+ event.currentTarget,
68
+ event.target === event.currentTarget,
69
+ )
70
+ }}
71
+ @click=${(event) => {
72
+ const isBackdropPress = backdropPresses.get(event.currentTarget)
73
+ backdropPresses.delete(event.currentTarget)
74
+ if (event.target === event.currentTarget && isBackdropPress) {
75
+ onBackdrop?.()
76
+ }
77
+ }}
78
+ @cancel=${(event) => {
79
+ // Without onClose nothing would take isOpen down, so Esc is refused.
80
+ if (!onClose) event.preventDefault()
81
+ }}
82
+ ${ref((element) => syncDialog(element, { isOpen, isModal, onClose }))}
39
83
  >
40
- <div
41
- class=${classMap({ "iw-dialog": true, ...extraClasses })}
42
- role="dialog"
43
- aria-modal="true"
44
- @click=${(event) => event.stopPropagation()}
45
- >
84
+ <div class="iw-dialog-surface">
46
85
  ${
47
86
  (title || onClose) &&
48
87
  html`<div class="iw-dialog-header">
49
- ${title ? html`<div class="iw-dialog-title">${title}</div>` : null}
88
+ ${
89
+ title
90
+ ? html`<div id=${titleId || nothing} class="iw-dialog-title">
91
+ ${title}
92
+ </div>`
93
+ : null
94
+ }
50
95
  ${
51
96
  onClose
52
97
  ? html`<button
@@ -63,11 +108,16 @@ export function render(props) {
63
108
  }
64
109
  ${
65
110
  description
66
- ? html`<div class="iw-dialog-description">${description}</div>`
111
+ ? html`<div
112
+ id=${descriptionId || nothing}
113
+ class="iw-dialog-description"
114
+ >
115
+ ${description}
116
+ </div>`
67
117
  : null
68
118
  }
69
119
  ${children ? html`<div class="iw-dialog-body">${children}</div>` : null}
70
120
  ${actions ? html`<div class="iw-dialog-actions">${actions}</div>` : null}
71
121
  </div>
72
- </div>`
122
+ </dialog>`
73
123
  }
@@ -8,6 +8,11 @@ export default {
8
8
  argTypes: {
9
9
  variant: { control: "select", options: ["linear", "circular"] },
10
10
  value: { control: "number" },
11
+ ariaLabel: {
12
+ control: "text",
13
+ description:
14
+ 'Names the indicator for assistive tech, e.g. "Uploading photo".',
15
+ },
11
16
  size: { control: "number" },
12
17
  thickness: { control: "number" },
13
18
  color: {
@@ -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
- width: 100%;
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
- transition: width 0.2s ease;
48
- min-width: 8px;
49
- }
37
+ --iw-progress-track: var(
38
+ --iw-progress-track-color,
39
+ color-mix(in srgb, currentColor 24%, transparent)
40
+ );
50
41
 
51
- .iw-progress-linear.iw-progress-indeterminate .iw-progress-linear-bar {
52
- width: 30%;
53
- animation: iw-progress-linear 1.4s ease-in-out infinite;
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
- transform: translateX(-100%);
82
+ background-position-x: -50%;
59
83
  }
60
84
  100% {
61
- transform: translateX(350%);
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
- return html`<div
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
- <div class="iw-progress-linear-track">
82
- <div
83
- class="iw-progress-linear-bar"
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>
@@ -30,7 +30,7 @@ export default {
30
30
  },
31
31
  gap: {
32
32
  control: "select",
33
- options: ["none", "sm", "md", "lg", "xl"],
33
+ options: ["none", "xs", "sm", "md", "lg", "xl", "2xl"],
34
34
  description: "Spacing between children.",
35
35
  },
36
36
  padding: {
@@ -85,6 +85,10 @@
85
85
  gap: 0;
86
86
  }
87
87
 
88
+ &.iw-flex-gap-xs {
89
+ gap: var(--iw-space-1);
90
+ }
91
+
88
92
  &.iw-flex-gap-sm {
89
93
  gap: var(--iw-space-2);
90
94
  }
@@ -101,6 +105,10 @@
101
105
  gap: var(--iw-space-8);
102
106
  }
103
107
 
108
+ &.iw-flex-gap-2xl {
109
+ gap: var(--iw-space-12);
110
+ }
111
+
104
112
  &.iw-flex-padding-none {
105
113
  padding: 0;
106
114
  }
@@ -19,13 +19,9 @@ export default {
19
19
  },
20
20
  variant: {
21
21
  control: "select",
22
- options: ["temporary", "persistent", "permanent", "responsive"],
23
- description: "Drawer behavior model.",
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
- top: 0;
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-start: 0;
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-end: 0;
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
- @media (min-width: 0rem) {
120
- .iw-drawer {
121
- box-shadow: var(--iw-shadow-lg);
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
- &.iw-drawer-open {
124
- transform: translateX(0);
125
- }
134
+ .iw-drawer-no-backdrop::backdrop {
135
+ background: transparent;
136
+ backdrop-filter: none;
137
+ }
126
138
 
127
- .iw-drawer-footer {
128
- display: none;
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
  }