@oscarpalmer/oui 0.21.0 → 0.22.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oscarpalmer/oui",
3
- "version": "0.21.0",
3
+ "version": "0.22.1",
4
4
  "description": "Styles, components, and utilities for building websites and applications.",
5
5
  "keywords": [
6
6
  "css",
@@ -37,10 +37,18 @@
37
37
  "types": "./dist/js/focus-trap/standalone.d.mts",
38
38
  "default": "./dist/js/focus-trap/standalone.mjs"
39
39
  },
40
+ "./movable": {
41
+ "types": "./dist/js/movable.d.mts",
42
+ "default": "./dist/js/movable.mjs"
43
+ },
40
44
  "./popover": {
41
45
  "types": "./dist/js/popover.d.mts",
42
46
  "default": "./dist/js/popover.mjs"
43
47
  },
48
+ "./sortable": {
49
+ "types": "./dist/js/sortable.d.mts",
50
+ "default": "./dist/js/sortable.mjs"
51
+ },
44
52
  "./splitter": {
45
53
  "types": "./dist/js/splitter.d.mts",
46
54
  "default": "./dist/js/splitter.mjs"
@@ -66,16 +74,16 @@
66
74
  "@oscarpalmer/toretto": "^0.46"
67
75
  },
68
76
  "devDependencies": {
69
- "@oxlint/plugins": "^1.67",
77
+ "@oxlint/plugins": "^1.68",
70
78
  "@vitest/coverage-istanbul": "^4.1",
71
79
  "autoprefixer": "^10.5",
72
- "concurrently": "^9.2",
80
+ "concurrently": "^10",
73
81
  "jsdom": "^29.1",
74
82
  "postcss-cli": "^11",
75
83
  "postcss-input-range": "^6",
76
84
  "postcss-scss": "^4",
77
85
  "sass": "^1.100",
78
- "stylelint": "^17.11",
86
+ "stylelint": "^17.12",
79
87
  "stylelint-config-standard-scss": "^17",
80
88
  "tsdown": "^0.22",
81
89
  "typescript": "^6",
@@ -84,4 +92,4 @@
84
92
  "vitest": "npm:@voidzero-dev/vite-plus-test@latest"
85
93
  },
86
94
  "packageManager": "npm@11.12.1"
87
- }
95
+ }
@@ -60,24 +60,20 @@
60
60
  margin: var(--margin-top, 0) 0 0 var(--margin-left, 0);
61
61
  pointer-events: all !important;
62
62
 
63
- &[position^='above'],
64
- &[position^='top'] {
65
- --margin-top: -0.5rem;
63
+ &[oui-position^='above'] {
64
+ margin: 0 0 0.5em;
66
65
  }
67
66
 
68
- &[position^='below'],
69
- &[position^='bottom'] {
70
- --margin-top: 0.5rem;
67
+ &[oui-position^='below'] {
68
+ margin: 0.5em 0 0;
71
69
  }
72
70
 
73
- &[position^='end'],
74
- &[position^='right'] {
75
- --margin-left: 0.5rem;
71
+ &[oui-position^='end'] {
72
+ margin: 0 0 0 0.5em;
76
73
  }
77
74
 
78
- &[position^='left'],
79
- &[position^='start'] {
80
- --margin-left: -0.5rem;
75
+ &[oui-position^='start'] {
76
+ margin: 0 0.5em 0 0;
81
77
  }
82
78
  }
83
79
 
@@ -6,7 +6,7 @@
6
6
  padding: 1rem;
7
7
  overscroll-behavior: contain;
8
8
  background-color: var(--oui-absolute);
9
- border-radius: 0.5rem;
9
+ border-radius: 0.25rem;
10
10
 
11
11
  &:focus {
12
12
  outline: none;
@@ -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,17 @@ 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
+ let anchorName = anchor.style.anchorName;
210
+
211
+ if (anchorName.length === 0) {
212
+ anchorName = getAnchorName();
213
+ }
214
+
215
+ anchor.style.anchorName = anchorName;
216
+
217
+ content.style.position = 'absolute';
218
+ content.style.positionAnchor = anchorName;
219
+ content.style.positionTry = 'normal flip-start';
359
220
 
360
221
  const state = getState();
361
222
 
@@ -411,20 +272,22 @@ function onKeyDown(event: KeyboardEvent): void {
411
272
  }
412
273
  }
413
274
 
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);
275
+ function setPosition(floatable: Floatable): void {
276
+ let position: Position =
277
+ (floatable.options.anchor.getAttribute(floatable.options.positionAttribute) as Position) ??
278
+ floatable.options.defaultPosition;
279
+
280
+ if (AREAS[position] == null) {
281
+ position = floatable.options.defaultPosition;
425
282
  }
426
283
 
427
- floater.setAttribute('position', actual ?? position);
284
+ const area = AREAS[position];
285
+
286
+ floatable.options.content.style.positionArea = area;
287
+
288
+ floatable.options.content.setAttribute(ATTRIBUTE_POSITION, POSITIONS[area]);
289
+
290
+ floatable.options.onAfter?.(true);
428
291
  }
429
292
 
430
293
  function stop(floatable: Floatable): void {
@@ -456,117 +319,41 @@ function toggle(anchor: HTMLElement): void {
456
319
  floatable.toggle(!active);
457
320
  }
458
321
 
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
322
  //
536
323
 
324
+ const AREAS: Record<Position, string> = {
325
+ above: 'block-start',
326
+ 'above-end': 'start span-start',
327
+ 'above-start': 'start span-end',
328
+ below: 'block-end',
329
+ 'below-end': 'end span-start',
330
+ 'below-start': 'end span-end',
331
+ end: 'center end',
332
+ 'end-bottom': 'span-start end',
333
+ 'end-top': 'span-end end',
334
+ start: 'center start',
335
+ 'start-bottom': 'span-start start',
336
+ 'start-top': 'span-end start',
337
+ };
338
+
537
339
  const ATTRIBUTE_ANCHOR = 'oui-floatable-anchor';
538
340
 
539
341
  const ATTRIBUTE_CONTENT = 'oui-floatable-content';
540
342
 
343
+ const ATTRIBUTE_POSITION = 'oui-position';
344
+
541
345
  const INSERT_AFTEREND = 'afterend';
542
346
 
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'];
347
+ const POSITIONS = Object.fromEntries(
348
+ Object.entries(AREAS).map(([position, area]) => [area, position]),
349
+ ) as Record<string, Position>;
565
350
 
566
351
  const SELECTOR_ALL = `[${ATTRIBUTE_ANCHOR}], [${ATTRIBUTE_CONTENT}]`;
567
352
 
568
353
  const SELECTOR_CONTENT = `[${ATTRIBUTE_CONTENT}]`;
569
354
 
355
+ let index = 0;
356
+
570
357
  //
571
358
 
572
359
  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);