@oscarpalmer/oui 0.15.0 → 0.17.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.
@@ -0,0 +1,87 @@
1
+ @use 'mixins.scss' as mixins;
2
+
3
+ oui-splitter {
4
+ :where(&) {
5
+ display: flex;
6
+ flex-flow: row nowrap;
7
+ flex: 1;
8
+ }
9
+
10
+ :where(& > :first-child) {
11
+ flex: var(--oui-splitter-size, .5);
12
+ }
13
+
14
+ :where(& > :last-child) {
15
+ flex: calc(1 - var(--oui-splitter-size, .5));
16
+ }
17
+ }
18
+
19
+ :where([oui-splitter-separator]) {
20
+ width: .25rem;
21
+ background-color: rgba(0, 0, 0, 0.1);
22
+ border-radius: .25rem;
23
+ position: relative;
24
+ transition: background .125s ease-in-out;
25
+ }
26
+
27
+ :where([oui-splitter-separator]:has([oui-splitter-handle]:focus)),
28
+ :where([oui-splitter-separator]:has([oui-splitter-handle]:hover)),
29
+ :where([oui-splitter-separator][oui-splitter-active]) {
30
+ background-color: color-mix(in oklab, var(--oui-blue-6), transparent 82.5%);
31
+ }
32
+
33
+ :where([oui-splitter-handle]) {
34
+ width: 2rem;
35
+ height: 4rem;
36
+ padding: .875rem;
37
+ position: absolute;
38
+ inset: 50% auto auto 50%;
39
+ transform: translate3d(-50%, -50%, 0);
40
+ cursor: ew-resize;
41
+
42
+ &:focus {
43
+ outline: none;
44
+ }
45
+
46
+ &::after {
47
+ content: '';
48
+ width: 100%;
49
+ height: 100%;
50
+ display: block;
51
+ background-color: rgba(0, 0, 0, 0.25);
52
+ outline: .25rem solid transparent;
53
+ outline-offset: .25rem;
54
+ border-radius: .25rem;
55
+ transition: background .125s ease-in-out, outline .125s ease-in-out;
56
+ }
57
+ }
58
+
59
+ :where([oui-splitter-handle]:focus),
60
+ :where([oui-splitter-handle]:hover),
61
+ :where([oui-splitter-handle][oui-splitter-active]) {
62
+ &::after {
63
+ background-color: color-mix(in oklab, var(--oui-blue-6), transparent 50%);
64
+ }
65
+ }
66
+
67
+ :where([oui-splitter-handle]:focus-visible)::after {
68
+ outline-color: color-mix(in oklab, var(--oui-blue-6), transparent 50%);
69
+ }
70
+
71
+ oui-splitter[type='horizontal' i],
72
+ oui-splitter[oui-splitter-horizontal] {
73
+ :where(&) {
74
+ flex-flow: column nowrap;
75
+ }
76
+
77
+ :where(& [oui-splitter-separator]) {
78
+ width: 100%;
79
+ height: .25rem;
80
+ }
81
+
82
+ :where(& [oui-splitter-handle]) {
83
+ width: 4rem;
84
+ height: 2rem;
85
+ cursor: ns-resize;
86
+ }
87
+ }
@@ -1,12 +1,18 @@
1
1
  export class Floatable {
2
2
  activate: number | undefined;
3
+
3
4
  active = false;
5
+
4
6
  deactivate: number | undefined;
7
+
5
8
  frame: DOMHighResTimeStamp | undefined;
6
9
 
7
10
  constructor(
8
11
  public anchor: HTMLElement,
9
12
  public content: HTMLElement,
13
+ readonly attribute: string,
14
+ readonly position: Position,
15
+ readonly preferAbove: boolean,
10
16
  ) {
11
17
  setStyles(content);
12
18
  }
@@ -21,6 +27,28 @@ export class Floatable {
21
27
  }
22
28
  }
23
29
 
30
+ type Position =
31
+ | 'above'
32
+ | 'above-end'
33
+ | 'above-start'
34
+ | 'below'
35
+ | 'below-end'
36
+ | 'below-start'
37
+ | 'end'
38
+ | 'end-bottom'
39
+ | 'end-top'
40
+ | 'horizontal'
41
+ | 'horizontal-bottom'
42
+ | 'horizontal-top'
43
+ | 'start'
44
+ | 'start-bottom'
45
+ | 'start-top'
46
+ | 'vertical'
47
+ | 'vertical-end'
48
+ | 'vertical-start';
49
+
50
+ //
51
+
24
52
  export function activate(
25
53
  floatable: Floatable,
26
54
  active: Set<Floatable>,
@@ -63,48 +91,78 @@ export function deactivate(
63
91
  }
64
92
  }
65
93
 
