@oscarpalmer/oui 0.15.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.
@@ -67,9 +67,6 @@
67
67
  :where(.oui-button-tiny) {
68
68
  --oui-foreground-percentage: 12.5%;
69
69
  font-size: 0.75em;
70
- font-weight: bold;
71
- letter-spacing: 0.1em;
72
- text-transform: uppercase;
73
70
  }
74
71
  :where(.oui-button:focus-visible) {
75
72
  --oui-outline-percentage: 75%;
@@ -0,0 +1,78 @@
1
+
2
+ :where(oui-splitter) {
3
+ display: flex;
4
+ flex-flow: row nowrap;
5
+ flex: 1;
6
+ }
7
+ :where(oui-splitter > :first-child) {
8
+ flex: var(--oui-splitter-size, 0.5);
9
+ }
10
+ :where(oui-splitter > :last-child) {
11
+ flex: calc(1 - var(--oui-splitter-size, 0.5));
12
+ }
13
+
14
+ :where([oui-splitter-separator]) {
15
+ width: 0.25rem;
16
+ background-color: rgba(0, 0, 0, 0.1);
17
+ border-radius: 0.25rem;
18
+ position: relative;
19
+ transition: background 0.125s ease-in-out;
20
+ }
21
+
22
+ :where([oui-splitter-separator]:has([oui-splitter-handle]:focus)),
23
+ :where([oui-splitter-separator]:has([oui-splitter-handle]:hover)),
24
+ :where([oui-splitter-separator][oui-splitter-active]) {
25
+ background-color: rgba(0, 0, 255, 0.1);
26
+ }
27
+
28
+ :where([oui-splitter-handle]) {
29
+ width: 2rem;
30
+ height: 4rem;
31
+ padding: 0.875rem;
32
+ position: absolute;
33
+ inset: 50% auto auto 50%;
34
+ transform: translate3d(-50%, -50%, 0);
35
+ cursor: ew-resize;
36
+ }
37
+ :where([oui-splitter-handle]):focus {
38
+ outline: none;
39
+ }
40
+ :where([oui-splitter-handle])::after {
41
+ content: "";
42
+ width: 100%;
43
+ height: 100%;
44
+ display: block;
45
+ background-color: rgba(0, 0, 0, 0.25);
46
+ outline: 0.25rem solid transparent;
47
+ outline-offset: 0.25rem;
48
+ border-radius: 0.25rem;
49
+ transition: background 0.125s ease-in-out, outline 0.125s ease-in-out;
50
+ }
51
+
52
+ :where([oui-splitter-handle]:focus)::after,
53
+ :where([oui-splitter-handle]:hover)::after,
54
+ :where([oui-splitter-handle][oui-splitter-active])::after {
55
+ background-color: rgba(0, 0, 255, 0.25);
56
+ }
57
+
58
+ :where([oui-splitter-handle]:focus-visible)::after {
59
+ outline-color: rgba(0, 0, 255, 0.25);
60
+ }
61
+
62
+ :where(oui-splitter[type=horizontal i],
63
+ oui-splitter[oui-splitter-horizontal]) {
64
+ flex-flow: column nowrap;
65
+ }
66
+ :where(oui-splitter[type=horizontal i] [oui-splitter-separator],
67
+ oui-splitter[oui-splitter-horizontal] [oui-splitter-separator]) {
68
+ width: 100%;
69
+ height: 0.25rem;
70
+ }
71
+ :where(oui-splitter[type=horizontal i] [oui-splitter-handle],
72
+ oui-splitter[oui-splitter-horizontal] [oui-splitter-handle]) {
73
+ width: 4rem;
74
+ height: 2rem;
75
+ cursor: ns-resize;
76
+ }
77
+
78
+
@@ -128,7 +128,7 @@ function stop(floatable) {
128
128
  floatable.deactivate = void 0;
129
129
  floatable.frame = void 0;
130
130
  }
131
- const margin = Number.parseInt(getComputedStyle(document.documentElement).fontSize, 10) / 4;
131
+ const margin = Number.parseInt(getComputedStyle(document.documentElement).fontSize, 10) / 2;
132
132
  exports.Floatable = Floatable;
133
133
  exports.activate = activate;
134
134
  exports.deactivate = deactivate;
@@ -126,7 +126,7 @@ function stop(floatable) {
126
126
  floatable.deactivate = void 0;
127
127
  floatable.frame = void 0;
128
128
  }
129
- const margin = Number.parseInt(getComputedStyle(document.documentElement).fontSize, 10) / 4;
129
+ const margin = Number.parseInt(getComputedStyle(document.documentElement).fontSize, 10) / 2;
130
130
  export {
131
131
  Floatable,
132
132
  activate,
@@ -95,7 +95,7 @@ function closeAbove(element) {
95
95
  floatable.deactivate(floatable$1, active, order);
96
96
  }
97
97
  }
98
- order[index2].content.focus();
98
+ focus(order[index2].content);
99
99
  }
