@oscarpalmer/oui 0.21.0 → 0.22.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.
@@ -37,8 +37,8 @@
37
37
  background-color: var(--oui-absolute);
38
38
  border-radius: 0.25rem;
39
39
  font-size: 0.875rem;
40
+ pointer-events: none;
40
41
  transition: opacity 0.125s ease-in-out;
41
- transition-delay: 0s;
42
42
 
43
43
  &::after {
44
44
  content: '';
@@ -46,49 +46,49 @@
46
46
  position: absolute;
47
47
  }
48
48
 
49
- &[position^='above'],
50
- &[position^='below'],
51
- &[position^='bottom'],
52
- &[position^='top'] {
49
+ &[oui-position^='above'],
50
+ &[oui-position^='below'],
51
+ &[oui-position^='bottom'],
52
+ &[oui-position^='top'] {
53
53
  &::after {
54
54
  width: 100%;
55
- height: 0.5rem;
55
+ height: 0.5em;
56
56
  }
57
57
  }
58
58
 
59
- &[position^='above'],
60
- &[position^='top'] {
59
+ &[oui-position^='above'],
60
+ &[oui-position^='top'] {
61
61
  &::after {
62
62
  inset: 100% 0 0;
63
63
  }
64
64
  }
65
65
 
66
- &[position^='below'],
67
- &[position^='bottom'] {
66
+ &[oui-position^='below'],
67
+ &[oui-position^='bottom'] {
68
68
  &::after {
69
- inset: -5rem 0 0;
69
+ inset: -0.5em 0 0;
70
70
  }
71
71
  }
72
72
 
73
- &[position^='end'],
74
- &[position^='left'],
75
- &[position^='right'],
76
- &[position^='start'] {
73
+ &[oui-position^='end'],
74
+ &[oui-position^='left'],
75
+ &[oui-position^='right'],
76
+ &[oui-position^='start'] {
77
77
  &::after {
78
- width: 0.5rem;
78
+ width: 0.5em;
79
79
  height: 100%;
80
80
  }
81
81
  }
82
82
 
83
- &[position^='end'],
84
- &[position^='right'] {
83
+ &[oui-position^='end'],
84
+ &[oui-position^='right'] {
85
85
  &::after {
86
- inset: 0 0 0 -0.5rem;
86
+ inset: 0 0 0 -0.5em;
87
87
  }
88
88
  }
89
89
 
90
- &[position^='left'],
91
- &[position^='start'] {
90
+ &[oui-position^='left'],
91
+ &[oui-position^='start'] {
92
92
  &::after {
93
93
  inset: 0 0 0 100%;
94
94
  }
@@ -97,5 +97,5 @@
97
97
 
98
98
  :where([oui-tooltip-content][oui-tooltip-content-active]) {
99
99
  opacity: 1;
100
- transition-delay: 0.25s;
100
+ pointer-events: auto;
101
101
  }
@@ -100,7 +100,7 @@ type Options = {
100
100
  defaultPosition: Position;
101
101
  interactive: boolean;
102
102
  positionAttribute: string;
103
- preferAbove: boolean;
103
+ reusable: boolean;
104
104
  onAfter?: (active: boolean) => void;
105
105
  };
106
106
 
@@ -114,15 +114,9 @@ type Position =
114
114
  | 'end'
115
115
  | 'end-bottom'
116
116
  | 'end-top'
117
- | 'horizontal'
118
- | 'horizontal-bottom'
119
- | 'horizontal-top'
120
117
  | 'start'
121
118
  | 'start-bottom'
122
- | 'start-top'
123
- | 'vertical'
124
- | 'vertical-end'
125
- | 'vertical-start';
119
+ | 'start-top';
126
120
 
127
121
  type State = {
128
122
  active: Set<Floatable>;
@@ -139,7 +133,7 @@ function activate(floatable: Floatable): void {
139
133
 
140
134
  floatable.options.content.hidden = false;
141
135
 
142
- updatePosition(floatable);
136
+ setPosition(floatable);
143
137
  }
144
138
 
145
139
  function closeAbove(element: HTMLElement): void {
@@ -185,166 +179,25 @@ function closeNonInteractive(state: State): void {
185
179
  export function deactivate(floatable: Floatable): void {
186
180
  floatable.active = false;
187
181
 
188
- floatable.options.content.hidden = true;
182
+ if (!floatable.options.reusable) {
183
+ floatable.options.content.hidden = true;
189
184
 
190
- floatable.options.anchor.insertAdjacentElement(INSERT_AFTEREND, floatable.options.content);
185
+ floatable.options.anchor.insertAdjacentElement(INSERT_AFTEREND, floatable.options.content);
186
+ }
191
187
 
192
188
  floatable.options.onAfter?.(false);
193
189
  }
194
190
 
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;
191
+ function getAnchorName(): string {
192
+ index += 1;
226
193
 
227
- return margin < 5 ? 5 : margin;
228
- } catch {
229
- return 5;
230
- }
194
+ return `--oui-floatable-anchor-${index}`;
231
195
  }
232
196
 
233
197
  function getState(): State {
234
198
  return (window as unknown as FloatableWindow)._oscarpalmer_oui_floatable;
235
199
  }
236
200
 
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;
241
-
242
- if (center + width + margin >= document.body.clientWidth) {
243
- return document.body.clientWidth - content.width - margin / 2;
244
- }
245
-
246
- if (center - width - margin <= 0) {
247
- return margin / 2;
248
- }
249
-
250
- return center - width;
251
- }
252
-
253
- if (position.startsWith('end')) {
254
- return anchor.right;
255
- }
256
-
257
- if (position.startsWith('start')) {
258
- return anchor.left - content.width;
259
- }
260
-
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;
268
- }
269
-
270
- return anchor.left - margin <= 0 ? margin / 2 : anchor.left;
271
- }
272
-
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
- }
280
-
281
- return anchor.right;
282
- }
283
-
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')) {
307
- return anchor.top - content.height;
308
- }
309
-
310
- if (position.startsWith('below')) {
311
- return anchor.bottom;
312
- }
313
-
314
- if (position.endsWith('bottom')) {
315
- return anchor.bottom - content.height - margin <= 0
316
- ? margin / 2
317
- : anchor.bottom - content.height;
318
- }
319
-
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;
324
- }
325
-
326
- if (!position.startsWith('vertical')) {
327
- return 0;
328
- }
329
-
330
- if (preferAbove) {
331
- if (
332
- anchor.top - content.height - margin <= 0 &&
333
- anchor.bottom + content.height + margin <= document.body.clientHeight
334
- ) {
335
- return anchor.bottom;
336
- }
337
-
338
- return anchor.top - content.height;
339
- }
340
-
341
- if (anchor.bottom + content.height + margin >= document.body.clientHeight) {
342
- return anchor.top - content.height;
343
- }
344
-
345
- return anchor.bottom;
346
- }
347
-
348
201
  function initialize(floatable: Floatable): void {
349
202
  const {anchor, content, interactive} = floatable.options;
350
203
 
@@ -353,9 +206,13 @@ function initialize(floatable: Floatable): void {
353
206
  content.setAttribute(ATTRIBUTE_CONTENT, '');
354
207
  }
355
208
 
356
- content.style.position = 'fixed';
357
- content.style.inset = '0 auto auto 0';
358
- content.style.zIndex = '10';
209
+ const anchorName = getAnchorName();
210
+
211
+ anchor.style.anchorName = anchorName;
212
+
213
+ content.style.position = 'absolute';
214
+ content.style.positionAnchor = anchorName;
215
+ content.style.positionTry = 'normal flip-start';
359
216
 
360
217
  const state = getState();
361
218
 
@@ -411,20 +268,22 @@ function onKeyDown(event: KeyboardEvent): void {
411
268
  }
412
269
  }
413
270
 
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);
271
+ function setPosition(floatable: Floatable): void {
272
+ let position: Position =
273
+ (floatable.options.anchor.getAttribute(floatable.options.positionAttribute) as Position) ??
274
+ floatable.options.defaultPosition;
275
+
276
+ if (AREAS[position] == null) {
277
+ position = floatable.options.defaultPosition;
425
278
  }
426
279
 
427
- floater.setAttribute('position', actual ?? position);
280
+ const area = AREAS[position];
281
+
282
+ floatable.options.content.style.positionArea = area;
283
+
284
+ floatable.options.content.setAttribute(ATTRIBUTE_POSITION, POSITIONS[area]);
285
+
286
+ floatable.options.onAfter?.(true);
428
287
  }
