@oscarpalmer/oui 0.14.0 → 0.16.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 (46) hide show
  1. package/README.md +2 -0
  2. package/dist/css/button.css +0 -3
  3. package/dist/css/popover.css +9 -0
  4. package/dist/css/splitter.css +78 -0
  5. package/dist/css/tooltip.css +0 -3
  6. package/dist/js/floatable.cjs +22 -9
  7. package/dist/js/floatable.js +22 -9
  8. package/dist/js/focus-trap/embedded.cjs +150 -0
  9. package/dist/js/focus-trap/embedded.js +150 -0
  10. package/dist/js/focus-trap/standalone.cjs +8 -0
  11. package/dist/js/focus-trap/standalone.js +7 -0
  12. package/dist/js/index.cjs +5 -1
  13. package/dist/js/index.js +6 -1
  14. package/dist/js/popover.cjs +160 -0
  15. package/dist/js/popover.js +160 -0
  16. package/dist/js/splitter.cjs +211 -0
  17. package/dist/js/splitter.js +211 -0
  18. package/dist/js/tooltip.cjs +2 -3
  19. package/dist/js/tooltip.js +2 -3
  20. package/package.json +25 -5
  21. package/src/css/button.scss +0 -3
  22. package/src/css/popover.scss +9 -0
  23. package/src/css/splitter.scss +87 -0
  24. package/src/css/tooltip.scss +0 -3
  25. package/src/js/floatable.ts +53 -25
  26. package/src/js/focus-trap/embedded.ts +215 -0
  27. package/src/js/focus-trap/standalone.ts +9 -0
  28. package/src/js/index.ts +3 -1
  29. package/src/js/popover.ts +221 -0
  30. package/src/js/splitter.ts +317 -0
  31. package/src/js/tooltip.ts +2 -4
  32. package/types/floatable.d.cts +2 -2
  33. package/types/floatable.d.ts +2 -2
  34. package/types/focus-trap/embedded.d.cts +15 -0
  35. package/types/focus-trap/embedded.d.ts +11 -0
  36. package/types/index.d.cts +11 -0
  37. package/types/index.d.ts +3 -1
  38. package/types/popover.d.cts +14 -0
  39. package/types/popover.d.ts +15 -0
  40. package/types/splitter.d.cts +10 -0
  41. package/types/splitter.d.ts +11 -0
  42. package/dist/js/focus-trap.cjs +0 -104
  43. package/dist/js/focus-trap.js +0 -103
  44. package/src/js/focus-trap.ts +0 -133
  45. /package/types/{focus-trap.d.cts → focus-trap/standalone.d.cts} +0 -0
  46. /package/types/{focus-trap.d.ts → focus-trap/standalone.d.ts} +0 -0
