@iyulab/components 1.47.0 → 1.48.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/CHANGELOG.md +30 -0
- package/dist/components/UOverlayElement.d.ts +2 -2
- package/dist/components/UOverlayElement.js +8 -4
- package/dist/index.js +2 -2
- package/dist/utilities/elements.d.ts +14 -0
- package/dist/utilities/elements.js +39 -1
- package/package.json +7 -3
- package/skills/iyulab-components/references/components/dialog.md +1 -1
- package/skills/iyulab-components/references/components/drawer.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.48.0] - 2026-09-28
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **`glob` is now an optional peer dependency instead of a dependency.** Only the build plugin
|
|
8
|
+
`@iyulab/components/plugins/vite-plugin-react-wrapper` uses it, yet every app installing the
|
|
9
|
+
components also installed `glob` and its five transitive packages. Browser apps install none of
|
|
10
|
+
them now. If your build imports that plugin, add `glob` to your `devDependencies`.
|
|
11
|
+
|
|
12
|
+
## [1.47.1] - 2026-09-28
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **`u-dialog` / `u-drawer` initial focus now reaches the control you asked for.** Three cases
|
|
17
|
+
sent it to the first tabbable element instead (often the close button):
|
|
18
|
+
- **`<u-input autofocus>`** (and any control component) — the component host was handed to the
|
|
19
|
+
focus trap, which does not count a host as focusable and fell back. The overlay now focuses the
|
|
20
|
+
target itself, so the component forwards focus to its inner field.
|
|
21
|
+
- **A form component slotted into the overlay** — `[autofocus]` and the first input control were
|
|
22
|
+
looked up with `querySelector`, which stops at shadow roots. The search now goes through them.
|
|
23
|
+
- **A `u-select` before the target** — once the search goes through shadow roots it must not stop
|
|
24
|
+
at the closed popover every `u-select` keeps inside (it carries an `autofocus` attribute of its
|
|
25
|
+
own). Only elements that can actually take focus are candidates.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **`querySelectorDeep(roots, selectors, accept?)` and `isFocusCandidate(el)`** in
|
|
30
|
+
`utilities/elements` — the shadow-piercing lookup and the focusability test the overlays use,
|
|
31
|
+
for other layers that place initial focus the same way.
|
|
32
|
+
|
|
3
33
|
## [1.47.0] - 2026-09-28
|
|
4
34
|
|
|
5
35
|
### Added
|
|
@@ -28,8 +28,8 @@ export declare abstract class UOverlayElement extends UElement {
|
|
|
28
28
|
contained: boolean;
|
|
29
29
|
/**
|
|
30
30
|
* 오버레이 모드
|
|
31
|
-
* - `modal` — focus trap, scroll lock, 백드롭 차단 (기본값)
|
|
32
|
-
* - `non-modal` — 주변 UI와 자유 상호작용, 배경 투명, scroll lock
|
|
31
|
+
* - `modal` — focus trap, 초기 포커스 배치, scroll lock, 백드롭 차단 (기본값)
|
|
32
|
+
* - `non-modal` — 주변 UI와 자유 상호작용, 배경 투명, scroll lock 없음, 열려도 포커스를 옮기지 않음
|
|
33
33
|
*
|
|
34
34
|
* @default 'modal'
|
|
35
35
|
*/
|
|
@@ -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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iyulab/components",
|
|
3
3
|
"description": "web-components library based on lit-element made by iyulab",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.48.0",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"iyulab",
|
|
7
7
|
"components",
|
|
@@ -66,13 +66,13 @@
|
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@floating-ui/dom": "^1.8.0",
|
|
68
68
|
"focus-trap": "^8.2.2",
|
|
69
|
-
"glob": "^13.0.6",
|
|
70
69
|
"lit": "^3.3.3",
|
|
71
70
|
"tabbable": "^6.5.0"
|
|
72
71
|
},
|
|
73
72
|
"peerDependencies": {
|
|
74
73
|
"@lit/react": ">=1.0.8",
|
|
75
|
-
"react": ">=18.0.0"
|
|
74
|
+
"react": ">=18.0.0",
|
|
75
|
+
"glob": ">=13.0.0"
|
|
76
76
|
},
|
|
77
77
|
"peerDependenciesMeta": {
|
|
78
78
|
"@lit/react": {
|
|
@@ -80,6 +80,9 @@
|
|
|
80
80
|
},
|
|
81
81
|
"react": {
|
|
82
82
|
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"glob": {
|
|
85
|
+
"optional": true
|
|
83
86
|
}
|
|
84
87
|
},
|
|
85
88
|
"devDependencies": {
|
|
@@ -89,6 +92,7 @@
|
|
|
89
92
|
"@vitest/browser-playwright": "^5.0.0",
|
|
90
93
|
"eslint": "^10.1.0",
|
|
91
94
|
"eslint-plugin-lit": "^2.3.1",
|
|
95
|
+
"glob": "^13.0.6",
|
|
92
96
|
"globals": "^17.7.0",
|
|
93
97
|
"happy-dom": "^20.10.6",
|
|
94
98
|
"react": "^19.2.7",
|
|
@@ -60,7 +60,7 @@ panel of that size. A short dialog stays short.
|
|
|
60
60
|
| `closable` | `boolean` | `false` | ✓ | Show close button |
|
|
61
61
|
| `placement` | `'top-start'\|'top'\|'top-end'\|'start'\|'center'\|'end'\|'bottom-start'\|'bottom'\|'bottom-end'` | `'center'` | ✓ | Position on screen |
|
|
62
62
|
| `offset` | `number` | `0` | ✓ | Distance from screen edge in px |
|
|
63
|
-
| `mode` | `'modal'\|'non-modal'` | `'modal'` | ✓ | `modal`
|
|
63
|
+
| `mode` | `'modal'\|'non-modal'` | `'modal'` | ✓ | `modal` traps focus and places initial focus (`[autofocus]` → first input control → first tabbable, through shadow roots); `non-modal` does neither |
|
|
64
64
|
| `contained` | `boolean` | `false` | ✓ | Position relative to parent element |
|
|
65
65
|
| `closeOn` | `string[]` | `['escape','backdrop','button']` | ✓ | Close triggers |
|
|
66
66
|
|
|
@@ -35,7 +35,7 @@ Side panel that slides in from any screen edge. Extends `UOverlayElement` (focus
|
|
|
35
35
|
| `open` | `boolean` | `false` | ✓ | Show/hide state |
|
|
36
36
|
| `closable` | `boolean` | `false` | ✓ | Show close button |
|
|
37
37
|
| `placement` | `'left'\|'right'\|'top'\|'bottom'` | `'left'` | ✓ | Slide-in direction |
|
|
38
|
-
| `mode` | `'modal'\|'non-modal'` | `'modal'` | ✓ |
|
|
38
|
+
| `mode` | `'modal'\|'non-modal'` | `'modal'` | ✓ | `modal` traps focus and places initial focus; `non-modal` does neither |
|
|
39
39
|
| `contained` | `boolean` | `false` | ✓ | Contained to parent element |
|
|
40
40
|
| `closeOn` | `string[]` | `['escape','backdrop','button']` | ✓ | Close triggers |
|
|
41
41
|
|
|
@@ -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 | `mode="modal"` (default): `[autofocus]` → first input control → first tabbable, searched through shadow roots (a form component slotted in works the same); focus is returned by the trap. `mode="non-modal"` moves no focus on open — the page stays interactive, so where focus goes is yours to decide (`element.focus()` after `show`) |
|
|
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']` |
|