@iyulab/components 1.6.0 → 1.7.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.7.0] - 2026-07-17
4
+
5
+ ### Fixed
6
+ - **폼 컨트롤 `change` 이벤트 의미론을 네이티브 규약으로 교정** — `USelect`/`URadio`/`URating`/`USlider`가 `updated()` 경로에서 무조건 `change`를 발화해, (1) 옵션 slot 등록 시 `value===undefined` 상태의 change가 발화되어 React 등 controlled 래퍼의 state를 오염시키고(옵션 등록 전 세팅한 초기값이 유실·서버 enum 기본값으로 저장되는 무증상 데이터 결함 — yesung 실측), (2) 프로그램적 `value` 세팅이 사용자 이벤트로 위장되어 에코 루프를 만들던 문제 수정. 이제 `change`는 **사용자 상호작용**(옵션 클릭·키보드·칩 제거·지우기·드래그 확정)에서만 발화한다(ISSUE-components-20260717-uselect-value-before-options). `UInput`(blur 발화)·`UMenu`/`UTree`(핸들러 발화)는 원래 규약대로였으며 변경 없음.
7
+ - `USlider`: 문서("드래그 완료 후 발생")와 달리 **드래그 중 매 pointermove마다 change가 연사**되던 결함 수정 — 이제 `pointerup` 시 값이 실제로 바뀐 경우 1회 발화. 단일 select에서 동일 옵션 재선택, 선택된 라디오 재클릭도 네이티브와 동일하게 미발화.
8
+ - `USelect`/`URadio`/`URating`의 `onChangeValue()`가 `updated()` 내부에서 `validate()`→`requestUpdate()`를 호출해 v1.5.1 검증 아키텍처를 위반하고 "scheduled an update after an update completed" Lit 경고를 재유발하던 잔재 제거 — 검증 UI 갱신(`validate()`)은 사용자 상호작용 경로에서만 수행하고, 프로그램적 세팅은 base의 silent `setValidity()`로 internals만 갱신한다.
9
+ - **마크업 `value` attribute 선언이 일반 문자열에서 silently null이 되던 갭 수정** — base가 `type: Object`(JSON.parse)여서 `<u-input value="hello">`·`<u-select value="b">`가 null로 해석됐다. 기본 해석을 raw 문자열로 바꾸고, `u-rating`/`u-slider`는 숫자, `u-select`(multiple)/`u-slider`(range)는 JSON 배열(`value='["a","b"]'`)을 지원한다.
10
+ - `USlider` range 표시 텍스트의 구분자 인코딩 오염(`5 ??10`) → `5 ~ 10`으로 교정. `URating` min/max 주석 모지바케 정리.
11
+
12
+ ### Changed
13
+ - 프로그램적 `value` 세팅은 이제 `change` 발화·`invalid` 플래그 자동 갱신을 하지 않는다(`ElementInternals` validity는 계속 동기화됨). 프로그램적 변경 후 검증 UI 갱신이 필요하면 `validate()`를 명시 호출할 것. `value='"quoted"'` 형태(JSON 문자열)로 attribute를 우회 선언하던 경우 이제 따옴표 포함 raw 문자열로 해석된다.
14
+
3
15
  ## [1.6.0] - 2026-07-16
4
16
 
5
17
  ### Added