66
- function getX(
67
- anchor: DOMRect,
68
- content: DOMRect,
69
- ): {left?: number; right?: number} {
70
- if (content.width + 2 * margin >= window.innerWidth) {
71
- return {
72
- left: margin,
73
- right: margin,
74
- };
94
+ function getX(anchor: DOMRect, content: DOMRect, position: Position): number {
95
+ if (position.endsWith('-end')) {
96
+ const end = anchor.right - content.width;
97
+
98
+ return end - margin < 0 ? margin : end;
99
+ }
100
+
101
+ if (position.endsWith('-start')) {
102
+ const start = anchor.left;
103
+
104
+ return start + content.width + margin > window.innerWidth
105
+ ? window.innerWidth - content.width - margin
106
+ : start;
75
107
  }
76
108
 
77
- const left = anchor.left + (anchor.width - content.width) / 2;
78
- const right = window.innerWidth - margin;
109
+ if (position.startsWith('end')) {
110
+ return anchor.right + margin;
111
+ }
79
112
 
80
- if (left < margin) {
81
- return {
82
- left: margin,
83
- };
113
+ if (position.startsWith('start')) {
114
+ return anchor.left - content.width - margin;
84
115
  }
85
116
 
86
- if (left + content.width > right) {
87
- return {
88
- right: margin,
89
- };
117
+ if (position.startsWith('horizontal')) {
118
+ if (anchor.right + content.width + margin <= window.innerWidth) {
119
+ return anchor.right + margin;
120
+ }
121
+
122
+ return anchor.left - content.width - margin;
90
123
  }
91
124
 
92
- return {
93
- left,
94
- };
125
+ return anchor.left + (anchor.width - content.width) / 2;
95
126
  }
96
127
 
97
- function getY(anchor: DOMRect, content: DOMRect): number {
98
- let top = anchor.top - content.height - margin;
128
+ function getY(
129
+ anchor: DOMRect,
130
+ content: DOMRect,
131
+ position: Position,
132
+ preferAbove: boolean,
133
+ ): number {
134
+ if (position.startsWith('above')) {
135
+ return anchor.top - content.height - margin;
136
+ }
137
+
138
+ if (position.startsWith('below')) {
139
+ return anchor.bottom + margin;
140
+ }
141
+
142
+ if (position.endsWith('bottom')) {
143
+ const bottom = anchor.bottom - content.height;
144
+
145
+ return bottom + margin < 0 ? margin : bottom;
146
+ }
147
+
148
+ if (position.endsWith('top')) {
149
+ const top = anchor.top;
99
150
 
100
- if (top - margin >= 0) {
101
- return top;
151
+ return top + content.height + margin > window.innerHeight
152
+ ? window.innerHeight - content.height - margin
153
+ : top;
102
154
  }
103
155
 
104
- top = anchor.bottom + margin;
156
+ if (position.startsWith('vertical')) {
157
+ if (preferAbove && anchor.top - content.height - margin >= 0) {
158
+ return anchor.top - content.height - margin;
159
+ }
160
+
161
+ if (anchor.bottom + content.height + margin <= document.body.clientHeight) {
162
+ return anchor.bottom + margin;
163
+ }
105
164
 
106
- if (top + content.height + margin <= window.innerHeight) {
107
- return top;
165
+ return anchor.top - content.height - margin;
108
166
  }
109
167
 
110
168
  return anchor.top + (anchor.height - content.height) / 2;
@@ -151,17 +209,25 @@ function onDeactivate(
151
209
  }
152
210
 
153
211
  function update(floatable: Floatable): void {
212
+ let position: Position =
213
+ (floatable.anchor.getAttribute(floatable.attribute)?.trim() as Position) ??
214
+ floatable.position;
215
+
216
+ if (!positions.has(position as Position)) {
217
+ position = floatable.position;
218
+ }
219
+
154
220
  function run(): void {
155
- const anchor = floatable.anchor.getBoundingClientRect();
156
- const content = floatable.content.getBoundingClientRect();
221
+ const anchor = floatable.anchor.getBoundingClientRect();
222
+ const content = floatable.content.getBoundingClientRect();
157
223
 
158
- const {left, right} = getX(anchor, content);
159
- const top = getY(anchor, content);
224
+ const left = getX(anchor, content, position);
225
+ const top = getY(anchor, content, position, floatable.preferAbove);
160
226
 
161
- floatable.content.style.inset = `${top}px ${right == null ? 'auto' : `${right}px`} auto ${left == null ? 'auto' : `${left}px`}`;
227
+ floatable.content.style.inset = `${top}px auto auto ${left}px`;
162
228
 
163
- floatable.frame = requestAnimationFrame(run);
164
- }
229
+ floatable.frame = requestAnimationFrame(run);
230
+ }
165
231
 
166
232
  floatable.frame = requestAnimationFrame(run);
167
233
  }
@@ -193,4 +259,25 @@ function stop(floatable: Floatable): void {
193
259
  //
194
260
 
195
261
  const margin =
196
- Number.parseInt(getComputedStyle(document.documentElement).fontSize, 10) / 4;
262
+ Number.parseInt(getComputedStyle(document.documentElement).fontSize, 10) / 2;
263
+
264
+ const positions = new Set<Position>([
265
+ 'above',
266
+ 'above-end',
267
+ 'above-start',
268
+ 'below',
269
+ 'below-end',
270
+ 'below-start',
271
+ 'end',
272
+ 'end-bottom',
273
+ 'end-top',
274
+ 'horizontal',
275
+ 'horizontal-bottom',
276
+ 'horizontal-top',
277
+ 'start',
278
+ 'start-bottom',
279
+ 'start-top',
280
+ 'vertical',
281
+ 'vertical-end',
282
+ 'vertical-start',
283
+ ]);
package/src/js/popover.ts CHANGED
@@ -50,7 +50,14 @@ export class OuiPopoverElement extends HTMLElement {
50
50
  toggle.setAttribute('aria-controls', content.id);
51
51
  toggle.setAttribute('aria-haspopup', 'dialog');
52
52
 
53
- this.#floatable = new Floatable(toggle, content);
53
+ this.#floatable = new Floatable(
54
+ toggle,
55
+ content,
56
+ 'position',
57
+ 'below-start',
58
+ false,
59
+ );
60
+
54
61
  this.#focusTrap = createFocusTrap(content);
55
62
 
56
63
  mapped.set(toggle, this.#floatable);
@@ -132,7 +139,7 @@ function closeAbove(element: HTMLElement): void {
132
139
  }
133
140
  }
134
141
 
135
- order[index].content.focus();
142
+ focus(order[index].content);
136
143
  }
137
144
 
138
145
  function focus(content: HTMLElement): void {