429
288
 
430
289
  function stop(floatable: Floatable): void {
@@ -456,117 +315,41 @@ function toggle(anchor: HTMLElement): void {
456
315
  floatable.toggle(!active);
457
316
  }
458
317
 
459
- function updatePosition(floatable: Floatable): void {
460
- let position: Position =
461
- (floatable.options.anchor
462
- .getAttribute(floatable.options.positionAttribute)
463
- ?.trim() as Position) ?? floatable.options.defaultPosition;
464
-
465
- if (!POSITIONS.has(position as Position)) {
466
- position = floatable.options.defaultPosition;
467
- }
468
-
469
- const margin = getMargin();
470
-
471
- let previousAnchor: DOMRect;
472
- let previousContent: DOMRect;
473
-
474
- let calculated = false;
475
- let runOnAfter = false;
476
-
477
- function run(): void {
478
- const anchor = floatable.options.anchor.getBoundingClientRect();
479
- const content = floatable.options.content.getBoundingClientRect();
480
-
481
- if (calculated) {
482
- if (previousAnchor != null && previousContent != null) {
483
- if (
484
- PROPERTIES.every(property => anchor[property] === previousAnchor[property]) &&
485
- PROPERTIES.every(property => content[property] === previousContent[property])
486
- ) {
487
- floatable.frame = requestAnimationFrame(run);
488
-
489
- return;
490
- }
491
- }
492
- } else {
493
- calculated = true;
494
- }
495
-
496
- update(anchor, content);
497
- }
498
-
499
- function update(anchor: DOMRect, content: DOMRect): void {
500
- previousAnchor = anchor;
501
- previousContent = content;
502
-
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);
507
-
508
- if (top + content.height > window.innerHeight) {
509
- floatable.options.content.style.height = `${window.innerHeight - top}px`;
510
- } else {
511
- floatable.options.content.style.height = '';
512
- }
513
-
514
- if (left + content.width > window.innerWidth) {
515
- floatable.options.content.style.width = `${window.innerWidth - left}px`;
516
- } else {
517
- floatable.options.content.style.width = '';
518
- }
519
-
520
- // floatable.options.content.style.inset = `${top}px auto auto ${left}px`;
521
- floatable.options.content.style.transform = `translate3d(${left}px, ${top}px, 0)`;
522
-
523
- floatable.frame = requestAnimationFrame(run);
524
-
525
- if (!runOnAfter) {
526
- runOnAfter = true;
527
-
528
- floatable.options.onAfter?.(true);
529
- }
530
- }
531
-
532
- floatable.frame = requestAnimationFrame(run);
533
- }
534
-
535
318
  //
