@sveltia/ui 0.2.0 → 0.2.1

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 (107) hide show
  1. package/package/components/composite/calendar.svelte +239 -0
  2. package/package/components/composite/calendar.svelte.d.ts +27 -0
  3. package/package/components/composite/checkbox-group.svelte +43 -0
  4. package/package/components/composite/checkbox-group.svelte.d.ts +34 -0
  5. package/package/components/composite/combobox.svelte +210 -0
  6. package/package/components/composite/combobox.svelte.d.ts +47 -0
  7. package/package/components/composite/disclosure.svelte +60 -0
  8. package/package/components/composite/disclosure.svelte.d.ts +35 -0
  9. package/package/components/composite/grid.svelte +24 -0
  10. package/package/components/composite/grid.svelte.d.ts +31 -0
  11. package/package/components/composite/listbox.svelte +63 -0
  12. package/package/components/composite/listbox.svelte.d.ts +52 -0
  13. package/package/components/composite/menu-item-group.svelte +31 -0
  14. package/package/components/composite/menu-item-group.svelte.d.ts +33 -0
  15. package/package/components/composite/menu.svelte +44 -0
  16. package/package/components/composite/menu.svelte.d.ts +41 -0
  17. package/package/components/composite/radio-button-group.svelte +45 -0
  18. package/package/components/composite/radio-button-group.svelte.d.ts +36 -0
  19. package/package/components/composite/select-button-group.svelte +71 -0
  20. package/package/components/composite/select-button-group.svelte.d.ts +44 -0
  21. package/package/components/composite/select.svelte +34 -0
  22. package/package/components/composite/select.svelte.d.ts +38 -0
  23. package/package/components/composite/tab-list.svelte +76 -0
  24. package/package/components/composite/tab-list.svelte.d.ts +55 -0
  25. package/package/components/core/button.svelte +216 -0
  26. package/package/components/core/button.svelte.d.ts +109 -0
  27. package/package/components/core/checkbox.svelte +114 -0
  28. package/package/components/core/checkbox.svelte.d.ts +45 -0
  29. package/package/components/core/dialog.svelte +288 -0
  30. package/package/components/core/dialog.svelte.d.ts +73 -0
  31. package/package/components/core/drawer.svelte +338 -0
  32. package/package/components/core/drawer.svelte.d.ts +59 -0
  33. package/package/components/core/grid-cell.svelte +14 -0
  34. package/package/components/core/grid-cell.svelte.d.ts +29 -0
  35. package/package/components/core/group.svelte +31 -0
  36. package/package/components/core/group.svelte.d.ts +33 -0
  37. package/package/components/core/icon.svelte +21 -0
  38. package/package/components/core/icon.svelte.d.ts +27 -0
  39. package/package/components/core/menu-button.svelte +57 -0
  40. package/package/components/core/menu-button.svelte.d.ts +46 -0
  41. package/package/components/core/menu-item-checkbox.svelte +24 -0
  42. package/package/components/core/menu-item-checkbox.svelte.d.ts +34 -0
  43. package/package/components/core/menu-item-radio.svelte +19 -0
  44. package/package/components/core/menu-item-radio.svelte.d.ts +34 -0
  45. package/package/components/core/menu-item.svelte +113 -0
  46. package/package/components/core/menu-item.svelte.d.ts +44 -0
  47. package/package/components/core/number-input.svelte +112 -0
  48. package/package/components/core/number-input.svelte.d.ts +42 -0
  49. package/package/components/core/option.svelte +65 -0
  50. package/package/components/core/option.svelte.d.ts +60 -0
  51. package/package/components/core/password-input.svelte +81 -0
  52. package/package/components/core/password-input.svelte.d.ts +36 -0
  53. package/package/components/core/radio-button.svelte +93 -0
  54. package/package/components/core/radio-button.svelte.d.ts +37 -0
  55. package/package/components/core/row-group.svelte +14 -0
  56. package/package/components/core/row-group.svelte.d.ts +29 -0
  57. package/package/components/core/row.svelte +14 -0
  58. package/package/components/core/row.svelte.d.ts +33 -0
  59. package/package/components/core/search-bar.svelte +91 -0
  60. package/package/components/core/search-bar.svelte.d.ts +49 -0
  61. package/package/components/core/select-button.svelte +31 -0
  62. package/package/components/core/select-button.svelte.d.ts +52 -0
  63. package/package/components/core/separator.svelte +28 -0
  64. package/package/components/core/separator.svelte.d.ts +26 -0
  65. package/package/components/core/slider.svelte +271 -0
  66. package/package/components/core/slider.svelte.d.ts +51 -0
  67. package/package/components/core/spacer.svelte +22 -0
  68. package/package/components/core/spacer.svelte.d.ts +25 -0
  69. package/package/components/core/switch.svelte +86 -0
  70. package/package/components/core/switch.svelte.d.ts +37 -0
  71. package/package/components/core/tab-panel.svelte +23 -0
  72. package/package/components/core/tab-panel.svelte.d.ts +33 -0
  73. package/package/components/core/tab.svelte +22 -0
  74. package/package/components/core/tab.svelte.d.ts +45 -0
  75. package/package/components/core/text-area.svelte +90 -0
  76. package/package/components/core/text-area.svelte.d.ts +57 -0
  77. package/package/components/core/text-input.svelte +146 -0
  78. package/package/components/core/text-input.svelte.d.ts +71 -0
  79. package/package/components/core/toolbar.svelte +74 -0
  80. package/package/components/core/toolbar.svelte.d.ts +35 -0
  81. package/package/components/editor/markdown.svelte +78 -0
  82. package/package/components/editor/markdown.svelte.d.ts +25 -0
  83. package/package/components/helpers/group.d.ts +37 -0
  84. package/package/components/helpers/group.js +246 -0
  85. package/package/components/helpers/popup.d.ts +31 -0
  86. package/package/components/helpers/popup.js +165 -0
  87. package/package/components/helpers/util.d.ts +1 -0
  88. package/package/components/helpers/util.js +8 -0
  89. package/package/components/util/app-shell.svelte +354 -0
  90. package/package/components/util/app-shell.svelte.d.ts +38 -0
  91. package/package/components/util/misc.d.ts +2 -0
  92. package/package/components/util/misc.js +22 -0
  93. package/package/components/util/popup.svelte +155 -0
  94. package/package/components/util/popup.svelte.d.ts +53 -0
  95. package/package/components/util/portal.svelte +34 -0
  96. package/package/components/util/portal.svelte.d.ts +28 -0
  97. package/package/index.d.ts +43 -0
  98. package/package/index.js +66 -0
  99. package/package/locales/en.d.ts +42 -0
  100. package/package/locales/en.js +41 -0
  101. package/package/locales/ja.d.ts +42 -0
  102. package/package/locales/ja.js +41 -0
  103. package/package/styles/core.scss +134 -0
  104. package/package/styles/variables.scss +184 -0
  105. package/package/typedef.d.ts +0 -0
  106. package/package/typedef.js +0 -0
  107. package/package.json +2 -3
