@oscarpalmer/oui 0.19.0 → 0.20.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 (50) hide show
  1. package/dist/css/a11y.css +0 -1
  2. package/dist/css/form.css +0 -1
  3. package/dist/css/popover.css +22 -0
  4. package/dist/css/reset.css +8 -8
  5. package/dist/css/table.css +1 -1
  6. package/dist/css/tooltip.css +43 -0
  7. package/dist/js/attributable.d.mts +4 -0
  8. package/dist/js/{attributable.full.js → attributable.mjs} +8 -1
  9. package/dist/js/floatable.d.mts +26 -0
  10. package/dist/js/{floatable.js → floatable.mjs} +73 -50
  11. package/dist/js/focus-trap/embedded.d.mts +26 -0
  12. package/dist/js/focus-trap/{embedded.js → embedded.mjs} +10 -8
  13. package/dist/js/focus-trap/standalone.d.mts +1 -0
  14. package/dist/js/focus-trap/{standalone.js → standalone.mjs} +5 -2
  15. package/dist/js/index.d.mts +4 -0
  16. package/dist/js/index.mjs +5 -0
  17. package/dist/js/popover.d.mts +20 -0
  18. package/dist/js/{popover.js → popover.mjs} +10 -8
  19. package/dist/js/splitter.d.mts +25 -0
  20. package/dist/js/{splitter.js → splitter.mjs} +58 -56
  21. package/dist/js/tooltip.d.mts +4 -0
  22. package/dist/js/{tooltip.js → tooltip.mjs} +20 -18
  23. package/package.json +64 -69
  24. package/src/css/mixins.scss +30 -1
  25. package/src/css/popover.scss +6 -1
  26. package/src/css/reset.scss +4 -4
  27. package/src/css/table.scss +1 -1
  28. package/src/css/tooltip.scss +55 -1
  29. package/src/js/attributable.ts +11 -1
  30. package/src/js/floatable.ts +136 -58
  31. package/src/js/focus-trap/embedded.ts +4 -10
  32. package/src/js/splitter.ts +2 -5
  33. package/src/js/tooltip.ts +5 -5
  34. package/dist/js/attributable.js +0 -36
  35. package/dist/js/floatable.full.js +0 -398
  36. package/dist/js/focus-trap/embedded.full.js +0 -408
  37. package/dist/js/focus-trap/standalone.full.js +0 -447
  38. package/dist/js/index.js +0 -5
  39. package/dist/js/oui.full.js +0 -1573
  40. package/dist/js/popover.full.js +0 -760
  41. package/dist/js/splitter.full.js +0 -769
  42. package/dist/js/tooltip.full.js +0 -613
  43. package/types/attributable.d.ts +0 -1
  44. package/types/floatable.d.ts +0 -24
  45. package/types/focus-trap/embedded.d.ts +0 -24
  46. package/types/focus-trap/standalone.d.ts +0 -1
  47. package/types/index.d.ts +0 -4
  48. package/types/popover.d.ts +0 -17
  49. package/types/splitter.d.ts +0 -23
  50. package/types/tooltip.d.ts +0 -2
@@ -91,7 +91,6 @@ export class Floatable {
91
91
  }
92
92
 
93
93
  type FloatableWindow = Window & {
94
- // biome-ignore lint/style/useNamingConvention: Non-standard name to avoid conflicts
95
94
  _oscarpalmer_oui_floatable: State;
96
95
  };
97
96
 
@@ -193,78 +192,157 @@ export function deactivate(floatable: Floatable): void {
193
192
  floatable.options.onAfter?.(false);
194
193
  }
195
194
 
195
+ function getAttribute(
196
+ position: Position,
197
+ anchor: DOMRect,
198
+ top: number,
199
+ left: number,
200
+ ): Position | undefined {
201
+ switch (position) {
202
+ case 'horizontal':
203
+ case 'horizontal-bottom':
204
+ case 'horizontal-top':
205
+ return position.replace('horizontal', left >= anchor.right ? 'right' : 'left') as Position;
206
+
207
+ case 'vertical':
208
+ case 'vertical-end':
209
+ case 'vertical-start':
210
+ return position.replace('vertical', top >= anchor.bottom ? 'bottom' : 'top') as Position;
211
+
212
+ default:
213
+ break;
214
+ }
215
+ }
216
+
217
+ function getMargin(): number {
218
+ try {
219
+ const fontsize = parseFloat(getComputedStyle(document.documentElement).fontSize);
220
+
221
+ if (Number.isNaN(fontsize)) {
222
+ return 5;
223
+ }
224
+
225
+ const margin = fontsize / 2;
226
+
227
+ return margin < 5 ? 5 : margin;
228
+ } catch {
229
+ return 5;
230
+ }
231
+ }
232
+
196
233
  function getState(): State {
197
234
  return (window as unknown as FloatableWindow)._oscarpalmer_oui_floatable;
198
235
  }