536
319
 
320
+ const AREAS: Record<Position, string> = {
321
+ above: 'block-start',
322
+ 'above-end': 'start span-start',
323
+ 'above-start': 'start span-end',
324
+ below: 'block-end',
325
+ 'below-end': 'end span-start',
326
+ 'below-start': 'end span-end',
327
+ end: 'center end',
328
+ 'end-bottom': 'span-start end',
329
+ 'end-top': 'span-end end',
330
+ start: 'center start',
331
+ 'start-bottom': 'span-start start',
332
+ 'start-top': 'span-end start',
333
+ };
334
+
537
335
  const ATTRIBUTE_ANCHOR = 'oui-floatable-anchor';
538
336
 
539
337
  const ATTRIBUTE_CONTENT = 'oui-floatable-content';
540
338
 
339
+ const ATTRIBUTE_POSITION = 'oui-position';
340
+
541
341
  const INSERT_AFTEREND = 'afterend';
542
342
 
543
- const POSITIONS: Set<Position> = new Set([
544
- 'above',
545
- 'above-end',
546
- 'above-start',
547
- 'below',
548
- 'below-end',
549
- 'below-start',
550
- 'end',
551
- 'end-bottom',
552
- 'end-top',
553
- 'horizontal',
554
- 'horizontal-bottom',
555
- 'horizontal-top',
556
- 'start',
557
- 'start-bottom',
558
- 'start-top',
559
- 'vertical',
560
- 'vertical-end',
561
- 'vertical-start',
562
- ]);
563
-
564
- const PROPERTIES: (keyof DOMRect)[] = ['bottom', 'left', 'right', 'top'];
343
+ const POSITIONS = Object.fromEntries(
344
+ Object.entries(AREAS).map(([position, area]) => [area, position]),
345
+ ) as Record<string, Position>;
565
346
 
566
347
  const SELECTOR_ALL = `[${ATTRIBUTE_ANCHOR}], [${ATTRIBUTE_CONTENT}]`;
567
348
 
568
349
  const SELECTOR_CONTENT = `[${ATTRIBUTE_CONTENT}]`;
569
350
 
351
+ let index = 0;
352
+
570
353
  //
571
354
 
572
355
  if ((window as unknown as FloatableWindow)._oscarpalmer_oui_floatable == null) {
package/src/js/popover.ts CHANGED
@@ -52,7 +52,7 @@ export class OuiPopoverElement extends HTMLElement {
52
52
  defaultPosition: 'below-start',
53
53
  interactive: true,
54
54
  positionAttribute: 'position',
55
- preferAbove: false,
55
+ reusable: false,
56
56
  onAfter: (active: boolean): void => {
57
57
  if (active) {
58
58
  focus(content);