@iyulab/components 1.47.0 → 1.47.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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.47.1] - 2026-09-28
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`u-dialog` / `u-drawer` initial focus now reaches the control you asked for.** Three cases
|
|
8
|
+
sent it to the first tabbable element instead (often the close button):
|
|
9
|
+
- **`<u-input autofocus>`** (and any control component) — the component host was handed to the
|
|
10
|
+
focus trap, which does not count a host as focusable and fell back. The overlay now focuses the
|
|
11
|
+
target itself, so the component forwards focus to its inner field.
|
|
12
|
+
- **A form component slotted into the overlay** — `[autofocus]` and the first input control were
|
|
13
|
+
looked up with `querySelector`, which stops at shadow roots. The search now goes through them.
|
|
14
|
+
- **A `u-select` before the target** — once the search goes through shadow roots it must not stop
|
|
15
|
+
at the closed popover every `u-select` keeps inside (it carries an `autofocus` attribute of its
|
|
16
|
+
own). Only elements that can actually take focus are candidates.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`querySelectorDeep(roots, selectors, accept?)` and `isFocusCandidate(el)`** in
|
|
21
|
+
`utilities/elements` — the shadow-piercing lookup and the focusability test the overlays use,
|
|
22
|
+
for other layers that place initial focus the same way.
|
|
23
|
+
|
|
3
24
|
## [1.47.0] - 2026-09-28
|
|
4
25
|
|
|
5
26
|
### Added
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { UElement } from "./UElement.js";
|
|
2
|
+
import { isFocusCandidate, querySelectorDeep } from "../utilities/elements.js";
|
|
2
3
|
import __decorateMetadata from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
3
4
|
import __decorate from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
4
5
|
import { OverlayManager } from "../utilities/OverlayManager.js";
|
|
@@ -129,10 +130,13 @@ var UOverlayElement = class extends UElement {
|
|
|
129
130
|
* (`tests/browser/overlay-initial-focus.browser.test.ts` 가 둘 다 감시한다.)
|
|
130
131
|
*/
|
|
131
132
|
resolveInitialFocus() {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
const content = Array.from(this.children);
|
|
134
|
+
const target = querySelectorDeep(content, "[autofocus]", isFocusCandidate) ?? querySelectorDeep(content, "input, select, textarea, u-input, u-textarea, u-select, u-checkbox, u-radio, u-switch, u-slider", isFocusCandidate);
|
|
135
|
+
if (!target) return void 0;
|
|
136
|
+
target.focus();
|
|
137
|
+
let active = document.activeElement;
|
|
138
|
+
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
|
139
|
+
for (let n = active; n; n = n.parentNode ?? n.host ?? null) if (n === target) return false;
|
|
136
140
|
}
|
|
137
141
|
/** 오버레이가 닫힐 때 설정을 해제합니다. */
|
|
138
142
|
cleanup() {
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { UElement } from "./components/UElement.js";
|
|
2
|
-
import { getParentElement, isCoarsePointer, querySelectorAllWithin, querySelectorWithin } from "./utilities/elements.js";
|
|
2
|
+
import { getParentElement, isCoarsePointer, isFocusCandidate, querySelectorAllWithin, querySelectorDeep, querySelectorWithin } from "./utilities/elements.js";
|
|
3
3
|
import { UFloatingElement } from "./components/UFloatingElement.js";
|
|
4
4
|
import { UFormControlElement } from "./components/UFormControlElement.js";
|
|
5
5
|
import { OverlayManager } from "./utilities/OverlayManager.js";
|
|
@@ -59,4 +59,4 @@ import { BrowserStorage } from "./utilities/BrowserStorage.js";
|
|
|
59
59
|
import { Dialog } from "./utilities/Dialog.js";
|
|
60
60
|
import { Theme } from "./utilities/Theme.js";
|
|
61
61
|
import { Toast } from "./utilities/Toast.js";
|
|
62
|
-
export { ALERT_STATUSES, BrowserStorage, Dialog, IconCache, IconRegistry, Locale, OverlayManager, Theme, Toast, UAlert, UAvatar, UBadge, UBreadcrumb, UBreadcrumbItem, UButton, UButtonGroup, UCard, UCarousel, UCheckbox, UChip, UCopyButton, UDatePicker, UDialog, UDivider, UDrawer, UElement, UExpander, UField, UFileInput, UFloatingElement, UForm, UFormControlElement, UIcon, UIconButton, UInput, UMenu, UMenuItem, UOption, UOverlayElement, UPanel, UPopover, UProgressBar, UProgressRing, URadio, URating, USelect, USkeleton, USlider, USpinner, USplitPanel, USwitch, UTab, UTabPanel, UTag, UText, UTextarea, UTooltip, UTree, UTreeItem, arrayAttrConverter, booleanAttrConverter, createDevWarner, dateAttrConverter, devWarnOnce, formatCurrency, formatDate, formatNumber, getDefaultBaseUrl, getParentElement, isCoarsePointer, jsonAttrConverter, querySelectorAllWithin, querySelectorWithin, resetDevWarnings, setDefaultBaseUrl, urlAttrConverter };
|
|
62
|
+
export { ALERT_STATUSES, BrowserStorage, Dialog, IconCache, IconRegistry, Locale, OverlayManager, Theme, Toast, UAlert, UAvatar, UBadge, UBreadcrumb, UBreadcrumbItem, UButton, UButtonGroup, UCard, UCarousel, UCheckbox, UChip, UCopyButton, UDatePicker, UDialog, UDivider, UDrawer, UElement, UExpander, UField, UFileInput, UFloatingElement, UForm, UFormControlElement, UIcon, UIconButton, UInput, UMenu, UMenuItem, UOption, UOverlayElement, UPanel, UPopover, UProgressBar, UProgressRing, URadio, URating, USelect, USkeleton, USlider, USpinner, USplitPanel, USwitch, UTab, UTabPanel, UTag, UText, UTextarea, UTooltip, UTree, UTreeItem, arrayAttrConverter, booleanAttrConverter, createDevWarner, dateAttrConverter, devWarnOnce, formatCurrency, formatDate, formatNumber, getDefaultBaseUrl, getParentElement, isCoarsePointer, isFocusCandidate, jsonAttrConverter, querySelectorAllWithin, querySelectorDeep, querySelectorWithin, resetDevWarnings, setDefaultBaseUrl, urlAttrConverter };
|
|
@@ -21,3 +21,17 @@ export declare function querySelectorAllWithin(element: Element, selectors: stri
|
|
|
21
21
|
* 컴포넌트가 이를 구분하지 않으면 열리자마자 닫히는 결함으로 이어집니다(모바일 실측).
|
|
22
22
|
*/
|
|
23
23
|
export declare function isCoarsePointer(event: PointerEvent): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* 포커스를 «받을 수 있는» 요소인가 — 초기 포커스 후보를 고를 때 쓴다.
|
|
26
|
+
* 네이티브 포커스 요소 · `focus()` 를 재정의한(안쪽으로 넘겨 주는) 컴포넌트 · `tabindex` 를 가진 요소이고,
|
|
27
|
+
* 비활성·`inert`·안 보임이 아니어야 한다. 걸러진 호스트는 그 안쪽(섀도 루트)에서 계속 찾는다.
|
|
28
|
+
* `u-select` 의 섀도에는 속성 `autofocus` 를 가진 닫힌 `u-popover` 가 늘 있다 — 이것을 집으면
|
|
29
|
+
* 포커스가 아무 데도 가지 않는다.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isFocusCandidate(el: Element): el is HTMLElement;
|
|
32
|
+
/**
|
|
33
|
+
* `roots` 와 그 자손에서(열린 섀도 루트 안쪽까지) `selectors` 에 맞는 첫 요소를 문서 순서로 찾는다.
|
|
34
|
+
* 섀도 루트 안은 호스트 자리에서 이어서 본다 — `querySelector` 는 섀도 경계에서 멈추므로,
|
|
35
|
+
* 슬롯 자식이 컴포넌트면 그 템플릿 안의 요소를 못 찾는다. `accept` 가 거른 요소도 그 안쪽은 계속 본다.
|
|
36
|
+
*/
|
|
37
|
+
export declare function querySelectorDeep(roots: Iterable<Element>, selectors: string, accept?: (el: Element) => boolean): HTMLElement | null;
|
|
@@ -42,5 +42,43 @@ function querySelectorAllWithin(element, selectors) {
|
|
|
42
42
|
function isCoarsePointer(event) {
|
|
43
43
|
return event.pointerType === "touch" || event.pointerType === "pen";
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* 포커스를 «받을 수 있는» 요소인가 — 초기 포커스 후보를 고를 때 쓴다.
|
|
47
|
+
* 네이티브 포커스 요소 · `focus()` 를 재정의한(안쪽으로 넘겨 주는) 컴포넌트 · `tabindex` 를 가진 요소이고,
|
|
48
|
+
* 비활성·`inert`·안 보임이 아니어야 한다. 걸러진 호스트는 그 안쪽(섀도 루트)에서 계속 찾는다.
|
|
49
|
+
* `u-select` 의 섀도에는 속성 `autofocus` 를 가진 닫힌 `u-popover` 가 늘 있다 — 이것을 집으면
|
|
50
|
+
* 포커스가 아무 데도 가지 않는다.
|
|
51
|
+
*/
|
|
52
|
+
function isFocusCandidate(el) {
|
|
53
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
54
|
+
const native = el.matches("input:not([type=\"hidden\"]), select, textarea, button, a[href], [contenteditable=\"\"], [contenteditable=\"true\"]");
|
|
55
|
+
const forwards = el.focus !== HTMLElement.prototype.focus;
|
|
56
|
+
if (!native && !forwards && !el.hasAttribute("tabindex")) return false;
|
|
57
|
+
if (el.matches(":disabled") || el.hasAttribute("disabled") || el.closest("[inert]")) return false;
|
|
58
|
+
return typeof el.checkVisibility === "function" ? el.checkVisibility() : true;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* `roots` 와 그 자손에서(열린 섀도 루트 안쪽까지) `selectors` 에 맞는 첫 요소를 문서 순서로 찾는다.
|
|
62
|
+
* 섀도 루트 안은 호스트 자리에서 이어서 본다 — `querySelector` 는 섀도 경계에서 멈추므로,
|
|
63
|
+
* 슬롯 자식이 컴포넌트면 그 템플릿 안의 요소를 못 찾는다. `accept` 가 거른 요소도 그 안쪽은 계속 본다.
|
|
64
|
+
*/
|
|
65
|
+
function querySelectorDeep(roots, selectors, accept = () => true) {
|
|
66
|
+
const visit = (start) => {
|
|
67
|
+
const walker = document.createTreeWalker(start, NodeFilter.SHOW_ELEMENT);
|
|
68
|
+
for (let el = walker.currentNode; el; el = walker.nextNode()) {
|
|
69
|
+
if (el.matches(selectors) && accept(el)) return el;
|
|
70
|
+
if (el.shadowRoot) for (const child of Array.from(el.shadowRoot.children)) {
|
|
71
|
+
const found = visit(child);
|
|
72
|
+
if (found) return found;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
for (const root of roots) {
|
|
78
|
+
const found = visit(root);
|
|
79
|
+
if (found) return found;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
45
83
|
//#endregion
|
|
46
|
-
export { getParentElement, isCoarsePointer, querySelectorAllWithin, querySelectorWithin };
|
|
84
|
+
export { getParentElement, isCoarsePointer, isFocusCandidate, querySelectorAllWithin, querySelectorDeep, querySelectorWithin };
|
package/package.json
CHANGED
|
@@ -92,7 +92,7 @@ whole contract. Measured in a real browser
|
|
|
92
92
|
|
|
93
93
|
| Requirement | How it is met |
|
|
94
94
|
|---|---|
|
|
95
|
-
| Focus the first input on open, restore the trigger on close | `[autofocus]` → first input control → first tabbable; focus is returned by the trap |
|
|
95
|
+
| Focus the first input on open, restore the trigger on close | `[autofocus]` → first input control → first tabbable, searched through shadow roots (a form component slotted in works the same); focus is returned by the trap |
|
|
96
96
|
| Body scrolls, footer stays visible | `part="body"` is `flex: 1; overflow: auto`; the `footer` slot is `flex-shrink: 0` |
|
|
97
97
|
| Focus cannot leave the panel | `mode="modal"` (default) activates the focus trap |
|
|
98
98
|
| `Esc` closes, background scroll is locked | `closeOn` defaults to `['escape','backdrop','button']` |
|