@iyulab/components 1.45.1 → 1.46.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 +33 -0
- package/dist/components/breadcrumb-item/UBreadcrumbItem.d.ts +2 -0
- package/dist/components/breadcrumb-item/UBreadcrumbItem.js +6 -0
- package/dist/components/button/UButton.d.ts +6 -0
- package/dist/components/button/UButton.js +6 -0
- package/dist/components/chip/UChip.d.ts +14 -0
- package/dist/components/chip/UChip.js +51 -9
- package/dist/components/copy-button/UCopyButton.d.ts +2 -0
- package/dist/components/copy-button/UCopyButton.js +6 -0
- package/dist/components/icon/UIcon.styles.js +7 -0
- package/dist/components/icon-button/UIconButton.d.ts +2 -0
- package/dist/components/icon-button/UIconButton.js +6 -0
- package/package.json +1 -1
- package/skills/iyulab-components/references/components/chip.md +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.46.0] - 2026-09-24
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **`u-icon` takes its 1em box before the SVG arrives.** Icons resolve asynchronously and draw
|
|
8
|
+
nothing until they do; the host had no size of its own, so its box was 0 × 0 until the SVG
|
|
9
|
+
landed and everything around it moved — a layout shift wherever an icon appears (a collapsed
|
|
10
|
+
sidebar item jumped from 16px to 36px tall on load). The host is now `1em` square from the first
|
|
11
|
+
paint, and `font-size` still sets the size.
|
|
12
|
+
⚠An empty `<u-icon>` (no `name`, no `src`) now also takes that box. If you rendered one as an
|
|
13
|
+
invisible placeholder, render nothing instead.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **A selectable `u-chip` is a keyboard-operable toggle button.** It had a click handler and
|
|
18
|
+
nothing else — no role, no tab stop, no key — so keyboard users could not reach it and assistive
|
|
19
|
+
tech did not know it toggles. It is now in the tab order with `role="button"` and `aria-pressed`,
|
|
20
|
+
and `Enter` / `Space` toggle it and fire `pick` like a click. A chip that is not selectable stays a
|
|
21
|
+
status label, out of the tab order; a `tabindex` you set is kept.
|
|
22
|
+
- **`focus()` now reaches the control inside `u-icon-button`, `u-copy-button` and
|
|
23
|
+
`u-breadcrumb-item`** (with `href`) — the same defect `u-button` had in 1.45.2: the host is not
|
|
24
|
+
focusable, so calling `focus()` on it did nothing.
|
|
25
|
+
|
|
26
|
+
## [1.45.2] - 2026-09-24
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **`u-button.focus()` now focuses the button.** The host itself is not focusable, so calling
|
|
31
|
+
`focus()` on a `u-button` did nothing — no error, no focus. Anything that moves focus to a
|
|
32
|
+
button (initial focus when a panel opens, returning focus when it closes) failed silently. The
|
|
33
|
+
host now delegates focus to the inner native `<button>` (or `<a>` when `href` is set), and the
|
|
34
|
+
host matches `:focus` while it holds it. A disabled button still cannot take focus.
|
|
35
|
+
|
|
3
36
|
## [1.45.1] - 2026-09-24
|
|
4
37
|
|
|
5
38
|
### Fixed
|
|
@@ -12,6 +12,8 @@ import { UElement } from '../UElement.js';
|
|
|
12
12
|
*/
|
|
13
13
|
export declare class UBreadcrumbItem extends UElement {
|
|
14
14
|
static styles: import('lit').CSSResultGroup[];
|
|
15
|
+
/** 호스트의 `.focus()` 를 안쪽 컨트롤로 위임한다 — `UButton` 과 같은 계약(호스트는 포커스 가능하지 않다). */
|
|
16
|
+
static shadowRootOptions: ShadowRootInit;
|
|
15
17
|
/** 비활성화 상태 */
|
|
16
18
|
disabled: boolean;
|
|
17
19
|
/** 링크 URL */
|
|
@@ -21,6 +21,12 @@ var UBreadcrumbItem = class UBreadcrumbItem extends UElement {
|
|
|
21
21
|
static {
|
|
22
22
|
this.styles = [super.styles, styles];
|
|
23
23
|
}
|
|
24
|
+
static {
|
|
25
|
+
this.shadowRootOptions = {
|
|
26
|
+
...UElement.shadowRootOptions,
|
|
27
|
+
delegatesFocus: true
|
|
28
|
+
};
|
|
29
|
+
}
|
|
24
30
|
render() {
|
|
25
31
|
if (this.href) return html`
|
|
26
32
|
<a part="link"
|
|
@@ -57,6 +57,12 @@ export type ButtonSize = "sm" | "md" | "lg";
|
|
|
57
57
|
export declare class UButton extends UElement {
|
|
58
58
|
static styles: import('lit').CSSResultGroup[];
|
|
59
59
|
static formAssociated: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* 호스트의 `.focus()` 를 섀도 안의 네이티브 `<button>`/`<a>` 로 위임한다. 호스트는 포커스
|
|
62
|
+
* 가능하지 않으므로 위임이 없으면 `el.focus()` 가 오류 없이 아무 일도 하지 않는다 —
|
|
63
|
+
* 네이티브 버튼을 대체하는 요소가 잃으면 안 되는 계약이다.
|
|
64
|
+
*/
|
|
65
|
+
static shadowRootOptions: ShadowRootInit;
|
|
60
66
|
/** 버튼 스타일 변형 */
|
|
61
67
|
variant: ButtonVariant;
|
|
62
68
|
/**
|
|
@@ -37,6 +37,12 @@ var UButton = class UButton extends UElement {
|
|
|
37
37
|
static {
|
|
38
38
|
this.formAssociated = true;
|
|
39
39
|
}
|
|
40
|
+
static {
|
|
41
|
+
this.shadowRootOptions = {
|
|
42
|
+
...UElement.shadowRootOptions,
|
|
43
|
+
delegatesFocus: true
|
|
44
|
+
};
|
|
45
|
+
}
|
|
40
46
|
set form(val) {
|
|
41
47
|
if (val) this.setAttribute("form", val);
|
|
42
48
|
else this.removeAttribute("form");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PropertyValues } from 'lit';
|
|
1
2
|
import { UElement } from '../UElement.js';
|
|
2
3
|
import { TagVariant, TagColor } from '../tag/UTag.js';
|
|
3
4
|
/**
|
|
@@ -33,7 +34,20 @@ export declare class UChip extends UElement {
|
|
|
33
34
|
/** 칩의 고유값 */
|
|
34
35
|
value: string;
|
|
35
36
|
render(): import('lit-html').TemplateResult<1>;
|
|
37
|
+
/** 이 요소가 스스로 붙인 `tabindex` 인가 — 소비자가 준 값은 건드리지 않는다. */
|
|
38
|
+
private ownsTabindex;
|
|
39
|
+
connectedCallback(): void;
|
|
40
|
+
disconnectedCallback(): void;
|
|
41
|
+
/**
|
|
42
|
+
* `selectable` 칩은 토글 버튼이다(WAI-ARIA APG «Button» · `aria-pressed`) — 역할·포커스·키가
|
|
43
|
+
* 없으면 키보드로 닿지 못하고 보조기기는 토글인지 모른다. 선택할 수 없는 칩은 상태 표시라
|
|
44
|
+
* 탭 순서에 들어오지 않는다.
|
|
45
|
+
*/
|
|
46
|
+
protected updated(changed: PropertyValues): void;
|
|
47
|
+
/** Enter 와 Space 는 클릭과 같다 — 안쪽 삭제 버튼에서 온 키는 그 버튼의 몫이다. */
|
|
48
|
+
private handleKeydown;
|
|
36
49
|
private handleTagClick;
|
|
50
|
+
private toggle;
|
|
37
51
|
private handleRemoveClick;
|
|
38
52
|
}
|
|
39
53
|
declare global {
|
|
@@ -20,16 +20,15 @@ var UChip = class UChip extends UElement {
|
|
|
20
20
|
this.selectable = false;
|
|
21
21
|
this.selected = false;
|
|
22
22
|
this.value = "";
|
|
23
|
+
this.ownsTabindex = false;
|
|
24
|
+
this.handleKeydown = (e) => {
|
|
25
|
+
if (!this.selectable || e.composedPath()[0] !== this) return;
|
|
26
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
this.toggle(e);
|
|
29
|
+
};
|
|
23
30
|
this.handleTagClick = (e) => {
|
|
24
|
-
|
|
25
|
-
this.selected = !this.selected;
|
|
26
|
-
this.fire("pick", { detail: {
|
|
27
|
-
value: this.value,
|
|
28
|
-
selected: this.selected,
|
|
29
|
-
shiftKey: e.shiftKey,
|
|
30
|
-
metaKey: e.metaKey,
|
|
31
|
-
ctrlKey: e.ctrlKey
|
|
32
|
-
} });
|
|
31
|
+
this.toggle(e);
|
|
33
32
|
};
|
|
34
33
|
this.handleRemoveClick = (e) => {
|
|
35
34
|
e.stopImmediatePropagation();
|
|
@@ -75,6 +74,49 @@ var UChip = class UChip extends UElement {
|
|
|
75
74
|
</u-tooltip>
|
|
76
75
|
`;
|
|
77
76
|
}
|
|
77
|
+
connectedCallback() {
|
|
78
|
+
super.connectedCallback();
|
|
79
|
+
this.addEventListener("keydown", this.handleKeydown);
|
|
80
|
+
}
|
|
81
|
+
disconnectedCallback() {
|
|
82
|
+
this.removeEventListener("keydown", this.handleKeydown);
|
|
83
|
+
super.disconnectedCallback();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* `selectable` 칩은 토글 버튼이다(WAI-ARIA APG «Button» · `aria-pressed`) — 역할·포커스·키가
|
|
87
|
+
* 없으면 키보드로 닿지 못하고 보조기기는 토글인지 모른다. 선택할 수 없는 칩은 상태 표시라
|
|
88
|
+
* 탭 순서에 들어오지 않는다.
|
|
89
|
+
*/
|
|
90
|
+
updated(changed) {
|
|
91
|
+
super.updated(changed);
|
|
92
|
+
if (!changed.has("selectable") && !changed.has("selected")) return;
|
|
93
|
+
if (this.selectable) {
|
|
94
|
+
this.setAttribute("role", "button");
|
|
95
|
+
this.setAttribute("aria-pressed", String(this.selected));
|
|
96
|
+
if (!this.hasAttribute("tabindex")) {
|
|
97
|
+
this.setAttribute("tabindex", "0");
|
|
98
|
+
this.ownsTabindex = true;
|
|
99
|
+
}
|
|
100
|
+
} else if (changed.has("selectable")) {
|
|
101
|
+
this.removeAttribute("role");
|
|
102
|
+
this.removeAttribute("aria-pressed");
|
|
103
|
+
if (this.ownsTabindex) {
|
|
104
|
+
this.removeAttribute("tabindex");
|
|
105
|
+
this.ownsTabindex = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
toggle(e) {
|
|
110
|
+
if (!this.selectable) return;
|
|
111
|
+
this.selected = !this.selected;
|
|
112
|
+
this.fire("pick", { detail: {
|
|
113
|
+
value: this.value,
|
|
114
|
+
selected: this.selected,
|
|
115
|
+
shiftKey: e.shiftKey,
|
|
116
|
+
metaKey: e.metaKey,
|
|
117
|
+
ctrlKey: e.ctrlKey
|
|
118
|
+
} });
|
|
119
|
+
}
|
|
78
120
|
};
|
|
79
121
|
__decorate([property({
|
|
80
122
|
type: String,
|
|
@@ -20,6 +20,8 @@ import { ButtonVariant } from '../button/UButton.js';
|
|
|
20
20
|
*/
|
|
21
21
|
export declare class UCopyButton extends UElement {
|
|
22
22
|
static styles: import('lit').CSSResultGroup[];
|
|
23
|
+
/** 호스트의 `.focus()` 를 안쪽 컨트롤로 위임한다 — `UButton` 과 같은 계약(호스트는 포커스 가능하지 않다). */
|
|
24
|
+
static shadowRootOptions: ShadowRootInit;
|
|
23
25
|
/** 버튼 스타일 변형 */
|
|
24
26
|
variant: ButtonVariant;
|
|
25
27
|
/** 원형 표시 여부 */
|
|
@@ -48,6 +48,12 @@ var UCopyButton = class UCopyButton extends UElement {
|
|
|
48
48
|
static {
|
|
49
49
|
this.styles = [super.styles, styles];
|
|
50
50
|
}
|
|
51
|
+
static {
|
|
52
|
+
this.shadowRootOptions = {
|
|
53
|
+
...UElement.shadowRootOptions,
|
|
54
|
+
delegatesFocus: true
|
|
55
|
+
};
|
|
56
|
+
}
|
|
51
57
|
render() {
|
|
52
58
|
const iconName = this.copied ? "check" : "copy";
|
|
53
59
|
if (this.label != null && this.label !== "") return html`
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { css } from "lit";
|
|
2
2
|
//#region src/components/icon/UIcon.styles.ts
|
|
3
3
|
var styles = css`
|
|
4
|
+
/*
|
|
5
|
+
* 상자는 SVG 가 도착하기 «전» 부터 1em × 1em 이다 — 아이콘은 비동기로 해석되고 그 전에는
|
|
6
|
+
* 아무것도 그리지 않으므로, 고유 크기가 없으면 로드 순간 주변이 밀린다(레이아웃 이동).
|
|
7
|
+
* 크기는 여전히 font-size 로 정한다.
|
|
8
|
+
*/
|
|
4
9
|
:host {
|
|
5
10
|
display: inline-flex;
|
|
11
|
+
width: 1em;
|
|
12
|
+
height: 1em;
|
|
6
13
|
color: inherit;
|
|
7
14
|
font-size: inherit;
|
|
8
15
|
}
|
|
@@ -14,6 +14,8 @@ import { IconLibrary } from '../icon/UIcon.js';
|
|
|
14
14
|
*/
|
|
15
15
|
export declare class UIconButton extends UElement {
|
|
16
16
|
static styles: import('lit').CSSResultGroup[];
|
|
17
|
+
/** 호스트의 `.focus()` 를 안쪽 컨트롤로 위임한다 — `UButton` 과 같은 계약(호스트는 포커스 가능하지 않다). */
|
|
18
|
+
static shadowRootOptions: ShadowRootInit;
|
|
17
19
|
/** 버튼 스타일 변형 */
|
|
18
20
|
variant: ButtonVariant;
|
|
19
21
|
/** 원형 표시 여부 */
|
|
@@ -22,6 +22,12 @@ var UIconButton = class UIconButton extends UElement {
|
|
|
22
22
|
static {
|
|
23
23
|
this.styles = [super.styles, styles];
|
|
24
24
|
}
|
|
25
|
+
static {
|
|
26
|
+
this.shadowRootOptions = {
|
|
27
|
+
...UElement.shadowRootOptions,
|
|
28
|
+
delegatesFocus: true
|
|
29
|
+
};
|
|
30
|
+
}
|
|
25
31
|
/**
|
|
26
32
|
* 호스트에 세팅된 `aria-label`을 내부 `<u-button>`으로 옮긴다 — `UButton`과 같은 이유
|
|
27
33
|
* (섀도우 경계 안쪽 엘리먼트가 실제 접근 가능한 이름 대상). 아이콘 전용
|
package/package.json
CHANGED
|
@@ -54,6 +54,13 @@ Interactive chip tag. Supports selectable toggle, removable button, and tooltip.
|
|
|
54
54
|
| `pick` | Fires when the chip is selected/deselected |
|
|
55
55
|
| `remove` | Fires when the remove button is clicked |
|
|
56
56
|
|
|
57
|
+
## Keyboard and accessibility
|
|
58
|
+
|
|
59
|
+
A `selectable` chip is a toggle button: it is a stop in the tab order, carries `role="button"` and
|
|
60
|
+
`aria-pressed`, and `Enter` / `Space` toggle it and fire `pick` exactly like a click. A chip that is
|
|
61
|
+
not selectable is a status label — no role, not in the tab order. A `tabindex` you set yourself is
|
|
62
|
+
kept. The remove button of a `removable` chip is its own tab stop.
|
|
63
|
+
|
|
57
64
|
## CSS Parts
|
|
58
65
|
|
|
59
66
|
| Part | Description |
|