@sveltia/ui 0.2.4 → 0.3.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 (122) hide show
  1. package/package/components/{core → button}/button.svelte +2 -1
  2. package/package/components/{core → button}/button.svelte.d.ts +6 -5
  3. package/package/components/{composite → button}/select-button-group.svelte +8 -5
  4. package/package/components/{composite → button}/select-button-group.svelte.d.ts +6 -6
  5. package/package/components/{core → button}/select-button.svelte +2 -2
  6. package/package/components/{core → button}/select-button.svelte.d.ts +2 -2
  7. package/package/components/{composite → calendar}/calendar.svelte +9 -5
  8. package/package/components/{composite → calendar}/calendar.svelte.d.ts +1 -0
  9. package/package/components/{composite → checkbox}/checkbox-group.svelte +3 -2
  10. package/package/components/{composite → checkbox}/checkbox-group.svelte.d.ts +2 -2
  11. package/package/components/{core → checkbox}/checkbox.svelte +38 -17
  12. package/package/components/{core → checkbox}/checkbox.svelte.d.ts +7 -3
  13. package/package/components/{core → dialog}/dialog.svelte +5 -4
  14. package/package/components/{core → dialog}/dialog.svelte.d.ts +2 -1
  15. package/package/components/{composite → disclosure}/disclosure.svelte +5 -4
  16. package/package/components/{composite → disclosure}/disclosure.svelte.d.ts +2 -1
  17. package/package/components/{core/separator.svelte → divider/divider.svelte} +5 -4
  18. package/package/components/divider/divider.svelte.d.ts +29 -0
  19. package/package/components/{core → divider}/spacer.svelte +4 -0
  20. package/package/components/{core → divider}/spacer.svelte.d.ts +1 -0
  21. package/package/components/{core → drawer}/drawer.svelte +5 -4
  22. package/package/components/{core → drawer}/drawer.svelte.d.ts +2 -1
  23. package/package/components/{core → icon}/icon.svelte +5 -0
  24. package/package/components/{core → icon}/icon.svelte.d.ts +6 -2
  25. package/package/components/listbox/listbox.svelte +74 -0
  26. package/package/components/{composite → listbox}/listbox.svelte.d.ts +3 -1
  27. package/package/components/listbox/option-group.svelte +47 -0
  28. package/package/components/listbox/option-group.svelte.d.ts +38 -0
  29. package/package/components/{core → listbox}/option.svelte +34 -2
  30. package/package/components/{core → listbox}/option.svelte.d.ts +7 -3
  31. package/package/components/{core → menu}/menu-button.svelte +3 -17
  32. package/package/components/{core → menu}/menu-button.svelte.d.ts +4 -1
  33. package/package/components/{core → menu}/menu-item-checkbox.svelte +1 -0
  34. package/package/components/{core → menu}/menu-item-checkbox.svelte.d.ts +4 -1
  35. package/package/components/{composite → menu}/menu-item-group.svelte +5 -1
  36. package/package/components/{composite → menu}/menu-item-group.svelte.d.ts +1 -0
  37. package/package/components/{core → menu}/menu-item-radio.svelte +2 -0
  38. package/package/components/{core → menu}/menu-item-radio.svelte.d.ts +5 -1
  39. package/package/components/{core → menu}/menu-item.svelte +6 -6
  40. package/package/components/{core → menu}/menu-item.svelte.d.ts +4 -1
  41. package/package/components/{composite → menu}/menu.svelte +3 -2
  42. package/package/components/{composite → menu}/menu.svelte.d.ts +2 -1
  43. package/package/components/{composite/radio-button-group.svelte → radio/radio-group.svelte} +15 -10
  44. package/package/components/radio/radio-group.svelte.d.ts +40 -0
  45. package/package/components/{core/radio-button.svelte → radio/radio.svelte} +45 -18
  46. package/package/components/radio/radio.svelte.d.ts +43 -0
  47. package/package/components/{composite → select}/combobox.svelte +7 -6
  48. package/package/components/{composite → select}/combobox.svelte.d.ts +4 -3
  49. package/package/components/{composite → select}/select.svelte +3 -1
  50. package/package/components/{composite → select}/select.svelte.d.ts +7 -3
  51. package/package/components/{core → slider}/slider.svelte +82 -57
  52. package/package/components/{core → slider}/slider.svelte.d.ts +12 -10
  53. package/package/components/{core → switch}/switch.svelte +36 -19
  54. package/package/components/{core → switch}/switch.svelte.d.ts +4 -3
  55. package/package/components/table/table-body.svelte +23 -0
  56. package/package/components/table/table-body.svelte.d.ts +34 -0
  57. package/package/components/table/table-cell.svelte +23 -0
  58. package/package/components/table/table-cell.svelte.d.ts +34 -0
  59. package/package/components/table/table-col-header.svelte +23 -0
  60. package/package/components/table/table-col-header.svelte.d.ts +34 -0
  61. package/package/components/table/table-foot.svelte +23 -0
  62. package/package/components/table/table-foot.svelte.d.ts +34 -0
  63. package/package/components/table/table-head.svelte +23 -0
  64. package/package/components/table/table-head.svelte.d.ts +34 -0
  65. package/package/components/table/table-row-header.svelte +23 -0
  66. package/package/components/table/table-row-header.svelte.d.ts +34 -0
  67. package/package/components/table/table-row.svelte +23 -0
  68. package/package/components/table/table-row.svelte.d.ts +38 -0
  69. package/package/components/table/table.svelte +44 -0
  70. package/package/components/table/table.svelte.d.ts +36 -0
  71. package/package/components/{composite → tabs}/tab-list.svelte +3 -2
  72. package/package/components/{composite → tabs}/tab-list.svelte.d.ts +7 -6
  73. package/package/components/{core → tabs}/tab-panel.svelte +2 -1
  74. package/package/components/{core → tabs}/tab-panel.svelte.d.ts +2 -1
  75. package/package/components/{core → tabs}/tab.svelte +3 -2
  76. package/package/components/{core → tabs}/tab.svelte.d.ts +2 -1
  77. package/package/components/{editor/markdown.svelte → text-field/markdown-editor.svelte} +10 -6
  78. package/package/components/text-field/markdown-editor.svelte.d.ts +26 -0
  79. package/package/components/{core → text-field}/number-input.svelte +22 -12
  80. package/package/components/{core → text-field}/number-input.svelte.d.ts +7 -3
  81. package/package/components/{core → text-field}/password-input.svelte +5 -2
  82. package/package/components/{core → text-field}/password-input.svelte.d.ts +8 -3
  83. package/package/components/{core → text-field}/search-bar.svelte +5 -2
  84. package/package/components/{core → text-field}/search-bar.svelte.d.ts +8 -3
  85. package/package/components/{core → text-field}/text-area.svelte +2 -0
  86. package/package/components/{core → text-field}/text-area.svelte.d.ts +9 -5
  87. package/package/components/{core → text-field}/text-input.svelte +3 -1
  88. package/package/components/{core → text-field}/text-input.svelte.d.ts +11 -7
  89. package/package/components/{core → toolbar}/toolbar.svelte +2 -1
  90. package/package/components/{core → toolbar}/toolbar.svelte.d.ts +3 -2
  91. package/package/components/util/app-shell.svelte +10 -36
  92. package/package/components/util/group.js +305 -0
  93. package/package/components/{core → util}/group.svelte +5 -11
  94. package/package/components/{core → util}/group.svelte.d.ts +4 -2
  95. package/package/components/util/popup.d.ts +30 -0
  96. package/package/components/{helpers → util}/popup.js +36 -26
  97. package/package/components/util/popup.svelte +14 -5
  98. package/package/components/util/{misc.d.ts → util.d.ts} +1 -0
  99. package/package/components/util/{misc.js → util.js} +15 -0
  100. package/package/index.d.ts +46 -41
  101. package/package/index.js +48 -83
  102. package/package/styles/core.scss +5 -34
  103. package/package/styles/variables.scss +5 -4
  104. package/package.json +362 -328
  105. package/package/components/composite/grid.svelte +0 -24
  106. package/package/components/composite/grid.svelte.d.ts +0 -31
  107. package/package/components/composite/listbox.svelte +0 -63
  108. package/package/components/composite/radio-button-group.svelte.d.ts +0 -36
  109. package/package/components/core/grid-cell.svelte +0 -13
  110. package/package/components/core/grid-cell.svelte.d.ts +0 -29
  111. package/package/components/core/radio-button.svelte.d.ts +0 -37
  112. package/package/components/core/row-group.svelte +0 -13
  113. package/package/components/core/row-group.svelte.d.ts +0 -29
  114. package/package/components/core/row.svelte +0 -13
  115. package/package/components/core/row.svelte.d.ts +0 -33
  116. package/package/components/core/separator.svelte.d.ts +0 -26
  117. package/package/components/editor/markdown.svelte.d.ts +0 -25
  118. package/package/components/helpers/group.js +0 -251
  119. package/package/components/helpers/popup.d.ts +0 -30
  120. package/package/components/helpers/util.d.ts +0 -1
  121. package/package/components/helpers/util.js +0 -14
  122. /package/package/components/{helpers → util}/group.d.ts +0 -0
