@ctrliq/quantic-components 1.63.2 → 1.63.4

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.
@@ -15,6 +15,7 @@ export { SelectSize } from './index.constants';
15
15
  import { LucideIconCircleAlert } from '../icon/__generated__/lucide/circle-alert';
16
16
  import { LucideIconChevronDown } from '../icon/__generated__/lucide/chevron-down';
17
17
  import { event, prop } from '../shared/component-meta.decorators';
18
+ import { emitValueChanged } from '../shared/shared.utils';
18
19
  LucideIconCircleAlert.ensureDefined();
19
20
  LucideIconChevronDown.ensureDefined();
20
21
  let Select = class Select extends A11yAttributesMixin(QuanticElement) {
@@ -32,13 +33,7 @@ let Select = class Select extends A11yAttributesMixin(QuanticElement) {
32
33
  this._slottedOptions = [];
33
34
  }
34
35
  dispatchValueChanged(e) {
35
- const selectEl = e.target;
36
- this.value = selectEl.value;
37
- this.dispatchEvent(new CustomEvent('value-changed', {
38
- detail: { value: this.value },
39
- bubbles: true,
40
- composed: true,
41
- }));
36
+ this.value = e.target.value;
42
37
  }
43
38
  openDropdown(e) {
44
39
  if (!this.disabled) {
@@ -57,6 +52,7 @@ let Select = class Select extends A11yAttributesMixin(QuanticElement) {
57
52
  select.value = option[1];
58
53
  }
59
54
  }
55
+ emitValueChanged(this, changed, this.value);
60
56
  }
61
57
  forwardNative(e) {
62
58
  this.dispatchEvent(new Event(e.type, { bubbles: true, composed: true }));
@@ -286,7 +282,7 @@ __decorate([
286
282
  ], Select.prototype, "invalid", void 0);
287
283
  Select = __decorate([
288
284
  event('value-changed', {
289
- description: "Fired whenever the input's value is updated (on input & change).",
285
+ description: 'Fired whenever the value changes, from user selection or a programmatic update. Not fired on initial render.',
290
286
  detail: '{ value: string }',
291
287
  }),
292
288
  event('focus', { forwarded: true }),
@@ -10,6 +10,28 @@ export declare function generateId(prefix?: string): string;
10
10
  * Removes null, undefined and empty string values from an array.
11
11
  */
12
12
  export declare function compact<T>(items: (T | null | undefined)[]): T[];
13
+ /**
14
+ * Dispatches the standard `value-changed` event for a form control from its
15
+ * `updated()` lifecycle, so it fires on real changes — user input *and*
16
+ * programmatic `el.value = ...` updates — but not on the initial render.
17
+ *
18
+ * Lit records the old value as `undefined` for any change made before the first
19
+ * update, so `changed.get(key) !== undefined` reliably skips initial hydration
20
+ * even when the value was set imperatively beforehand.
21
+ *
22
+ * @example
23
+ * updated(changed: Map<string, unknown>) {
24
+ * if (changed.has('value')) {
25
+ * this.internals.setFormValue(this.value);
26
+ * this.updateValidity();
27
+ * }
28
+ * emitValueChanged(this, changed, this.value);
29
+ * }
30
+ */
31
+ export declare function emitValueChanged(host: EventTarget, changed: {
32
+ has(key: string): boolean;
33
+ get(key: string): unknown;
34
+ }, value: unknown, key?: string): void;
13
35
  /**
14
36
  * Ensure a custom element is defined
15
37
  *
@@ -33,6 +33,33 @@ export function generateId(prefix) {
33
33
  export function compact(items) {
34
34
  return items.filter((item) => item != null && item !== '');
35
35
  }
36
+ /**
37
+ * Dispatches the standard `value-changed` event for a form control from its
38
+ * `updated()` lifecycle, so it fires on real changes — user input *and*
39
+ * programmatic `el.value = ...` updates — but not on the initial render.
40
+ *
41
+ * Lit records the old value as `undefined` for any change made before the first
42
+ * update, so `changed.get(key) !== undefined` reliably skips initial hydration
43
+ * even when the value was set imperatively beforehand.
44
+ *
45
+ * @example
46
+ * updated(changed: Map<string, unknown>) {
47
+ * if (changed.has('value')) {
48
+ * this.internals.setFormValue(this.value);
49
+ * this.updateValidity();
50
+ * }
51
+ * emitValueChanged(this, changed, this.value);
52
+ * }
53
+ */
54
+ export function emitValueChanged(host, changed, value, key = 'value') {
55
+ if (changed.has(key) && changed.get(key) !== undefined) {
56
+ host.dispatchEvent(new CustomEvent('value-changed', {
57
+ detail: { value },
58
+ bubbles: true,
59
+ composed: true,
60
+ }));
61
+ }
62
+ }
36
63
  /**
37
64
  * Ensure a custom element is defined
38
65
  *
@@ -1,5 +1,9 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { clamp } from './shared.utils';
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { clamp, emitValueChanged } from './shared.utils';
3
+ function fakeHost() {
4
+ const dispatchEvent = vi.fn();
5
+ return { host: { dispatchEvent }, dispatchEvent };
6
+ }
3
7
  describe('clamp', () => {
4
8
  it('returns value when within range', () => {
5
9
  expect(clamp(5, 0, 10)).toBe(5);
@@ -26,3 +30,36 @@ describe('clamp', () => {
26
30
  expect(clamp(10, 5, 5)).toBe(5);
27
31
  });
28
32
  });
33
+ describe('emitValueChanged', () => {
34
+ it('dispatches value-changed carrying the given value on a real change', () => {
35
+ const { host, dispatchEvent } = fakeHost();
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ emitValueChanged(host, new Map([['value', 'old']]), 'new');
38
+ expect(dispatchEvent).toHaveBeenCalledTimes(1);
39
+ const event = dispatchEvent.mock.calls[0][0];
40
+ expect(event.type).toBe('value-changed');
41
+ expect(event.detail).toEqual({ value: 'new' });
42
+ expect(event.bubbles).toBe(true);
43
+ expect(event.composed).toBe(true);
44
+ });
45
+ it('does not dispatch on the initial render (Lit reports undefined old value)', () => {
46
+ const { host, dispatchEvent } = fakeHost();
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
+ emitValueChanged(host, new Map([['value', undefined]]), 'preset');
49
+ expect(dispatchEvent).not.toHaveBeenCalled();
50
+ });
51
+ it('does not dispatch when the key did not change', () => {
52
+ const { host, dispatchEvent } = fakeHost();
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ emitValueChanged(host, new Map([['other', 'x']]), 'same');
55
+ expect(dispatchEvent).not.toHaveBeenCalled();
56
+ });
57
+ it('supports gating on a custom key', () => {
58
+ const { host, dispatchEvent } = fakeHost();
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ emitValueChanged(host, new Map([['count', 0]]), 42, 'count');
61
+ expect(dispatchEvent).toHaveBeenCalledTimes(1);
62
+ const event = dispatchEvent.mock.calls[0][0];
63
+ expect(event.detail).toEqual({ value: 42 });
64
+ });
65
+ });
@@ -16,6 +16,7 @@ import { Button } from '../button';
16
16
  import { TextInputSize } from './index.constants';
17
17
  import { A11yAttributesMixin } from '../shared/a11y-attributes.mixin';
18
18
  import { event, prop } from '../shared/component-meta.decorators';
19
+ import { emitValueChanged } from '../shared/shared.utils';
19
20
  export { TextInputSize } from './index.constants';
20
21
  LucideIconCircleAlert.ensureDefined();
21
22
  LucideIconEye.ensureDefined();
@@ -54,6 +55,7 @@ let TextInput = class TextInput extends A11yAttributesMixin(QuanticElement) {
54
55
  this.internals.setFormValue(this.value);
55
56
  this.updateValidity();
56
57
  }
58
+ emitValueChanged(this, changedProperties, this.value);
57
59
  }
58
60
  forwardNative(e) {
59
61
  const target = e.target;
@@ -67,16 +69,7 @@ let TextInput = class TextInput extends A11yAttributesMixin(QuanticElement) {
67
69
  }
68
70
  }
69
71
  dispatchValueChanged(e) {
70
- const inputEl = e.target;
71
- this.value = inputEl.value;
72
- // Update form value
73
- this.internals.setFormValue(this.value);
74
- this.updateValidity();
75
- this.dispatchEvent(new CustomEvent('value-changed', {
76
- detail: { value: this.value },
77
- bubbles: true,
78
- composed: true,
79
- }));
72
+ this.value = e.target.value;
80
73
  }
81
74
  formResetCallback() {
82
75
  this.value = '';
@@ -376,7 +369,7 @@ __decorate([
376
369
  ], TextInput.prototype, "showPassword", void 0);
377
370
  TextInput = __decorate([
378
371
  event('value-changed', {
379
- description: 'Fired on every input and change event.',
372
+ description: 'Fired whenever the value changes, from user input or a programmatic update. Not fired on initial render.',
380
373
  detail: '{ value: string }',
381
374
  }),
382
375
  event('input', { forwarded: true }),
@@ -13,6 +13,7 @@ import { LucideIconCircleAlert } from '../icon/__generated__/lucide/circle-alert
13
13
  import { TextareaSize, TextareaResize } from './index.constants';
14
14
  import { A11yAttributesMixin } from '../shared/a11y-attributes.mixin';
15
15
  import { event, prop } from '../shared/component-meta.decorators';
16
+ import { emitValueChanged } from '../shared/shared.utils';
16
17
  export { TextareaSize, TextareaResize } from './index.constants';
17
18
  LucideIconCircleAlert.ensureDefined();
18
19
  let Textarea = class Textarea extends A11yAttributesMixin(QuanticElement) {
@@ -36,6 +37,7 @@ let Textarea = class Textarea extends A11yAttributesMixin(QuanticElement) {
36
37
  this.internals.setFormValue(this.value);
37
38
  this.updateValidity();
38
39
  }
40
+ emitValueChanged(this, changedProperties, this.value);
39
41
  }
40
42
  forwardNative(e) {
41
43
  const target = e.target;
@@ -43,15 +45,7 @@ let Textarea = class Textarea extends A11yAttributesMixin(QuanticElement) {
43
45
  this.dispatchEvent(new Event(e.type, { bubbles: true, composed: true }));
44
46
  }
45
47
  dispatchValueChanged(e) {
46
- const textareaEl = e.target;
47
- this.value = textareaEl.value;
48
- this.internals.setFormValue(this.value);
49
- this.updateValidity();
50
- this.dispatchEvent(new CustomEvent('value-changed', {
51
- detail: { value: this.value },
52
- bubbles: true,
53
- composed: true,
54
- }));
48
+ this.value = e.target.value;
55
49
  }
56
50
  formResetCallback() {
57
51
  this.value = '';
@@ -312,7 +306,7 @@ __decorate([
312
306
  ], Textarea.prototype, "autoComplete", void 0);
313
307
  Textarea = __decorate([
314
308
  event('value-changed', {
315
- description: 'Fired on every input and change event.',
309
+ description: 'Fired whenever the value changes, from user input or a programmatic update. Not fired on initial render.',
316
310
  detail: '{ value: string }',
317
311
  }),
318
312
  event('input', { forwarded: true }),
@@ -46,7 +46,7 @@ export declare class Combobox extends Combobox_base {
46
46
  private nextNavigableIndex;
47
47
  private openList;
48
48
  private closeList;
49
- private commitCustomValue;
49
+ private commitInputValue;
50
50
  private positionListbox;
51
51
  private selectOption;
52
52
  private handleInput;
@@ -10,6 +10,28 @@ export declare function generateId(prefix?: string): string;
10
10
  * Removes null, undefined and empty string values from an array.
11
11
  */
12
12
  export declare function compact<T>(items: (T | null | undefined)[]): T[];
13
+ /**
14
+ * Dispatches the standard `value-changed` event for a form control from its
15
+ * `updated()` lifecycle, so it fires on real changes — user input *and*
16
+ * programmatic `el.value = ...` updates — but not on the initial render.
17
+ *
18
+ * Lit records the old value as `undefined` for any change made before the first
19
+ * update, so `changed.get(key) !== undefined` reliably skips initial hydration
20
+ * even when the value was set imperatively beforehand.
21
+ *
22
+ * @example
23
+ * updated(changed: Map<string, unknown>) {
24
+ * if (changed.has('value')) {
25
+ * this.internals.setFormValue(this.value);
26
+ * this.updateValidity();
27
+ * }
28
+ * emitValueChanged(this, changed, this.value);
29
+ * }
30
+ */
31
+ export declare function emitValueChanged(host: EventTarget, changed: {
32
+ has(key: string): boolean;
33
+ get(key: string): unknown;
34
+ }, value: unknown, key?: string): void;
13
35
  /**
14
36
  * Ensure a custom element is defined
15
37
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrliq/quantic-components",
3
- "version": "1.63.2",
3
+ "version": "1.63.4",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/types/index.d.ts",
6
6
  "exports": {