@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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/src/controls/button/template.js +4 -4
  3. package/src/controls/combobox/style.css +14 -7
  4. package/src/controls/combobox/template.js +12 -2
  5. package/src/controls/rating/style.css +26 -4
  6. package/src/controls/rating/template.js +29 -12
  7. package/src/controls/switch/template.js +3 -1
  8. package/src/data-display/tooltip/style.css +22 -14
  9. package/src/examples/dashboard/primitive-content.js +6 -6
  10. package/src/examples/dashboard/style.css +11 -5
  11. package/src/feedback/dialog/dialog.stories.js +9 -0
  12. package/src/feedback/dialog/style.css +13 -12
  13. package/src/feedback/dialog/template.js +63 -18
  14. package/src/feedback/progress/style.css +47 -23
  15. package/src/feedback/progress/template.js +18 -11
  16. package/src/feedback/snackbar/style.css +13 -0
  17. package/src/feedback/snackbar/template.js +7 -0
  18. package/src/navigation/drawer/drawer.stories.js +3 -7
  19. package/src/navigation/drawer/style.css +59 -18
  20. package/src/navigation/drawer/template.js +137 -80
  21. package/src/navigation/menu/menu.stories.js +15 -0
  22. package/src/navigation/menu/style.css +19 -0
  23. package/src/navigation/menu/template.js +59 -6
  24. package/src/navigation/speed-dial/style.css +32 -5
  25. package/src/navigation/speed-dial/template.js +41 -12
  26. package/src/navigation/tabs/template.js +52 -4
  27. package/src/shared/anchor.js +11 -0
  28. package/src/shared/top-layer.js +83 -0
  29. package/src/surfaces/accordion/style.css +14 -7
  30. package/src/surfaces/accordion/template.js +27 -20
  31. package/types/controls/rating.d.ts +4 -0
  32. package/types/feedback/dialog.d.ts +3 -1
  33. package/types/feedback/progress.d.ts +2 -0
  34. package/types/navigation/drawer.d.ts +8 -3
  35. package/types/navigation/menu.d.ts +17 -0
  36. package/types/navigation/speed-dial.d.ts +5 -0
  37. 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": "8.6.0",
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 = false,
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
- position: relative; /* required for dropdown positioning */
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: absolute;
187
- top: 100%;
188
- left: 0;
189
- right: 0;
190
- z-index: 1000;
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-top: -1px;
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
- ${ref((el) => trackOutsideClick(props, el))}
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
- border: 0;
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
- <div class=${classMap(classes)} role="radiogroup" aria-label="rating">
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
- <button
45
- type="button"
46
- class="iw-rating-item"
47
- aria-label=${`rate-${ratingValue}`}
48
- ?disabled=${isDisabled || isReadOnly}
49
- @click=${() => onChange?.(ratingValue)}
50
- >
51
- ${isActive ? symbol : emptySymbol}
52
- </button>
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
- </div>
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
- position: relative;
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: absolute;
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
- top: calc(100% + var(--iw-space-2));
42
- bottom: auto;
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
- left: auto;
47
- right: calc(100% + var(--iw-space-2));
48
- bottom: 50%;
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
- left: calc(100% + var(--iw-space-2));
54
- bottom: 50%;
55
- transform: translateY(50%);
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
- isOpen: true,
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
- isOpen: true,
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
- isOpen: true,
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-track {
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
- .iw-dialog-backdrop {
2
- position: fixed;
3
- inset: 0;
4
- background: color-mix(in srgb, var(--iw-color-black) 55%, transparent);
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
- * 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,
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
- return html`<div
37
- class=${classMap({ "iw-dialog-backdrop": true })}
38
- @click=${onBackdropClick ?? onClose}
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
- ${title ? html`<div class="iw-dialog-title">${title}</div>` : null}
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 class="iw-dialog-description">${description}</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
- </div>`
117
+ </dialog>`
73
118
  }