@@ -0,0 +1,317 @@
1
+ import {clamp} from '@oscarpalmer/atoms/number';
2
+ import {getPosition, on} from '@oscarpalmer/toretto/event';
3
+ import {findAncestor} from '@oscarpalmer/toretto/find';
4
+
5
+ declare global {
6
+ interface HTMLElementTagNameMap {
7
+ 'oui-splitter': OuiSplitterElement;
8
+ }
9
+ }
10
+
11
+ type Flex = {
12
+ previous: number;
13
+ value: number;
14
+ };
15
+
16
+ export class OuiSplitterElement extends HTMLElement {
17
+ #splitter: Splitter;
18
+
19
+ constructor() {
20
+ super();
21
+
22
+ const panels = [...this.children].filter(
23
+ child => child instanceof HTMLElement,
24
+ );
25
+
26
+ if (panels.length !== 2) {
27
+ throw new Error('oui-splitter must have exactly two panels.');
28
+ }
29
+
30
+ this.#splitter = new Splitter(this, panels);
31
+ }
32
+
33
+ connectedCallback(): void {
34
+ if (this.#splitter.types.auto) {
35
+ observer.observe(this);
36
+ }
37
+ }
38
+
39
+ disconnectedCallback(): void {
40
+ if (this.#splitter.types.auto) {
41
+ observer.unobserve(this);
42
+ }
43
+ }
44
+ }
45
+
46
+ class Splitter {
47
+ flex: Flex = {
48
+ previous: 0.5,
49
+ value: 0.5,
50
+ };
51
+
52
+ handle: HTMLSpanElement;
53
+
54
+ rectangle: DOMRect;
55
+
56
+ separator: HTMLDivElement;
57
+
58
+ types: Types;
59
+
60
+ constructor(
61
+ public element: OuiSplitterElement,
62
+ public panels: HTMLElement[],
63
+ ) {
64
+ this.handle = createHandle();
65
+ this.rectangle = element.getBoundingClientRect();
66
+ this.separator = createSeparator();
67
+
68
+ this.separator.append(this.handle);
69
+
70
+ panels[0].insertAdjacentElement('afterend', this.separator);
71
+
72
+ mapped.set(element, this);
73
+ mapped.set(this.handle, this);
74
+
75
+ setSize(this, this.flex.value);
76
+
77
+ this.types = getTypes(this);
78
+
79
+ if (this.types.horizontal) {
80
+ this.element.setAttribute('oui-splitter-horizontal', '');
81
+ }
82
+ }
83
+ }
84
+
85
+ type Types = {
86
+ auto: boolean;
87
+ horizontal: boolean;
88
+ };
89
+
90
+ //
91
+
92
+ function createHandle(): HTMLSpanElement {
93
+ const handle = document.createElement('span');
94
+
95
+ handle.tabIndex = 0;
96
+
97
+ handle.setAttribute('aria-label', 'Resize panels');
98
+ handle.setAttribute('oui-splitter-handle', '');
99
+
100
+ return handle;
101
+ }
102
+
103
+ function createSeparator(): HTMLDivElement {
104
+ const separator = document.createElement('div');
105
+
106
+ separator.setAttribute('oui-splitter-separator', '');
107
+
108
+ return separator;
109
+ }
110
+
111
+ function getFlex(splitter: Splitter, value: number): number {
112
+ const {height, left, top, width} = splitter.rectangle;
113
+
114
+ const offset = value - (splitter.types.horizontal ? top : left);
115
+
116
+ const fraction = offset / (splitter.types.horizontal ? height : width);
117
+
118
+ return clamp(fraction, 0.1, 0.9);
119
+ }
120
+
121
+ function getTypes(splitter: Splitter, width?: number, height?: number): Types {
122
+ const type = splitter.element.getAttribute('type')?.trim() ?? '';
123
+
124
+ if (type === 'horizontal') {
125
+ return {
126
+ auto: false,
127
+ horizontal: true,
128
+ };
129
+ }
130
+
131
+ if (type === 'auto') {
132
+ return {
133
+ auto: true,
134
+ horizontal:
135
+ (height ?? splitter.rectangle.height) >
136
+ (width ?? splitter.rectangle.width),
137
+ };
138
+ }
139
+
140
+ return {
141
+ auto: false,
142
+ horizontal: false,
143
+ };
144
+ }
145
+
146
+ function onEnd(reset: boolean): void {
147
+ if (splitter != null) {
148
+ splitter.handle.removeAttribute('oui-splitter-active');
149
+ splitter.separator.removeAttribute('oui-splitter-active');
150
+
151
+ if (reset) {
152
+ setSize(splitter, splitter.flex.previous);
153
+ } else {
154
+ splitter.flex.previous = splitter.flex.value;
155
+ }
156
+ }
157
+
158
+ splitter = undefined;
159
+ }
160
+
161
+ function onKeydown(event: KeyboardEvent): void {
162
+ switch (true) {
163
+ case allKeys.has(event.key):
164
+ onNavigate(event);
165
+ break;
166
+
167
+ case event.key === 'Escape':
168
+ onEnd(true);
169
+ break;
170
+
171
+ default:
172
+ break;
173
+ }
174
+ }
175
+
176
+ function onMousedown(event: Event): void {
177
+ splitter = mapped.get(
178
+ findAncestor(
179
+ event.target as never,
180
+ '[oui-splitter-handle]',
181
+ ) as HTMLSpanElement,
182
+ );
183
+
184
+ if (splitter == null) {
185
+ return;
186
+ }
187
+
188
+ splitter.handle.setAttribute('oui-splitter-active', '');
189
+ splitter.separator.setAttribute('oui-splitter-active', '');
190
+
191
+ splitter.rectangle = splitter.element.getBoundingClientRect();
192
+ }
193
+
194
+ function onMousemove(event: MouseEvent): void {
195
+ if (splitter == null) {
196
+ return;
197
+ }
198
+
199
+ const position = getPosition(event);
200
+
201
+ if (position != null) {
202
+ splitter.flex.value = getFlex(
203
+ splitter,
204
+ splitter.types.horizontal ? position.y : position.x,
205
+ );
206
+
207
+ setSize(splitter, splitter.flex.value);
208
+ }
209
+ }
210
+
211
+ function onMouseup(): void {
212
+ if (splitter != null) {
213
+ onEnd(false);
214
+ }
215
+ }
216
+
217
+ function onNavigate(event: KeyboardEvent): void {
218
+ const splitter = mapped.get(
219
+ findAncestor(
220
+ event.target as never,
221
+ '[oui-splitter-handle]',
222
+ ) as HTMLSpanElement,
223
+ );
224
+
225
+ if (splitter == null) {
226
+ return;
227
+ }
228
+
229
+ if (absoluteKeys.has(event.key)) {
230
+ setSize(splitter, event.key === 'End' ? 0.9 : 0.1, true);
231
+
232
+ return;
233
+ }
234
+
235
+ if (
236
+ (splitter.types.horizontal && !horizontalKeys.has(event.key)) ||
237
+ (!splitter.types.horizontal && !verticalKeys.has(event.key))
238
+ ) {
239
+ return;
240
+ }
241
+
242
+ const modifier = negativeKeys.has(event.key) ? -1 : 1;
243
+
244
+ setSize(splitter, splitter.flex.value + modifier * 0.05, true);
245
+ }
246
+
247
+ function onObservation(entries: ResizeObserverEntry[]): void {
248
+ if (frame != null) {
249
+ cancelAnimationFrame(frame);
250
+ }
251
+
252
+ frame = requestAnimationFrame(() => {
253
+ for (const entry of entries) {
254
+ const splitter = mapped.get(entry.target as HTMLElement);
255
+
256
+ if (splitter == null) {
257
+ return;
258
+ }
259
+
260
+ splitter.types = getTypes(
261
+ splitter,
262
+ entry.contentRect.width,
263
+ entry.contentRect.height,
264
+ );
265
+
266
+ if (splitter.types.horizontal) {
267
+ splitter.element.setAttribute('oui-splitter-horizontal', '');
268
+ } else {
269
+ splitter.element.removeAttribute('oui-splitter-horizontal');
270
+ }
271
+ }
272
+
273
+ frame = undefined;
274
+ });
275
+ }
276
+
277
+ function setSize(splitter: Splitter, size: number, previous?: boolean): void {
278
+ splitter.flex.value = size;
279
+
280
+ if (previous) {
281
+ splitter.flex.previous = size;
282
+ }
283
+
284
+ splitter.element.style.setProperty('--oui-splitter-size', `${size}`);
285
+ }
286
+
287
+ //
288
+
289
+ const absoluteKeys = new Set(['End', 'Home']);
290
+
291
+ const horizontalKeys = new Set(['ArrowDown', 'ArrowUp']);
292
+
293
+ const mapped = new WeakMap<HTMLElement, Splitter>();
294
+
295
+ const observer = new ResizeObserver(onObservation);
296
+
297
+ const negativeKeys = new Set(['ArrowLeft', 'ArrowUp']);
298
+
299
+ const verticalKeys = new Set(['ArrowLeft', 'ArrowRight']);
300
+
301
+ const allKeys = new Set([...absoluteKeys, ...horizontalKeys, ...verticalKeys]);
302
+
303
+ let frame: DOMHighResTimeStamp | undefined;
304
+
305
+ let splitter: Splitter | undefined;
306
+
307
+ //
308
+
309
+ customElements.define('oui-splitter', OuiSplitterElement);
310
+
311
+ on(document, 'keydown', onKeydown);
312
+
313
+ on(document, 'mousedown', onMousedown);
314
+
315
+ on(document, 'mousemove', onMousemove);
316
+
317
+ on(document, 'mouseup', onMouseup);
package/src/js/tooltip.ts CHANGED
@@ -162,7 +162,7 @@ function initialize(tooltip: Tooltip): void {
162
162
  element,
163
163
  event,
164
164
  () => {
165
- activate(floatable, active, 250);
165
+ activate(floatable, active, undefined, 250);
166
166
  },
167
167
  {
168
168
  signal: abortController.signal,
@@ -175,7 +175,7 @@ function initialize(tooltip: Tooltip): void {
175
175
  element,
176
176
  event,
177
177
  () => {
178
- deactivate(floatable, active, 250);
178
+ deactivate(floatable, active, undefined, 250);
179
179
  },
180
180
  {
181
181
  signal: abortController.signal,
@@ -191,8 +191,6 @@ function removeTooltip(element: HTMLElement): void {
191
191
  }
192
192
 
193
193
  function reset(anchor: HTMLElement, attributes: Attributes): void {
194
- console.log('reset', anchor, attributes);
195
-
196
194
  if (attributes.ariaDescribedby == null) {
197
195
  anchor.removeAttribute('aria-describedby');
198
196
  } else {
@@ -10,7 +10,7 @@ export declare class Floatable {
10
10
  constructor(anchor: HTMLElement, content: HTMLElement);
11
11
  destroy(): void;
12
12
  }
13
- export declare function activate(floatable: Floatable, active: Set<Floatable>, time?: number): void;
14
- export declare function deactivate(floatable: Floatable, active: Set<Floatable>, time?: number): void;
13
+ export declare function activate(floatable: Floatable, active: Set<Floatable>, order?: Floatable[], time?: number): void;
14
+ export declare function deactivate(floatable: Floatable, active: Set<Floatable>, order?: Floatable[], time?: number): void;
15
15
 
16
16
  export {};
@@ -8,5 +8,5 @@ export declare class Floatable {
8
8
  constructor(anchor: HTMLElement, content: HTMLElement);
9
9
  destroy(): void;
10
10
  }
11
- export declare function activate(floatable: Floatable, active: Set<Floatable>, time?: number): void;
12
- export declare function deactivate(floatable: Floatable, active: Set<Floatable>, time?: number): void;
11
+ export declare function activate(floatable: Floatable, active: Set<Floatable>, order?: Floatable[], time?: number): void;
12
+ export declare function deactivate(floatable: Floatable, active: Set<Floatable>, order?: Floatable[], time?: number): void;
@@ -0,0 +1,15 @@
1
+ // Generated by dts-bundle-generator v9.5.1
2
+
3
+ export declare class FocusTrap {
4
+ element: HTMLElement;
5
+ active: boolean;
6
+ constructor(element: HTMLElement);
7
+ destroy(): void;
8
+ disable(): void;
9
+ enable(): void;
10
+ }
11
+ export declare function createFocusTrap(element: HTMLElement): FocusTrap;
12
+ export declare const focusTraps: WeakMap<HTMLElement, FocusTrap>;
13
+ export declare const selector = "oui-focus-trap";
14
+
15
+ export {};
@@ -0,0 +1,11 @@
1
+ export declare class FocusTrap {
2
+ element: HTMLElement;
3
+ active: boolean;
4
+ constructor(element: HTMLElement);
5
+ destroy(): void;
6
+ disable(): void;
7
+ enable(): void;
8
+ }
9
+ export declare function createFocusTrap(element: HTMLElement): FocusTrap;
10
+ export declare const focusTraps: WeakMap<HTMLElement, FocusTrap>;
11
+ export declare const selector = "oui-focus-trap";
package/types/index.d.cts CHANGED
@@ -1,3 +1,14 @@
1
1
  // Generated by dts-bundle-generator v9.5.1
2
2
 
3
+ export declare class OuiPopoverElement extends HTMLElement {
4
+ #private;
5
+ constructor();
6
+ close(): void;
7
+ hidePopover(): void;
8
+ open(): void;
9
+ showPopover(): void;
10
+ toggle(): void;
11
+ togglePopover(options?: boolean): boolean;
12
+ }
13
+
3
14
  export {};
package/types/index.d.ts CHANGED
@@ -1 +1,3 @@
1
- export * from './focus-trap';
1
+ export * from './focus-trap/standalone';
2
+ export * from './popover';
3
+ export * from './tooltip';
@@ -0,0 +1,14 @@
1
+ // Generated by dts-bundle-generator v9.5.1
2
+
3
+ export declare class OuiPopoverElement extends HTMLElement {
4
+ #private;
5
+ constructor();
6
+ close(): void;
7
+ hidePopover(): void;
8
+ open(): void;
9
+ showPopover(): void;
10
+ toggle(): void;
11
+ togglePopover(options?: boolean): boolean;
12
+ }
13
+
14
+ export {};
@@ -0,0 +1,15 @@
1
+ declare global {
2
+ interface HTMLElementTagNameMap {
3
+ 'oui-popover': OuiPopoverElement;
4
+ }
5
+ }
6
+ export declare class OuiPopoverElement extends HTMLElement {
7
+ #private;
8
+ constructor();
9
+ close(): void;
10
+ hidePopover(): void;
11
+ open(): void;
12
+ showPopover(): void;
13
+ toggle(): void;
14
+ togglePopover(options?: boolean): boolean;
15
+ }
@@ -0,0 +1,10 @@
1
+ // Generated by dts-bundle-generator v9.5.1
2
+
3
+ export declare class OuiSplitterElement extends HTMLElement {
4
+ #private;
5
+ constructor();
6
+ connectedCallback(): void;
7
+ disconnectedCallback(): void;
8
+ }
9
+
10
+ export {};
@@ -0,0 +1,11 @@
1
+ declare global {
2
+ interface HTMLElementTagNameMap {
3
+ 'oui-splitter': OuiSplitterElement;
4
+ }
5
+ }
6
+ export declare class OuiSplitterElement extends HTMLElement {
7
+ #private;
8
+ constructor();
9
+ connectedCallback(): void;
10
+ disconnectedCallback(): void;
11
+ }
@@ -1,104 +0,0 @@
1
- "use strict";
2
- const number = require("@oscarpalmer/atoms/number");
3
- const event = require("@oscarpalmer/toretto/event");
4
- const find = require("@oscarpalmer/toretto/find");
5
- const focusable = require("@oscarpalmer/toretto/focusable");
6
- const attributable = require("./attributable.cjs");
7
- class FocusTrap {
8
- constructor(element) {
9
- this.element = element;
10
- element.setAttribute("aria-modal", "true");
11
- element.setAttribute("tabindex", "-1");
12
- }
13
- active = true;
14
- activate() {
15
- this.element.setAttribute("aria-modal", "true");
16
- this.active = true;
17
- }
18
- destroy() {
19
- this.active = false;
20
- this.element = void 0;
21
- }
22
- inactivate() {
23
- this.element.setAttribute("aria-modal", "false");
24
- this.active = false;
25
- inactive.add(this);
26
- }
27
- }
28
- function addFocusTrap(element) {
29
- if (!focusTraps.has(element)) {
30
- focusTraps.set(element, new FocusTrap(element));
31
- }
32
- }
33
- function onEscape(event2) {
34
- if (event2.key !== "Escape") {
35
- return;
36
- }
37
- const element = find.findAncestor(
38
- document.activeElement,
39
- `[${selector}]`
40
- );
41
- focusTraps.get(element)?.inactivate();
42
- }
43
- function onFocusOut() {
44
- if (inactive.size === 0) {
45
- return;
46
- }
47
- requestAnimationFrame(() => {
48
- const element = find.findAncestor(
49
- document.activeElement,
50
- `[${selector}]`
51
- );
52
- if (!focusTraps.has(element)) {
53
- for (const focusTrap of inactive) {
54
- focusTrap.activate();
55
- }
56
- }
57
- });
58
- }
59
- function onTab(event2) {
60
- if (event2.key !== "Tab") {
61
- return;
62
- }
63
- const element = find.findAncestor(
64
- event2.target,
65
- `[${selector}]`
66
- );
67
- if (!(focusTraps.get(element)?.active ?? false)) {
68
- return;
69
- }
70
- event2.preventDefault();
71
- const elements = focusable.getFocusable(element);
72
- const { length } = elements;
73
- const index = elements.indexOf(event2.target);
74
- if (index === -1 || length === 0) {
75
- if (length === 0) {
76
- element.focus();
77
- } else {
78
- elements[0].focus();
79
- }
80
- return;
81
- }
82
- const next = number.clamp(index + (event2.shiftKey ? -1 : 1), 0, length - 1, true);
83
- if (next !== index) {
84
- elements[next].focus();
85
- }
86
- }
87
- function removeFocusTraps(element) {
88
- focusTraps.get(element)?.destroy();
89
- focusTraps.delete(element);
90
- }
91
- const focusTraps = /* @__PURE__ */ new WeakMap();
92
- const inactive = /* @__PURE__ */ new Set();
93
- const selector = "oui-focus-trap";
94
- attributable.attributable(selector, addFocusTrap, removeFocusTraps);
95
- event.on(document, "focusout", onFocusOut, {
96
- capture: true
97
- });
98
- event.on(document, "keydown", onTab, {
99
- capture: true,
100
- passive: false
101
- });
102
- event.on(document, "keydown", onEscape, {
103
- passive: false
104
- });
@@ -1,103 +0,0 @@
1
- import { clamp } from "@oscarpalmer/atoms/number";
2
- import { on } from "@oscarpalmer/toretto/event";
3
- import { findAncestor } from "@oscarpalmer/toretto/find";
4
- import { getFocusable } from "@oscarpalmer/toretto/focusable";
5
- import { attributable } from "./attributable.js";
6
- class FocusTrap {
7
- constructor(element) {
8
- this.element = element;
9
- element.setAttribute("aria-modal", "true");
10
- element.setAttribute("tabindex", "-1");
11
- }
12
- active = true;
13
- activate() {
14
- this.element.setAttribute("aria-modal", "true");
15
- this.active = true;
16
- }
17
- destroy() {
18
- this.active = false;
19
- this.element = void 0;
20
- }
21
- inactivate() {
22
- this.element.setAttribute("aria-modal", "false");
23
- this.active = false;
24
- inactive.add(this);
25
- }
26
- }
27
- function addFocusTrap(element) {
28
- if (!focusTraps.has(element)) {
29
- focusTraps.set(element, new FocusTrap(element));
30
- }
31
- }
32
- function onEscape(event) {
33
- if (event.key !== "Escape") {
34
- return;
35
- }
36
- const element = findAncestor(
37
- document.activeElement,
38
- `[${selector}]`
39
- );
40
- focusTraps.get(element)?.inactivate();
41
- }
42
- function onFocusOut() {
43
- if (inactive.size === 0) {
44
- return;
45
- }
46
- requestAnimationFrame(() => {
47
- const element = findAncestor(
48
- document.activeElement,
49
- `[${selector}]`
50
- );
51
- if (!focusTraps.has(element)) {
52
- for (const focusTrap of inactive) {
53
- focusTrap.activate();
54
- }
55
- }
56
- });
57
- }
58
- function onTab(event) {
59
- if (event.key !== "Tab") {
60
- return;
61
- }
62
- const element = findAncestor(
63
- event.target,
64
- `[${selector}]`
65
- );
66
- if (!(focusTraps.get(element)?.active ?? false)) {
67
- return;
68
- }
69
- event.preventDefault();
70
- const elements = getFocusable(element);
71
- const { length } = elements;
72
- const index = elements.indexOf(event.target);
73
- if (index === -1 || length === 0) {
74
- if (length === 0) {
75
- element.focus();
76
- } else {
77
- elements[0].focus();
78
- }
79
- return;
80
- }
81
- const next = clamp(index + (event.shiftKey ? -1 : 1), 0, length - 1, true);
82
- if (next !== index) {
83
- elements[next].focus();
84
- }
85
- }
86
- function removeFocusTraps(element) {
87
- focusTraps.get(element)?.destroy();
88
- focusTraps.delete(element);
89
- }
90
- const focusTraps = /* @__PURE__ */ new WeakMap();
91
- const inactive = /* @__PURE__ */ new Set();
92
- const selector = "oui-focus-trap";
93
- attributable(selector, addFocusTrap, removeFocusTraps);
94
- on(document, "focusout", onFocusOut, {
95
- capture: true
96
- });
97
- on(document, "keydown", onTab, {
98
- capture: true,
99
- passive: false
100
- });
101
- on(document, "keydown", onEscape, {
102
- passive: false
103
- });