@sveltia/ui 0.72.2 → 0.73.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 (89) hide show
  1. package/dist/components/alert/alert.svelte +29 -5
  2. package/dist/components/alert/alert.svelte.d.ts +8 -4
  3. package/dist/components/alert/infobar.svelte +21 -5
  4. package/dist/components/alert/infobar.svelte.d.ts +10 -4
  5. package/dist/components/button/button.svelte +5 -1
  6. package/dist/components/button/select-button-group.svelte +1 -1
  7. package/dist/components/calendar/calendar.d.ts +1 -4
  8. package/dist/components/calendar/calendar.js +17 -0
  9. package/dist/components/calendar/calendar.svelte +67 -11
  10. package/dist/components/checkbox/checkbox-group.svelte +3 -1
  11. package/dist/components/checkbox/checkbox.svelte +3 -1
  12. package/dist/components/dialog/dialog.svelte +14 -2
  13. package/dist/components/dialog/prompt-dialog.svelte +9 -2
  14. package/dist/components/dialog/prompt-dialog.svelte.d.ts +4 -2
  15. package/dist/components/disclosure/disclosure.svelte +2 -1
  16. package/dist/components/drawer/drawer.svelte +28 -3
  17. package/dist/components/drawer/drawer.svelte.d.ts +16 -2
  18. package/dist/components/grid/grid-body.svelte +3 -1
  19. package/dist/components/grid/grid-foot.svelte +3 -1
  20. package/dist/components/grid/grid-head.svelte +3 -1
  21. package/dist/components/listbox/option-group.svelte +2 -1
  22. package/dist/components/listbox/option.svelte +4 -0
  23. package/dist/components/menu/menu-item-group.svelte +3 -1
  24. package/dist/components/radio/radio.svelte +21 -3
  25. package/dist/components/radio/radio.svelte.d.ts +12 -2
  26. package/dist/components/resizable-pane/resizable-handle.svelte +12 -0
  27. package/dist/components/select/combobox.svelte +69 -1
  28. package/dist/components/select/select-tags.svelte +34 -23
  29. package/dist/components/slider/slider.d.ts +6 -0
  30. package/dist/components/slider/slider.js +40 -0
  31. package/dist/components/slider/slider.svelte +45 -11
  32. package/dist/components/slider/slider.svelte.d.ts +14 -2
  33. package/dist/components/switch/switch.svelte +1 -1
  34. package/dist/components/table/table-body.svelte +3 -1
  35. package/dist/components/table/table-foot.svelte +3 -1
  36. package/dist/components/table/table-head.svelte +3 -1
  37. package/dist/components/text-editor/code-editor.svelte +22 -1
  38. package/dist/components/text-editor/code-editor.svelte.d.ts +28 -0
  39. package/dist/components/text-editor/shiki/version.d.ts +1 -1
  40. package/dist/components/text-editor/shiki/version.js +1 -1
  41. package/dist/components/text-editor/text-editor.svelte +23 -0
  42. package/dist/components/text-editor/text-editor.svelte.d.ts +28 -0
  43. package/dist/components/text-field/number-input.svelte +7 -17
  44. package/dist/components/text-field/search-bar.svelte +1 -0
  45. package/dist/components/text-field/text-input.svelte +1 -1
  46. package/dist/components/toast/toast.svelte +58 -4
  47. package/dist/components/toast/toast.svelte.d.ts +4 -2
  48. package/dist/components/util/app-shell.svelte +289 -5
  49. package/dist/components/util/popup.svelte +1 -1
  50. package/dist/locales/ar.yaml +20 -0
  51. package/dist/locales/bg.yaml +20 -0
  52. package/dist/locales/ca.yaml +20 -0
  53. package/dist/locales/cs.yaml +20 -0
  54. package/dist/locales/da.yaml +20 -0
  55. package/dist/locales/de.yaml +20 -0
  56. package/dist/locales/el.yaml +20 -0
  57. package/dist/locales/en-CA.yaml +20 -0
  58. package/dist/locales/en-GB.yaml +20 -0
  59. package/dist/locales/en-US.yaml +20 -0
  60. package/dist/locales/es-CO.yaml +20 -0
  61. package/dist/locales/fa.yaml +20 -0
  62. package/dist/locales/fi.yaml +20 -0
  63. package/dist/locales/fr.yaml +20 -0
  64. package/dist/locales/hr.yaml +20 -0
  65. package/dist/locales/it.yaml +20 -0
  66. package/dist/locales/ja.yaml +20 -0
  67. package/dist/locales/ko.yaml +20 -0
  68. package/dist/locales/nl.yaml +20 -0
  69. package/dist/locales/pl.yaml +20 -0
  70. package/dist/locales/pt-BR.yaml +20 -0
  71. package/dist/locales/pt-PT.yaml +20 -0
  72. package/dist/locales/ru.yaml +20 -0
  73. package/dist/locales/sv.yaml +20 -0
  74. package/dist/locales/tr.yaml +20 -0
  75. package/dist/locales/uk.yaml +20 -0
  76. package/dist/locales/vi.yaml +20 -0
  77. package/dist/locales/zh-CN.yaml +20 -0
  78. package/dist/locales/zh-TW.yaml +20 -0
  79. package/dist/services/group.svelte.js +60 -0
  80. package/dist/services/popup.svelte.js +24 -1
  81. package/dist/services/select.svelte.d.ts +5 -0
  82. package/dist/services/select.svelte.js +8 -0
  83. package/dist/services/type-ahead.d.ts +26 -0
  84. package/dist/services/type-ahead.js +81 -0
  85. package/dist/styles/core.scss +14 -3
  86. package/dist/styles/variables.scss +118 -1
  87. package/dist/typedefs.d.ts +7 -1
  88. package/dist/typedefs.js +4 -1
  89. package/package.json +3 -3
