@sveltia/ui 0.2.5 → 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 (120) hide show
  1. package/package/components/{core → button}/button.svelte +3 -2
  2. package/package/components/{core → button}/button.svelte.d.ts +5 -4
  3. package/package/components/{composite → button}/select-button-group.svelte +6 -3
  4. package/package/components/{composite → calendar}/calendar.svelte +9 -5
  5. package/package/components/{composite → calendar}/calendar.svelte.d.ts +1 -0
  6. package/package/components/{composite → checkbox}/checkbox-group.svelte +1 -1
  7. package/package/components/{composite → checkbox}/checkbox-group.svelte.d.ts +2 -2
  8. package/package/components/{core → checkbox}/checkbox.svelte +7 -5
  9. package/package/components/{core → checkbox}/checkbox.svelte.d.ts +4 -2
  10. package/package/components/{core → dialog}/dialog.svelte +4 -3
  11. package/package/components/{core → dialog}/dialog.svelte.d.ts +1 -0
  12. package/package/components/{composite → disclosure}/disclosure.svelte +5 -4
  13. package/package/components/{composite → disclosure}/disclosure.svelte.d.ts +2 -1
  14. package/package/components/{core/separator.svelte → divider/divider.svelte} +5 -4
  15. package/package/components/divider/divider.svelte.d.ts +29 -0
  16. package/package/components/{core → divider}/spacer.svelte +4 -0
  17. package/package/components/{core → divider}/spacer.svelte.d.ts +1 -0
  18. package/package/components/{core → drawer}/drawer.svelte +4 -3
  19. package/package/components/{core → drawer}/drawer.svelte.d.ts +1 -0
  20. package/package/components/{core → icon}/icon.svelte +5 -0
  21. package/package/components/{core → icon}/icon.svelte.d.ts +6 -2
  22. package/package/components/listbox/listbox.svelte +74 -0
  23. package/package/components/{composite → listbox}/listbox.svelte.d.ts +2 -0
  24. package/package/components/listbox/option-group.svelte +47 -0
  25. package/package/components/listbox/option-group.svelte.d.ts +38 -0
  26. package/package/components/{core → listbox}/option.svelte +34 -2
  27. package/package/components/{core → listbox}/option.svelte.d.ts +7 -3
  28. package/package/components/{core → menu}/menu-button.svelte +2 -16
  29. package/package/components/{core → menu}/menu-button.svelte.d.ts +4 -1
  30. package/package/components/{core → menu}/menu-item-checkbox.svelte +1 -0
  31. package/package/components/{core → menu}/menu-item-checkbox.svelte.d.ts +4 -1
  32. package/package/components/{composite → menu}/menu-item-group.svelte +5 -1
  33. package/package/components/{composite → menu}/menu-item-group.svelte.d.ts +1 -0
  34. package/package/components/{core → menu}/menu-item-radio.svelte +2 -0
  35. package/package/components/{core → menu}/menu-item-radio.svelte.d.ts +5 -1
  36. package/package/components/{core → menu}/menu-item.svelte +6 -6
  37. package/package/components/{core → menu}/menu-item.svelte.d.ts +4 -1
  38. package/package/components/{composite → menu}/menu.svelte +2 -1
  39. package/package/components/{composite → menu}/menu.svelte.d.ts +1 -0
  40. package/package/components/{composite → radio}/radio-group.svelte +6 -2
  41. package/package/components/{composite → radio}/radio-group.svelte.d.ts +1 -1
  42. package/package/components/{core → radio}/radio.svelte +6 -4
  43. package/package/components/{core → radio}/radio.svelte.d.ts +4 -2
  44. package/package/components/{composite → select}/combobox.svelte +7 -6
  45. package/package/components/{composite → select}/combobox.svelte.d.ts +3 -2
  46. package/package/components/{composite → select}/select.svelte +3 -1
  47. package/package/components/{composite → select}/select.svelte.d.ts +7 -3
  48. package/package/components/{core → slider}/slider.svelte +58 -47
  49. package/package/components/{core → slider}/slider.svelte.d.ts +12 -10
  50. package/package/components/{core → switch}/switch.svelte +36 -19
  51. package/package/components/{core → switch}/switch.svelte.d.ts +3 -2
  52. package/package/components/table/table-body.svelte +23 -0
  53. package/package/components/table/table-body.svelte.d.ts +34 -0
  54. package/package/components/table/table-cell.svelte +23 -0
  55. package/package/components/table/table-cell.svelte.d.ts +34 -0
  56. package/package/components/table/table-col-header.svelte +23 -0
  57. package/package/components/table/table-col-header.svelte.d.ts +34 -0
  58. package/package/components/table/table-foot.svelte +23 -0
  59. package/package/components/table/table-foot.svelte.d.ts +34 -0
  60. package/package/components/table/table-head.svelte +23 -0
  61. package/package/components/table/table-head.svelte.d.ts +34 -0
  62. package/package/components/table/table-row-header.svelte +23 -0
  63. package/package/components/table/table-row-header.svelte.d.ts +34 -0
  64. package/package/components/table/table-row.svelte +23 -0
  65. package/package/components/table/table-row.svelte.d.ts +38 -0
  66. package/package/components/table/table.svelte +44 -0
  67. package/package/components/table/table.svelte.d.ts +36 -0
  68. package/package/components/{composite → tabs}/tab-list.svelte +3 -2
  69. package/package/components/{composite → tabs}/tab-list.svelte.d.ts +6 -5
  70. package/package/components/{core → tabs}/tab-panel.svelte +1 -0
  71. package/package/components/{core → tabs}/tab-panel.svelte.d.ts +1 -0
  72. package/package/components/{core → tabs}/tab.svelte +2 -1
  73. package/package/components/{core → tabs}/tab.svelte.d.ts +1 -0
  74. package/package/components/{editor/markdown.svelte → text-field/markdown-editor.svelte} +10 -6
  75. package/package/components/text-field/markdown-editor.svelte.d.ts +26 -0
  76. package/package/components/{core → text-field}/number-input.svelte +13 -7
  77. package/package/components/{core → text-field}/number-input.svelte.d.ts +7 -3
  78. package/package/components/{core → text-field}/password-input.svelte +6 -3
  79. package/package/components/{core → text-field}/password-input.svelte.d.ts +8 -3
  80. package/package/components/{core → text-field}/search-bar.svelte +5 -2
  81. package/package/components/{core → text-field}/search-bar.svelte.d.ts +8 -3
  82. package/package/components/{core → text-field}/text-area.svelte +3 -1
  83. package/package/components/{core → text-field}/text-area.svelte.d.ts +9 -5
  84. package/package/components/{core → text-field}/text-input.svelte +6 -4
  85. package/package/components/{core → text-field}/text-input.svelte.d.ts +11 -7
  86. package/package/components/{core → toolbar}/toolbar.svelte +1 -0
  87. package/package/components/{core → toolbar}/toolbar.svelte.d.ts +2 -1
  88. package/package/components/util/app-shell.svelte +11 -40
  89. package/package/components/util/group.js +305 -0
  90. package/package/components/{core → util}/group.svelte +5 -11
  91. package/package/components/{core → util}/group.svelte.d.ts +4 -2
  92. package/package/components/util/popup.d.ts +30 -0
  93. package/package/components/{helpers → util}/popup.js +26 -25
  94. package/package/components/util/popup.svelte +14 -5
  95. package/package/components/util/{misc.d.ts → util.d.ts} +1 -0
  96. package/package/components/util/{misc.js → util.js} +15 -0
  97. package/package/index.d.ts +46 -41
  98. package/package/index.js +48 -83
  99. package/package/styles/core.scss +5 -34
  100. package/package/styles/variables.scss +6 -8
  101. package/package.json +351 -317
  102. package/package/components/composite/grid.svelte +0 -24
  103. package/package/components/composite/grid.svelte.d.ts +0 -31
  104. package/package/components/composite/listbox.svelte +0 -63
  105. package/package/components/core/grid-cell.svelte +0 -13
  106. package/package/components/core/grid-cell.svelte.d.ts +0 -29
  107. package/package/components/core/row-group.svelte +0 -13
  108. package/package/components/core/row-group.svelte.d.ts +0 -29
  109. package/package/components/core/row.svelte +0 -13
  110. package/package/components/core/row.svelte.d.ts +0 -33
  111. package/package/components/core/separator.svelte.d.ts +0 -26
  112. package/package/components/editor/markdown.svelte.d.ts +0 -25
  113. package/package/components/helpers/group.js +0 -253
  114. package/package/components/helpers/popup.d.ts +0 -30
  115. package/package/components/helpers/util.d.ts +0 -1
  116. package/package/components/helpers/util.js +0 -14
  117. package/package/components/{composite → button}/select-button-group.svelte.d.ts +4 -4
  118. /package/package/components/{core → button}/select-button.svelte +0 -0
  119. /package/package/components/{core → button}/select-button.svelte.d.ts +0 -0
  120. /package/package/components/{helpers → util}/group.d.ts +0 -0