@@ -23,7 +23,9 @@ export declare abstract class UFormControlElement<T> extends UElement {
23
23
  description?: string;
24
24
  /** 폼 제출 시 사용되는 이름 */
25
25
  name?: string;
26
- /** 폼 제출 시 사용되는 */
26
+ /** 폼 제출 시 사용되는 값. attribute 선언 시 기본은 raw 문자열로 해석한다 —
27
+ * (이전의 type: Object는 JSON.parse 실패로 일반 문자열 attribute가 silently null이 되는 갭이 있었다)
28
+ * string이 아닌 값 타입을 갖는 컨트롤(URating/USlider/USelect multiple)은 자신의 converter로 override한다. */
27
29
  value?: T;
28
30
  /**
29
31
  * ElementInternals는 폼과의 연동, 유효성 검사 상태 관리 등을 지원하는 네이티브 API입니다.
@@ -116,6 +116,6 @@ __decorate([property({ type: Boolean }), __decorateMetadata("design:type", Boole
116
116
  __decorate([property({ type: String }), __decorateMetadata("design:type", String)], UFormControlElement.prototype, "label", void 0);
117
117
  __decorate([property({ type: String }), __decorateMetadata("design:type", String)], UFormControlElement.prototype, "description", void 0);
118
118
  __decorate([property({ type: String }), __decorateMetadata("design:type", String)], UFormControlElement.prototype, "name", void 0);
119
- __decorate([property({ type: Object }), __decorateMetadata("design:type", Object)], UFormControlElement.prototype, "value", void 0);
119
+ __decorate([property(), __decorateMetadata("design:type", Object)], UFormControlElement.prototype, "value", void 0);
120
120
  //#endregion
121
121
  export { UFormControlElement };
@@ -11,7 +11,8 @@ export type RadioOrientation = "vertical" | "horizontal";
11
11
  * @csspart field - u-field 요소
12
12
  * @csspart container - 옵션들을 감싸는 컨테이너
13
13
  *
14
- * @event change - 선택 값이 변경될 때 발생
14
+ * @event change - 사용자 상호작용(옵션 클릭·키보드)으로 선택 값이 변경될 때 발생.
15
+ * 네이티브 라디오와 동일하게 프로그램적 value 세팅·옵션 등록으로는 발화하지 않는다.
15
16
  */
16
17
  export declare class URadio extends UFormControlElement<string> {
17
18
  static styles: import('lit').CSSResultGroup[];
@@ -30,6 +31,11 @@ export declare class URadio extends UFormControlElement<string> {
30
31
  private setup;
31
32
  private cleanup;
32
33
  private onChangeValue;
34
+ /** 사용자 상호작용으로 값이 바뀐 경로에서만 호출한다 — 프로그램적 value 세팅은
35
+ * 네이티브 폼 컨트롤과 동일하게 change를 발화하지 않는다.
36
+ * UI 재렌더를 동반한 validate()도 이 경로에서만 수행한다(v1.5.1 검증 아키텍처 —
37
+ * updated() 경로는 base의 silent setValidity()만 수행해 Lit 중복 업데이트를 피한다). */
38
+ private emitChange;
33
39
  private handleSlotChange;
34
40
  private handleOptionClick;
35
41
  private handleOptionKeyDown;
@@ -25,7 +25,9 @@ var URadio = class URadio extends UFormControlElement {
25
25
  if (this.readonly || this.disabled) return;
26
26
  const option = e.currentTarget;
27
27
  if (option.disabled) return;
28
+ if (option.value === this.value) return;
28
29
  this.value = option.value;
30
+ this.emitChange();
29
31
  };
30
32
  this.handleOptionKeyDown = (e) => {
31
33
  if (this.readonly || this.disabled) return;
@@ -125,6 +127,12 @@ var URadio = class URadio extends UFormControlElement {
125
127
  option.selected = option.value === this.value;
126
128
  });
127
129
  this.internals?.setFormValue(this.value || "");
130
+ }
131
+ /** 사용자 상호작용으로 값이 바뀐 경로에서만 호출한다 — 프로그램적 value 세팅은
132
+ * 네이티브 폼 컨트롤과 동일하게 change를 발화하지 않는다.
133
+ * UI 재렌더를 동반한 validate()도 이 경로에서만 수행한다(v1.5.1 검증 아키텍처 —
134
+ * updated() 경로는 base의 silent setValidity()만 수행해 Lit 중복 업데이트를 피한다). */
135
+ emitChange() {
128
136
  if (!this.novalidate) this.validate();
129
137
  this.dispatchEvent(new Event("change", {
130
138
  bubbles: true,
@@ -14,16 +14,19 @@ import { UFormControlElement } from '../UFormControlElement.js';
14
14
  * @cssprop --rating-symbol-color - 활성화된 심볼 색상
15
15
  * @cssprop --rating-symbol-off-color - 비활성화된 심볼 색상
16
16
  *
17
- * @event change - 레이팅 변경 발생
17
+ * @event change - 사용자 상호작용(심볼 클릭·키보드)으로 레이팅 값이 변경될 발생.
18
+ * 프로그램적 value 세팅으로는 발화하지 않는다.
18
19
  */
19
20
  export declare class URating extends UFormControlElement<number> {
20
21
  static styles: import('lit').CSSResultGroup[];
21
- /** 理쒖냼 媛?*/
22
+ /** 최소 값 */
22
23
  min: number;
23
- /** 理쒕? 媛?*/
24
+ /** 최대 값 */
24
25
  max: number;
25
26
  /** 정밀도 (0.5 등) */
26
27
  precision: number;
28
+ /** 레이팅 값. attribute 선언 시 숫자로 해석한다. */
29
+ value?: number;
27
30
  private buffer;
28
31
  private symbol;
29
32
  private symbolOff;
@@ -35,6 +38,11 @@ export declare class URating extends UFormControlElement<number> {
35
38
  private isStepMismatch;
36
39
  reset(): void;
37
40
  private onChangeValue;
41
+ /** 사용자 상호작용으로 값이 바뀐 경로에서만 호출한다 — 프로그램적 value 세팅은
42
+ * 네이티브 폼 컨트롤과 동일하게 change를 발화하지 않는다.
43
+ * UI 재렌더를 동반한 validate()도 이 경로에서만 수행한다(v1.5.1 검증 아키텍처 —
44
+ * updated() 경로는 base의 silent setValidity()만 수행해 Lit 중복 업데이트를 피한다). */
45
+ private emitChange;
38
46
  private handleSlotChange;
39
47
  private handleSymbolPointerMove;
40
48
  private handleSymbolPointerLeave;
@@ -37,7 +37,10 @@ var URating = class URating extends UFormControlElement {
37
37
  if (this.disabled || this.readonly) return;
38
38
  const score = Number(e.currentTarget.dataset.score);
39
39
  const val = this.precision < 1 ? this.calibrate(e, score) : score;
40
- this.value = this.value === val ? 0 : val;
40
+ const next = this.value === val ? 0 : val;
41
+ if (next === this.value) return;
42
+ this.value = next;
43
+ this.emitChange();
41
44
  };
42
45
  this.handleSymbolKeydown = (e) => {
43
46
  if (this.disabled || this.readonly) return;
@@ -68,7 +71,11 @@ var URating = class URating extends UFormControlElement {
68
71
  case "Enter": {
69
72
  e.preventDefault();
70
73
  const score = Number(e.currentTarget.dataset.score);
71
- this.value = this.value === score ? 0 : score;
74
+ const next = this.value === score ? 0 : score;
75
+ if (next !== this.value) {
76
+ this.value = next;
77
+ this.emitChange();
78
+ }
72
79
  return;
73
80
  }
74
81
  default: return;
@@ -166,6 +173,12 @@ var URating = class URating extends UFormControlElement {
166
173
  onChangeValue() {
167
174
  this.buffer = -1;
168
175
  this.internals?.setFormValue(this.value?.toString() || "");
176
+ }
177
+ /** 사용자 상호작용으로 값이 바뀐 경로에서만 호출한다 — 프로그램적 value 세팅은
178
+ * 네이티브 폼 컨트롤과 동일하게 change를 발화하지 않는다.
179
+ * UI 재렌더를 동반한 validate()도 이 경로에서만 수행한다(v1.5.1 검증 아키텍처 —
180
+ * updated() 경로는 base의 silent setValidity()만 수행해 Lit 중복 업데이트를 피한다). */
181
+ emitChange() {
169
182
  if (!this.novalidate) this.validate();
170
183
  this.dispatchEvent(new Event("change", {
171
184
  bubbles: true,
@@ -183,6 +196,7 @@ var URating = class URating extends UFormControlElement {
183
196
  __decorate([property({ type: Number }), __decorateMetadata("design:type", Object)], URating.prototype, "min", void 0);
184
197
  __decorate([property({ type: Number }), __decorateMetadata("design:type", Object)], URating.prototype, "max", void 0);
185
198
  __decorate([property({ type: Number }), __decorateMetadata("design:type", Object)], URating.prototype, "precision", void 0);
199
+ __decorate([property({ type: Number }), __decorateMetadata("design:type", Number)], URating.prototype, "value", void 0);
186
200
  __decorate([state(), __decorateMetadata("design:type", Object)], URating.prototype, "buffer", void 0);
187
201
  __decorate([state(), __decorateMetadata("design:type", Object)], URating.prototype, "symbol", void 0);
188
202
  __decorate([state(), __decorateMetadata("design:type", Object)], URating.prototype, "symbolOff", void 0);
@@ -22,7 +22,8 @@ export type SelectVariant = 'outlined' | 'filled' | 'underlined' | 'borderless';
22
22
  * @cssprop --select-popover-min-height - 팝오버의 최소 높이 (기본값: 0px)
23
23
  * @cssprop --select-popover-max-height - 팝오버의 최대 높이 (기본값: 50vh)
24
24
  *
25
- * @event change - 선택 값이 변경될 때 발생
25
+ * @event change - 사용자 상호작용(옵션 클릭·칩 제거·지우기)으로 선택 값이 변경될 때 발생.
26
+ * 네이티브 select와 동일하게 프로그램적 value 세팅·옵션 등록으로는 발화하지 않는다.
26
27
  */
27
28
  export declare class USelect extends UFormControlElement<string | string[]> {
28
29
  static styles: import('lit').CSSResultGroup[];
@@ -42,6 +43,8 @@ export declare class USelect extends UFormControlElement<string | string[]> {
42
43
  maxCount?: number;
43
44
  /** placeholder 텍스트 */
44
45
  placeholder?: string;
46
+ /** 폼 제출 시 사용되는 값. attribute로는 단일 값 문자열 또는 JSON 배열(`value='["a","b"]'`)을 지원한다. */
47
+ value?: string | string[];
45
48
  containerEl?: HTMLElement;
46
49
  popoverEl?: UPopover;
47
50
  private options;
@@ -58,6 +61,11 @@ export declare class USelect extends UFormControlElement<string | string[]> {
58
61
  private setup;
59
62
  private cleanup;
60
63
  private onChangeValue;
64
+ /** 사용자 상호작용으로 값이 바뀐 경로에서만 호출한다 — 프로그램적 value 세팅은
65
+ * 네이티브 폼 컨트롤과 동일하게 change를 발화하지 않는다.
66
+ * UI 재렌더를 동반한 validate()도 이 경로에서만 수행한다(v1.5.1 검증 아키텍처 —
67
+ * updated() 경로는 base의 silent setValidity()만 수행해 Lit 중복 업데이트를 피한다). */
68
+ private emitChange;
61
69
  private handleSlotChange;
62
70
  private handleOptionClick;
63
71
  private handleOptionKeydown;
@@ -36,8 +36,11 @@ var USelect = class USelect extends UFormControlElement {
36
36
  if (this.maxCount != null && values.length >= this.maxCount) return;
37
37
  this.value = [...values, option.value];
38
38
  }
39
+ this.emitChange();
39
40
  } else {
41
+ const changed = option.value !== this.value;
40
42
  this.value = option.value;
43
+ if (changed) this.emitChange();
41
44
  this.popoverEl?.hide();
42
45
  }
43
46
  };
@@ -104,12 +107,14 @@ var USelect = class USelect extends UFormControlElement {
104
107
  e.preventDefault();
105
108
  e.stopPropagation();
106
109
  this.reset();
110
+ this.emitChange();
107
111
  this.containerEl?.click();
108
112
  };
109
113
  this.handleChipRemove = (e) => {
110
114
  const value = e.currentTarget.dataset.value;
111
115
  if (!value) return;
112
116
  this.value = this.valueAsArray.filter((v) => v !== value);
117
+ this.emitChange();
113
118
  };
114
119
  }
115
120
  static {
@@ -263,6 +268,12 @@ var USelect = class USelect extends UFormControlElement {
263
268
  for (const option of this.options) if (this.multiple) option.selected = values.includes(option.value);
264
269
  else option.selected = option.value === this.value;
265
270
  this.internals?.setFormValue(this.valueAsString);
271
+ }
272
+ /** 사용자 상호작용으로 값이 바뀐 경로에서만 호출한다 — 프로그램적 value 세팅은
273
+ * 네이티브 폼 컨트롤과 동일하게 change를 발화하지 않는다.
274
+ * UI 재렌더를 동반한 validate()도 이 경로에서만 수행한다(v1.5.1 검증 아키텍처 —
275
+ * updated() 경로는 base의 silent setValidity()만 수행해 Lit 중복 업데이트를 피한다). */
276
+ emitChange() {
266
277
  if (!this.novalidate) this.validate();
267
278
  this.dispatchEvent(new Event("change", {
268
279
  bubbles: true,
@@ -299,6 +310,15 @@ __decorate([property({
299
310
  attribute: "max-count"
300
311
  }), __decorateMetadata("design:type", Number)], USelect.prototype, "maxCount", void 0);
301
312
  __decorate([property({ type: String }), __decorateMetadata("design:type", String)], USelect.prototype, "placeholder", void 0);
313
+ __decorate([property({ converter: { fromAttribute: (value) => {
314
+ if (value == null) return void 0;
315
+ if (value.trim().startsWith("[")) try {
316
+ return JSON.parse(value);
317
+ } catch {
318
+ return value;
319
+ }
320
+ return value;
321
+ } } }), __decorateMetadata("design:type", Object)], USelect.prototype, "value", void 0);
302
322
  __decorate([query(".container", true), __decorateMetadata("design:type", typeof HTMLElement === "undefined" ? Object : HTMLElement)], USelect.prototype, "containerEl", void 0);
303
323
  __decorate([query("u-popover", true), __decorateMetadata("design:type", typeof UPopover === "undefined" ? Object : UPopover)], USelect.prototype, "popoverEl", void 0);
304
324
  __decorate([state(), __decorateMetadata("design:type", Array)], USelect.prototype, "options", void 0);
@@ -32,7 +32,8 @@ export type SliderFormatter = (value: number) => string;
32
32
  * @cssprop --slider-mark-color - 마크 색상
33
33
  * @cssprop --slider-mark-border-color - 마크 테두리 색상
34
34
  *
35
- * @event change - 드래그 완료 값이 확정됐을 때 발생
35
+ * @event change - 사용자 상호작용으로 값이 확정됐을 때 발생 — 드래그는 완료(pointerup) 시,
36
+ * 키보드는 조작마다. 프로그램적 value 세팅으로는 발화하지 않는다.
36
37
  */
37
38
  export declare class USlider extends UFormControlElement<number | number[]> {
38
39
  static styles: import('lit').CSSResultGroup[];
@@ -54,6 +55,8 @@ export declare class USlider extends UFormControlElement<number | number[]> {
54
55
  max: number;
55
56
  /** 값 변경 단위 */
56
57
  step: number;
58
+ /** 슬라이더 값. attribute로는 숫자 또는 range용 JSON 배열(`value='[10,20]'`)을 지원한다. */
59
+ value?: number | number[];
57
60
  private dragging;
58
61
  private trackEl;
59
62
  private containerEl?;
@@ -72,6 +75,9 @@ export declare class USlider extends UFormControlElement<number | number[]> {
72
75
  protected setValidity(): void;
73
76
  reset(): void;
74
77
  private onChangeValue;
78
+ /** 사용자 상호작용으로 값이 확정된 경로에서만 호출한다 — 프로그램적 value 세팅은
79
+ * 네이티브 폼 컨트롤과 동일하게 change를 발화하지 않는다. */
80
+ private emitChange;
75
81
  private renderThumb;
76
82
  private formatDisplay;
77
83
  private formatValue;
@@ -26,6 +26,7 @@ var USlider = class USlider extends UFormControlElement {
26
26
  const pct = Math.max(0, Math.min(rect.width, ev.clientX - rect.left)) / rect.width * 100;
27
27
  return this.percentToVal(pct);
28
28
  };
29
+ const before = JSON.stringify(this.value);
29
30
  const val = getPointerValue(e);
30
31
  if (this.range) {
31
32
  const closer = Math.abs(val - this.minVal) <= Math.abs(val - this.maxVal) ? "min" : "max";
@@ -47,6 +48,7 @@ var USlider = class USlider extends UFormControlElement {
47
48
  document.removeEventListener("pointermove", handleDocumentMove);
48
49
  document.removeEventListener("pointerup", handleDocumentUp);
49
50
  this.dragging = null;
51
+ if (JSON.stringify(this.value) !== before) this.emitChange();
50
52
  };
51
53
  document.addEventListener("pointermove", handleDocumentMove);
52
54
  document.addEventListener("pointerup", handleDocumentUp);
@@ -82,9 +84,11 @@ var USlider = class USlider extends UFormControlElement {
82
84
  }
83
85
  e.preventDefault();
84
86
  const newVal = this.snap(currentVal + delta);
87
+ const before = JSON.stringify(this.value);
85
88
  if (thumb === "min") if (this.range) this.minVal = Math.min(newVal, this.maxVal);
86
89
  else this.value = newVal;
87
90
  else this.maxVal = Math.max(newVal, this.minVal);
91
+ if (JSON.stringify(this.value) !== before) this.emitChange();
88
92
  };
89
93
  }
90
94
  static {
@@ -197,6 +201,10 @@ var USlider = class USlider extends UFormControlElement {
197
201
  if (Array.isArray(this.value)) this.value = [this.snap(this.value[0]), this.snap(this.value[1])];
198
202
  else if (typeof this.value === "number") this.value = this.snap(this.value);
199
203
  this.internals?.setFormValue(this.valueAsNumber.toString());
204
+ }
205
+ /** 사용자 상호작용으로 값이 확정된 경로에서만 호출한다 — 프로그램적 value 세팅은
206
+ * 네이티브 폼 컨트롤과 동일하게 change를 발화하지 않는다. */
207
+ emitChange() {
200
208
  this.dispatchEvent(new Event("change", {
201
209
  bubbles: true,
202
210
  composed: true
@@ -229,7 +237,7 @@ var USlider = class USlider extends UFormControlElement {
229
237
  `;
230
238
  }
231
239
  formatDisplay() {
232
- if (this.range) return `${this.formatValue(this.minVal)} ??${this.formatValue(this.maxVal)}`;
240
+ if (this.range) return `${this.formatValue(this.minVal)} ~ ${this.formatValue(this.maxVal)}`;
233
241
  else return this.formatValue(this.valueAsNumber);
234
242
  }
235
243
  formatValue(val) {
@@ -269,6 +277,16 @@ __decorate([property({ type: Number }), __decorateMetadata("design:type", Number
269
277
  __decorate([property({ type: Number }), __decorateMetadata("design:type", Number)], USlider.prototype, "min", void 0);
270
278
  __decorate([property({ type: Number }), __decorateMetadata("design:type", Number)], USlider.prototype, "max", void 0);
271
279
  __decorate([property({ type: Number }), __decorateMetadata("design:type", Number)], USlider.prototype, "step", void 0);
280
+ __decorate([property({ converter: { fromAttribute: (value) => {
281
+ if (value == null) return void 0;
282
+ if (value.trim().startsWith("[")) try {
283
+ return JSON.parse(value);
284
+ } catch {
285
+ return;
286
+ }
287
+ const n = Number(value);
288
+ return Number.isNaN(n) ? void 0 : n;
289
+ } } }), __decorateMetadata("design:type", Object)], USlider.prototype, "value", void 0);
272
290
  __decorate([state(), __decorateMetadata("design:type", Object)], USlider.prototype, "dragging", void 0);
273
291
  __decorate([query(".track"), __decorateMetadata("design:type", typeof HTMLElement === "undefined" ? Object : HTMLElement)], USlider.prototype, "trackEl", void 0);
274
292
  __decorate([query(".container"), __decorateMetadata("design:type", typeof HTMLElement === "undefined" ? Object : HTMLElement)], USlider.prototype, "containerEl", void 0);
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.6.0",
4
+ "version": "1.7.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "components",