@@ -0,0 +1,305 @@
1
+ import { getRandomId, sleep } from './util';
2
+
3
+ /**
4
+ * @type {{ [role: string]: {
5
+ * orientation: string,
6
+ * childRoles: string[],
7
+ * childSelectedAttr: string,
8
+ * focusChild: boolean
9
+ * } }}
10
+ */
11
+ const config = {
12
+ grid: {
13
+ orientation: 'vertical',
14
+ childRoles: ['row'],
15
+ childSelectedAttr: 'aria-selected',
16
+ focusChild: true,
17
+ },
18
+ listbox: {
19
+ orientation: 'vertical',
20
+ childRoles: ['option'],
21
+ childSelectedAttr: 'aria-selected',
22
+ focusChild: false,
23
+ },
24
+ menu: {
25
+ orientation: 'vertical',
26
+ childRoles: ['menuitem', 'menuitemcheckbox', 'menuitemradio'],
27
+ childSelectedAttr: 'aria-checked',
28
+ focusChild: true,
29
+ },
30
+ menubar: {
31
+ orientation: 'horizontal',
32
+ childRoles: ['menuitem', 'menuitemcheckbox', 'menuitemradio'],
33
+ childSelectedAttr: 'aria-checked',
34
+ focusChild: true,
35
+ },
36
+ radiogroup: {
37
+ orientation: 'horizontal',
38
+ childRoles: ['radio'],
39
+ childSelectedAttr: 'aria-checked',
40
+ focusChild: true,
41
+ },
42
+ tablist: {
43
+ orientation: 'horizontal',
44
+ childRoles: ['tab'],
45
+ childSelectedAttr: 'aria-selected',
46
+ focusChild: true,
47
+ },
48
+ };
49
+
50
+ /**
51
+ * Implement keyboard and mouse interactions for a grouping composite widget.
52
+ */
53
+ class Group {
54
+ /**
55
+ * Initialize a new `Group` instance.
56
+ * @param {HTMLElement} parent Parent element.
57
+ * @todo Check for added elements probably with `MutationObserver`.
58
+ */
59
+ constructor(parent) {
60
+ this.parent = parent;
61
+ this.role = parent.getAttribute('role');
62
+ this.grid = this.role === 'listbox' && parent.matches('.grid');
63
+ this.multi = this.parent.getAttribute('aria-multiselectable') === 'true';
64
+ this.id = getRandomId(this.role);
65
+ this.parentGroupSelector = `[role="group"], [role="${this.role}"]`;
66
+
67
+ const { orientation, childRoles, childSelectedAttr, focusChild } = config[this.role];
68
+
69
+ this.orientation = this.grid
70
+ ? 'horizontal'
71
+ : this.parent.getAttribute('aria-orientation') || orientation;
72
+ this.childRoles = childRoles;
73
+ this.childSelectedAttr = childSelectedAttr;
74
+ this.focusChild = focusChild;
75
+
76
+ const { allMembers } = this;
77
+
78
+ const hasSelected = allMembers.some((element) =>
79
+ element.matches(`[${childSelectedAttr}="true"]`),
80
+ );
81
+
82
+ allMembers.forEach((element, index) => {
83
+ const isSelected = element.matches(`[${childSelectedAttr}="true"]`);
84
+ const controls = document.querySelector(`#${element.getAttribute('aria-controls')}`);
85
+
86
+ element.id ||= `${this.id}-item-${index}`;
87
+ element.tabIndex ||= isSelected || (!hasSelected && index === 0) ? 0 : -1;
88
+ element.setAttribute(this.childSelectedAttr, String(isSelected));
89
+ controls?.setAttribute('aria-labelledby', element.id);
90
+ controls?.setAttribute('aria-hidden', String(!isSelected));
91
+ });
92
+
93
+ parent.addEventListener('click', (event) => {
94
+ if (/** @type {HTMLElement} */ (event.target).matches(this.selector)) {
95
+ this.onClick(event);
96
+ }
97
+ });
98
+
99
+ parent.addEventListener('keydown', (event) => {
100
+ this.onKeyDown(event);
101
+ });
102
+ }
103
+
104
+ /** @type {string} */
105
+ get selector() {
106
+ return this.childRoles.map((role) => `[role="${role}"]`).join(',');
107
+ }
108
+
109
+ /** @type {HTMLElement[]} */
110
+ get allMembers() {
111
+ // @ts-ignore
112
+ return [...this.parent.querySelectorAll(this.selector)];
113
+ }
114
+
115
+ /** @type {HTMLElement[]} */
116
+ get activeMembers() {
117
+ return this.allMembers.filter((element) => !element.matches('[aria-disabled="true"]'));
118
+ }
119
+
120
+ /**
121
+ * Select (and move focus to) the given target.
122
+ * @param {(MouseEvent | KeyboardEvent)} event Triggered event.
123
+ * @param {HTMLElement} newTarget Target element.
124
+ */
125
+ selectTarget(event, newTarget) {
126
+ const targetParentGroup = newTarget.closest(this.parentGroupSelector);
127
+
128
+ this.activeMembers.forEach((element) => {
129
+ const isTarget = element === newTarget;
130
+ const isSelected = element.matches('[aria-selected="true"]');
131
+ const controls = element.getAttribute('aria-controls');
132
+
133
+ if (this.multi && isTarget && event.type === 'click') {
134
+ element.setAttribute(this.childSelectedAttr, String(!isSelected));
135
+ element.dispatchEvent(new CustomEvent(isSelected ? 'unselect' : 'select'));
136
+ }
137
+
138
+ if (
139
+ (element.matches('[role="menuitemradio"]') &&
140
+ element.closest(this.parentGroupSelector) === targetParentGroup) ||
141
+ !this.multi
142
+ ) {
143
+ element.setAttribute(this.childSelectedAttr, String(isTarget));
144
+ element.dispatchEvent(new CustomEvent(isTarget ? 'select' : 'unselect'));
145
+ }
146
+
147
+ if (this.focusChild) {
148
+ element.tabIndex = isTarget ? 0 : -1;
149
+
150
+ if (isTarget) {
151
+ element.focus();
152
+ }
153
+ } else {
154
+ element.classList.toggle('focused', isTarget);
155
+ }
156
+
157
+ if (controls) {
158
+ document.getElementById(controls)?.setAttribute('aria-hidden', String(!isTarget));
159
+ }
160
+ });
161
+
162
+ this.parent.dispatchEvent(
163
+ new CustomEvent('select', {
164
+ detail: {
165
+ // @ts-ignore
166
+ value: newTarget.value,
167
+ // @ts-ignore
168
+ name: newTarget.name,
169
+ },
170
+ }),
171
+ );
172
+ }
173
+
174
+ /**
175
+ * Handle the `click` event on the widget.
176
+ * @param {MouseEvent} event `click` event.
177
+ */
178
+ onClick(event) {
179
+ // eslint-disable-next-line prefer-destructuring
180
+ const target = /** @type {HTMLElement} */ (event.target);
181
+ const newTarget = target.matches(this.selector) ? target : undefined;
182
+
183
+ if (!newTarget || event.button !== 0) {
184
+ return;
185
+ }
186
+
187
+ this.selectTarget(event, newTarget);
188
+ }
189
+
190
+ /**
191
+ * Handle the `keydown` event on the widget.
192
+ * @param {KeyboardEvent} event `keydown` event.
193
+ */
194
+ onKeyDown(event) {
195
+ const { key, ctrlKey, metaKey, shiftKey, altKey } = event;
196
+ const hasModifier = shiftKey || altKey || ctrlKey || metaKey;
197
+
198
+ if (hasModifier) {
199
+ return;
200
+ }
201
+
202
+ // eslint-disable-next-line prefer-destructuring
203
+ const target = /** @type {HTMLElement} */ (event.target);
204
+ const { allMembers, activeMembers } = this;
205
+
206
+ const currentTarget = (() => {
207
+ if (!this.focusChild) {
208
+ return activeMembers.find((member) => member.matches('.focused')) || activeMembers[0];
209
+ }
210
+
211
+ if (target.matches(this.selector)) {
212
+ return target;
213
+ }
214
+
215
+ return undefined;
216
+ })();
217
+
218
+ if (!currentTarget) {
219
+ return;
220
+ }
221
+
222
+ if (['Enter', ' ', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(key)) {
223
+ event.preventDefault();
224
+ }
225
+
226
+ if (['Enter', ' '].includes(key)) {
227
+ currentTarget.click();
228
+
229
+ return;
230
+ }
231
+
232
+ let index;
233
+ let newTarget;
234
+
235
+ if (this.grid) {
236
+ const colCount = Math.floor(this.parent.clientWidth / currentTarget.clientWidth);
237
+
238
+ index = allMembers.indexOf(currentTarget);
239
+
240
+ if (key === 'ArrowUp' && index > 0) {
241
+ newTarget = allMembers[index - colCount];
242
+ }
243
+
244
+ if (key === 'ArrowDown' && index < allMembers.length - 1) {
245
+ newTarget = allMembers[index + colCount];
246
+ }
247
+
248
+ if (key === 'ArrowLeft' && index > 0) {
249
+ newTarget = allMembers[index - 1];
250
+ }
251
+
252
+ if (key === 'ArrowRight' && index < allMembers.length - 1) {
253
+ newTarget = allMembers[index + 1];
254
+ }
255
+
256
+ if (newTarget?.getAttribute('aria-disabled') === 'true') {
257
+ newTarget = undefined;
258
+ }
259
+ } else {
260
+ index = activeMembers.indexOf(currentTarget);
261
+
262
+ if (key === (this.orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp')) {
263
+ if (index > 0) {
264
+ // Previous member
265
+ newTarget = activeMembers[index - 1];
266
+ }
267
+
268
+ if (index === 0) {
269
+ // Last member
270
+ newTarget = activeMembers[activeMembers.length - 1];
271
+ }
272
+ }
273
+
274
+ if (key === (this.orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown')) {
275
+ if (index < activeMembers.length - 1) {
276
+ // Next member
277
+ newTarget = activeMembers[index + 1];
278
+ }
279
+
280
+ if (index === activeMembers.length - 1) {
281
+ // First member
282
+ [newTarget] = activeMembers;
283
+ }
284
+ }
285
+ }
286
+
287
+ if (newTarget && newTarget !== currentTarget) {
288
+ this.selectTarget(event, newTarget);
289
+ }
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Activate a new group.
295
+ * @param {...any} args Arguments.
296
+ */
297
+ export const activateGroup = (...args) => {
298
+ (async () => {
299
+ // Wait a bit before the relevant components, including the `aria-controls` target are mounted
300
+ await sleep(100);
301
+
302
+ // @ts-ignore
303
+ return new Group(...args);
304
+ })();
305
+ };
@@ -1,6 +1,9 @@
1
+ <!--
2
+ @component
3
+ A generic group layout.
4
+ @see https://w3c.github.io/aria/#group
5
+ -->
1
6
  <script>
2
- import { getRandomId } from '../helpers/util';
3
-
4
7
  /**
5
8
  * CSS class name on the button.
6
9
  * @type {string}
@@ -9,23 +12,14 @@
9
12
 
10
13
  export { className as class };
11
14
 
12
- export let title = '';
13
-
14
15
  export let ariaLabel = '';
15
-
16
- const id = getRandomId('group');
17
16
  </script>
18
17
 
19
18
  <div
20
19
  class="sui group {className}"
21
20
  role="group"
22
- {id}
23
21
  aria-label={ariaLabel || undefined}
24
- aria-labelledby={title ? '{id}-title' : undefined}
25
22
  {...$$restProps}
26
23
  >
27
- {#if title}
28
- <div class="title" id="{id}-title">{title}</div>
29
- {/if}
30
24
  <slot />
31
25
  </div>
@@ -1,9 +1,12 @@
1
1
  /** @typedef {typeof __propDef.props} GroupProps */
2
2
  /** @typedef {typeof __propDef.events} GroupEvents */
3
3
  /** @typedef {typeof __propDef.slots} GroupSlots */
4
+ /**
5
+ * A generic group layout.
6
+ * @see https://w3c.github.io/aria/#group
7
+ */
4
8
  export default class Group extends SvelteComponentTyped<{
5
9
  [x: string]: any;
6
- title?: string;
7
10
  class?: string;
8
11
  ariaLabel?: string;
9
12
  }, {
@@ -19,7 +22,6 @@ import { SvelteComponentTyped } from "svelte";
19
22
  declare const __propDef: {
20
23
  props: {
21
24
  [x: string]: any;
22
- title?: string;
23
25
  class?: string;
24
26
  ariaLabel?: string;
25
27
  };
@@ -0,0 +1,30 @@
1
+ export function activatePopup(...args: any[]): Popup;
2
+ /**
3
+ * Implement the popup handler.
4
+ */
5
+ declare class Popup {
6
+ /**
7
+ * Initialize a new `Popup` instance.
8
+ * @param {HTMLButtonElement} anchorElement `<button>` element that triggers the popup.
9
+ * @param {HTMLDialogElement} popupElement `<dialog>` element to be used for the popup.
10
+ * @param {PopupPosition} position Where to show the popup content.
11
+ */
12
+ constructor(anchorElement: HTMLButtonElement, popupElement: HTMLDialogElement, position: PopupPosition);
13
+ open: import("svelte/store").Writable<boolean>;
14
+ style: import("svelte/store").Writable<{
15
+ inset: any;
16
+ zIndex: any;
17
+ width: any;
18
+ height: any;
19
+ }>;
20
+ observer: IntersectionObserver;
21
+ anchorElement: HTMLButtonElement;
22
+ popupElement: HTMLDialogElement;
23
+ position: PopupPosition;
24
+ id: string;
25
+ /**
26
+ * Continue checking the position in case the window or parent element resizes.
27
+ */
28
+ checkPosition(): void;
29
+ }
30
+ export {};
@@ -4,7 +4,7 @@ import { get, writable } from 'svelte/store';
4
4
  import { getRandomId } from './util';
5
5
 
6
6
  /**
7
- *
7
+ * Implement the popup handler.
8
8
  */
9
9
  class Popup {
10
10
  open = writable(false);
@@ -76,12 +76,14 @@ class Popup {
76
76
  });
77
77
 
78
78
  /**
79
- *
80
- * @param {HTMLElement} anchorElement
81
- * @param {HTMLElement} popupElement
82
- * @param {PopupPosition} position
79
+ * Initialize a new `Popup` instance.
80
+ * @param {HTMLButtonElement} anchorElement `<button>` element that triggers the popup.
81
+ * @param {HTMLDialogElement} popupElement `<dialog>` element to be used for the popup.
82
+ * @param {PopupPosition} position Where to show the popup content.
83
83
  */
84
84
  constructor(anchorElement, popupElement, position) {
85
+ console.info({ anchorElement, popupElement, position });
86
+
85
87
  this.anchorElement = anchorElement;
86
88
  this.popupElement = popupElement; // = backdrop
87
89
  this.position = position;
@@ -97,37 +99,44 @@ class Popup {
97
99
  });
98
100
 
99
101
  this.anchorElement.addEventListener('keydown', (event) => {
102
+ console.info(event);
103
+
100
104
  const { key, ctrlKey, metaKey, shiftKey, altKey } = event;
105
+ const hasModifier = shiftKey || altKey || ctrlKey || metaKey;
101
106
 
102
- if (!ctrlKey && !metaKey && !shiftKey && !altKey) {
103
- if (key === ' ' || key === 'Enter') {
104
- event.stopPropagation();
105
- this.open.set(!get(this.open));
106
- }
107
+ if (['Enter', ' '].includes(key) && !hasModifier) {
108
+ event.preventDefault();
109
+ event.stopPropagation();
110
+ this.open.set(!get(this.open));
107
111
  }
108
112
  });
109
113
 
114
+ // Close the popup when the backdrop, a menu item or an option is clicked
110
115
  this.popupElement.addEventListener('click', (event) => {
111
- if (get(this.open) && event.target !== this.anchorElement) {
116
+ event.stopPropagation();
117
+
118
+ // eslint-disable-next-line prefer-destructuring
119
+ const target = /** @type {HTMLElement} */ (event.target);
120
+
121
+ if (
122
+ get(this.open) &&
123
+ (target === this.popupElement || target.matches('[role^="menuitem"], [role="option"]'))
124
+ ) {
112
125
  this.open.set(false);
113
126
  }
114
127
  });
115
128
 
116
- [this.anchorElement, this.popupElement].forEach((element) => {
117
- element.addEventListener('keydown', (event) => {
118
- const { key, ctrlKey, metaKey, shiftKey, altKey } = event;
119
-
120
- if (
121
- get(this.open) &&
122
- ['Escape'].includes(key) &&
123
- !ctrlKey &&
124
- !metaKey &&
125
- !shiftKey &&
126
- !altKey
127
- ) {
128
- this.open.set(false);
129
- }
130
- });
129
+ this.popupElement.addEventListener('keydown', (event) => {
130
+ console.info(event);
131
+
132
+ const { key, ctrlKey, metaKey, shiftKey, altKey } = event;
133
+ const hasModifier = shiftKey || altKey || ctrlKey || metaKey;
134
+
135
+ if (key === 'Escape' && !hasModifier) {
136
+ event.preventDefault();
137
+ event.stopPropagation();
138
+ this.open.set(false);
139
+ }
131
140
  });
132
141
 
133
142
  this.open.subscribe((open) => {
@@ -135,6 +144,7 @@ class Popup {
135
144
  this.checkPosition();
136
145
  } else if (this.anchorElement.getAttribute('aria-expanded') === 'true') {
137
146
  this.anchorElement.focus();
147
+ this.anchorElement.removeAttribute('aria-controls');
138
148
  }
139
149
 
140
150
  this.anchorElement.setAttribute('aria-expanded', String(open));
@@ -7,7 +7,8 @@
7
7
  <script>
8
8
  import { onMount } from 'svelte';
9
9
  import { writable } from 'svelte/store';
10
- import { activatePopup } from '../helpers/popup';
10
+ import { activatePopup } from './popup';
11
+ import { sleep } from './util';
11
12
 
12
13
  /** @type {HTMLElement?} */
13
14
  export let anchor = undefined;
@@ -58,12 +59,20 @@
58
59
  showContent = true;
59
60
  dialog.showModal();
60
61
 
61
- window.requestAnimationFrame(() => {
62
+ window.requestAnimationFrame(async () => {
62
63
  showDialog = true;
64
+ await sleep(100);
63
65
 
64
- window.requestAnimationFrame(() => {
65
- /** @type {HTMLElement} */ (dialog.querySelector('[tabindex]') || dialog).focus();
66
- });
66
+ const target = /** @type {HTMLElement} */ (
67
+ content.querySelector('[tabindex]:not([aria-disabled="true"])')
68
+ );
69
+
70
+ if (target) {
71
+ target.focus();
72
+ } else {
73
+ content.tabIndex = -1;
74
+ content.focus();
75
+ }
67
76
  });
68
77
  };
69
78
 
@@ -1,2 +1,3 @@
1
+ export function getRandomId(prefix?: string, length?: number): string;
1
2
  export function isObject(input: any): boolean;
2
3
  export function sleep(ms?: number): Promise<any>;
@@ -1,3 +1,18 @@
1
+ /**
2
+ * Get a random ID that can be used for elements.
3
+ * @param {string} [prefix] Prefix to be added to the ID, e.g. `popup`.
4
+ * @param {number} [length] Number of characters to be used in the ID.
5
+ * @returns {string} Generated ID.
6
+ */
7
+ export const getRandomId = (prefix = '', length = 7) =>
8
+ [
9
+ prefix,
10
+ new Array(length)
11
+ .fill()
12
+ .map(() => '0123456789abcdef'[Math.floor(Math.random() * 12)])
13
+ .join(''),
14
+ ].join('-');
15
+
1
16
  /**
2
17
  * Check if the given input is a simple object.
3
18
  * @param {*} input Input, probably an object.
@@ -2,45 +2,50 @@ export function initLocales({ fallbackLocale, initialLocale }?: {
2
2
  fallbackLocale?: string;
3
3
  initialLocale?: string;
4
4
  }): void;
5
- export { default as Calendar } from "./components/composite/calendar.svelte";
6
- export { default as CheckboxGroup } from "./components/composite/checkbox-group.svelte";
7
- export { default as Combobox } from "./components/composite/combobox.svelte";
8
- export { default as Disclosure } from "./components/composite/disclosure.svelte";
9
- export { default as Grid } from "./components/composite/grid.svelte";
10
- export { default as Listbox } from "./components/composite/listbox.svelte";
11
- export { default as MenuItemGroup } from "./components/composite/menu-item-group.svelte";
12
- export { default as Menu } from "./components/composite/menu.svelte";
13
- export { default as RadioButtonGroup } from "./components/composite/radio-button-group.svelte";
14
- export { default as SelectButtonGroup } from "./components/composite/select-button-group.svelte";
15
- export { default as Select } from "./components/composite/select.svelte";
16
- export { default as TabList } from "./components/composite/tab-list.svelte";
17
- export { default as Button } from "./components/core/button.svelte";
18
- export { default as Checkbox } from "./components/core/checkbox.svelte";
19
- export { default as Dialog } from "./components/core/dialog.svelte";
20
- export { default as Drawer } from "./components/core/drawer.svelte";
21
- export { default as GridCell } from "./components/core/grid-cell.svelte";
22
- export { default as Group } from "./components/core/group.svelte";
23
- export { default as Icon } from "./components/core/icon.svelte";
24
- export { default as MenuButton } from "./components/core/menu-button.svelte";
25
- export { default as MenuItemCheckbox } from "./components/core/menu-item-checkbox.svelte";
26
- export { default as MenuItemRadio } from "./components/core/menu-item-radio.svelte";
27
- export { default as MenuItem } from "./components/core/menu-item.svelte";
28
- export { default as NumberInput } from "./components/core/number-input.svelte";
29
- export { default as Option } from "./components/core/option.svelte";
30
- export { default as PasswordInput } from "./components/core/password-input.svelte";
31
- export { default as RadioButton } from "./components/core/radio-button.svelte";
32
- export { default as RowGroup } from "./components/core/row-group.svelte";
33
- export { default as Row } from "./components/core/row.svelte";
34
- export { default as SearchBar } from "./components/core/search-bar.svelte";
35
- export { default as SelectButton } from "./components/core/select-button.svelte";
36
- export { default as Separator } from "./components/core/separator.svelte";
37
- export { default as Slider } from "./components/core/slider.svelte";
38
- export { default as Spacer } from "./components/core/spacer.svelte";
39
- export { default as Switch } from "./components/core/switch.svelte";
40
- export { default as TabPanel } from "./components/core/tab-panel.svelte";
41
- export { default as Tab } from "./components/core/tab.svelte";
42
- export { default as TextArea } from "./components/core/text-area.svelte";
43
- export { default as TextInput } from "./components/core/text-input.svelte";
44
- export { default as Toolbar } from "./components/core/toolbar.svelte";
45
- export { default as MarkdownEditor } from "./components/editor/markdown.svelte";
5
+ export { default as Button } from "./components/button/button.svelte";
6
+ export { default as SelectButtonGroup } from "./components/button/select-button-group.svelte";
7
+ export { default as SelectButton } from "./components/button/select-button.svelte";
8
+ export { default as Calendar } from "./components/calendar/calendar.svelte";
9
+ export { default as CheckboxGroup } from "./components/checkbox/checkbox-group.svelte";
10
+ export { default as Checkbox } from "./components/checkbox/checkbox.svelte";
11
+ export { default as Dialog } from "./components/dialog/dialog.svelte";
12
+ export { default as Disclosure } from "./components/disclosure/disclosure.svelte";
13
+ export { default as Divider } from "./components/divider/divider.svelte";
14
+ export { default as Spacer } from "./components/divider/spacer.svelte";
15
+ export { default as Drawer } from "./components/drawer/drawer.svelte";
16
+ export { default as Icon } from "./components/icon/icon.svelte";
17
+ export { default as Listbox } from "./components/listbox/listbox.svelte";
18
+ export { default as OptionGroup } from "./components/listbox/option-group.svelte";
19
+ export { default as Option } from "./components/listbox/option.svelte";
20
+ export { default as MenuButton } from "./components/menu/menu-button.svelte";
21
+ export { default as MenuItemCheckbox } from "./components/menu/menu-item-checkbox.svelte";
22
+ export { default as MenuItemGroup } from "./components/menu/menu-item-group.svelte";
23
+ export { default as MenuItemRadio } from "./components/menu/menu-item-radio.svelte";
24
+ export { default as MenuItem } from "./components/menu/menu-item.svelte";
25
+ export { default as Menu } from "./components/menu/menu.svelte";
26
+ export { default as RadioGroup } from "./components/radio/radio-group.svelte";
27
+ export { default as Radio } from "./components/radio/radio.svelte";
28
+ export { default as Combobox } from "./components/select/combobox.svelte";
29
+ export { default as Select } from "./components/select/select.svelte";
30
+ export { default as Slider } from "./components/slider/slider.svelte";
31
+ export { default as Switch } from "./components/switch/switch.svelte";
32
+ export { default as TableBody } from "./components/table/table-body.svelte";
33
+ export { default as TableCell } from "./components/table/table-cell.svelte";
34
+ export { default as TableColHeader } from "./components/table/table-col-header.svelte";
35
+ export { default as TableFoot } from "./components/table/table-foot.svelte";
36
+ export { default as TableHead } from "./components/table/table-head.svelte";
37
+ export { default as TableRowHeader } from "./components/table/table-row-header.svelte";
38
+ export { default as TableRow } from "./components/table/table-row.svelte";
39
+ export { default as Table } from "./components/table/table.svelte";
40
+ export { default as TabList } from "./components/tabs/tab-list.svelte";
41
+ export { default as TabPanel } from "./components/tabs/tab-panel.svelte";
42
+ export { default as Tab } from "./components/tabs/tab.svelte";
43
+ export { default as MarkdownEditor } from "./components/text-field/markdown-editor.svelte";
44
+ export { default as NumberInput } from "./components/text-field/number-input.svelte";
45
+ export { default as PasswordInput } from "./components/text-field/password-input.svelte";
46
+ export { default as SearchBar } from "./components/text-field/search-bar.svelte";
47
+ export { default as TextArea } from "./components/text-field/text-area.svelte";
48
+ export { default as TextInput } from "./components/text-field/text-input.svelte";
49
+ export { default as Toolbar } from "./components/toolbar/toolbar.svelte";
46
50
  export { default as AppShell } from "./components/util/app-shell.svelte";
51
+ export { default as Group } from "./components/util/group.svelte";