@@ -1,11 +1,13 @@
1
1
  <!--
2
2
  @component
3
+ A generic, single-line text field. The equivalent of the HTML `<input type="text">` element.
4
+ @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/text
3
5
  @see https://w3c.github.io/aria/#textbox
4
6
  -->
5
7
  <svelte:options accessors={true} />
6
8
 
7
9
  <script>
8
- import { getRandomId } from '../helpers/util';
10
+ import { getRandomId } from '../util/util';
9
11
 
10
12
  /**
11
13
  * CSS class name on the button.
@@ -84,7 +86,7 @@ input {
84
86
  display: inline-block;
85
87
  flex: auto;
86
88
  border-width: 1px;
87
- border-color: var(--secondary-control-border-color);
89
+ border-color: var(--control-border-color);
88
90
  border-radius: var(--input--medium--border-radius);
89
91
  padding: 0 8px;
90
92
  min-width: 0;
@@ -101,7 +103,7 @@ input:focus {
101
103
  }
102
104
  input:read-only {
103
105
  color: var(--tertiary-foreground-color);
104
- border-color: var(--secondary-control-border-color) !important;
106
+ border-color: var(--control-border-color) !important;
105
107
  }
106
108
  input:disabled {
107
109
  background-color: var(--disabled-background-color);
@@ -115,7 +117,7 @@ input ~ :global(button) {
115
117
  flex: none;
116
118
  margin-left: -1px;
117
119
  border-width: 1px;
118
- border-color: var(--secondary-control-border-color);
120
+ border-color: var(--control-border-color);
119
121
  height: var(--input--medium--height);
120
122
  aspect-ratio: 1/1;
121
123
  }
@@ -1,15 +1,19 @@
1
1
  /** @typedef {typeof __propDef.props} TextInputProps */
