@oscarpalmer/oui 0.8.0 → 0.10.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.
- package/dist/css/a11y.css +3 -0
- package/dist/css/badge.css +30 -22
- package/dist/css/button.css +89 -0
- package/dist/css/color.css +103 -99
- package/dist/css/flex.css +2 -0
- package/dist/css/form.css +354 -52
- package/dist/css/message.css +30 -22
- package/dist/css/reset.css +66 -50
- package/dist/css/table.css +4 -1
- package/dist/js/focus-trap.cjs +147 -0
- package/dist/js/focus-trap.js +146 -0
- package/dist/js/index.cjs +2 -0
- package/dist/js/index.js +1 -0
- package/package.json +59 -6
- package/src/css/badge.scss +33 -22
- package/src/css/button.scss +121 -0
- package/src/css/color.scss +101 -99
- package/src/css/form.scss +233 -60
- package/src/css/message.scss +36 -23
- package/src/css/reset.scss +12 -17
- package/src/css/table.scss +3 -1
- package/src/js/focus-trap.ts +181 -0
- package/src/js/index.ts +1 -0
- package/types/focus-trap.d.cts +3 -0
- package/types/focus-trap.d.ts +1 -0
- package/types/index.d.cts +3 -0
- package/types/index.d.ts +1 -0
package/dist/css/reset.css
CHANGED
|
@@ -43,10 +43,11 @@
|
|
|
43
43
|
font-family: system-ui, sans-serif;
|
|
44
44
|
font-size: 100%;
|
|
45
45
|
line-height: 1.5;
|
|
46
|
-
-moz-text-size-adjust: none;
|
|
47
46
|
-webkit-text-size-adjust: none;
|
|
48
|
-
text-size-adjust: none;
|
|
47
|
+
-moz-text-size-adjust: none;
|
|
48
|
+
text-size-adjust: none; /* Prevent text size inflation (e.g., after orientation change) [AB][RE][SE]*/
|
|
49
49
|
-moz-tab-size: 4;
|
|
50
|
+
-o-tab-size: 4;
|
|
50
51
|
tab-size: 4;
|
|
51
52
|
}
|
|
52
53
|
|
|
@@ -59,39 +60,43 @@
|
|
|
59
60
|
* General
|
|
60
61
|
*/
|
|
61
62
|
:where(a) {
|
|
63
|
+
-webkit-text-decoration-skip: ink;
|
|
62
64
|
text-decoration-skip-ink: auto;
|
|
63
65
|
text-underline-offset: 0.125em;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
:where(abbr[title]) {
|
|
67
|
-
text-decoration: underline;
|
|
68
|
-
text-decoration: underline dotted;
|
|
69
|
+
text-decoration: underline; /* Add the correct text decoration in Safari [RS][SE]*/
|
|
70
|
+
-webkit-text-decoration: underline dotted;
|
|
71
|
+
text-decoration: underline dotted; /* ditto*/
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
:where(b, strong) {
|
|
72
|
-
font-weight: bolder;
|
|
75
|
+
font-weight: bolder; /* Add the correct font weight in Chrome, Edge, and Safari [RS][SE]*/
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
:where(button, h1, h2, h3, h4, h5, h6, input, label, legend) {
|
|
76
|
-
line-height: 1.125;
|
|
79
|
+
line-height: 1.125; /* Set shorter line heights on headings and interactive elements [AB]*/
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
:where(button, [type=button i], [type=reset i], [type=submit i]) {
|
|
80
83
|
-webkit-appearance: button;
|
|
84
|
+
-moz-appearance: button;
|
|
85
|
+
appearance: button; /* Correct the inability to style clickable types in iOS [RS][SE]*/
|
|
81
86
|
cursor: pointer;
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
:where(code, kbd, pre, samp) {
|
|
85
|
-
font-family: monospace, monospace;
|
|
86
|
-
font-size: 1em;
|
|
90
|
+
font-family: monospace, monospace; /* Specify the font family of code elements [RS][SE]*/
|
|
91
|
+
font-size: 1em; /* Correct the odd 'em' font sizing in all browsers [RS][SE]*/
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
:where(details > summary:first-of-type) {
|
|
90
|
-
display: list-item;
|
|
95
|
+
display: list-item; /* Add the correct display in Safari [SE]*/
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
:where(dl, ol, ul) :where(dl, ol, ul) {
|
|
94
|
-
margin: 0;
|
|
99
|
+
margin: 0; /* Remove the margin on nested lists in Chrome, Edge, and Safari [SE]*/
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
:where(h1, h2, h3, h4, h5, h6) {
|
|
@@ -99,11 +104,11 @@
|
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
:where(hr) {
|
|
102
|
-
height: 0;
|
|
103
|
-
overflow: visible;
|
|
107
|
+
height: 0; /* Add the correct box sizing in Firefox [CR][RS][SE]*/
|
|
108
|
+
overflow: visible; /* Show the overflow in Edge and IE [CR][RS]*/
|
|
104
109
|
border-style: solid;
|
|
105
110
|
border-width: 1px 0 0;
|
|
106
|
-
color: inherit;
|
|
111
|
+
color: inherit; /* Correct the inheritance of border colour in Firefox [CR][RS][SE]*/
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
:where(nav) :where(ol, ul),
|
|
@@ -113,13 +118,13 @@
|
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
:where(nav li)::before {
|
|
116
|
-
content: "";
|
|
121
|
+
content: ""; /* Prevent VoiceOver from ignoring list semantics in Safari [SE]*/
|
|
117
122
|
float: left;
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
:where(pre) {
|
|
121
|
-
overflow: auto;
|
|
122
|
-
white-space: pre-wrap;
|
|
126
|
+
overflow: auto; /* Prevent overflow of the container in all browsers [SE]*/
|
|
127
|
+
white-space: pre-wrap; /* Overflow by default is bad… [CR]*/
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
:where(small) {
|
|
@@ -143,83 +148,92 @@
|
|
|
143
148
|
|
|
144
149
|
:where(table) {
|
|
145
150
|
border-collapse: collapse;
|
|
146
|
-
border-color: currentColor;
|
|
147
|
-
text-indent: 0;
|
|
151
|
+
border-color: currentColor; /* Correct table border colour in Chrome, Edge, and Safari [SE]*/
|
|
152
|
+
text-indent: 0; /* Remove text indentation from table contents in Chrome, Edge, and Safari [RS][SE]*/
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
/**
|
|
151
156
|
* Form-related
|
|
152
157
|
*/
|
|
153
158
|
:where(button, input, optgroup, select, textarea) {
|
|
154
|
-
font: inherit;
|
|
159
|
+
font: inherit; /* Correct the font inheritance in Firefox, Edge, and Safari [RS][SE]*/
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
:where(button, input, select, textarea) {
|
|
158
|
-
padding: 0.25em 0.5em;
|
|
159
|
-
background-color: transparent;
|
|
160
|
-
border: 1px solid WindowFrame;
|
|
161
|
-
color: inherit;
|
|
162
|
-
letter-spacing: inherit;
|
|
163
|
+
padding: 0.25em 0.5em; /* Change the inconsistent appearance in all browsers [SE]*/
|
|
164
|
+
background-color: transparent; /* ditto*/
|
|
165
|
+
border: 1px solid WindowFrame; /* ditto*/
|
|
166
|
+
color: inherit; /* ditto*/
|
|
167
|
+
letter-spacing: inherit; /* ditto*/
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
:where(fieldset) {
|
|
166
|
-
border: 1px solid darkgrey;
|
|
171
|
+
border: 1px solid darkgrey; /* Change the inconsistent appearance in all browsers [SE]*/
|
|
167
172
|
}
|
|
168
173
|
|
|
169
174
|
:where(optgroup) {
|
|
170
|
-
font-weight: bold;
|
|
175
|
+
font-weight: bold; /* Restore the font weight unset by previous rules [RS]*/
|
|
171
176
|
}
|
|
172
177
|
|
|
173
178
|
:where(progress) {
|
|
174
|
-
vertical-align: baseline;
|
|
179
|
+
vertical-align: baseline; /* Add the correct vertical alignment in Chrome, Edge, and Firefox [RS][SE]*/
|
|
175
180
|
}
|
|
176
181
|
|
|
177
|
-
:where(select) {
|
|
178
|
-
-moz-appearance: none;
|
|
182
|
+
:where(select, input[list]) {
|
|
179
183
|
-webkit-appearance: none;
|
|
180
|
-
appearance: none;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
-moz-appearance: none;
|
|
185
|
+
appearance: none; /* Change the inconsistent appearance in all browsers [SE]*/
|
|
186
|
+
padding-right: 1em; /* ditto*/
|
|
187
|
+
background: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='4'%3E%3Cpath d='M4 0h6L7 4'/%3E%3C/svg%3E") no-repeat right center/1em; /* ditto*/
|
|
188
|
+
border-radius: 0; /* ditto*/
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
:where(select[multiple]) {
|
|
187
|
-
background-image: none;
|
|
192
|
+
background-image: none; /* Don't show the arrow for multiple choice selects [SE]*/
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
:where(textarea) {
|
|
191
|
-
resize: vertical;
|
|
196
|
+
resize: vertical; /* Change the resize direction in all browsers [RS][SE]*/
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
:where([type=color i], [type=range i]) {
|
|
195
|
-
padding: 0;
|
|
196
|
-
border-width: 0;
|
|
200
|
+
padding: 0; /* Remove the padding in all browsers [SE]*/
|
|
201
|
+
border-width: 0; /* Remove the border in all browsers [SE]*/
|
|
197
202
|
}
|
|
198
203
|
|
|
199
204
|
:where([type=search i]) {
|
|
200
205
|
-webkit-appearance: textfield;
|
|
201
|
-
|
|
206
|
+
-moz-appearance: textfield;
|
|
207
|
+
appearance: textfield; /* Correct the odd appearance in Chrome, Edge, and Safari [RS][SE]*/
|
|
208
|
+
outline-offset: -2px; /* Correct the outline style in Safari [RS][SE]*/
|
|
202
209
|
}
|
|
203
210
|
|
|
204
|
-
|
|
211
|
+
::file-selector-button {
|
|
205
212
|
-webkit-appearance: button;
|
|
213
|
+
-moz-appearance: button;
|
|
214
|
+
appearance: button; /* Correct the inability to style upload buttons in iOS and Safari [RS][SE]*/
|
|
206
215
|
color: inherit;
|
|
207
216
|
font: inherit;
|
|
208
217
|
line-height: 1.25;
|
|
209
218
|
}
|
|
210
219
|
|
|
211
|
-
::-
|
|
212
|
-
|
|
213
|
-
|
|
220
|
+
::-moz-placeholder {
|
|
221
|
+
opacity: 0.54; /* Correct the text style of placeholders in Chrome, Edge, and Safari. [SE]*/
|
|
222
|
+
color: inherit; /* Correct the text style of placeholders in Chrome, Edge, and Safari. [SE]*/
|
|
214
223
|
}
|
|
215
224
|
|
|
216
|
-
|
|
217
|
-
opacity: 0.54;
|
|
218
|
-
color: inherit;
|
|
225
|
+
::placeholder {
|
|
226
|
+
opacity: 0.54; /* Correct the text style of placeholders in Chrome, Edge, and Safari. [SE]*/
|
|
227
|
+
color: inherit; /* Correct the text style of placeholders in Chrome, Edge, and Safari. [SE]*/
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
::-webkit-inner-spin-button,
|
|
231
|
+
::-webkit-outer-spin-button {
|
|
232
|
+
height: auto; /* Correct the cursor style of increment and decrement buttons in Safari [RS][SE]*/
|
|
219
233
|
}
|
|
220
234
|
|
|
221
235
|
::-webkit-search-decoration {
|
|
222
|
-
-webkit-appearance: none;
|
|
236
|
+
-webkit-appearance: none; /* Remove the inner padding in Chrome, Edge, and Safari on macOS [SE]*/
|
|
223
237
|
}
|
|
224
238
|
|
|
225
239
|
/**
|
|
@@ -235,7 +249,7 @@
|
|
|
235
249
|
}
|
|
236
250
|
|
|
237
251
|
:where(audio) {
|
|
238
|
-
width: 100%;
|
|
252
|
+
width: 100%; /* Better default width [CR]*/
|
|
239
253
|
}
|
|
240
254
|
|
|
241
255
|
:where(audio:not([controls])) {
|
|
@@ -243,7 +257,7 @@
|
|
|
243
257
|
}
|
|
244
258
|
|
|
245
259
|
:where(canvas, img, svg, video) {
|
|
246
|
-
height: auto;
|
|
260
|
+
height: auto; /* Maintain aspect ratios [CR]*/
|
|
247
261
|
}
|
|
248
262
|
|
|
249
263
|
:where(iframe) {
|
|
@@ -251,11 +265,11 @@
|
|
|
251
265
|
}
|
|
252
266
|
|
|
253
267
|
:where(picture) {
|
|
254
|
-
display: contents;
|
|
268
|
+
display: contents; /* Remove the unnecessary wrapping 'picture', while maintaining contents [CR]*/
|
|
255
269
|
}
|
|
256
270
|
|
|
257
271
|
:where(source) {
|
|
258
|
-
display: none;
|
|
272
|
+
display: none; /* Source elements have nothing to display, so we hide them entirely [CR]*/
|
|
259
273
|
}
|
|
260
274
|
|
|
261
275
|
:where(svg:not([fill])) {
|
|
@@ -280,3 +294,5 @@
|
|
|
280
294
|
:where([aria-controls]) {
|
|
281
295
|
cursor: pointer;
|
|
282
296
|
}
|
|
297
|
+
|
|
298
|
+
|
package/dist/css/table.css
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/*! https://github.com/oscarpalmer/oui */
|
|
1
2
|
:where(.oui-table) {
|
|
2
3
|
width: 100%;
|
|
3
4
|
}
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
top: 0;
|
|
23
24
|
z-index: 2;
|
|
24
25
|
}
|
|
25
|
-
:where(.oui-table tbody tr:nth-
|
|
26
|
+
:where(.oui-table tbody tr:nth-of-type(2n)) {
|
|
26
27
|
background-color: color-mix(in oklab, var(--oui-blue-0), transparent 67%);
|
|
27
28
|
}
|
|
28
29
|
:where(.oui-table th, .oui-table tfoot td) {
|
|
@@ -63,3 +64,5 @@
|
|
|
63
64
|
:where(.oui-table tr th:last-child, .oui-table tr td:last-child) {
|
|
64
65
|
border-right-width: 0;
|
|
65
66
|
}
|
|
67
|
+
|
|
68
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
4
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
5
|
+
const number = require("@oscarpalmer/atoms/number");
|
|
6
|
+
const event = require("@oscarpalmer/toretto/event");
|
|
7
|
+
const find = require("@oscarpalmer/toretto/find");
|
|
8
|
+
const focusable = require("@oscarpalmer/toretto/focusable");
|
|
9
|
+
class FocusTrap {
|
|
10
|
+
constructor(element) {
|
|
11
|
+
__publicField(this, "active", true);
|
|
12
|
+
this.element = element;
|
|
13
|
+
element.setAttribute("aria-modal", "true");
|
|
14
|
+
element.setAttribute("tabindex", "-1");
|
|
15
|
+
}
|
|
16
|
+
activate() {
|
|
17
|
+
this.element.setAttribute("aria-modal", "true");
|
|
18
|
+
this.active = true;
|
|
19
|
+
}
|
|
20
|
+
destroy() {
|
|
21
|
+
this.active = false;
|
|
22
|
+
this.element = void 0;
|
|
23
|
+
}
|
|
24
|
+
inactivate() {
|
|
25
|
+
this.element.setAttribute("aria-modal", "false");
|
|
26
|
+
this.active = false;
|
|
27
|
+
inactive.add(this);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function addFocusTraps(nodes) {
|
|
31
|
+
const elements = [...nodes].filter(
|
|
32
|
+
(node) => node instanceof HTMLElement
|
|
33
|
+
);
|
|
34
|
+
const { length } = elements;
|
|
35
|
+
for (let index = 0; index < length; index += 1) {
|
|
36
|
+
const element = elements[index];
|
|
37
|
+
if (element.hasAttribute(selector)) {
|
|
38
|
+
focusTraps.set(element, new FocusTrap(element));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function onEscape(event2) {
|
|
43
|
+
var _a;
|
|
44
|
+
if (event2.key !== "Escape") {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const element = find.findAncestor(
|
|
48
|
+
document.activeElement,
|
|
49
|
+
`[${selector}]`
|
|
50
|
+
);
|
|
51
|
+
(_a = focusTraps.get(element)) == null ? void 0 : _a.inactivate();
|
|
52
|
+
}
|
|
53
|
+
function onFocusOut() {
|
|
54
|
+
if (inactive.size === 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
requestAnimationFrame(() => {
|
|
58
|
+
const element = find.findAncestor(
|
|
59
|
+
document.activeElement,
|
|
60
|
+
`[${selector}]`
|
|
61
|
+
);
|
|
62
|
+
if (!focusTraps.has(element)) {
|
|
63
|
+
for (const focusTrap of inactive) {
|
|
64
|
+
focusTrap.activate();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function onTab(event2) {
|
|
70
|
+
var _a;
|
|
71
|
+
if (event2.key !== "Tab") {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const element = find.findAncestor(
|
|
75
|
+
event2.target,
|
|
76
|
+
`[${selector}]`
|
|
77
|
+
);
|
|
78
|
+
if (!(((_a = focusTraps.get(element)) == null ? void 0 : _a.active) ?? false)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
event2.preventDefault();
|
|
82
|
+
const elements = focusable.getFocusable(element);
|
|
83
|
+
const { length } = elements;
|
|
84
|
+
const index = elements.indexOf(event2.target);
|
|
85
|
+
if (index === -1 || length === 0) {
|
|
86
|
+
if (length === 0) {
|
|
87
|
+
element.focus();
|
|
88
|
+
} else {
|
|
89
|
+
elements[0].focus();
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const next = number.clamp(index + (event2.shiftKey ? -1 : 1), 0, length - 1, true);
|
|
94
|
+
if (next !== index) {
|
|
95
|
+
elements[next].focus();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function removeFocusTraps(nodes) {
|
|
99
|
+
var _a;
|
|
100
|
+
const elements = [...nodes].filter(
|
|
101
|
+
(node) => node instanceof HTMLElement
|
|
102
|
+
);
|
|
103
|
+
const { length } = elements;
|
|
104
|
+
for (let index = 0; index < length; index += 1) {
|
|
105
|
+
const element = elements[index];
|
|
106
|
+
(_a = focusTraps.get(element)) == null ? void 0 : _a.destroy();
|
|
107
|
+
focusTraps.delete(element);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const focusTraps = /* @__PURE__ */ new WeakMap();
|
|
111
|
+
const inactive = /* @__PURE__ */ new Set();
|
|
112
|
+
const observer = new MutationObserver((entries) => {
|
|
113
|
+
const { length } = entries;
|
|
114
|
+
for (let index = 0; index < length; index += 1) {
|
|
115
|
+
const entry = entries[index];
|
|
116
|
+
if (entry.type === "attributes" && entry.target instanceof HTMLElement) {
|
|
117
|
+
if (entry.target.hasAttribute(selector)) {
|
|
118
|
+
addFocusTraps([entry.target]);
|
|
119
|
+
} else {
|
|
120
|
+
removeFocusTraps([entry.target]);
|
|
121
|
+
}
|
|
122
|
+
} else if (entry.type === "childList") {
|
|
123
|
+
addFocusTraps(entry.addedNodes);
|
|
124
|
+
removeFocusTraps(entry.removedNodes);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
const selector = "oui-focus-trap";
|
|
129
|
+
observer.observe(document, {
|
|
130
|
+
attributeFilter: [selector],
|
|
131
|
+
attributes: true,
|
|
132
|
+
childList: true,
|
|
133
|
+
subtree: true
|
|
134
|
+
});
|
|
135
|
+
requestAnimationFrame(() => {
|
|
136
|
+
addFocusTraps(document.querySelectorAll(`[${selector}]`));
|
|
137
|
+
event.on(document, "focusout", onFocusOut, {
|
|
138
|
+
capture: true
|
|
139
|
+
});
|
|
140
|
+
event.on(document, "keydown", onTab, {
|
|
141
|
+
capture: true,
|
|
142
|
+
passive: false
|
|
143
|
+
});
|
|
144
|
+
event.on(document, "keydown", onEscape, {
|
|
145
|
+
passive: false
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
import { clamp } from "@oscarpalmer/atoms/number";
|
|
5
|
+
import { on } from "@oscarpalmer/toretto/event";
|
|
6
|
+
import { findAncestor } from "@oscarpalmer/toretto/find";
|
|
7
|
+
import { getFocusable } from "@oscarpalmer/toretto/focusable";
|
|
8
|
+
class FocusTrap {
|
|
9
|
+
constructor(element) {
|
|
10
|
+
__publicField(this, "active", true);
|
|
11
|
+
this.element = element;
|
|
12
|
+
element.setAttribute("aria-modal", "true");
|
|
13
|
+
element.setAttribute("tabindex", "-1");
|
|
14
|
+
}
|
|
15
|
+
activate() {
|
|
16
|
+
this.element.setAttribute("aria-modal", "true");
|
|
17
|
+
this.active = true;
|
|
18
|
+
}
|
|
19
|
+
destroy() {
|
|
20
|
+
this.active = false;
|
|
21
|
+
this.element = void 0;
|
|
22
|
+
}
|
|
23
|
+
inactivate() {
|
|
24
|
+
this.element.setAttribute("aria-modal", "false");
|
|
25
|
+
this.active = false;
|
|
26
|
+
inactive.add(this);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function addFocusTraps(nodes) {
|
|
30
|
+
const elements = [...nodes].filter(
|
|
31
|
+
(node) => node instanceof HTMLElement
|
|
32
|
+
);
|
|
33
|
+
const { length } = elements;
|
|
34
|
+
for (let index = 0; index < length; index += 1) {
|
|
35
|
+
const element = elements[index];
|
|
36
|
+
if (element.hasAttribute(selector)) {
|
|
37
|
+
focusTraps.set(element, new FocusTrap(element));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function onEscape(event) {
|
|
42
|
+
var _a;
|
|
43
|
+
if (event.key !== "Escape") {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const element = findAncestor(
|
|
47
|
+
document.activeElement,
|
|
48
|
+
`[${selector}]`
|
|
49
|
+
);
|
|
50
|
+
(_a = focusTraps.get(element)) == null ? void 0 : _a.inactivate();
|
|
51
|
+
}
|
|
52
|
+
function onFocusOut() {
|
|
53
|
+
if (inactive.size === 0) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
requestAnimationFrame(() => {
|
|
57
|
+
const element = findAncestor(
|
|
58
|
+
document.activeElement,
|
|
59
|
+
`[${selector}]`
|
|
60
|
+
);
|
|
61
|
+
if (!focusTraps.has(element)) {
|
|
62
|
+
for (const focusTrap of inactive) {
|
|
63
|
+
focusTrap.activate();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function onTab(event) {
|
|
69
|
+
var _a;
|
|
70
|
+
if (event.key !== "Tab") {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const element = findAncestor(
|
|
74
|
+
event.target,
|
|
75
|
+
`[${selector}]`
|
|
76
|
+
);
|
|
77
|
+
if (!(((_a = focusTraps.get(element)) == null ? void 0 : _a.active) ?? false)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
const elements = getFocusable(element);
|
|
82
|
+
const { length } = elements;
|
|
83
|
+
const index = elements.indexOf(event.target);
|
|
84
|
+
if (index === -1 || length === 0) {
|
|
85
|
+
if (length === 0) {
|
|
86
|
+
element.focus();
|
|
87
|
+
} else {
|
|
88
|
+
elements[0].focus();
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const next = clamp(index + (event.shiftKey ? -1 : 1), 0, length - 1, true);
|
|
93
|
+
if (next !== index) {
|
|
94
|
+
elements[next].focus();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function removeFocusTraps(nodes) {
|
|
98
|
+
var _a;
|
|
99
|
+
const elements = [...nodes].filter(
|
|
100
|
+
(node) => node instanceof HTMLElement
|
|
101
|
+
);
|
|
102
|
+
const { length } = elements;
|
|
103
|
+
for (let index = 0; index < length; index += 1) {
|
|
104
|
+
const element = elements[index];
|
|
105
|
+
(_a = focusTraps.get(element)) == null ? void 0 : _a.destroy();
|
|
106
|
+
focusTraps.delete(element);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const focusTraps = /* @__PURE__ */ new WeakMap();
|
|
110
|
+
const inactive = /* @__PURE__ */ new Set();
|
|
111
|
+
const observer = new MutationObserver((entries) => {
|
|
112
|
+
const { length } = entries;
|
|
113
|
+
for (let index = 0; index < length; index += 1) {
|
|
114
|
+
const entry = entries[index];
|
|
115
|
+
if (entry.type === "attributes" && entry.target instanceof HTMLElement) {
|
|
116
|
+
if (entry.target.hasAttribute(selector)) {
|
|
117
|
+
addFocusTraps([entry.target]);
|
|
118
|
+
} else {
|
|
119
|
+
removeFocusTraps([entry.target]);
|
|
120
|
+
}
|
|
121
|
+
} else if (entry.type === "childList") {
|
|
122
|
+
addFocusTraps(entry.addedNodes);
|
|
123
|
+
removeFocusTraps(entry.removedNodes);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
const selector = "oui-focus-trap";
|
|
128
|
+
observer.observe(document, {
|
|
129
|
+
attributeFilter: [selector],
|
|
130
|
+
attributes: true,
|
|
131
|
+
childList: true,
|
|
132
|
+
subtree: true
|
|
133
|
+
});
|
|
134
|
+
requestAnimationFrame(() => {
|
|
135
|
+
addFocusTraps(document.querySelectorAll(`[${selector}]`));
|
|
136
|
+
on(document, "focusout", onFocusOut, {
|
|
137
|
+
capture: true
|
|
138
|
+
});
|
|
139
|
+
on(document, "keydown", onTab, {
|
|
140
|
+
capture: true,
|
|
141
|
+
passive: false
|
|
142
|
+
});
|
|
143
|
+
on(document, "keydown", onEscape, {
|
|
144
|
+
passive: false
|
|
145
|
+
});
|
|
146
|
+
});
|
package/dist/js/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./focus-trap.js";
|
package/package.json
CHANGED
|
@@ -3,13 +3,54 @@
|
|
|
3
3
|
"name": "Oscar Palmér",
|
|
4
4
|
"url": "https://oscarpalmer.se"
|
|
5
5
|
},
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@oscarpalmer/atoms": "^0.102",
|
|
8
|
+
"@oscarpalmer/toretto": "^0.20"
|
|
9
|
+
},
|
|
6
10
|
"description": "Styles, components, and utilities for building websites and applications.",
|
|
7
11
|
"devDependencies": {
|
|
8
12
|
"@biomejs/biome": "^1.9",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
13
|
+
"@rollup/plugin-node-resolve": "^16",
|
|
14
|
+
"@rollup/plugin-typescript": "^12.1",
|
|
15
|
+
"@types/node": "^24",
|
|
16
|
+
"@vitest/coverage-istanbul": "^3.2",
|
|
17
|
+
"autoprefixer": "^10.4",
|
|
18
|
+
"dts-bundle-generator": "^9.5",
|
|
19
|
+
"glob": "^11",
|
|
20
|
+
"jsdom": "^26.1",
|
|
21
|
+
"postcss-cli": "^11",
|
|
22
|
+
"postcss-input-range": "^6",
|
|
23
|
+
"postcss-scss": "^4",
|
|
24
|
+
"sass": "^1.89",
|
|
25
|
+
"stylelint": "^16.20",
|
|
26
|
+
"tslib": "^2.8",
|
|
27
|
+
"typescript": "^5.8",
|
|
28
|
+
"vite": "^6.3",
|
|
29
|
+
"vitest": "^3.2"
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"import": {
|
|
34
|
+
"types": "./types/index.d.ts",
|
|
35
|
+
"default": "./dist/js/index.js"
|
|
36
|
+
},
|
|
37
|
+
"require": {
|
|
38
|
+
"types": "./types/index.d.cts",
|
|
39
|
+
"default": "./dist/js/index.cjs"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"./focus-trap": {
|
|
43
|
+
"import": {
|
|
44
|
+
"types": "./types/focus-trap.d.ts",
|
|
45
|
+
"default": "./dist/js/focus-trap.js"
|
|
46
|
+
},
|
|
47
|
+
"require": {
|
|
48
|
+
"types": "./types/focus-trap.d.cts",
|
|
49
|
+
"default": "./dist/js/focus-trap.cjs"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
11
52
|
},
|
|
12
|
-
"files": ["dist", "src"],
|
|
53
|
+
"files": ["dist", "src", "types"],
|
|
13
54
|
"keywords": [
|
|
14
55
|
"css",
|
|
15
56
|
"scss",
|
|
@@ -21,15 +62,27 @@
|
|
|
21
62
|
"utility"
|
|
22
63
|
],
|
|
23
64
|
"license": "MIT",
|
|
65
|
+
"main": "dist/js/index.cjs",
|
|
66
|
+
"module": "dist/js/index.js",
|
|
24
67
|
"name": "@oscarpalmer/oui",
|
|
25
68
|
"repository": {
|
|
26
69
|
"type": "git",
|
|
27
70
|
"url": "git+https://github.com/oscarpalmer/oui.git"
|
|
28
71
|
},
|
|
29
72
|
"scripts": {
|
|
30
|
-
"build": "
|
|
31
|
-
"
|
|
73
|
+
"build": "npm run clean && npm run build:css && npm run build:js",
|
|
74
|
+
"build:css": "npm run build:postcss && npx sass ./postcss:./dist/css --no-source-map",
|
|
75
|
+
"build:postcss": "npx postcss ./src/css/*.scss -d ./postcss -u autoprefixer postcss-input-range --parser postcss-scss",
|
|
76
|
+
"build:js": "npx vite build && npm run types",
|
|
77
|
+
"clean": "rm -rf ./dist && rm -rf ./types && rm -f ./tsconfig.tsbuildinfo",
|
|
78
|
+
"rollup": "npx rollup -c --watch",
|
|
79
|
+
"test": "npx vitest --coverage",
|
|
80
|
+
"types": "npx tsc && npx dts-bundle-generator --config ./dts.config.cts --silent",
|
|
81
|
+
"watch:css": "npx sass ./src/css:./dist/css --no-source-map --watch",
|
|
82
|
+
"watch:postcss": "npx postcss ./src/css/*.scss -d ./postcss -u autoprefixer postcss-input-range --parser postcss-scss --watch",
|
|
83
|
+
"watch:js": "npx vite build --watch"
|
|
32
84
|
},
|
|
33
85
|
"type": "module",
|
|
34
|
-
"
|
|
86
|
+
"types": "types/index.d.cts",
|
|
87
|
+
"version": "0.10.0"
|
|
35
88
|
}
|