100
100
  function focus(content) {
101
101
  let target = focusable.getFocusable(content)[0];
@@ -93,7 +93,7 @@ function closeAbove(element) {
93
93
  deactivate(floatable, active, order);
94
94
  }
95
95
  }
96
- order[index2].content.focus();
96
+ focus(order[index2].content);
97
97
  }
98
98
  function focus(content) {
99
99
  let target = getFocusable(content)[0];
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const number = require("@oscarpalmer/atoms/number");
4
+ const event = require("@oscarpalmer/toretto/event");
5
+ const find = require("@oscarpalmer/toretto/find");
6
+ class OuiSplitterElement extends HTMLElement {
7
+ #splitter;
8
+ constructor() {
9
+ super();
10
+ const panels = [...this.children].filter(
11
+ (child) => child instanceof HTMLElement
12
+ );
13
+ if (panels.length !== 2) {
14
+ throw new Error("oui-splitter must have exactly two panels.");
15
+ }
16
+ this.#splitter = new Splitter(this, panels);
17
+ }
18
+ connectedCallback() {
19
+ if (this.#splitter.types.auto) {
20
+ observer.observe(this);
21
+ }
22
+ }
23
+ disconnectedCallback() {
24
+ if (this.#splitter.types.auto) {
25
+ observer.unobserve(this);
26
+ }
27
+ }
28
+ }
29
+ class Splitter {
30
+ constructor(element, panels) {
31
+ this.element = element;
32
+ this.panels = panels;
33
+ this.handle = createHandle();
34
+ this.rectangle = element.getBoundingClientRect();
35
+ this.separator = createSeparator();
36
+ this.separator.append(this.handle);
37
+ panels[0].insertAdjacentElement("afterend", this.separator);
38
+ mapped.set(element, this);
39
+ mapped.set(this.handle, this);
40
+ setSize(this, this.flex.value);
41
+ this.types = getTypes(this);
42
+ if (this.types.horizontal) {
43
+ this.element.setAttribute("oui-splitter-horizontal", "");
44
+ }
45
+ }
46
+ flex = {
47
+ previous: 0.5,
48
+ value: 0.5
49
+ };
50
+ handle;
51
+ rectangle;
52
+ separator;
53
+ types;
54
+ }
55
+ function createHandle() {
56
+ const handle = document.createElement("span");
57
+ handle.tabIndex = 0;
58
+ handle.setAttribute("aria-label", "Resize panels");
59
+ handle.setAttribute("oui-splitter-handle", "");
60
+ return handle;
61
+ }
62
+ function createSeparator() {
63
+ const separator = document.createElement("div");
64
+ separator.setAttribute("oui-splitter-separator", "");
65
+ return separator;
66
+ }
67
+ function getFlex(splitter2, value) {
68
+ const { height, left, top, width } = splitter2.rectangle;
69
+ const offset = value - (splitter2.types.horizontal ? top : left);
70
+ const fraction = offset / (splitter2.types.horizontal ? height : width);
71
+ return number.clamp(fraction, 0.1, 0.9);
72
+ }
73
+ function getTypes(splitter2, width, height) {
74
+ const type = splitter2.element.getAttribute("type")?.trim() ?? "";
75
+ if (type === "horizontal") {
76
+ return {
77
+ auto: false,
78
+ horizontal: true
79
+ };
80
+ }
81
+ if (type === "auto") {
82
+ return {
83
+ auto: true,
84
+ horizontal: (height ?? splitter2.rectangle.height) > (width ?? splitter2.rectangle.width)
85
+ };
86
+ }
87
+ return {
88
+ auto: false,
89
+ horizontal: false
90
+ };
91
+ }
92
+ function onEnd(reset) {
93
+ if (splitter != null) {
94
+ splitter.handle.removeAttribute("oui-splitter-active");
95
+ splitter.separator.removeAttribute("oui-splitter-active");
96
+ if (reset) {
97
+ setSize(splitter, splitter.flex.previous);
98
+ } else {
99
+ splitter.flex.previous = splitter.flex.value;
100
+ }
101
+ }
102
+ splitter = void 0;
103
+ }
104
+ function onKeydown(event2) {
105
+ switch (true) {
106
+ case allKeys.has(event2.key):
107
+ onNavigate(event2);
108
+ break;
109
+ case event2.key === "Escape":
110
+ onEnd(true);
111
+ break;
112
+ }
113
+ }
114
+ function onMousedown(event2) {
115
+ splitter = mapped.get(
116
+ find.findAncestor(
117
+ event2.target,
118
+ "[oui-splitter-handle]"
119
+ )
120
+ );
121
+ if (splitter == null) {
122
+ return;
123
+ }
124
+ splitter.handle.setAttribute("oui-splitter-active", "");
125
+ splitter.separator.setAttribute("oui-splitter-active", "");
126
+ splitter.rectangle = splitter.element.getBoundingClientRect();
127
+ }
128
+ function onMousemove(event$1) {
129
+ if (splitter == null) {
130
+ return;
131
+ }
132
+ const position = event.getPosition(event$1);
133
+ if (position != null) {
134
+ splitter.flex.value = getFlex(
135
+ splitter,
136
+ splitter.types.horizontal ? position.y : position.x
137
+ );
138
+ setSize(splitter, splitter.flex.value);
139
+ }
140
+ }
141
+ function onMouseup() {
142
+ if (splitter != null) {
143
+ onEnd(false);
144
+ }
145
+ }
146
+ function onNavigate(event2) {
147
+ const splitter2 = mapped.get(
148
+ find.findAncestor(
149
+ event2.target,
150
+ "[oui-splitter-handle]"
151
+ )
152
+ );
153
+ if (splitter2 == null) {
154
+ return;
155
+ }
156
+ if (absoluteKeys.has(event2.key)) {
157
+ setSize(splitter2, event2.key === "End" ? 0.9 : 0.1, true);
158
+ return;
159
+ }
160
+ if (splitter2.types.horizontal && !horizontalKeys.has(event2.key) || !splitter2.types.horizontal && !verticalKeys.has(event2.key)) {
161
+ return;
162
+ }
163
+ const modifier = negativeKeys.has(event2.key) ? -1 : 1;
164
+ setSize(splitter2, splitter2.flex.value + modifier * 0.05, true);
165
+ }
166
+ function onObservation(entries) {
167
+ if (frame != null) {
168
+ cancelAnimationFrame(frame);
169
+ }
170
+ frame = requestAnimationFrame(() => {
171
+ for (const entry of entries) {
172
+ const splitter2 = mapped.get(entry.target);
173
+ if (splitter2 == null) {
174
+ return;
175
+ }
176
+ splitter2.types = getTypes(
177
+ splitter2,
178
+ entry.contentRect.width,
179
+ entry.contentRect.height
180
+ );
181
+ if (splitter2.types.horizontal) {
182
+ splitter2.element.setAttribute("oui-splitter-horizontal", "");
183
+ } else {
184
+ splitter2.element.removeAttribute("oui-splitter-horizontal");
185
+ }
186
+ }
187
+ frame = void 0;
188
+ });
189
+ }
190
+ function setSize(splitter2, size, previous) {
191
+ splitter2.flex.value = size;
192
+ if (previous) {
193
+ splitter2.flex.previous = size;
194
+ }
195
+ splitter2.element.style.setProperty("--oui-splitter-size", `${size}`);
196
+ }
197
+ const absoluteKeys = /* @__PURE__ */ new Set(["End", "Home"]);
198
+ const horizontalKeys = /* @__PURE__ */ new Set(["ArrowDown", "ArrowUp"]);
199
+ const mapped = /* @__PURE__ */ new WeakMap();
200
+ const observer = new ResizeObserver(onObservation);
201
+ const negativeKeys = /* @__PURE__ */ new Set(["ArrowLeft", "ArrowUp"]);
202
+ const verticalKeys = /* @__PURE__ */ new Set(["ArrowLeft", "ArrowRight"]);
203
+ const allKeys = /* @__PURE__ */ new Set([...absoluteKeys, ...horizontalKeys, ...verticalKeys]);
204
+ let frame;
205
+ let splitter;
206
+ customElements.define("oui-splitter", OuiSplitterElement);
207
+ event.on(document, "keydown", onKeydown);
208
+ event.on(document, "mousedown", onMousedown);
209
+ event.on(document, "mousemove", onMousemove);
210
+ event.on(document, "mouseup", onMouseup);
211
+ exports.OuiSplitterElement = OuiSplitterElement;
@@ -0,0 +1,211 @@
1
+ import { clamp } from "@oscarpalmer/atoms/number";
2
+ import { on, getPosition } from "@oscarpalmer/toretto/event";
3
+ import { findAncestor } from "@oscarpalmer/toretto/find";
4
+ class OuiSplitterElement extends HTMLElement {
5
+ #splitter;
6
+ constructor() {
7
+ super();
8
+ const panels = [...this.children].filter(
9
+ (child) => child instanceof HTMLElement
10
+ );
11
+ if (panels.length !== 2) {
12
+ throw new Error("oui-splitter must have exactly two panels.");
13
+ }
14
+ this.#splitter = new Splitter(this, panels);
15
+ }
16
+ connectedCallback() {
17
+ if (this.#splitter.types.auto) {
18
+ observer.observe(this);
19
+ }
20
+ }
21
+ disconnectedCallback() {
22
+ if (this.#splitter.types.auto) {
23
+ observer.unobserve(this);
24
+ }
25
+ }
26
+ }
27
+ class Splitter {
28
+ constructor(element, panels) {
29
+ this.element = element;
30
+ this.panels = panels;
31
+ this.handle = createHandle();
32
+ this.rectangle = element.getBoundingClientRect();
33
+ this.separator = createSeparator();
34
+ this.separator.append(this.handle);
35
+ panels[0].insertAdjacentElement("afterend", this.separator);
36
+ mapped.set(element, this);
37
+ mapped.set(this.handle, this);
38
+ setSize(this, this.flex.value);
39
+ this.types = getTypes(this);
40
+ if (this.types.horizontal) {
41
+ this.element.setAttribute("oui-splitter-horizontal", "");
42
+ }
43
+ }
44
+ flex = {
45
+ previous: 0.5,
46
+ value: 0.5
47
+ };
48
+ handle;
49
+ rectangle;
50
+ separator;
51
+ types;
52
+ }
53
+ function createHandle() {
54
+ const handle = document.createElement("span");
55
+ handle.tabIndex = 0;
56
+ handle.setAttribute("aria-label", "Resize panels");
57
+ handle.setAttribute("oui-splitter-handle", "");
58
+ return handle;
59
+ }
60
+ function createSeparator() {
61
+ const separator = document.createElement("div");
62
+ separator.setAttribute("oui-splitter-separator", "");
63
+ return separator;
64
+ }
65
+ function getFlex(splitter2, value) {
66
+ const { height, left, top, width } = splitter2.rectangle;
67
+ const offset = value - (splitter2.types.horizontal ? top : left);
68
+ const fraction = offset / (splitter2.types.horizontal ? height : width);
69
+ return clamp(fraction, 0.1, 0.9);
70
+ }
71
+ function getTypes(splitter2, width, height) {
72
+ const type = splitter2.element.getAttribute("type")?.trim() ?? "";
73
+ if (type === "horizontal") {
74
+ return {
75
+ auto: false,
76
+ horizontal: true
77
+ };
78
+ }
79
+ if (type === "auto") {
80
+ return {
81
+ auto: true,
82
+ horizontal: (height ?? splitter2.rectangle.height) > (width ?? splitter2.rectangle.width)
83
+ };
84
+ }
85
+ return {
86
+ auto: false,
87
+ horizontal: false
88
+ };
89
+ }
90
+ function onEnd(reset) {
91
+ if (splitter != null) {
92
+ splitter.handle.removeAttribute("oui-splitter-active");
93
+ splitter.separator.removeAttribute("oui-splitter-active");
94
+ if (reset) {
95
+ setSize(splitter, splitter.flex.previous);
96
+ } else {
97
+ splitter.flex.previous = splitter.flex.value;
98
+ }
99
+ }
100
+ splitter = void 0;
101
+ }
102
+ function onKeydown(event) {
103
+ switch (true) {
104
+ case allKeys.has(event.key):
105
+ onNavigate(event);
106
+ break;
107
+ case event.key === "Escape":
108
+ onEnd(true);
109
+ break;
110
+ }
111
+ }
112
+ function onMousedown(event) {
113
+ splitter = mapped.get(
114
+ findAncestor(
115
+ event.target,
116
+ "[oui-splitter-handle]"
117
+ )
118
+ );
119
+ if (splitter == null) {
120
+ return;
121
+ }
122
+ splitter.handle.setAttribute("oui-splitter-active", "");
123
+ splitter.separator.setAttribute("oui-splitter-active", "");
124
+ splitter.rectangle = splitter.element.getBoundingClientRect();
125
+ }
126
+ function onMousemove(event) {
127
+ if (splitter == null) {
128
+ return;
129
+ }
130
+ const position = getPosition(event);
131
+ if (position != null) {
132
+ splitter.flex.value = getFlex(
133
+ splitter,
134
+ splitter.types.horizontal ? position.y : position.x
135
+ );
136
+ setSize(splitter, splitter.flex.value);
137
+ }
138
+ }
139
+ function onMouseup() {
140
+ if (splitter != null) {
141
+ onEnd(false);
142
+ }
143
+ }
144
+ function onNavigate(event) {
145
+ const splitter2 = mapped.get(
146
+ findAncestor(
147
+ event.target,
148
+ "[oui-splitter-handle]"
149
+ )
150
+ );
151
+ if (splitter2 == null) {
152
+ return;
153
+ }
154
+ if (absoluteKeys.has(event.key)) {
155
+ setSize(splitter2, event.key === "End" ? 0.9 : 0.1, true);
156
+ return;
157
+ }
158
+ if (splitter2.types.horizontal && !horizontalKeys.has(event.key) || !splitter2.types.horizontal && !verticalKeys.has(event.key)) {
159
+ return;
160
+ }
161
+ const modifier = negativeKeys.has(event.key) ? -1 : 1;
162
+ setSize(splitter2, splitter2.flex.value + modifier * 0.05, true);
163
+ }
164
+ function onObservation(entries) {
165
+ if (frame != null) {
166
+ cancelAnimationFrame(frame);
167
+ }
168
+ frame = requestAnimationFrame(() => {
169
+ for (const entry of entries) {
170
+ const splitter2 = mapped.get(entry.target);
171
+ if (splitter2 == null) {
172
+ return;
173
+ }
174
+ splitter2.types = getTypes(
175
+ splitter2,
176
+ entry.contentRect.width,
177
+ entry.contentRect.height
178
+ );
179
+ if (splitter2.types.horizontal) {
180
+ splitter2.element.setAttribute("oui-splitter-horizontal", "");
181
+ } else {
182
+ splitter2.element.removeAttribute("oui-splitter-horizontal");
183
+ }
184
+ }
185
+ frame = void 0;
186
+ });
187
+ }
188
+ function setSize(splitter2, size, previous) {
189
+ splitter2.flex.value = size;
190
+ if (previous) {
191
+ splitter2.flex.previous = size;
192
+ }
193
+ splitter2.element.style.setProperty("--oui-splitter-size", `${size}`);
194
+ }
195
+ const absoluteKeys = /* @__PURE__ */ new Set(["End", "Home"]);
196
+ const horizontalKeys = /* @__PURE__ */ new Set(["ArrowDown", "ArrowUp"]);
197
+ const mapped = /* @__PURE__ */ new WeakMap();
198
+ const observer = new ResizeObserver(onObservation);
199
+ const negativeKeys = /* @__PURE__ */ new Set(["ArrowLeft", "ArrowUp"]);
200
+ const verticalKeys = /* @__PURE__ */ new Set(["ArrowLeft", "ArrowRight"]);
201
+ const allKeys = /* @__PURE__ */ new Set([...absoluteKeys, ...horizontalKeys, ...verticalKeys]);
202
+ let frame;
203
+ let splitter;
204
+ customElements.define("oui-splitter", OuiSplitterElement);
205
+ on(document, "keydown", onKeydown);
206
+ on(document, "mousedown", onMousedown);
207
+ on(document, "mousemove", onMousemove);
208
+ on(document, "mouseup", onMouseup);
209
+ export {
210
+ OuiSplitterElement
211
+ };
package/package.json CHANGED
@@ -49,6 +49,26 @@
49
49
  "default": "./dist/js/focus-trap/standalone.cjs"
50
50
  }