@@ -5,6 +5,7 @@
5
5
  @see https://www.w3.org/WAI/ARIA/apg/patterns/alert/
6
6
  -->
7
7
  <script>
8
+ import { _ } from '@sveltia/i18n';
8
9
  import Icon from '../icon/icon.svelte';
9
10
 
10
11
  /**
@@ -13,8 +14,10 @@
13
14
 
14
15
  /**
15
16
  * @typedef {object} Props
16
- * @property {'error' | 'warning' | 'info' | 'success'} status Alert status.
17
- * @property {'off' | 'polite' | 'assertive'} [ariaLive] ARIA live region politeness.
17
+ * @property {'error' | 'warning' | 'info' | 'success'} status Alert status. Errors and warnings
18
+ * are `role="alert"` and interrupt; information and success are `role="status"` and wait.
19
+ * @property {'off' | 'polite' | 'assertive'} [ariaLive] ARIA live region politeness. Defaults to
20
+ * `assertive` for an alert and `polite` for a status.
18
21
  * @property {Snippet} [children] Primary slot content.
19
22
  * @property {Snippet} [icon] Icon slot content.
20
23
  */
@@ -25,24 +28,45 @@
25
28
  let {
26
29
  /* eslint-disable prefer-const */
27
30
  status,
28
- ariaLive = 'assertive',
31
+ ariaLive = undefined,
29
32
  children,
30
33
  icon,
31
34
  ...restProps
32
35
  /* eslint-enable prefer-const */
33
36
  } = $props();
37
+
38
+ const role = $derived(status === 'error' || status === 'warning' ? 'alert' : 'status');
34
39
  </script>
35
40
 