199
236
 
200
- function getX(anchor: DOMRect, content: DOMRect, position: Position): number {
201
- if (position.endsWith(POSITION_SUFFIX_END)) {
202
- const end = anchor.right - content.width;
237
+ function getX(anchor: DOMRect, content: DOMRect, position: Position, margin: number): number {
238
+ if (position === 'above' || position === 'below' || position === 'vertical') {
239
+ const center = anchor.left + anchor.width / 2;
240
+ const width = content.width / 2;
203
241
 
204
- return end < 0 ? 0 : end;
205
- }
242
+ if (center + width + margin >= document.body.clientWidth) {
243
+ return document.body.clientWidth - content.width - margin / 2;
244
+ }
206
245
 
207
- if (position.endsWith(POSITION_SUFFIX_START)) {
208
- const start = anchor.left;
246
+ if (center - width - margin <= 0) {
247
+ return margin / 2;
248
+ }
209
249
 
210
- return start + content.width > window.innerWidth ? window.innerWidth - content.width : start;
250
+ return center - width;
211
251
  }
212
252
 
213
- if (position.startsWith(POSITION_END)) {
253
+ if (position.startsWith('end')) {
214
254
  return anchor.right;
215
255
  }
216
256
 
217
- if (position.startsWith(POSITION_START)) {
257
+ if (position.startsWith('start')) {
218
258
  return anchor.left - content.width;
219
259
  }
220
260
 
221
- if (position.startsWith(POSITION_HORIZONTAL)) {
222
- if (anchor.right + content.width <= window.innerWidth) {
223
- return anchor.right;
261
+ if (position.endsWith('end')) {
262
+ return anchor.right - content.width - margin <= 0 ? margin / 2 : anchor.right - content.width;
263
+ }
264
+
265
+ if (position.endsWith('start')) {
266
+ if (anchor.left + content.width + margin >= document.body.clientWidth) {
267
+ return document.body.clientWidth - content.width - margin / 2;
224
268
  }
225
269
 
226
- return anchor.left - content.width;
270
+ return anchor.left - margin <= 0 ? margin / 2 : anchor.left;
227
271
  }
228
272
 
229
- const left = anchor.left + (anchor.width - content.width) / 2;
273
+ if (!position.startsWith('horizontal')) {
274
+ return 0;
275
+ }
276
+
277
+ if (anchor.right + content.width + margin >= document.body.clientWidth) {
278
+ return anchor.left - content.width;
279
+ }
230
280
 
231
- return left < 0 ? 0 : left;
281
+ return anchor.right;
232
282
  }
233
283
 
234
- function getY(anchor: DOMRect, content: DOMRect, position: Position, preferAbove: boolean): number {
235
- if (position.startsWith(POSITION_ABOVE)) {
284
+ function getY(
285
+ anchor: DOMRect,
286
+ content: DOMRect,
287
+ position: Position,
288
+ margin: number,
289
+ preferAbove: boolean,
290
+ ): number {
291
+ if (position === 'end' || position === 'start' || position === 'horizontal') {
292
+ const center = anchor.top + anchor.height / 2;
293
+ const height = content.height / 2;
294
+
295
+ if (center + height + margin >= document.body.clientHeight) {
296
+ return document.body.clientHeight - content.height - margin / 2;
297
+ }
298
+
299
+ if (center - height - margin <= 0) {
300
+ return margin / 2;
301
+ }
302
+
303
+ return center - height;
304
+ }
305
+
306
+ if (position.startsWith('above')) {
236
307
  return anchor.top - content.height;
237
308
  }
238
309
 
239
- if (position.startsWith(POSITION_BELOW)) {
310
+ if (position.startsWith('below')) {
240
311
  return anchor.bottom;
241
312
  }
242
313
 
243
- if (position.endsWith(POSITION_BOTTOM)) {
244
- const bottom = anchor.bottom - content.height;
245
-
246
- return bottom < 0 ? 0 : bottom;
314
+ if (position.endsWith('bottom')) {
315
+ return anchor.bottom - content.height - margin <= 0
316
+ ? margin / 2
317
+ : anchor.bottom - content.height;
247
318
  }
248
319
 
249
- if (position.endsWith(POSITION_TOP)) {
250
- const top = anchor.top;
251
-
252
- return top + content.height > window.innerHeight ? window.innerHeight - content.height : top;
320
+ if (position.endsWith('top')) {
321
+ return anchor.top + content.height + margin >= document.body.clientHeight
322
+ ? document.body.clientHeight - content.height - margin
323
+ : anchor.top;
253
324
  }
254
325
 
255
- if (position.startsWith(POSITION_VERTICAL)) {
256
- if (preferAbove && anchor.top - content.height >= 0) {
257
- return anchor.top - content.height;
258
- }
326
+ if (!position.startsWith('vertical')) {
327
+ return 0;
328
+ }
259
329
 
260
- if (anchor.bottom + content.height <= document.body.clientHeight) {
330
+ if (preferAbove) {
331
+ if (
332
+ anchor.top - content.height - margin <= 0 &&
333
+ anchor.bottom + content.height + margin <= document.body.clientHeight
334
+ ) {
261
335
  return anchor.bottom;
262
336
  }
263
337
 
264
338
  return anchor.top - content.height;
265
339
  }
266
340
 
267
- return anchor.top + (anchor.height - content.height) / 2;
341
+ if (anchor.bottom + content.height + margin >= document.body.clientHeight) {
342
+ return anchor.top - content.height;
343
+ }
344
+
345
+ return anchor.bottom;
268
346
  }
269
347
 
270
348
  function initialize(floatable: Floatable): void {
@@ -294,7 +372,7 @@ function isDisabled(element: HTMLElement): boolean {
294
372
  }
295
373
 
296
374
  function onClick(event: PointerEvent): void {
297
- const related = findAncestor(event.target as never, SELECTOR_ALL);
375
+ const related = findAncestor(event, SELECTOR_ALL);
298
376
 
299
377
  if (!(related instanceof HTMLElement)) {
300
378
  closeAll();
@@ -318,7 +396,7 @@ function onKeyDown(event: KeyboardEvent): void {
318
396
 
319
397
  closeNonInteractive(state);
320
398
 
321
- const related = findAncestor(event.target as never, SELECTOR_CONTENT);
399
+ const related = findAncestor(event, SELECTOR_CONTENT);
322
400
 
323
401
  if (!(related instanceof HTMLElement)) {
324
402
  return;
@@ -333,6 +411,22 @@ function onKeyDown(event: KeyboardEvent): void {
333
411
  }
334
412
  }
335
413
 
414
+ function setAttribute(
415
+ position: Position,
416
+ anchor: DOMRect,
417
+ floater: HTMLElement,
418
+ top: number,
419
+ left: number,
420
+ ): void {
421
+ let actual: Position | undefined;
422
+
423
+ if (/^(horizontal|vertical)/.test(position)) {
424
+ actual = getAttribute(position, anchor, top, left);
425
+ }
426
+
427
+ floater.setAttribute('position', actual ?? position);
428
+ }
429
+
336
430
  function stop(floatable: Floatable): void {
337
431
  if (floatable.frame != null) {
338
432
  cancelAnimationFrame(floatable.frame);
@@ -372,6 +466,8 @@ function updatePosition(floatable: Floatable): void {
372
466
  position = floatable.options.defaultPosition;
373
467
  }
374
468
 
469
+ const margin = getMargin();
470
+
375
471
  let previousAnchor: DOMRect;
376
472
  let previousContent: DOMRect;
377
473
 
@@ -404,8 +500,10 @@ function updatePosition(floatable: Floatable): void {
404
500
  previousAnchor = anchor;
405
501
  previousContent = content;
406
502
 
407
- const left = getX(anchor, content, position);
408
- const top = getY(anchor, content, position, floatable.options.preferAbove);
503
+ const left = getX(anchor, content, position, margin);
504
+ const top = getY(anchor, content, position, margin, floatable.options.preferAbove);
505
+
506
+ setAttribute(position, anchor, floatable.options.content, top, left);
409
507
 
410
508
  if (top + content.height > window.innerHeight) {
411
509
  floatable.options.content.style.height = `${window.innerHeight - top}px`;
@@ -442,26 +540,6 @@ const ATTRIBUTE_CONTENT = 'oui-floatable-content';
442
540
 
443
541
  const INSERT_AFTEREND = 'afterend';
444
542
 
445
- const POSITION_ABOVE = 'above';
446
-
447
- const POSITION_BELOW = 'below';
448
-
449
- const POSITION_BOTTOM = 'bottom';
450
-
451
- const POSITION_END = 'end';
452
-
453
- const POSITION_HORIZONTAL = 'horizontal';
454
-
455
- const POSITION_START = 'start';
456
-
457
- const POSITION_SUFFIX_END = '-end';
458
-
459
- const POSITION_SUFFIX_START = '-start';
460
-
461
- const POSITION_TOP = 'top';
462
-
463
- const POSITION_VERTICAL = 'vertical';
464
-
465
543
  const POSITIONS: Set<Position> = new Set([
466
544
  'above',
467
545
  'above-end',
@@ -83,7 +83,7 @@ export function createFocusTrap(element: HTMLElement, options?: Options): FocusT
83
83
  }
84
84
 
85
85
  function onEscape(event: KeyboardEvent): void {
86
- let element = findAncestor(event.target as HTMLElement, ATTRIBUTE_SELECTOR) as HTMLElement;
86
+ let element = findAncestor(event, ATTRIBUTE_SELECTOR) as HTMLElement;
87
87
 
88
88
  let focusTrap = FOCUSTRAPS_ALL.get(element);
89
89
 
@@ -112,9 +112,7 @@ function onEscape(event: KeyboardEvent): void {
112
112
  }
113
113
 
114
114
  function onFocusIn(event: Event): void {
115
- const focusTrap = FOCUSTRAPS_ALL.get(
116
- findAncestor(event.target as HTMLElement, ATTRIBUTE_SELECTOR) as HTMLElement,
117
- );
115
+ const focusTrap = FOCUSTRAPS_ALL.get(findAncestor(event, ATTRIBUTE_SELECTOR) as HTMLElement);
118
116
 
119
117
  lastTarget = focusTrap == null ? undefined : (event.target as HTMLElement);
120
118
  }
@@ -136,11 +134,7 @@ function onKeydown(event: KeyboardEvent): void {
136
134
 
137
135
  function onPointerdown(event: MouseEvent | TouchEvent): void {
138
136
  const lastFocusTrap = findAncestor(lastTarget as HTMLElement, ATTRIBUTE_SELECTOR) as HTMLElement;
139
-
140
- const nextFocusTrap = findAncestor(
141
- event.target as HTMLElement,
142
- ATTRIBUTE_SELECTOR,
143
- ) as HTMLElement;
137
+ const nextFocusTrap = findAncestor(event, ATTRIBUTE_SELECTOR) as HTMLElement;
144
138
 
145
139
  if (lastFocusTrap != null && nextFocusTrap !== lastFocusTrap) {
146
140
  if (FOCUSTRAPS_ALL.get(lastFocusTrap)?.contain ?? false) {
@@ -162,7 +156,7 @@ function onPointerdown(event: MouseEvent | TouchEvent): void {
162
156
  }
163
157
 
164
158
  function onTab(event: KeyboardEvent): void {
165
- const element = findAncestor(event.target as HTMLElement, ATTRIBUTE_SELECTOR) as HTMLElement;
159
+ const element = findAncestor(event, ATTRIBUTE_SELECTOR) as HTMLElement;
166
160
 
167
161
  const focusTrap = FOCUSTRAPS_ALL.get(element);
168
162
 
@@ -7,7 +7,6 @@ import {type StyleToggler, toggleStyles} from '@oscarpalmer/toretto/style';
7
7
  import supportsTouch from '@oscarpalmer/toretto/touch';
8
8
 
9
9
  declare global {
10
- // biome-ignore lint/nursery/useConsistentTypeDefinitions: Extending builtins to allow custom element type help
11
10
  interface HTMLElementTagNameMap {
12
11
  [SELECTOR]: OuiSplitterElement;
13
12
  }
@@ -324,7 +323,7 @@ function onKeydown(event: KeyboardEvent): void {
324
323
 
325
324
  function onNavigate(event: KeyboardEvent): void {
326
325
  const splitter = MAPPED_ELEMENTS.get(
327
- findAncestor(event.target as never, '[oui-splitter-separator]') as HTMLSpanElement,
326
+ findAncestor(event, '[oui-splitter-separator]') as HTMLSpanElement,
328
327
  );
329
328
 
330
329
  if (splitter == null) {
@@ -391,9 +390,7 @@ function onPointerdown(event: Event): void {
391
390
  return;
392
391
  }
393
392
 
394
- splitter = MAPPED_ELEMENTS.get(
395
- findAncestor(event.target as never, SELECTOR_HANDLE) as HTMLSpanElement,
396
- );
393
+ splitter = MAPPED_ELEMENTS.get(findAncestor(event, SELECTOR_HANDLE) as HTMLSpanElement);
397
394
 
398
395
  if (splitter == null) {
399
396
  return;
package/src/js/tooltip.ts CHANGED
@@ -79,12 +79,12 @@ function closeTooltips(next: Tooltip | undefined): void {
79
79
  clearTimeout(active.timer);
80
80
  }
81
81
 
82
- active.floatable.options.content.removeAttribute('oui-tooltip-content-active');
82
+ active.floatable?.options.content.removeAttribute('oui-tooltip-content-active');
83
83
 
84
84
  requestAnimationFrame(() => {
85
85
  active.timer = setTimeout(() => {
86
86
  TOOLTIPS_ACTIVE.delete(active);
87
- active.floatable.toggle(false);
87
+ active.floatable?.toggle(false);
88
88
  }, delay) as never;
89
89
  });
90
90
  }
@@ -192,7 +192,7 @@ function onActivate(event: Event): void {
192
192
  }
193
193
 
194
194
  function onClick(event: Event): void {
195
- if (findAncestor(event.target as HTMLElement, SELECTOR_CONTENT) != null) {
195
+ if (findAncestor(event, SELECTOR_CONTENT) != null) {
196
196
  event.stopPropagation();
197
197
  }
198
198
  }
@@ -209,7 +209,7 @@ function onToggle(event: Event, activate: boolean): void {
209
209
  toggle = requestAnimationFrame(() => {
210
210
  toggle = undefined;
211
211
 
212
- const element = findAncestor(event.target as HTMLElement, SELECTOR_FULL) as HTMLElement;
212
+ const element = findAncestor(event, SELECTOR_FULL) as HTMLElement;
213
213
 
214
214
  const tooltip = TOOLTIPS_ALL.get(element);
215
215
 
@@ -286,7 +286,7 @@ const TOOLTIPS_ACTIVE: Set<Tooltip> = new Set();
286
286
 
287
287
  const TOOLTIPS_ALL: WeakMap<HTMLElement, Tooltip> = new WeakMap();
288
288
 
289
- let delay = 250;
289
+ let delay = 500;
290
290
 
291
291
  let index = 0;
292
292
 
@@ -1,36 +0,0 @@
1
- function attributable(attribute, add, remove) {
2
- new MutationObserver((entries) => {
3
- const { length } = entries;
4
- for (let index = 0; index < length; index += 1) {
5
- const entry = entries[index];
6
- if (entry.type === "attributes" && entry.target instanceof HTMLElement) if (entry.target.hasAttribute(attribute)) handleAdded(attribute, [entry.target], add);
7
- else handleRemoved([entry.target], remove);
8
- else if (entry.type === "childList") {
9
- handleAdded(attribute, entry.addedNodes, add);
10
- handleRemoved(entry.removedNodes, remove);
11
- }
12
- }
13
- }).observe(document, {
14
- attributeFilter: [attribute],
15
- attributes: true,
16
- childList: true,
17
- subtree: true
18
- });
19
- requestAnimationFrame(() => {
20
- handleAdded(attribute, document.querySelectorAll(`[${attribute}]`), add);
21
- });
22
- }
23
- function handleAdded(attribute, nodes, add) {
24
- const elements = [...nodes].filter((node) => node instanceof HTMLElement);
25
- const { length } = elements;
26
- for (let index = 0; index < length; index += 1) {
27
- const element = elements[index];
28
- if (element.hasAttribute(attribute)) add(element);
29
- }
30
- }
31
- function handleRemoved(nodes, remove) {
32
- const elements = [...nodes].filter((node) => node instanceof HTMLElement);
33
- const { length } = elements;
34
- for (let index = 0; index < length; index += 1) remove(elements[index]);
35
- }
36
- export { attributable };