2
2
  /** @typedef {typeof __propDef.events} TextInputEvents */
3
3
  /** @typedef {typeof __propDef.slots} TextInputSlots */
4
- /** @see https://w3c.github.io/aria/#textbox */
4
+ /**
5
+ * A generic, single-line text field. The equivalent of the HTML `<input type="text">` element.
6
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/text
7
+ * @see https://w3c.github.io/aria/#textbox
8
+ */
5
9
  export default class TextInput extends SvelteComponentTyped<{
6
10
  [x: string]: any;
7
- class?: string;
8
- element?: HTMLInputElement;
9
- role?: "textbox" | "searchbox" | "combobox" | "spinbutton";
10
11
  disabled?: boolean;
11
12
  name?: string;
12
13
  value?: string | number;
14
+ class?: string;
15
+ element?: HTMLInputElement;
16
+ role?: "textbox" | "searchbox" | "combobox" | "spinbutton";
13
17
  readOnly?: boolean;
14
18
  }, {
15
19
  input: Event;
@@ -49,12 +53,12 @@ import { SvelteComponentTyped } from "svelte";
49
53
  declare const __propDef: {
50
54
  props: {
51
55
  [x: string]: any;
52
- class?: string;
53
- element?: HTMLInputElement | null;
54
- role?: ('textbox' | 'searchbox' | 'combobox' | 'spinbutton');
55
56
  disabled?: boolean;
56
57
  name?: string;
57
58
  value?: (string | number | undefined);
59
+ class?: string;
60
+ element?: HTMLInputElement | null;
61
+ role?: ('textbox' | 'searchbox' | 'combobox' | 'spinbutton');
58
62
  readOnly?: boolean;
59
63
  };
60
64
  events: {
@@ -1,5 +1,6 @@
1
1
  <!--
2
2
  @component
3
+ A toolbar layout that can contain `<Button>`, `<Select>` and other widgets.
3
4
  @see https://w3c.github.io/aria/#toolbar
4
5
  @see https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/
5
6
  -->
@@ -2,13 +2,14 @@
2
2
  /** @typedef {typeof __propDef.events} ToolbarEvents */
3
3
  /** @typedef {typeof __propDef.slots} ToolbarSlots */
4
4
  /**
5
+ * A toolbar layout that can contain `<Button>`, `<Select>` and other widgets.
5
6
  * @see https://w3c.github.io/aria/#toolbar
6
7
  * @see https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/
7
8
  */
8
9
  export default class Toolbar extends SvelteComponentTyped<{
9
10
  [x: string]: any;
10
11
  class?: string;
11
- orientation?: "horizontal" | "vertical";
12
+ orientation?: "vertical" | "horizontal";
12
13
  }, {
13
14
  [evt: string]: CustomEvent<any>;
14
15
  }, {
@@ -52,14 +52,13 @@
52
52
  --foreground-color-2-hsl: var(--base-hue) 5% 20%;
53
53
  --foreground-color-3-hsl: var(--base-hue) 5% 40%;
54
54
  --foreground-color-4-hsl: var(--base-hue) 5% 60%;
55
- --border-color-1-hsl: var(--base-hue) 5% 30%;
56
- --border-color-2-hsl: var(--base-hue) 5% 50%;
57
- --border-color-3-hsl: var(--base-hue) 5% 70%;
58
55
  --background-color-1-hsl: var(--base-hue) 5% 100%;
59
56
  --background-color-2-hsl: var(--base-hue) 5% 98%;
60
57
  --background-color-3-hsl: var(--base-hue) 5% 94%;
61
58
  --background-color-4-hsl: var(--base-hue) 5% 90%;
62
59
  --background-color-5-hsl: var(--base-hue) 5% 86%;
60
+ --border-color-1-hsl: var(--base-hue) 5% 78%;
61
+ --border-color-2-hsl: var(--base-hue) 5% 82%;
63
62
  --shadow-color: var(--base-hue) 10% 0%;
64
63
  --primary-accent-color: hsl(var(--base-hue) 80% 45%);
65
64
  --primary-accent-color-lighter: hsl(var(--base-hue) 80% 40%);
@@ -82,14 +81,13 @@
82
81
  --foreground-color-2-hsl: var(--base-hue) 10% 80%;
83
82
  --foreground-color-3-hsl: var(--base-hue) 10% 60%;
84
83
  --foreground-color-4-hsl: var(--base-hue) 10% 40%;
85
- --border-color-1-hsl: var(--base-hue) 10% 70%;
86
- --border-color-2-hsl: var(--base-hue) 10% 50%;
87
- --border-color-3-hsl: var(--base-hue) 10% 30%;
88
84
  --background-color-1-hsl: var(--base-hue) 10% 10%;
89
85
  --background-color-2-hsl: var(--base-hue) 10% 12%;
90
86
  --background-color-3-hsl: var(--base-hue) 10% 16%;
91
87
  --background-color-4-hsl: var(--base-hue) 10% 20%;
92
88
  --background-color-5-hsl: var(--base-hue) 10% 24%;
89
+ --border-color-1-hsl: var(--base-hue) 10% 32%;
90
+ --border-color-2-hsl: var(--base-hue) 10% 28%;
93
91
  --shadow-color: var(--base-hue) 10% 0%;
94
92
  --primary-accent-color: hsl(var(--base-hue) 100% 45%);
95
93
  --primary-accent-color-lighter: hsl(var(--base-hue) 100% 55%);
@@ -158,8 +156,8 @@
158
156
  );
159
157
  --primary-border-color: hsl(var(--border-color-1-hsl));
160
158
  --secondary-border-color: hsl(var(--border-color-2-hsl));
161
- --primary-control-border-color: hsl(var(--border-color-2-hsl));
162
- --secondary-control-border-color: hsl(var(--border-color-3-hsl));
159
+ --control-border-color: hsl(var(--border-color-2-hsl));
160
+ --checkbox-border-color: hsl(var(--foreground-color-3-hsl));
163
161
  --danger-border-color: hsl(
164
162
  var(--danger-color-hue) var(--alert-border-color-saturation) var(--alert-border-color-lightness)
165
163
  );
@@ -227,6 +225,10 @@
227
225
  :global(*) {
228
226
  scroll-behavior: smooth;
229
227
  box-sizing: border-box;
228
+ outline-offset: 1px;
229
+ outline-width: 2px !important;
230
+ outline-style: solid;
231
+ outline-color: transparent;
230
232
  border-width: 0;
231
233
  border-style: solid;
232
234
  }
@@ -237,10 +239,7 @@
237
239
  }
238
240
 
239
241
  :global(:focus-visible) {
240
- outline-offset: -2px;
241
- outline-width: 2px !important;
242
- outline-style: solid;
243
- outline-color: hsl(var(--hue), 100%, 50%, 25%);
242
+ outline-color: var(--primary-accent-color-lighter);
244
243
  }
245
244
 
246
245
  :global(h1),
@@ -283,17 +282,6 @@
283
282
  line-height: 1.75;
284
283
  }
285
284
 
286
- :global([role=grid]) {
287
- display: table;
288
- width: 100%;
289
- }
290
- :global([role=grid]) :global(.colgroup) {
291
- display: table-column-group;
292
- }
293
- :global([role=grid]) :global(.colgroup) :global(.col) {
294
- display: table-column;
295
- }
296
-
297
285
  :global(code),
298
286
  :global(pre) {
299
287
  font-family: var(--font-family--monospace);
@@ -327,23 +315,6 @@
327
315
  background: transparent;
328
316
  }
329
317
 
330
- :global(.thead[role=rowgroup]) {
331
- display: table-header-group;
332
- }
333
-
334
- :global(.tbody[role=rowgroup]) {
335
- display: table-row-group;
336
- }
337
-
338
- :global([role=row]) {
339
- display: table-row;
340
- }
341
-
342
- :global([role=columnheader]),
343
- :global([role=gridcell]) {
344
- display: table-cell;
345
- }
346
-
347
318
  :global(.app-shell) {
348
319
  position: fixed;
349
320
  inset: 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,13 +99,15 @@ 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
 
@@ -122,21 +126,17 @@ class Popup {
122
126
  }
123
127
  });
124
128
 
125
- [this.anchorElement, this.popupElement].forEach((element) => {
126
- element.addEventListener('keydown', (event) => {
127
- const { key, ctrlKey, metaKey, shiftKey, altKey } = event;
128
-
129
- if (
130
- get(this.open) &&
131
- ['Escape'].includes(key) &&
132
- !ctrlKey &&
133
- !metaKey &&
134
- !shiftKey &&
135
- !altKey
136
- ) {
137
- this.open.set(false);
138
- }
139
- });
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
+ }
140
140
  });
141
141
 
142
142
  this.open.subscribe((open) => {
@@ -144,6 +144,7 @@ class Popup {
144
144
  this.checkPosition();
145
145
  } else if (this.anchorElement.getAttribute('aria-expanded') === 'true') {
146
146
  this.anchorElement.focus();
147
+ this.anchorElement.removeAttribute('aria-controls');
147
148
  }
148
149
 
149
150
  this.anchorElement.setAttribute('aria-expanded', String(open));