51
51
  },
52
+ "./popover": {
53
+ "import": {
54
+ "types": "./types/popover.d.ts",
55
+ "default": "./dist/js/popover.js"
56
+ },
57
+ "require": {
58
+ "types": "./types/popover.d.cts",
59
+ "default": "./dist/js/popover.cjs"
60
+ }
61
+ },
62
+ "./splitter": {
63
+ "import": {
64
+ "types": "./types/splitter.d.ts",
65
+ "default": "./dist/js/splitter.js"
66
+ },
67
+ "require": {
68
+ "types": "./types/splitter.d.cts",
69
+ "default": "./dist/js/splitter.cjs"
70
+ }
71
+ },
52
72
  "./tooltip": {
53
73
  "import": {
54
74
  "types": "./types/tooltip.d.ts",
@@ -94,5 +114,5 @@
94
114
  },
95
115
  "type": "module",
96
116
  "types": "types/index.d.cts",
97
- "version": "0.15.0"
117
+ "version": "0.16.0"
98
118
  }
@@ -58,9 +58,6 @@
58
58
  --oui-foreground-percentage: 12.5%;
59
59
 
60
60
  font-size: 0.75em;
61
- font-weight: bold;
62
- letter-spacing: 0.1em;
63
- text-transform: uppercase;
64
61
  }
65
62
 
66
63
  :where(&:focus-visible) {
@@ -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: rgba(0, 0, 255, .1);
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: rgba(0, 0, 255, .25);
64
+ }
65
+ }
66
+
67
+ :where([oui-splitter-handle]:focus-visible)::after {
68
+ outline-color: rgba(0, 0, 255, .25);
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
+ }
@@ -193,4 +193,4 @@ function stop(floatable: Floatable): void {
193
193
  //
194
194
 
195
195
  const margin =
196
- Number.parseInt(getComputedStyle(document.documentElement).fontSize, 10) / 4;
196
+ Number.parseInt(getComputedStyle(document.documentElement).fontSize, 10) / 2;
package/src/js/popover.ts CHANGED
@@ -132,7 +132,7 @@ function closeAbove(element: HTMLElement): void {
132
132
  }
133
133
  }
134
134
 
135
- order[index].content.focus();
135
+ focus(order[index].content);
136
136
  }
137
137
 
138
138
  function focus(content: HTMLElement): void {
@@ -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);
@@ -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
+ }