@@ -0,0 +1,35 @@
1
+ /** @typedef {typeof __propDef.props} ToolbarProps */
2
+ /** @typedef {typeof __propDef.events} ToolbarEvents */
3
+ /** @typedef {typeof __propDef.slots} ToolbarSlots */
4
+ /**
5
+ * @see https://w3c.github.io/aria/#toolbar
6
+ * @see https://w3c.github.io/aria-practices/#toolbar
7
+ */
8
+ export default class Toolbar extends SvelteComponentTyped<{
9
+ [x: string]: any;
10
+ class?: string;
11
+ orientation?: "horizontal" | "vertical";
12
+ }, {
13
+ [evt: string]: CustomEvent<any>;
14
+ }, {
15
+ default: {};
16
+ }> {
17
+ }
18
+ export type ToolbarProps = typeof __propDef.props;
19
+ export type ToolbarEvents = typeof __propDef.events;
20
+ export type ToolbarSlots = typeof __propDef.slots;
21
+ import { SvelteComponentTyped } from "svelte";
22
+ declare const __propDef: {
23
+ props: {
24
+ [x: string]: any;
25
+ class?: string;
26
+ orientation?: ('horizontal' | 'vertical');
27
+ };
28
+ events: {
29
+ [evt: string]: CustomEvent<any>;
30
+ };
31
+ slots: {
32
+ default: {};
33
+ };
34
+ };
35
+ export {};
@@ -0,0 +1,78 @@
1
+ <script>
2
+ import { _ } from 'svelte-i18n';
3
+ import Button from '../core/button.svelte';
4
+ import Icon from '../core/icon.svelte';
5
+ import Separator from '../core/separator.svelte';
6
+ import TextArea from '../core/text-area.svelte';
7
+ import Toolbar from '../core/toolbar.svelte';
8
+
9
+ /** @type {(String|undefined)} */
10
+ export let value = undefined;
11
+
12
+ export let disabled = false;
13
+
14
+ const defaultButtons = [
15
+ { name: 'bold', label: $_('sui.markdown_editor.bold'), icon: 'format_bold' },
16
+ { name: 'italic', label: $_('sui.markdown_editor.italic'), icon: 'format_italic' },
17
+ { name: 'code', label: $_('sui.markdown_editor.code'), icon: 'code' },
18
+ { name: 'link', label: $_('sui.markdown_editor.link'), icon: 'link' },
19
+ { separator: true },
20
+ {
21
+ name: 'heading-one',
22
+ label: $_('sui.markdown_editor.heading_x', { values: { level: 1 } }),
23
+ icon: 'format_h1',
24
+ },
25
+ {
26
+ name: 'heading-two',
27
+ label: $_('sui.markdown_editor.heading_x', { values: { level: 2 } }),
28
+ icon: 'format_h2',
29
+ },
30
+ { name: 'quote', label: $_('sui.markdown_editor.quote'), icon: 'format_quote' },
31
+ { separator: true },
32
+ {
33
+ name: 'bulleted-list',
34
+ label: $_('sui.markdown_editor.bulleted_list'),
35
+ icon: 'format_list_bulleted',
36
+ },
37
+ {
38
+ name: 'numbered-list',
39
+ label: $_('sui.markdown_editor.numbered_list'),
40
+ icon: 'format_list_numbered',
41
+ },
42
+ ];
43
+ </script>
44
+
45
+ <div>
46
+ <Toolbar aria-label={$_('sui.markdown_editor.markdown_editor')}>
47
+ {#each defaultButtons as { label, icon, separator }}
48
+ {#if separator}
49
+ <Separator />
50
+ {:else}
51
+ <Button {disabled}>
52
+ <Icon slot="start-icon" name={icon} {label} />
53
+ </Button>
54
+ {/if}
55
+ {/each}
56
+ </Toolbar>
57
+ <TextArea autoResize={true} {disabled} bind:value />
58
+ </div>
59
+
60
+ <style>div {
61
+ display: contents;
62
+ }
63
+ div :global([role="toolbar"]) {
64
+ display: flex;
65
+ gap: 8px;
66
+ border-radius: 4px 4px 0 0;
67
+ padding: 8px;
68
+ background-color: var(--tertiary-background-color);
69
+ }
70
+ div :global([role="toolbar"]) :global(button) {
71
+ flex: none;
72
+ }
73
+ div :global([role="toolbar"]) + :global(div) {
74
+ width: 100%;
75
+ }
76
+ div :global([role="toolbar"]) + :global(div) :global(textarea) {
77
+ border-radius: 0 0 4px 4px !important;
78
+ }</style>
@@ -0,0 +1,25 @@
1
+ /** @typedef {typeof __propDef.props} MarkdownProps */
2
+ /** @typedef {typeof __propDef.events} MarkdownEvents */
3
+ /** @typedef {typeof __propDef.slots} MarkdownSlots */
4
+ export default class Markdown extends SvelteComponentTyped<{
5
+ disabled?: boolean;
6
+ value?: string;
7
+ }, {
8
+ [evt: string]: CustomEvent<any>;
9
+ }, {}> {
10
+ }
11
+ export type MarkdownProps = typeof __propDef.props;
12
+ export type MarkdownEvents = typeof __propDef.events;
13
+ export type MarkdownSlots = typeof __propDef.slots;
14
+ import { SvelteComponentTyped } from "svelte";
15
+ declare const __propDef: {
16
+ props: {
17
+ disabled?: boolean;
18
+ value?: (string | undefined);
19
+ };
20
+ events: {
21
+ [evt: string]: CustomEvent<any>;
22
+ };
23
+ slots: {};
24
+ };
25
+ export {};
@@ -0,0 +1,37 @@
1
+ export function activateGroup(...args: any[]): Promise<Group>;
2
+ /**
3
+ * Implement keyboard and mouse interactions for a grouping composite widget.
4
+ */
5
+ declare class Group {
6
+ /**
7
+ *
8
+ * @param {HTMLElement} parent
9
+ * @todo Check for added elements probably with `MutationObserver`.
10
+ */
11
+ constructor(parent: HTMLElement);
12
+ parent: HTMLElement;
13
+ role: string;
14
+ grid: boolean;
15
+ multi: boolean;
16
+ id: string;
17
+ parentGroupSelector: string;
18
+ orientation: any;
19
+ childSelectedAttr: any;
20
+ /** @type {string} */
21
+ get selector(): string;
22
+ /** @type {HTMLElement[]} */
23
+ get allMembers(): HTMLElement[];
24
+ /** @type {HTMLElement[]} */
25
+ get activeMembers(): HTMLElement[];
26
+ /**
27
+ *
28
+ * @param {KeyboardEvent}
29
+ */
30
+ onClick(event: any): void;
31
+ /**
32
+ *
33
+ * @param {KeyboardEvent}
34
+ */
35
+ onKeyDown(event: any): void;
36
+ }
37
+ export {};
@@ -0,0 +1,246 @@
1
+ /* eslint-disable no-plusplus */
2
+ /* eslint-disable no-param-reassign */
3
+
4
+ import { sleep } from '../util/misc';
5
+ import { getRandomId } from './util';
6
+
7
+ const config = {
8
+ grid: {
9
+ orientation: 'vertical',
10
+ childRoles: ['row'],
11
+ childSelectedAttr: 'aria-selected',
12
+ },
13
+ listbox: {
14
+ orientation: 'vertical',
15
+ childRoles: ['option'],
16
+ childSelectedAttr: 'aria-selected',
17
+ },
18
+ menu: {
19
+ orientation: 'vertical',
20
+ childRoles: ['menuitem', 'menuitemcheckbox', 'menuitemradio'],
21
+ childSelectedAttr: 'aria-checked',
22
+ },
23
+ menubar: {
24
+ orientation: 'horizontal',
25
+ childRoles: ['menuitem', 'menuitemcheckbox', 'menuitemradio'],
26
+ childSelectedAttr: 'aria-checked',
27
+ },
28
+ radiogroup: {
29
+ orientation: 'horizontal',
30
+ childRoles: ['radio'],
31
+ childSelectedAttr: 'aria-checked',
32
+ },
33
+ tablist: {
34
+ orientation: 'horizontal',
35
+ childRoles: ['tab'],
36
+ childSelectedAttr: 'aria-selected',
37
+ },
38
+ };
39
+
40
+ /**
41
+ * Implement keyboard and mouse interactions for a grouping composite widget.
42
+ */
43
+ class Group {
44
+ /**
45
+ *
46
+ * @param {HTMLElement} parent
47
+ * @todo Check for added elements probably with `MutationObserver`.
48
+ */
49
+ constructor(parent) {
50
+ this.parent = parent;
51
+ this.role = parent.getAttribute('role');
52
+ this.grid = this.role === 'listbox' && parent.matches('.grid');
53
+ this.multi = this.parent.getAttribute('aria-multiselectable') === 'true';
54
+ this.id = getRandomId(this.role);
55
+ this.parentGroupSelector = `[role="group"], [role="${this.role}"]`;
56
+
57
+ const { orientation, childSelectedAttr } = config[this.role];
58
+
59
+ this.orientation = this.grid
60
+ ? 'horizontal'
61
+ : this.parent.getAttribute('aria-orientation') || orientation;
62
+ this.childSelectedAttr = childSelectedAttr;
63
+
64
+ const { allMembers } = this;
65
+
66
+ const hasSelected = allMembers.some((element) =>
67
+ element.matches(`[${childSelectedAttr}="true"]`),
68
+ );
69
+
70
+ allMembers.forEach((element, index) => {
71
+ const isSelected = element.matches(`[${childSelectedAttr}="true"]`);
72
+ const controls = document.querySelector(`#${element.getAttribute('aria-controls')}`);
73
+
74
+ element.id ||= `${this.id}-item-${index}`;
75
+ element.tabIndex ||= isSelected || (!hasSelected && index === 0) ? 0 : -1;
76
+ element.setAttribute(this.childSelectedAttr, isSelected);
77
+ controls?.setAttribute('aria-labelledby', element.id);
78
+ controls?.setAttribute('aria-hidden', !isSelected);
79
+ });
80
+
81
+ parent.addEventListener('click', (event) => {
82
+ this.onClick(event);
83
+ });
84
+
85
+ parent.addEventListener('keydown', (event) => {
86
+ this.onKeyDown(event);
87
+ });
88
+ }
89
+
90
+ /** @type {string} */
91
+ get selector() {
92
+ const roles = config[this.role].childRoles;
93
+
94
+ return roles ? roles.map((role) => `[role="${role}"]`).join(',') : '';
95
+ }
96
+
97
+ /** @type {HTMLElement[]} */
98
+ get allMembers() {
99
+ return [...this.parent.querySelectorAll(this.selector)];
100
+ }
101
+
102
+ /** @type {HTMLElement[]} */
103
+ get activeMembers() {
104
+ return this.allMembers.filter((element) => !element.matches('[aria-disabled="true"]'));
105
+ }
106
+
107
+ /**
108
+ *
109
+ * @param {KeyboardEvent}
110
+ */
111
+ onClick(event) {
112
+ const { target } = event;
113
+
114
+ if (!target.matches(this.selector)) {
115
+ return;
116
+ }
117
+
118
+ const targetParentGroup = target.closest(this.parentGroupSelector);
119
+
120
+ this.allMembers.forEach((element) => {
121
+ const isTarget = element === target;
122
+
123
+ element.tabIndex = element === target ? 0 : -1;
124
+
125
+ // Groups can be nested, e.g. `menu` > `group` > `menuitem`, so check if the parent is the
126
+ // same as the target’s parent
127
+ if (
128
+ (element.matches('[role="radio"], [role="menuitemradio"]') ||
129
+ (element.matches('[role="row"], [role="option"], [role="tab"]') && !this.multi)) &&
130
+ element.closest(this.parentGroupSelector) === targetParentGroup
131
+ ) {
132
+ element.setAttribute(this.childSelectedAttr, isTarget);
133
+ }
134
+
135
+ const controls = element.getAttribute('aria-controls');
136
+
137
+ if (controls) {
138
+ document.getElementById(controls)?.setAttribute('aria-hidden', !isTarget);
139
+ }
140
+ });
141
+
142
+ this.parent.dispatchEvent(
143
+ new CustomEvent('select', {
144
+ detail: {
145
+ value: target.value,
146
+ name: target.name,
147
+ },
148
+ }),
149
+ );
150
+ }
151
+
152
+ /**
153
+ *
154
+ * @param {KeyboardEvent}
155
+ */
156
+ onKeyDown(event) {
157
+ const { target, key, ctrlKey, metaKey, shiftKey, altKey } = event;
158
+
159
+ if (target.matches(this.selector) && !ctrlKey && !metaKey && !shiftKey && !altKey) {
160
+ if ([' ', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(key)) {
161
+ event.preventDefault();
162
+ }
163
+
164
+ if (key === ' ' || (key === 'Enter' && !target.matches('button'))) {
165
+ event.preventDefault();
166
+ target.click();
167
+
168
+ return;
169
+ }
170
+
171
+ const { allMembers, activeMembers } = this;
172
+ let index;
173
+ let newTarget;
174
+
175
+ if (this.grid) {
176
+ const colCount = Math.floor(this.parent.clientWidth / target.clientWidth);
177
+
178
+ index = allMembers.indexOf(target);
179
+
180
+ if (key === 'ArrowUp' && index > 0) {
181
+ newTarget = allMembers[index - colCount];
182
+ }
183
+
184
+ if (key === 'ArrowDown' && index < allMembers.length - 1) {
185
+ newTarget = allMembers[index + colCount];
186
+ }
187
+
188
+ if (key === 'ArrowLeft' && index > 0) {
189
+ newTarget = allMembers[index - 1];
190
+ }
191
+
192
+ if (key === 'ArrowRight' && index < allMembers.length - 1) {
193
+ newTarget = allMembers[index + 1];
194
+ }
195
+
196
+ if (newTarget?.getAttribute('aria-disabled') === 'true') {
197
+ newTarget = undefined;
198
+ }
199
+ } else {
200
+ index = activeMembers.indexOf(target);
201
+
202
+ if (key === (this.orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp')) {
203
+ if (index > 0) {
204
+ // Previous member
205
+ newTarget = activeMembers[index - 1];
206
+ }
207
+
208
+ if (index === 0) {
209
+ // Last member
210
+ newTarget = activeMembers[activeMembers.length - 1];
211
+ }
212
+ }
213
+
214
+ if (key === (this.orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown')) {
215
+ if (index < activeMembers.length - 1) {
216
+ // Next member
217
+ newTarget = activeMembers[index + 1];
218
+ }
219
+
220
+ if (index === activeMembers.length - 1) {
221
+ // First member
222
+ [newTarget] = activeMembers;
223
+ }
224
+ }
225
+ }
226
+
227
+ if (newTarget && newTarget !== target) {
228
+ activeMembers.forEach((element) => {
229
+ element.tabIndex = element === newTarget ? 0 : -1;
230
+ });
231
+
232
+ newTarget.focus();
233
+ }
234
+ }
235
+ }
236
+ }
237
+
238
+ /**
239
+ *
240
+ */
241
+ export const activateGroup = async (...args) => {
242
+ // Wait a bit before the relevant components, including the `aria-controls` target are mounted
243
+ await sleep(100);
244
+
245
+ return new Group(...args);
246
+ };
@@ -0,0 +1,31 @@
1
+ export function activatePopup(...args: any[]): Popup;
2
+ export type PopupPosition = ('top-left' | 'top-right' | 'right-top' | 'right-bottom' | 'bottom-left' | 'bottom-right' | 'left-top' | 'left-bottom');
3
+ /**
4
+ * @typedef {('top-left'|'top-right'|'right-top'|'right-bottom'|'bottom-left'|'bottom-right'|'left-top'|'left-bottom')} PopupPosition
5
+ */
6
+ declare class Popup {
7
+ /**
8
+ *
9
+ * @param {HTMLElement} anchorElement
10
+ * @param {HTMLElement} popupElement
11
+ * @param {PopupPosition} position
12
+ */
13
+ constructor(anchorElement: HTMLElement, popupElement: HTMLElement, position: PopupPosition);
14
+ open: import("svelte/store").Writable<boolean>;
15
+ style: import("svelte/store").Writable<{
16
+ inset: any;
17
+ zIndex: any;
18
+ width: any;
19
+ height: any;
20
+ }>;
21
+ observer: IntersectionObserver;
22
+ anchorElement: HTMLElement;
23
+ popupElement: HTMLElement;
24
+ position: PopupPosition;
25
+ id: string;
26
+ /**
27
+ * Continue checking the position in case the window or parent element resizes.
28
+ */
29
+ checkPosition(): void;
30
+ }
31
+ export {};
@@ -0,0 +1,165 @@
1
+ /* eslint-disable max-len */
2
+ /* eslint-disable no-nested-ternary */
3
+
4
+ import { get, writable } from 'svelte/store';
5
+ import { getRandomId } from './util';
6
+
7
+ /**
8
+ * @typedef {('top-left'|'top-right'|'right-top'|'right-bottom'|'bottom-left'|'bottom-right'|'left-top'|'left-bottom')} PopupPosition
9
+ */
10
+
11
+ class Popup {
12
+ open = writable(false);
13
+
14
+ style = writable({ inset: undefined, zIndex: undefined, width: undefined, height: undefined });
15
+
16
+ observer = new IntersectionObserver((entries) => {
17
+ entries.forEach(({ intersectionRect, rootBounds }) => {
18
+ if (!intersectionRect || !rootBounds) {
19
+ return;
20
+ }
21
+
22
+ const contentHeight = this.popupElement.querySelector('.content').scrollHeight;
23
+ const topMargin = intersectionRect.top - 8;
24
+ const bottomMargin = rootBounds.height - intersectionRect.bottom - 8;
25
+ let { position } = this;
26
+ let height;
27
+
28
+ // Alter the position if the space is limited
29
+ // @todo Handle more overflow cases
30
+ if (position.startsWith('bottom-')) {
31
+ if (contentHeight > bottomMargin) {
32
+ if (topMargin > bottomMargin) {
33
+ position = position.replace('bottom-', 'top-');
34
+ height = topMargin;
35
+ } else {
36
+ height = bottomMargin;
37
+ }
38
+ }
39
+ }
40
+
41
+ const top = position.startsWith('bottom-')
42
+ ? `${Math.round(intersectionRect.bottom)}px`
43
+ : position.endsWith('-top')
44
+ ? `${Math.round(intersectionRect.top)}px`
45
+ : 'auto';
46
+
47
+ const right = position.startsWith('left-')
48
+ ? `${Math.round(rootBounds.width - intersectionRect.left)}px`
49
+ : position.endsWith('-right')
50
+ ? `${Math.round(rootBounds.width - intersectionRect.right)}px`
51
+ : 'auto';
52
+
53
+ const bottom = position.startsWith('top-')
54
+ ? `${Math.round(rootBounds.height - intersectionRect.top)}px`
55
+ : position.endsWith('-bottom')
56
+ ? `${Math.round(rootBounds.height - intersectionRect.bottom)}px`
57
+ : 'auto';
58
+
59
+ const left = position.startsWith('right-')
60
+ ? `${Math.round(intersectionRect.right)}px`
61
+ : position.endsWith('-left')
62
+ ? `${Math.round(intersectionRect.left)}px`
63
+ : 'auto';
64
+
65
+ const anchorPopup = this.anchorElement.closest('.popup');
66
+
67
+ const style = {
68
+ inset: [top, right, bottom, left].join(' '),
69
+ zIndex: anchorPopup ? Number(anchorPopup.style.zIndex) + 1 : 1000,
70
+ width: `${Math.round(intersectionRect.width)}px`,
71
+ height: height ? `${Math.round(height)}px` : 'auto',
72
+ };
73
+
74
+ if (JSON.stringify(style) !== JSON.stringify(get(this.style))) {
75
+ this.style.set(style);
76
+ }
77
+ });
78
+ });
79
+
80
+ /**
81
+ *
82
+ * @param {HTMLElement} anchorElement
83
+ * @param {HTMLElement} popupElement
84
+ * @param {PopupPosition} position
85
+ */
86
+ constructor(anchorElement, popupElement, position) {
87
+ this.anchorElement = anchorElement;
88
+ this.popupElement = popupElement; // = backdrop
89
+ this.position = position;
90
+ this.id = getRandomId('popup');
91
+
92
+ this.anchorElement.setAttribute('aria-controls', this.id);
93
+ this.popupElement.setAttribute('id', this.id);
94
+
95
+ this.anchorElement.addEventListener('click', () => {
96
+ if (!this.anchorElement.matches('[aria-disabled="true"]')) {
97
+ this.open.set(!get(this.open));
98
+ }
99
+ });
100
+
101
+ this.anchorElement.addEventListener('keydown', (event) => {
102
+ const { key, ctrlKey, metaKey, shiftKey, altKey } = event;
103
+
104
+ if (!ctrlKey && !metaKey && !shiftKey && !altKey) {
105
+ if (key === ' ' || key === 'Enter') {
106
+ event.stopPropagation();
107
+ this.open.set(!get(this.open));
108
+ }
109
+ }
110
+ });
111
+
112
+ this.popupElement.addEventListener('click', (event) => {
113
+ if (get(this.open) && event.target !== this.anchorElement) {
114
+ this.open.set(false);
115
+ }
116
+ });
117
+
118
+ [this.anchorElement, this.popupElement].forEach((element) => {
119
+ element.addEventListener('keydown', (event) => {
120
+ const { key, ctrlKey, metaKey, shiftKey, altKey } = event;
121
+
122
+ if (
123
+ get(this.open) &&
124
+ ['Escape'].includes(key) &&
125
+ !ctrlKey &&
126
+ !metaKey &&
127
+ !shiftKey &&
128
+ !altKey
129
+ ) {
130
+ this.open.set(false);
131
+ }
132
+ });
133
+ });
134
+
135
+ this.open.subscribe((open) => {
136
+ if (open) {
137
+ this.checkPosition();
138
+ } else if (this.anchorElement.getAttribute('aria-expanded') === 'true') {
139
+ this.anchorElement.focus();
140
+ }
141
+
142
+ this.anchorElement.setAttribute('aria-expanded', open);
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Continue checking the position in case the window or parent element resizes.
148
+ */
149
+ checkPosition() {
150
+ this.observer.observe(this.anchorElement);
151
+
152
+ window.requestAnimationFrame(() => {
153
+ this.observer.unobserve(this.anchorElement);
154
+
155
+ if (get(this.open)) {
156
+ this.checkPosition();
157
+ }
158
+ });
159
+ }
160
+ }
161
+
162
+ /**
163
+ *
164
+ */
165
+ export const activatePopup = (...args) => new Popup(...args);
@@ -0,0 +1 @@
1
+ export function getRandomId(prefix?: string, length?: number): string;
@@ -0,0 +1,8 @@
1
+ export const getRandomId = (prefix = '', length = 7) =>
2
+ [
3
+ prefix,
4
+ new Array(length)
5
+ .fill()
6
+ .map(() => '0123456789abcdef'[Math.floor(Math.random() * 12)])
7
+ .join(''),
8
+ ].join('-');