36
- <div {...restProps} role="alert" class="sui alert {status}" aria-live={ariaLive}>
41
+ <!--
42
+ The colour and icon tell sighted users how serious the message is; the icon is hidden from
43
+ assistive technology, so the status is also spelled out for screen readers, off screen.
44
+ -->
45
+ <div
46
+ {...restProps}
47
+ {role}
48
+ class="sui alert {status}"
49
+ aria-live={ariaLive ?? (role === 'alert' ? 'assertive' : 'polite')}
50
+ >
37
51
  {#if icon}
38
52
  {@render icon()}
39
53
  {:else}
40
54
  <Icon name={status === 'success' ? 'check_circle' : status} />
41
55
  {/if}
56
+ <span class="status-label">{_(`_sui.alert.${status}`)}</span>
42
57
  {@render children?.()}
43
58
  </div>
44
59
 
45
- <style>.alert {
60
+ <style>.status-label {
61
+ position: absolute;
62
+ overflow: hidden;
63
+ clip-path: inset(50%);
64
+ width: 1px;
65
+ height: 1px;
66
+ white-space: nowrap;
67
+ }
68
+
69
+ .alert {
46
70
  display: flex;
47
71
  align-items: center;
48
72
  gap: var(--gap, 8px);
@@ -10,11 +10,13 @@ type Alert = {
10
10
  */
11
11
  declare const Alert: import("svelte").Component<{
12
12
  /**
13
- * Alert status.
13
+ * Alert status. Errors and warnings
14
+ * are `role="alert"` and interrupt; information and success are `role="status"` and wait.
14
15
  */
15
16
  status: "error" | "warning" | "info" | "success";
16
17
  /**
17
- * ARIA live region politeness.
18
+ * ARIA live region politeness. Defaults to
19
+ * `assertive` for an alert and `polite` for a status.
18
20
  */
19
21
  ariaLive?: "off" | "polite" | "assertive" | undefined;
20
22
  /**
@@ -28,11 +30,13 @@ declare const Alert: import("svelte").Component<{
28
30
  } & Record<string, any>, {}, "">;
29
31
  type Props = {
30
32
  /**
31
- * Alert status.
33
+ * Alert status. Errors and warnings
34
+ * are `role="alert"` and interrupt; information and success are `role="status"` and wait.
32
35
  */
33
36
  status: "error" | "warning" | "info" | "success";
34
37
  /**
35
- * ARIA live region politeness.
38
+ * ARIA live region politeness. Defaults to
39
+ * `assertive` for an alert and `polite` for a status.
36
40
  */
37
41
  ariaLive?: "off" | "polite" | "assertive" | undefined;
38
42
  /**
@@ -11,8 +11,11 @@
11
11
  * @typedef {object} Props
12
12
  * @property {boolean} [show] Whether to show the toast.
13
13
  * @property {boolean} [dismissible] Whether to show the close button.
14
- * @property {'error' | 'warning' | 'info' | 'success'} [status] Information status.
15
- * @property {'off' | 'polite' | 'assertive'} [ariaLive] ARIA live region politeness.
14
+ * @property {'error' | 'warning' | 'info' | 'success'} [status] Information status. Errors and
15
+ * warnings are `role="alert"` and interrupt; information and success are `role="status"` and
16
+ * wait.
17
+ * @property {'off' | 'polite' | 'assertive'} [ariaLive] ARIA live region politeness. Defaults to
18
+ * `assertive` for an alert and `polite` for a status.
16
19
  * @property {Snippet} [children] Primary slot content.
17
20
  * @property {Snippet} [icon] Icon slot content.
18
21
  * @property {() => void} [onDismiss] Callback invoked when the close button is clicked.
@@ -24,22 +27,26 @@
24
27
  show = $bindable(true),
25
28
  dismissible = true,
26
29
  status = 'info',
27
- ariaLive = 'polite',
30
+ ariaLive = undefined,
28
31
  children = undefined,
29
32
  icon = undefined,
30
33
  onDismiss,
31
34
  /* eslint-enable prefer-const */
32
35
  } = $props();
36
+
37
+ const role = $derived(status === 'error' || status === 'warning' ? 'alert' : 'status');
33
38
  </script>
34
39
 
35
40
  {#if show}
36
41
  <div role="none" class={['infobar', status]}>
37
- <div role="alert" class="message" aria-live={ariaLive}>
42
+ <!-- The status is spelled out off screen for screen readers; see `<Alert>` -->
43
+ <div {role} class="message" aria-live={ariaLive ?? (role === 'alert' ? 'assertive' : 'polite')}>
38
44
  {#if icon}
39
45
  {@render icon()}
40
46
  {:else}
41
47
  <Icon name={status === 'success' ? 'check_circle' : status} />
42
48
  {/if}
49
+ <span class="status-label">{_(`_sui.alert.${status}`)}</span>
43
50
  {@render children?.()}
44
51
  </div>
45
52
  {#if dismissible}
@@ -61,7 +68,16 @@
61
68
  </div>
62
69
  {/if}
63
70
 
64
- <style>.infobar {
71
+ <style>.status-label {
72
+ position: absolute;
73
+ overflow: hidden;
74
+ clip-path: inset(50%);
75
+ width: 1px;
76
+ height: 1px;
77
+ white-space: nowrap;
78
+ }
79
+
80
+ .infobar {
65
81
  flex: none;
66
82
  display: flex;
67
83
  align-items: center;
@@ -13,11 +13,14 @@ declare const Infobar: import("svelte").Component<{
13
13
  */
14
14
  dismissible?: boolean | undefined;
15
15
  /**
16
- * Information status.
16
+ * Information status. Errors and
17
+ * warnings are `role="alert"` and interrupt; information and success are `role="status"` and
18
+ * wait.
17
19
  */
18
20
  status?: "error" | "warning" | "info" | "success" | undefined;
19
21
  /**
20
- * ARIA live region politeness.
22
+ * ARIA live region politeness. Defaults to
23
+ * `assertive` for an alert and `polite` for a status.
21
24
  */
22
25
  ariaLive?: "off" | "polite" | "assertive" | undefined;
23
26
  /**
@@ -43,11 +46,14 @@ type Props = {
43
46
  */
44
47
  dismissible?: boolean | undefined;
45
48
  /**
46
- * Information status.
49
+ * Information status. Errors and
50
+ * warnings are `role="alert"` and interrupt; information and success are `role="status"` and
51
+ * wait.
47
52
  */
48
53
  status?: "error" | "warning" | "info" | "success" | undefined;
49
54
  /**
50
- * ARIA live region politeness.
55
+ * ARIA live region politeness. Defaults to
56
+ * `assertive` for an alert and `polite` for a status.
51
57
  */
52
58
  ariaLive?: "off" | "polite" | "assertive" | undefined;
53
59
  /**
@@ -51,6 +51,10 @@
51
51
  </script>
52
52
 
53
53
  <!--
54
+ `aria-readonly` is only rendered when it applies: the attribute isn’t supported on the `button`
55
+ role (or `option`, `tab` and `menuitem`, which also wrap this component), and Svelte would
56
+ otherwise serialize `false` as the literal string "false".
57
+
54
58
  The key shortcut handler is attached only when there are shortcuts to bind. A falsy value counts
55
59
  as no attachment at all, which saves an effect per button — and both `<Option>` and `<MenuItem>`
56
60
  wrap one, so a long list would otherwise pay for an effect per item that has nothing to do.
@@ -70,7 +74,7 @@
70
74
  {role}
71
75
  aria-hidden={hidden}
72
76
  aria-disabled={disabled}
73
- aria-readonly={readonly}
77
+ aria-readonly={readonly || undefined}
74
78
  aria-pressed={pressed}
75
79
  data-type={valueType}
76
80
  data-name={name}
@@ -101,7 +101,7 @@
101
101
  filter: grayscale(0) opacity(1);
102
102
  }
103
103
  .select-button-group :global([aria-disabled=false]) :global(button[aria-disabled=true]) :global(*) {
104
- filter: grayscale(1) opacity(0.35);
104
+ filter: grayscale(1) opacity(var(--sui-disabled-opacity, 0.35));
105
105
  }
106
106
 
107
107
  .inner {
@@ -1,7 +1,4 @@
1
- /**
2
- * Date helpers behind `<Calendar>`, kept apart from the component so the grid can be tested without
3
- * rendering anything.
4
- */
1
+ export function formatDate(value: Date, options: Intl.DateTimeFormatOptions): string;
5
2
  /**
6
3
  * Number of cells in the day grid: six weeks, enough to show any month in full from the Sunday on
7
4
  * or before its first day.
@@ -1,8 +1,25 @@
1
+ import { date as formatLocaleDate } from '@sveltia/i18n';
2
+
1
3
  /**
2
4
  * Date helpers behind `<Calendar>`, kept apart from the component so the grid can be tested without
3
5
  * rendering anything.
4
6
  */
5
7
 
8
+ /**
9
+ * Format a date in the current locale. Falls back to the browser’s locale if `@sveltia/i18n` has
10
+ * not been initialized yet, in which case its formatter has no locale to work with and throws.
11
+ * @param {Date} value Date to format.
12
+ * @param {Intl.DateTimeFormatOptions} options Formatting options.
13
+ * @returns {string} Formatted date.
14
+ */
15
+ export const formatDate = (value, options) => {
16
+ try {
17
+ return formatLocaleDate(value, options);
18
+ } catch {
19
+ return value.toLocaleDateString(undefined, options);
20
+ }
21
+ };
22
+
6
23
  /**
7
24
  * Number of cells in the day grid: six weeks, enough to show any month in full from the Sunday on
8
25
  * or before its first day.
@@ -4,12 +4,15 @@
4
4
  -->
5
5
  <script>
6
6
  import { _, isRTL } from '@sveltia/i18n';
7
+ import { tick } from 'svelte';
8
+ import { activateGroup } from '../../services/group.svelte.js';
7
9
  import Button from '../button/button.svelte';
8
10
  import Divider from '../divider/divider.svelte';
9
11
  import Spacer from '../divider/spacer.svelte';
10
12
  import Icon from '../icon/icon.svelte';
11
13
  import {
12
14
  addMonths,
15
+ formatDate,
13
16
  getCalendarDays,
14
17
  getFirstDayOfMonth,
15
18
  isSameDay,
@@ -37,16 +40,48 @@
37
40
  */
38
41
  let firstDay = $derived(getFirstDayOfMonth(date));
39
42
  const dayList = $derived(getCalendarDays(firstDay).map((day) => ({ day })));
43
+ /**
44
+ * Month and year being displayed, in the current locale. It’s the caption of the day grid as
45
+ * well as the label of the month switcher.
46
+ */
47
+ const monthLabel = $derived(formatDate(firstDay, { year: 'numeric', month: 'short' }));
48
+ const id = $props.id();
49
+ /** @type {HTMLElement | undefined} */
50
+ let grid = $state();
51
+
52
+ // Start the keyboard cursor on the selected day. The group service tracks the cursor with the
53
+ // `focused` class and `aria-activedescendant`, and keeps it as the selection moves; it only
54
+ // needs a starting point here, where nothing has been focused yet.
55
+ $effect(() => {
56
+ void value;
57
+
58
+ // The grid is bound before any effect runs
59
+ /* v8 ignore next */
60
+ if (!grid) {
61
+ return;
62
+ }
63
+
64
+ const listbox = grid;
65
+
66
+ tick().then(() => {
67
+ if (listbox.querySelector('.focused')) {
68
+ return;
69
+ }
70
+
71
+ const option = listbox.querySelector('[role="option"][aria-selected="true"]');
72
+
73
+ if (option) {
74
+ option.classList.add('focused');
75
+ listbox.setAttribute('aria-activedescendant', option.id);
76
+ }
77
+ });
78
+ });
40
79
  </script>
41
80
 
42
81
  <div role="group">
43
82
  <input type="hidden" bind:value />
44
83
  <div role="none" class="header">
45
- <Button
46
- variant="ghost"
47
- label={firstDay.toLocaleDateString('en', { year: 'numeric', month: 'short' })}
48
- aria-haspopup="dialog"
49
- >
84
+ <Button variant="ghost" label={monthLabel} aria-haspopup="dialog">
50
85
  {#snippet endIcon()}
51
86
  <Icon name="arrow_drop_down" class="small-arrow" />
52
87
  {/snippet}
@@ -115,13 +150,31 @@
115
150
  <Icon name={isRTL() ? 'chevron_left' : 'chevron_right'} />
116
151
  </Button>
117
152
  </div>
118
- <div role="listbox" class="grid">
119
- {#each dayList.slice(0, 7) as { day } (day)}
153
+ <!--
154
+ The day grid is a listbox laid out as a grid, so the group service moves through it with all
155
+ four arrow keys. Each day is named by its full date: the digit alone means nothing out of
156
+ context. The days are UTC-based, like the `value`, so the names are formatted the same way.
157
+ The cells are keyed by date string rather than by `Date` object, which is recreated on every
158
+ update: a day that stays on the grid when the month changes — the last days of the previous
159
+ month, say — keeps its element, and with it the keyboard cursor.
160
+ -->
161
+ <div
162
+ bind:this={grid}
163
+ role="listbox"
164
+ class="grid"
165
+ aria-label={monthLabel}
166
+ onChange={(/** @type {CustomEvent} */ event) => {
167
+ value = event.detail.value;
168
+ }}
169
+ {@attach activateGroup()}
170
+ >
171
+ {#each dayList.slice(0, 7) as { day } (day.getUTCDay())}
120
172
  <div role="none" class="weekday">
121
- {day.toLocaleDateString('en', { weekday: 'narrow' })}
173
+ {formatDate(day, { weekday: 'narrow' })}
122
174
  </div>
123
175
  {/each}
124
- {#each dayList as { day } (day)}
176
+ {#each dayList as { day } (toDateString(day))}
177
+ {@const dateString = toDateString(day)}
125
178
  <div
126
179
  role="none"
127
180
  class:other-month={day.getUTCMonth() !== firstDay.getUTCMonth()}
@@ -129,9 +182,12 @@
129
182
  >
130
183
  <Button
131
184
  role="option"
132
- aria-selected={false}
185
+ id="{id}-{dateString}"
186
+ value={dateString}
187
+ aria-label={formatDate(day, { dateStyle: 'full', timeZone: 'UTC' })}
188
+ aria-selected={dateString === value}
133
189
  onclick={() => {
134
- value = toDateString(day);
190
+ value = dateString;
135
191
  }}
136
192
  >
137
193
  {day.getUTCDate()}
@@ -3,6 +3,8 @@
3
3
  The layout container of `<Checkbox>`es.
4
4
  -->
5
5
  <script>
6
+ import { _ } from '@sveltia/i18n';
7
+
6
8
  /**
7
9
  * @import { Snippet } from 'svelte';
8
10
  */
@@ -42,7 +44,7 @@
42
44
  {hidden}
43
45
  aria-hidden={hidden}
44
46
  aria-disabled={disabled}
45
- aria-roledescription="checkbox group"
47
+ aria-roledescription={_('_sui.role_descriptions.checkbox_group')}
46
48
  aria-label={ariaLabel}
47
49
  >
48
50
  <div role="none" class="inner" inert={disabled}>
@@ -36,7 +36,7 @@
36
36
  */
37
37
  let {
38
38
  /* eslint-disable prefer-const */
39
- checked = $bindable(),
39
+ checked = $bindable(false),
40
40
  class: className,
41
41
  name = undefined,
42
42
  value = undefined,
@@ -167,6 +167,8 @@
167
167
  align-items: center;
168
168
  gap: 8px;
169
169
  margin: var(--sui-focus-ring-width);
170
+ min-width: 24px;
171
+ min-height: 24px;
170
172
  color: var(--sui-control-foreground-color);
171
173
  font-family: var(--sui-control-font-family);
172
174
  font-size: var(--sui-control-font-size);
@@ -24,6 +24,7 @@
24
24
  open = $bindable(false),
25
25
  value = $bindable(''),
26
26
  title,
27
+ ariaLabelledby = undefined,
27
28
  role = 'dialog',
28
29
  size = 'medium',
29
30
  class: className,
@@ -53,6 +54,14 @@
53
54
  * @type {string}
54
55
  */
55
56
  const id = $props.id();
57
+
58
+ /**
59
+ * Accessible name for the `<dialog>`. The built-in header renders the title in an element the
60
+ * dialog can point at; with a custom header the title is only text, so it becomes `aria-label`
61
+ * unless the consumer names the labelling element.
62
+ */
63
+ const labelledby = $derived(ariaLabelledby ?? (!header && title ? `${id}-title` : undefined));
64
+ const label = $derived(labelledby ? undefined : title || undefined);
56
65
  /**
57
66
  * A reference to the modal component.
58
67
  * @type {Modal | undefined}
@@ -109,8 +118,8 @@
109
118
  {role}
110
119
  {id}
111
120
  class="dialog"
112
- aria-label={header ? undefined : title}
113
- aria-labelledby={header ? title : `${id}-title`}
121
+ aria-label={label}
122
+ aria-labelledby={labelledby}
114
123
  aria-describedby="{id}-body"
115
124
  bind:open
116
125
  showBackdrop
@@ -191,6 +200,9 @@
191
200
  display: flex;
192
201
  flex-direction: column;
193
202
  overflow: hidden;
203
+ border-width: var(--sui-dialog-content-border-width, var(--sui-modal-border-width, 0));
204
+ border-style: solid;
205
+ border-color: var(--sui-dialog-content-border-color, var(--sui-primary-border-color));
194
206
  border-radius: var(--sui-dialog-content-border-radius, 4px);
195
207
  max-width: calc(100dvw - var(--sui-dialog-content-margin, 16px) * 2);
196
208
  background-color: var(--sui-dialog-content-background-color, var(--sui-secondary-background-color-translucent));
@@ -23,7 +23,8 @@
23
23
  /**
24
24
  * @typedef {object} Props
25
25
  * @property {string} [value] Value entered on the textbox.
26
- * @property {object} [textboxAttrs] Extra attributes for the `<TextInput>`.
26
+ * @property {object} [textboxAttrs] Extra attributes for the `<TextInput>`. The text box is
27
+ * labelled by the dialog `title` unless an `aria-label` or `aria-labelledby` is given here.
27
28
  * @property {Snippet} [children] Primary slot content.
28
29
  * @property {Snippet} [input] Input slot content.
29
30
  */
@@ -36,6 +37,7 @@
36
37
  /* eslint-disable prefer-const */
37
38
  open = $bindable(false),
38
39
  value = $bindable(''),
40
+ title,
39
41
  textboxAttrs = {},
40
42
  children,
41
43
  input,
@@ -48,7 +50,11 @@
48
50
  } = $props();
49
51
  </script>
50
52
 
51
- <Dialog {...restProps} bind:open role="alertdialog">
53
+ <!--
54
+ The built-in text box is the point of the dialog, so it takes the dialog title as its name unless
55
+ the consumer labels it; the dialog itself is still named by the title as usual.
56
+ -->
57
+ <Dialog {...restProps} {title} bind:open role="alertdialog">
52
58
  {@render children?.()}
53
59
  <div class="input-outer">
54
60
  {#if input}
@@ -59,6 +65,7 @@
59
65
  bind:value
60
66
  flex
61
67
  autofocus
68
+ aria-label={'aria-labelledby' in textboxAttrs ? undefined : title}
62
69
  {...textboxAttrs}
63
70
  {onkeydown}
64
71
  {onkeyup}
@@ -16,7 +16,8 @@ declare const PromptDialog: import("svelte").Component<ModalProps & DialogProps
16
16
  */
17
17
  value?: string | undefined;
18
18
  /**
19
- * Extra attributes for the `<TextInput>`.
19
+ * Extra attributes for the `<TextInput>`. The text box is
20
+ * labelled by the dialog `title` unless an `aria-label` or `aria-labelledby` is given here.
20
21
  */
21
22
  textboxAttrs?: object | undefined;
22
23
  /**
@@ -34,7 +35,8 @@ type Props = {
34
35
  */
35
36
  value?: string | undefined;
36
37
  /**
37
- * Extra attributes for the `<TextInput>`.
38
+ * Extra attributes for the `<TextInput>`. The text box is
39
+ * labelled by the dialog `title` unless an `aria-label` or `aria-labelledby` is given here.
38
40
  */
39
41
  textboxAttrs?: object | undefined;
40
42
  /**
@@ -5,6 +5,7 @@
5
5
  @see https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
6
6
  -->
7
7
  <script>
8
+ import { _ } from '@sveltia/i18n';
8
9
  import Button from '../button/button.svelte';
9
10
  import Icon from '../icon/icon.svelte';
10
11
 
@@ -56,7 +57,7 @@
56
57
  aria-hidden={hidden}
57
58
  aria-disabled={disabled}
58
59
  aria-labelledby="{id}-header"
59
- aria-roledescription="disclosure"
60
+ aria-roledescription={_('_sui.role_descriptions.disclosure')}
60
61
  >
61
62
  <div role="none" class="inner" inert={disabled}>
62
63
  <Button
@@ -20,7 +20,11 @@
20
20
  * @typedef {object} Props
21
21
  * @property {string} [class] The `class` attribute on the content element.
22
22
  * @property {boolean} [open] Whether to open the drawer.
23
- * @property {string} [title] Title text displayed on the header.
23
+ * @property {string} [title] Title text displayed on the header. With a custom `header` snippet,
24
+ * it’s used as the drawer’s `aria-label` instead.
25
+ * @property {string} [ariaLabelledby] ID of the element that labels the drawer, typically a
26
+ * heading inside a custom `header` snippet. Takes precedence over `title` for the accessible
27
+ * name.
24
28
  * @property {'top' | 'right' | 'bottom' | 'left'} [position] Position of the drawer.
25
29
  * @property {'small' | 'medium' | 'large' | 'x-large' | 'full'} [size] Width or height of the
26
30
  * drawer.
@@ -41,6 +45,7 @@
41
45
  open = $bindable(false),
42
46
  class: className,
43
47
  title = '',
48
+ ariaLabelledby = undefined,
44
49
  position = 'right',
45
50
  size = 'small',
46
51
  showClose = 'outside',
@@ -68,9 +73,26 @@
68
73
  const orientation = $derived(
69
74
  position === 'right' || position === 'left' ? 'vertical' : 'horizontal',
70
75
  );
76
+
77
+ /**
78
+ * Accessible name for the `<dialog>`, resolved the same way as `<Dialog>`: the built-in header
79
+ * renders the title in an element the dialog can point at; with a custom header the title is
80
+ * only text, so it becomes `aria-label` unless the consumer names the labelling element.
81
+ */
82
+ const labelledby = $derived(ariaLabelledby ?? (!header && title ? `${id}-title` : undefined));
83
+ const label = $derived(labelledby ? undefined : title || undefined);
71
84
  </script>
72
85
 
73
- <Modal bind:this={modal} {...restProps} bind:open {id} class="drawer" showBackdrop>
86
+ <Modal
87
+ bind:this={modal}
88
+ {...restProps}
89
+ bind:open
90
+ {id}
91
+ class="drawer"
92
+ aria-label={label}
93
+ aria-labelledby={labelledby}
94
+ showBackdrop
95
+ >
74
96
  <div role="none" class={['content', className, size, position, orientation]}>
75
97
  <div role="none" class="extra-control">
76
98
  {#if showClose === 'outside'}
@@ -99,7 +121,7 @@
99
121
  {@render header()}
100
122
  {:else}
101
123
  <div role="none" class="header">
102
- <div role="none" class="title">
124
+ <div role="none" id="{id}-title" class="title">
103
125
  {title}
104
126
  </div>
105
127
  <Spacer flex={true} />
@@ -144,6 +166,9 @@
144
166
  flex-direction: column;
145
167
  max-width: 100dvw;
146
168
  max-height: 100dvh;
169
+ border-width: var(--sui-drawer-content-border-width, var(--sui-modal-border-width, 0));
170
+ border-style: solid;
171
+ border-color: var(--sui-drawer-content-border-color, var(--sui-primary-border-color));
147
172
  background-color: var(--sui-secondary-background-color-translucent);
148
173
  box-shadow: 0 8px 16px var(--sui-popup-shadow-color);
149
174
  -webkit-backdrop-filter: blur(16px);
@@ -18,9 +18,16 @@ declare const Drawer: import("svelte").Component<ModalProps & {
18
18
  */
19
19
  open?: boolean | undefined;
20
20
  /**
21
- * Title text displayed on the header.
21
+ * Title text displayed on the header. With a custom `header` snippet,
22
+ * it’s used as the drawer’s `aria-label` instead.
22
23
  */
23
24
  title?: string | undefined;
25
+ /**
26
+ * ID of the element that labels the drawer, typically a
27
+ * heading inside a custom `header` snippet. Takes precedence over `title` for the accessible
28
+ * name.
29
+ */
30
+ ariaLabelledby?: string | undefined;
24
31
  /**
25
32
  * Position of the drawer.
26
33
  */
@@ -69,9 +76,16 @@ type Props = {
69
76
  */
70
77
  open?: boolean | undefined;
71
78
  /**
72
- * Title text displayed on the header.
79
+ * Title text displayed on the header. With a custom `header` snippet,
80
+ * it’s used as the drawer’s `aria-label` instead.
73
81
  */
74
82
  title?: string | undefined;
83
+ /**
84
+ * ID of the element that labels the drawer, typically a
85
+ * heading inside a custom `header` snippet. Takes precedence over `title` for the accessible
86
+ * name.
87
+ */
88
+ ariaLabelledby?: string | undefined;
75
89
  /**
76
90
  * Position of the drawer.
77
91
  */
@@ -5,6 +5,8 @@
5
5
  @see https://w3c.github.io/aria/#rowgroup
6
6
  -->
7
7
  <script>
8
+ import { _ } from '@sveltia/i18n';
9
+
8
10
  /**
9
11
  * @import { Snippet } from 'svelte';
10
12
  */
@@ -36,7 +38,7 @@
36
38
  role="rowgroup"
37
39
  class="sui grid-body row-group {className}"
38
40
  aria-labelledby={label ? `${id}-label` : undefined}
39
- aria-roledescription="grid body"
41
+ aria-roledescription={_('_sui.role_descriptions.grid_body')}
40
42
  >
41
43
  {#if label}
42
44
  <div role="row" class="row-group-caption">