@neumorphism-ui/elements 1.0.0-next.1 → 1.0.0-next.3

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.
Files changed (183) hide show
  1. package/README.md +12 -1
  2. package/cdn/neu.min.js +176 -51
  3. package/custom-elements.json +2555 -358
  4. package/dist/all.d.ts +8 -0
  5. package/dist/all.js +8 -0
  6. package/dist/checkbox.d.ts +3 -0
  7. package/dist/checkbox.js +4 -0
  8. package/dist/components/avatar/avatar.d.ts +0 -3
  9. package/dist/components/avatar/avatar.def.d.ts +0 -3
  10. package/dist/components/avatar/avatar.def.js +4 -18
  11. package/dist/components/avatar/avatar.docs.d.ts +2 -0
  12. package/dist/components/avatar/avatar.docs.js +22 -0
  13. package/dist/components/avatar/avatar.js +7 -1
  14. package/dist/components/badge/badge.d.ts +0 -2
  15. package/dist/components/badge/badge.def.d.ts +0 -2
  16. package/dist/components/badge/badge.def.js +1 -14
  17. package/dist/components/badge/badge.docs.d.ts +2 -0
  18. package/dist/components/badge/badge.docs.js +17 -0
  19. package/dist/components/button/button.d.ts +0 -2
  20. package/dist/components/button/button.def.d.ts +0 -5
  21. package/dist/components/button/button.def.js +3 -25
  22. package/dist/components/button/button.docs.d.ts +2 -0
  23. package/dist/components/button/button.docs.js +29 -0
  24. package/dist/components/button/button.js +3 -36
  25. package/dist/components/button-group/button-group.d.ts +0 -2
  26. package/dist/components/button-group/button-group.def.d.ts +0 -2
  27. package/dist/components/button-group/button-group.def.js +1 -10
  28. package/dist/components/button-group/button-group.docs.d.ts +2 -0
  29. package/dist/components/button-group/button-group.docs.js +13 -0
  30. package/dist/components/checkbox/checkbox.d.ts +40 -0
  31. package/dist/components/checkbox/checkbox.def.d.ts +27 -0
  32. package/dist/components/checkbox/checkbox.def.js +21 -0
  33. package/dist/components/checkbox/checkbox.docs.d.ts +2 -0
  34. package/dist/components/checkbox/checkbox.docs.js +27 -0
  35. package/dist/components/checkbox/checkbox.js +40 -0
  36. package/dist/components/checkbox/checkbox.styles.d.ts +1 -0
  37. package/dist/components/checkbox/checkbox.styles.js +28 -0
  38. package/dist/components/divider/divider.d.ts +0 -1
  39. package/dist/components/divider/divider.def.d.ts +0 -1
  40. package/dist/components/divider/divider.def.js +0 -9
  41. package/dist/components/divider/divider.docs.d.ts +2 -0
  42. package/dist/components/divider/divider.docs.js +12 -0
  43. package/dist/components/icon/icon.d.ts +0 -3
  44. package/dist/components/icon/icon.def.d.ts +0 -3
  45. package/dist/components/icon/icon.def.js +3 -10
  46. package/dist/components/icon/icon.docs.d.ts +2 -0
  47. package/dist/components/icon/icon.docs.js +13 -0
  48. package/dist/components/icon/system-icons.d.ts +10 -0
  49. package/dist/components/icon/system-icons.js +22 -11
  50. package/dist/components/icon-button/icon-button.d.ts +0 -5
  51. package/dist/components/icon-button/icon-button.def.d.ts +0 -5
  52. package/dist/components/icon-button/icon-button.def.js +3 -17
  53. package/dist/components/icon-button/icon-button.docs.d.ts +2 -0
  54. package/dist/components/icon-button/icon-button.docs.js +26 -0
  55. package/dist/components/number-field/number-field.d.ts +79 -0
  56. package/dist/components/number-field/number-field.def.d.ts +115 -0
  57. package/dist/components/number-field/number-field.def.js +40 -0
  58. package/dist/components/number-field/number-field.docs.d.ts +2 -0
  59. package/dist/components/number-field/number-field.docs.js +45 -0
  60. package/dist/components/number-field/number-field.js +121 -0
  61. package/dist/components/number-field/number-field.styles.d.ts +1 -0
  62. package/dist/components/number-field/number-field.styles.js +15 -0
  63. package/dist/components/option/option-data.d.ts +8 -0
  64. package/dist/components/option/option-data.js +12 -0
  65. package/dist/components/option/option.d.ts +17 -0
  66. package/dist/components/option/option.def.d.ts +8 -0
  67. package/dist/components/option/option.def.js +12 -0
  68. package/dist/components/option/option.docs.d.ts +2 -0
  69. package/dist/components/option/option.docs.js +12 -0
  70. package/dist/components/option/option.js +17 -0
  71. package/dist/components/option/option.styles.d.ts +2 -0
  72. package/dist/components/option/option.styles.js +5 -0
  73. package/dist/components/radio/radio-link.d.ts +11 -0
  74. package/dist/components/radio/radio-link.js +5 -0
  75. package/dist/components/radio/radio.d.ts +27 -0
  76. package/dist/components/radio/radio.def.d.ts +9 -0
  77. package/dist/components/radio/radio.def.js +12 -0
  78. package/dist/components/radio/radio.docs.d.ts +2 -0
  79. package/dist/components/radio/radio.docs.js +18 -0
  80. package/dist/components/radio/radio.js +43 -0
  81. package/dist/components/radio/radio.styles.d.ts +1 -0
  82. package/dist/components/radio/radio.styles.js +21 -0
  83. package/dist/components/radio-group/radio-group.d.ts +76 -0
  84. package/dist/components/radio-group/radio-group.def.d.ts +36 -0
  85. package/dist/components/radio-group/radio-group.def.js +40 -0
  86. package/dist/components/radio-group/radio-group.docs.d.ts +2 -0
  87. package/dist/components/radio-group/radio-group.docs.js +37 -0
  88. package/dist/components/radio-group/radio-group.js +208 -0
  89. package/dist/components/radio-group/radio-group.styles.d.ts +1 -0
  90. package/dist/components/radio-group/radio-group.styles.js +13 -0
  91. package/dist/components/select/select.d.ts +115 -0
  92. package/dist/components/select/select.def.d.ts +34 -0
  93. package/dist/components/select/select.def.js +35 -0
  94. package/dist/components/select/select.docs.d.ts +2 -0
  95. package/dist/components/select/select.docs.js +65 -0
  96. package/dist/components/select/select.js +391 -0
  97. package/dist/components/select/select.styles.d.ts +1 -0
  98. package/dist/components/select/select.styles.js +35 -0
  99. package/dist/components/shared/checkable.d.ts +45 -0
  100. package/dist/components/shared/checkable.js +108 -0
  101. package/dist/components/shared/field.styles.d.ts +1 -0
  102. package/dist/components/shared/field.styles.js +34 -0
  103. package/dist/components/shared/press.d.ts +7 -0
  104. package/dist/components/shared/press.js +42 -0
  105. package/dist/components/shared/step.d.ts +2 -0
  106. package/dist/components/shared/step.js +2 -0
  107. package/dist/components/shared/text-control.d.ts +72 -0
  108. package/dist/components/shared/text-control.js +191 -0
  109. package/dist/components/slider/slider.d.ts +67 -0
  110. package/dist/components/slider/slider.def.d.ts +37 -0
  111. package/dist/components/slider/slider.def.js +54 -0
  112. package/dist/components/slider/slider.docs.d.ts +2 -0
  113. package/dist/components/slider/slider.docs.js +45 -0
  114. package/dist/components/slider/slider.js +170 -0
  115. package/dist/components/slider/slider.styles.d.ts +1 -0
  116. package/dist/components/slider/slider.styles.js +16 -0
  117. package/dist/components/spinner/spinner.d.ts +0 -1
  118. package/dist/components/spinner/spinner.def.d.ts +0 -1
  119. package/dist/components/spinner/spinner.def.js +1 -13
  120. package/dist/components/spinner/spinner.docs.d.ts +2 -0
  121. package/dist/components/spinner/spinner.docs.js +17 -0
  122. package/dist/components/surface/surface.d.ts +0 -4
  123. package/dist/components/surface/surface.def.d.ts +0 -4
  124. package/dist/components/surface/surface.def.js +3 -12
  125. package/dist/components/surface/surface.docs.d.ts +2 -0
  126. package/dist/components/surface/surface.docs.js +15 -0
  127. package/dist/components/switch/switch.d.ts +4 -35
  128. package/dist/components/switch/switch.def.d.ts +0 -5
  129. package/dist/components/switch/switch.def.js +5 -21
  130. package/dist/components/switch/switch.docs.d.ts +2 -0
  131. package/dist/components/switch/switch.docs.js +26 -0
  132. package/dist/components/switch/switch.js +5 -117
  133. package/dist/components/text-field/implicit-submit.d.ts +5 -0
  134. package/dist/components/text-field/implicit-submit.js +8 -3
  135. package/dist/components/text-field/text-field.d.ts +9 -42
  136. package/dist/components/text-field/text-field.def.d.ts +0 -12
  137. package/dist/components/text-field/text-field.def.js +5 -30
  138. package/dist/components/text-field/text-field.docs.d.ts +2 -0
  139. package/dist/components/text-field/text-field.docs.js +45 -0
  140. package/dist/components/text-field/text-field.js +21 -178
  141. package/dist/components/text-field/text-field.styles.d.ts +1 -1
  142. package/dist/components/text-field/text-field.styles.js +2 -35
  143. package/dist/components/textarea/textarea.d.ts +92 -0
  144. package/dist/components/textarea/textarea.def.d.ts +127 -0
  145. package/dist/components/textarea/textarea.def.js +44 -0
  146. package/dist/components/textarea/textarea.docs.d.ts +2 -0
  147. package/dist/components/textarea/textarea.docs.js +40 -0
  148. package/dist/components/textarea/textarea.js +101 -0
  149. package/dist/components/textarea/textarea.styles.d.ts +1 -0
  150. package/dist/components/textarea/textarea.styles.js +9 -0
  151. package/dist/components/theme-docs.d.ts +3 -0
  152. package/dist/components/theme-docs.js +8 -0
  153. package/dist/core/define.d.ts +20 -2
  154. package/dist/core/dom.d.ts +4 -0
  155. package/dist/core/dom.js +7 -0
  156. package/dist/core/form-control.js +6 -1
  157. package/dist/core/neu-element.d.ts +2 -2
  158. package/dist/core/neu-element.js +10 -7
  159. package/dist/core/props.d.ts +13 -1
  160. package/dist/core/props.js +13 -3
  161. package/dist/core/styles.d.ts +0 -2
  162. package/dist/core/styles.js +0 -7
  163. package/dist/core/validation.d.ts +6 -2
  164. package/dist/core/validation.js +15 -8
  165. package/dist/docs.d.ts +3 -0
  166. package/dist/docs.js +41 -0
  167. package/dist/number-field.d.ts +3 -0
  168. package/dist/number-field.js +4 -0
  169. package/dist/option.d.ts +3 -0
  170. package/dist/option.js +4 -0
  171. package/dist/radio-group.d.ts +4 -0
  172. package/dist/radio-group.js +6 -0
  173. package/dist/radio.d.ts +3 -0
  174. package/dist/radio.js +4 -0
  175. package/dist/select.d.ts +3 -0
  176. package/dist/select.js +4 -0
  177. package/dist/slider.d.ts +3 -0
  178. package/dist/slider.js +4 -0
  179. package/dist/ssr.d.ts +10 -6
  180. package/dist/ssr.js +26 -6
  181. package/dist/textarea.d.ts +3 -0
  182. package/dist/textarea.js +4 -0
  183. package/package.json +45 -1
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Static brand of the custom elements that block implicit submission like a text input. A
3
+ * subclass inherits it, so it counts under any tag name.
4
+ */
5
+ export declare const BLOCKS_IMPLICIT_SUBMIT: unique symbol;
1
6
  /**
2
7
  * Enter in a text control submits like a native input (HTML §4.10.21.2). With a default button
3
8
  * (the form's first submit button, form="id" ones included) it clicks that button unless it is
@@ -14,6 +14,13 @@ const BLOCKING_TYPES = new Set([
14
14
  'datetime-local',
15
15
  'number',
16
16
  ]);
17
+ /**
18
+ * Static brand of the custom elements that block implicit submission like a text input. A
19
+ * subclass inherits it, so it counts under any tag name.
20
+ */
21
+ export const BLOCKS_IMPLICIT_SUBMIT = Symbol.for('neu-ui.blocks-implicit-submit');
22
+ const blocks = (el) => (el instanceof HTMLInputElement && BLOCKING_TYPES.has(el.type)) ||
23
+ el.constructor[BLOCKS_IMPLICIT_SUBMIT] === true;
17
24
  /**
18
25
  * Enter in a text control submits like a native input (HTML §4.10.21.2). With a default button
19
26
  * (the form's first submit button, form="id" ones included) it clicks that button unless it is
@@ -33,9 +40,7 @@ export function implicitSubmit(form) {
33
40
  defaultButton.click();
34
41
  return;
35
42
  }
36
- const blocking = elements.filter((el) => (el instanceof HTMLInputElement && BLOCKING_TYPES.has(el.type)) ||
37
- el.localName === 'neu-text-field');
38
- if (blocking.length !== 1)
43
+ if (elements.filter(blocks).length !== 1)
39
44
  return;
40
45
  markSubmitAttempt(form);
41
46
  form.requestSubmit();
@@ -1,18 +1,17 @@
1
- import { FormControlElement } from '../../core/form-control.js';
2
- export declare class NeuTextField extends FormControlElement {
1
+ import { TextControlElement } from '../shared/text-control.js';
2
+ import { BLOCKS_IMPLICIT_SUBMIT } from './implicit-submit.js';
3
+ export declare class NeuTextField extends TextControlElement {
3
4
  static def: import("../../ssr.js").ComponentDef<{
4
5
  readonly type: {
5
6
  readonly type: "string";
6
7
  readonly default: "text";
7
8
  readonly values: readonly ["text", "email", "password", "search", "tel", "url"];
8
- readonly reflect: true;
9
9
  };
10
10
  readonly value: {
11
11
  readonly type: "string";
12
12
  };
13
13
  readonly name: {
14
14
  readonly type: "string";
15
- readonly reflect: true;
16
15
  };
17
16
  readonly label: {
18
17
  readonly type: "string";
@@ -28,15 +27,12 @@ export declare class NeuTextField extends FormControlElement {
28
27
  };
29
28
  readonly required: {
30
29
  readonly type: "boolean";
31
- readonly reflect: true;
32
30
  };
33
31
  readonly disabled: {
34
32
  readonly type: "boolean";
35
- readonly reflect: true;
36
33
  };
37
34
  readonly readOnly: {
38
35
  readonly type: "boolean";
39
- readonly reflect: true;
40
36
  readonly attribute: "readonly";
41
37
  };
42
38
  readonly minLength: {
@@ -67,50 +63,21 @@ export declare class NeuTextField extends FormControlElement {
67
63
  readonly type: "string";
68
64
  readonly default: "md";
69
65
  readonly values: readonly ["sm", "md", "lg"];
70
- readonly reflect: true;
71
66
  };
72
67
  }>;
68
+ static readonly [BLOCKS_IMPLICIT_SUBMIT] = true;
73
69
  static extraProperties: string[];
74
70
  type: 'text' | 'email' | 'password' | 'search' | 'tel' | 'url';
75
- name: string;
76
- label: string;
77
- placeholder: string;
78
- helpText: string;
79
- errorText: string;
80
- required: boolean;
81
- disabled: boolean;
82
- readOnly: boolean;
83
71
  minLength: number;
84
72
  maxLength: number;
85
73
  pattern: string;
86
- autocomplete: string;
87
74
  size: 'sm' | 'md' | 'lg';
88
- /** True once the value was edited or set as a property; the default then stops applying. */
89
- private dirty;
90
- /** A value set before the shadow root exists. */
91
- private pendingValue;
92
- get value(): string;
93
- set value(next: string);
94
- get defaultValue(): string;
95
- set defaultValue(next: string);
96
- /** Focuses the inner input directly, so the caret stays where it was (no select-all). */
97
- focus(options?: FocusOptions): void;
98
- select(): void;
99
- setSelectionRange(start: number, end: number, direction?: 'forward' | 'backward' | 'none'): void;
100
- private get p();
101
- private get input();
75
+ protected readonly commitProps: string[];
76
+ private get tp();
102
77
  /** A detached input of this type holding `value`, sanitized the way the browser does it. */
103
- private probe;
104
- /** Stands in for the inner input until it exists, so validity is right from the start. */
105
- private validityProbe;
106
- protected rendered(): void;
78
+ protected probe(value: string): HTMLInputElement;
79
+ protected controlKeyDown(event: KeyboardEvent): void;
107
80
  protected update(changed: ReadonlySet<string>): void;
108
- /** update() only runs once connected; before that, keep the value and validity current here. */
109
- protected willUpdate(changed: ReadonlySet<string>): void;
110
- protected formValue(): string;
111
- /** Like a native input, a password or autocomplete=off field is never saved for restore. */
81
+ /** Like a native input, a password field is never saved for restore. */
112
82
  protected formState(): string | null;
113
- protected validate(): void;
114
- protected resetToDefault(): void;
115
- protected restoreState(state: string): void;
116
83
  }
@@ -4,14 +4,12 @@ declare const props: {
4
4
  readonly type: "string";
5
5
  readonly default: "text";
6
6
  readonly values: readonly ["text", "email", "password", "search", "tel", "url"];
7
- readonly reflect: true;
8
7
  };
9
8
  readonly value: {
10
9
  readonly type: "string";
11
10
  };
12
11
  readonly name: {
13
12
  readonly type: "string";
14
- readonly reflect: true;
15
13
  };
16
14
  readonly label: {
17
15
  readonly type: "string";
@@ -27,15 +25,12 @@ declare const props: {
27
25
  };
28
26
  readonly required: {
29
27
  readonly type: "boolean";
30
- readonly reflect: true;
31
28
  };
32
29
  readonly disabled: {
33
30
  readonly type: "boolean";
34
- readonly reflect: true;
35
31
  };
36
32
  readonly readOnly: {
37
33
  readonly type: "boolean";
38
- readonly reflect: true;
39
34
  readonly attribute: "readonly";
40
35
  };
41
36
  readonly minLength: {
@@ -66,7 +61,6 @@ declare const props: {
66
61
  readonly type: "string";
67
62
  readonly default: "md";
68
63
  readonly values: readonly ["sm", "md", "lg"];
69
- readonly reflect: true;
70
64
  };
71
65
  };
72
66
  export type TextFieldProps = InferProps<typeof props>;
@@ -75,14 +69,12 @@ export declare const textFieldDef: import("../../core/define.js").ComponentDef<{
75
69
  readonly type: "string";
76
70
  readonly default: "text";
77
71
  readonly values: readonly ["text", "email", "password", "search", "tel", "url"];
78
- readonly reflect: true;
79
72
  };
80
73
  readonly value: {
81
74
  readonly type: "string";
82
75
  };
83
76
  readonly name: {
84
77
  readonly type: "string";
85
- readonly reflect: true;
86
78
  };
87
79
  readonly label: {
88
80
  readonly type: "string";
@@ -98,15 +90,12 @@ export declare const textFieldDef: import("../../core/define.js").ComponentDef<{
98
90
  };
99
91
  readonly required: {
100
92
  readonly type: "boolean";
101
- readonly reflect: true;
102
93
  };
103
94
  readonly disabled: {
104
95
  readonly type: "boolean";
105
- readonly reflect: true;
106
96
  };
107
97
  readonly readOnly: {
108
98
  readonly type: "boolean";
109
- readonly reflect: true;
110
99
  readonly attribute: "readonly";
111
100
  };
112
101
  readonly minLength: {
@@ -137,7 +126,6 @@ export declare const textFieldDef: import("../../core/define.js").ComponentDef<{
137
126
  readonly type: "string";
138
127
  readonly default: "md";
139
128
  readonly values: readonly ["sm", "md", "lg"];
140
- readonly reflect: true;
141
129
  };
142
130
  }>;
143
131
  export {};
@@ -1,30 +1,28 @@
1
1
  import { defineComponent } from '../../core/define.js';
2
2
  import { html } from '../../core/html.js';
3
- import { themeCssProperties } from '../../core/styles.js';
4
3
  import { textFieldStyles } from './text-field.styles.js';
5
4
  const props = {
6
5
  type: {
7
6
  type: 'string',
8
7
  default: 'text',
9
8
  values: ['text', 'email', 'password', 'search', 'tel', 'url'],
10
- reflect: true,
11
9
  },
12
10
  value: { type: 'string' },
13
- name: { type: 'string', reflect: true },
11
+ name: { type: 'string' },
14
12
  label: { type: 'string' },
15
13
  placeholder: { type: 'string' },
16
14
  helpText: { type: 'string' },
17
15
  errorText: { type: 'string' },
18
- required: { type: 'boolean', reflect: true },
19
- disabled: { type: 'boolean', reflect: true },
20
- readOnly: { type: 'boolean', reflect: true, attribute: 'readonly' },
16
+ required: { type: 'boolean' },
17
+ disabled: { type: 'boolean' },
18
+ readOnly: { type: 'boolean', attribute: 'readonly' },
21
19
  minLength: { type: 'number', default: -1, attribute: 'minlength' },
22
20
  maxLength: { type: 'number', default: -1, attribute: 'maxlength' },
23
21
  pattern: { type: 'string' },
24
22
  autocomplete: { type: 'string' },
25
23
  inputMode: { type: 'string', attribute: 'inputmode' },
26
24
  enterKeyHint: { type: 'string', attribute: 'enterkeyhint' },
27
- size: { type: 'string', default: 'md', values: ['sm', 'md', 'lg'], reflect: true },
25
+ size: { type: 'string', default: 'md', values: ['sm', 'md', 'lg'] },
28
26
  };
29
27
  /** Attributes of the inner <input> that only appear when set. */
30
28
  function inputAttributes(p) {
@@ -37,27 +35,4 @@ export const textFieldDef = defineComponent({
37
35
  form: { valueProp: 'value' },
38
36
  styles: textFieldStyles,
39
37
  template: (p) => html `<div part="base" class="base"><label part="label" class="label" for="input"><slot name="label"><span class="label-text" data-ref="label-text">${p.label}</span></slot></label><div part="control" class="control"><span part="prefix" class="prefix"><slot name="prefix"></slot></span><input part="input" class="input" id="input" data-ref="input" type="${p.type}" value="${p.value}" aria-describedby="help"${inputAttributes(p)}><span part="suffix" class="suffix"><slot name="suffix"></slot></span></div><div part="help-text" class="help" id="help"><slot name="help-text"><span class="help-text" data-ref="help-text">${p.helpText}</span></slot><span class="error-text" data-ref="error-text">${p.errorText}</span></div></div>`,
40
- docs: {
41
- summary: 'A single-line text input with an inset field. The value attribute is the default value.',
42
- events: [
43
- { name: 'input', description: 'Fired on every edit (the native input event, composed).' },
44
- { name: 'change', description: 'Fired when an edit is committed, usually on blur.' },
45
- ],
46
- slots: [
47
- { name: 'label', description: 'Rich label; replaces the label attribute.' },
48
- { name: 'prefix', description: 'Content before the input, such as an icon.' },
49
- { name: 'suffix', description: 'Content after the input.' },
50
- { name: 'help-text', description: 'Rich help text; replaces the help-text attribute.' },
51
- ],
52
- parts: [
53
- { name: 'base', description: 'Outer wrapper.' },
54
- { name: 'label', description: 'The <label> element.' },
55
- { name: 'control', description: 'The inset field around the input.' },
56
- { name: 'input', description: 'The native <input>.' },
57
- { name: 'prefix', description: 'Wrapper around the prefix slot.' },
58
- { name: 'suffix', description: 'Wrapper around the suffix slot.' },
59
- { name: 'help-text', description: 'Help or error text area.' },
60
- ],
61
- cssProperties: themeCssProperties,
62
- },
63
38
  });
@@ -0,0 +1,2 @@
1
+ import type { ComponentDocs } from '../../core/define.js';
2
+ export declare const textFieldDocs: ComponentDocs;
@@ -0,0 +1,45 @@
1
+ import { themeCssProperties } from '../theme-docs.js';
2
+ export const textFieldDocs = {
3
+ summary: 'A single-line text input with an inset field. The value attribute is the default value.',
4
+ descriptions: {
5
+ type: 'The kind of text the field accepts, which sets its keyboard and masking and drives email, url and tel validation.',
6
+ value: 'The current text as typed or set by script.',
7
+ name: 'The name under which the text is submitted with a form.',
8
+ label: 'Visible label text; the label slot replaces it.',
9
+ placeholder: 'Hint shown while the field is empty.',
10
+ helpText: 'Guidance shown below the field; the help-text slot replaces it.',
11
+ errorText: "Shown in place of the browser's validation message once the field is invalid after user interaction; does not itself make the field invalid.",
12
+ required: 'Makes the form invalid while the field is empty.',
13
+ disabled: 'Prevents interaction and leaves the field out of form submission.',
14
+ readOnly: 'Keeps the text visible and submitted but not editable.',
15
+ minLength: 'The fewest characters the text must have; checked after user edits, like native, not on script-set values.',
16
+ maxLength: 'The most characters the text may have; checked after user edits, like native, not a hard limit on script-set values.',
17
+ pattern: 'A regular expression the whole text must match.',
18
+ autocomplete: "Hint for the browser's autofill, as for the native attribute.",
19
+ inputMode: 'Hint for which virtual keyboard to show.',
20
+ enterKeyHint: "Hint for the label of the virtual keyboard's Enter key.",
21
+ size: 'The field size.',
22
+ defaultValue: 'The text the field starts with, which a form reset restores; set by the value attribute.',
23
+ },
24
+ events: [
25
+ { name: 'input', description: 'Fired on every edit (the native input event, composed).' },
26
+ { name: 'change', description: 'Fired when an edit is committed, usually on blur.' },
27
+ ],
28
+ slots: [
29
+ { name: 'label', description: 'Rich label; replaces the label attribute.' },
30
+ { name: 'prefix', description: 'Content before the input, such as an icon.' },
31
+ { name: 'suffix', description: 'Content after the input.' },
32
+ { name: 'help-text', description: 'Rich help text; replaces the help-text attribute.' },
33
+ ],
34
+ parts: [
35
+ { name: 'base', description: 'Outer wrapper.' },
36
+ { name: 'label', description: 'The <label> element.' },
37
+ { name: 'control', description: 'The inset field around the input.' },
38
+ { name: 'input', description: 'The native <input>.' },
39
+ { name: 'prefix', description: 'Wrapper around the prefix slot.' },
40
+ { name: 'suffix', description: 'Wrapper around the suffix slot.' },
41
+ { name: 'help-text', description: 'Help or error text area.' },
42
+ ],
43
+ cssProperties: themeCssProperties,
44
+ keyboard: [],
45
+ };
@@ -1,207 +1,50 @@
1
- import { afterDispatch, dispatch, setOptionalAttribute, setText } from '../../core/dom.js';
2
- import { FormControlElement } from '../../core/form-control.js';
3
- import { copyValidity } from '../../core/validation.js';
4
- import { implicitSubmit } from './implicit-submit.js';
1
+ import { afterDispatch, composing, setOptionalAttribute } from '../../core/dom.js';
2
+ import { baseCommitProps, TextControlElement } from '../shared/text-control.js';
3
+ import { BLOCKS_IMPLICIT_SUBMIT, implicitSubmit } from './implicit-submit.js';
5
4
  import { textFieldDef } from './text-field.def.js';
6
- /** Props that can change the form value, its restore state or its validity. */
7
- const COMMIT_PROPS = [
8
- 'value',
9
- 'type',
10
- 'required',
11
- 'disabled',
12
- 'readOnly',
13
- 'minLength',
14
- 'maxLength',
15
- 'pattern',
16
- 'errorText',
17
- 'autocomplete',
18
- ];
19
- function needsCommit(changed) {
20
- return COMMIT_PROPS.some((key) => changed.has(key));
21
- }
22
- export class NeuTextField extends FormControlElement {
5
+ export class NeuTextField extends TextControlElement {
23
6
  static def = textFieldDef;
7
+ static [BLOCKS_IMPLICIT_SUBMIT] = true;
24
8
  static extraProperties = ['defaultValue'];
25
9
  // inputMode and enterKeyHint are inherited from HTMLElement with the right type.
26
- /** True once the value was edited or set as a property; the default then stops applying. */
27
- dirty = false;
28
- /** A value set before the shadow root exists. */
29
- pendingValue = null;
30
- get value() {
31
- return this.input?.value ?? this.pendingValue ?? this.p.value;
32
- }
33
- set value(next) {
34
- const text = next === null || next === undefined ? '' : String(next);
35
- this.dirty = true;
36
- const input = this.input;
37
- if (input)
38
- input.value = text;
39
- else
40
- this.pendingValue = text;
41
- this.commit();
42
- }
43
- get defaultValue() {
44
- return this.p.value;
45
- }
46
- set defaultValue(next) {
47
- this.setAttribute('value', String(next));
48
- }
49
- /** Focuses the inner input directly, so the caret stays where it was (no select-all). */
50
- focus(options) {
51
- const input = this.input;
52
- if (input)
53
- input.focus(options);
54
- else
55
- super.focus(options);
56
- }
57
- select() {
58
- this.input?.select();
59
- }
60
- setSelectionRange(start, end, direction) {
61
- this.input?.setSelectionRange(start, end, direction);
62
- }
63
- get p() {
10
+ commitProps = [...baseCommitProps, 'type', 'pattern'];
11
+ get tp() {
64
12
  return this.props;
65
13
  }
66
- get input() {
67
- return this.ref('input');
68
- }
69
14
  /** A detached input of this type holding `value`, sanitized the way the browser does it. */
70
15
  probe(value) {
71
16
  const probe = document.createElement('input');
72
- probe.type = this.p.type;
17
+ probe.type = this.tp.type;
73
18
  probe.value = value;
19
+ if (this.tp.pattern)
20
+ probe.setAttribute('pattern', this.tp.pattern);
74
21
  return probe;
75
22
  }
76
- /** Stands in for the inner input until it exists, so validity is right from the start. */
77
- validityProbe() {
78
- const p = this.p;
79
- const probe = this.probe(this.value);
80
- probe.required = p.required;
81
- probe.readOnly = p.readOnly;
82
- probe.disabled = this.isDisabled;
83
- if (p.pattern)
84
- probe.setAttribute('pattern', p.pattern);
85
- if (p.minLength >= 0)
86
- probe.setAttribute('minlength', String(p.minLength));
87
- if (p.maxLength >= 0)
88
- probe.setAttribute('maxlength', String(p.maxLength));
89
- return probe;
90
- }
91
- rendered() {
92
- const input = this.input;
93
- // A declarative root adopted without its input (cut short by the parser): nothing to wire up.
94
- if (!input)
23
+ controlKeyDown(event) {
24
+ if (event.key !== 'Enter' || composing(event))
95
25
  return;
96
- // An SSR'd input works before JS loads; keep whatever was typed or autofilled meanwhile.
97
- // Compare with the sanitized default: the browser trims or strips some values by itself.
98
- if (input.value !== this.probe(this.p.value).value)
99
- this.dirty = true;
100
- if (this.pendingValue !== null) {
101
- input.value = this.pendingValue;
102
- this.pendingValue = null;
103
- }
104
- input.addEventListener('input', () => {
105
- this.dirty = true;
106
- this.commit();
107
- });
108
- input.addEventListener('change', () => {
109
- this.markInteracted();
110
- dispatch(this, 'change');
111
- });
112
- input.addEventListener('keydown', (event) => {
113
- if (event.key !== 'Enter' || event.isComposing)
114
- return;
115
- // Like a native input: submit once the keydown has finished dispatching, unless a listener
116
- // (on the host or further up) cancelled it.
117
- afterDispatch(event, () => {
118
- if (!event.defaultPrevented)
119
- implicitSubmit(this.internals.form);
120
- });
26
+ // Like a native input: submit once the keydown has finished dispatching, unless a listener
27
+ // (on the host or further up) cancelled it.
28
+ afterDispatch(event, () => {
29
+ if (!event.defaultPrevented)
30
+ implicitSubmit(this.internals.form);
121
31
  });
122
32
  }
123
33
  update(changed) {
124
- const p = this.p;
34
+ const p = this.tp;
125
35
  const input = this.input;
126
36
  if (input) {
127
37
  if (changed.has('type') && input.type !== p.type)
128
38
  input.type = p.type;
129
- if (changed.has('value')) {
130
- if (input.defaultValue !== p.value)
131
- input.defaultValue = p.value;
132
- if (!this.dirty)
133
- input.value = p.value;
134
- }
135
- if (changed.has('placeholder'))
136
- setOptionalAttribute(input, 'placeholder', p.placeholder);
137
- if (changed.has('required'))
138
- input.required = p.required;
139
- if (changed.has('disabled'))
140
- input.disabled = this.isDisabled;
141
- if (changed.has('readOnly'))
142
- input.readOnly = p.readOnly;
143
- if (changed.has('minLength')) {
144
- setOptionalAttribute(input, 'minlength', p.minLength >= 0 ? String(p.minLength) : '');
145
- }
146
- if (changed.has('maxLength')) {
147
- setOptionalAttribute(input, 'maxlength', p.maxLength >= 0 ? String(p.maxLength) : '');
148
- }
149
39
  if (changed.has('pattern'))
150
40
  setOptionalAttribute(input, 'pattern', p.pattern);
151
- if (changed.has('autocomplete'))
152
- setOptionalAttribute(input, 'autocomplete', p.autocomplete);
153
41
  if (changed.has('inputMode'))
154
42
  setOptionalAttribute(input, 'inputmode', p.inputMode);
155
- if (changed.has('enterKeyHint'))
156
- setOptionalAttribute(input, 'enterkeyhint', p.enterKeyHint);
157
43
  }
158
- if (changed.has('label'))
159
- setText(this.ref('label-text'), p.label);
160
- if (changed.has('helpText'))
161
- setText(this.ref('help-text'), p.helpText);
162
- if (needsCommit(changed))
163
- this.commit();
44
+ super.update(changed);
164
45
  }
165
- /** update() only runs once connected; before that, keep the value and validity current here. */
166
- willUpdate(changed) {
167
- if (!this.isConnected && !this.input && needsCommit(changed))
168
- this.commit();
169
- }
170
- formValue() {
171
- return this.value;
172
- }
173
- /** Like a native input, a password or autocomplete=off field is never saved for restore. */
46
+ /** Like a native input, a password field is never saved for restore. */
174
47
  formState() {
175
- const p = this.p;
176
- if (p.type === 'password' || p.autocomplete.trim().toLowerCase() === 'off')
177
- return null;
178
- return this.value;
179
- }
180
- validate() {
181
- const input = this.input;
182
- const source = input ?? this.validityProbe();
183
- // A disabled or readonly input still sets flags such as typeMismatch, but is barred from
184
- // validation and has no message; setValidity() would throw on flags without a message.
185
- if (!source.willValidate || source.validity.valid)
186
- this.internals.setValidity({});
187
- else {
188
- this.internals.setValidity(copyValidity(source.validity), source.validationMessage, input ?? undefined);
189
- }
190
- setText(this.ref('error-text'), this.p.errorText || input?.validationMessage || '');
191
- }
192
- resetToDefault() {
193
- this.dirty = false;
194
- this.pendingValue = null;
195
- const input = this.input;
196
- if (input)
197
- input.value = this.p.value;
198
- }
199
- restoreState(state) {
200
- this.dirty = true;
201
- const input = this.input;
202
- if (input)
203
- input.value = state;
204
- else
205
- this.pendingValue = state;
48
+ return this.tp.type === 'password' ? null : super.formState();
206
49
  }
207
50
  }
@@ -1 +1 @@
1
- export declare const textFieldStyles = "\n:host {\n --_neu-bg: var(--neu-bg, #e0e5ec);\n --_neu-accent-c: var(--neu-accent, #6d5dfc);\n --_neu-d: var(--neu-distance, 6px);\n --_neu-k: var(--neu-intensity, 1);\n --_neu-r: var(--neu-radius, 12px);\n --_neu-danger: var(--neu-danger, #e5484d);\n --_neu-dur: var(--neu-duration, 150ms);\n --_neu-sd: var(--neu-shadow-dark, color-mix(in oklab, var(--_neu-bg), black min(100%, calc(var(--_neu-shade, 18%) * var(--_neu-k)))));\n --_neu-sl: var(--neu-shadow-light, color-mix(in oklab, var(--_neu-bg), white min(100%, calc(var(--_neu-light, 100%) * var(--_neu-k)))));\n --_neu-fg: var(--neu-fg, color-mix(in oklab, var(--_neu-bg), var(--_neu-ink, black) var(--_neu-fg-mix, 32%)));\n --_neu-fg-muted: var(--neu-fg-muted, color-mix(in oklab, var(--_neu-bg), var(--_neu-ink, black) var(--_neu-fg-muted-mix, 18%)));\n --_neu-accent-soft: color-mix(in oklab, var(--_neu-accent-c) 25%, var(--_neu-bg));\n --_neu-focus: 0 0 0 3px color-mix(in oklab, var(--_neu-accent-c) 45%, transparent);\n --_neu-raised: var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) var(--_neu-sd), calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n --_neu-raised-sm: calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) var(--_neu-sd), calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);\n --_neu-inset: inset var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) var(--_neu-sd), inset calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n --_neu-inset-sm: inset calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) var(--_neu-sd), inset calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);\n font-family: var(--neu-font);\n color: var(--_neu-fg);\n -webkit-tap-highlight-color: transparent;\n}\n:host(:not(:defined)) { visibility: inherit !important; min-block-size: auto !important; }\n:host([hidden]) { display: none !important; }\n:host(:focus) { outline: none; }\n*, *::before, *::after { box-sizing: border-box; }\n\n:host { display: block; }\n:host(:disabled), :host([disabled]) { cursor: not-allowed; }\n.base { display: flex; flex-direction: column; }\n.label-text, ::slotted([slot='label']) {\n display: block; margin-block-end: 0.375rem;\n font-size: 0.8125rem; font-weight: 600; color: var(--_neu-fg-muted);\n}\n.label-text:empty { display: none; }\n.control {\n display: flex; align-items: center; min-block-size: 2.75rem; padding-inline: 0.875rem;\n border-radius: var(--_neu-r); background: var(--_neu-bg); box-shadow: var(--_neu-inset);\n transition: box-shadow var(--_neu-dur) ease;\n}\n:host([size='sm']) .control { min-block-size: 2.25rem; padding-inline: 0.75rem; box-shadow: var(--_neu-inset-sm); }\n:host([size='lg']) .control { min-block-size: 3.25rem; padding-inline: 1rem; }\n:host(:focus-within) .control { box-shadow: var(--_neu-inset), var(--_neu-focus); }\n:host(:state(user-invalid)) .control {\n box-shadow:\n inset var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) color-mix(in oklab, var(--_neu-danger) 35%, var(--_neu-sd)),\n inset calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n}\n:host(:disabled) .control, :host([disabled]) .control { box-shadow: var(--_neu-inset-sm); opacity: 0.6; }\n.input {\n flex: 1; min-inline-size: 0; align-self: stretch; padding: 0; border: none; outline: none;\n background: transparent; color: inherit; font: inherit; font-size: 0.9375rem;\n}\n.input::placeholder { color: var(--_neu-fg-muted); opacity: 1; }\n.prefix, .suffix { display: inline-flex; }\n::slotted([slot='prefix']) { margin-inline-end: 0.5rem; }\n::slotted([slot='suffix']) { margin-inline-start: 0.5rem; }\n.help { font-size: 0.75rem; color: var(--_neu-fg-muted); }\n.help-text, ::slotted([slot='help-text']) { display: block; margin-block-start: 0.375rem; }\n.help-text:empty { display: none; }\n.error-text { display: none; margin-block-start: 0.375rem; color: var(--_neu-danger); }\n:host(:state(user-invalid)) slot[name='help-text'] { display: none; }\n:host(:state(user-invalid)) .error-text:not(:empty) { display: block; }\n";
1
+ export declare const textFieldStyles = "\n:host {\n --_neu-bg: var(--neu-bg, #e0e5ec);\n --_neu-accent-c: var(--neu-accent, #6d5dfc);\n --_neu-d: var(--neu-distance, 6px);\n --_neu-k: var(--neu-intensity, 1);\n --_neu-r: var(--neu-radius, 12px);\n --_neu-danger: var(--neu-danger, #e5484d);\n --_neu-dur: var(--neu-duration, 150ms);\n --_neu-sd: var(--neu-shadow-dark, color-mix(in oklab, var(--_neu-bg), black min(100%, calc(var(--_neu-shade, 18%) * var(--_neu-k)))));\n --_neu-sl: var(--neu-shadow-light, color-mix(in oklab, var(--_neu-bg), white min(100%, calc(var(--_neu-light, 100%) * var(--_neu-k)))));\n --_neu-fg: var(--neu-fg, color-mix(in oklab, var(--_neu-bg), var(--_neu-ink, black) var(--_neu-fg-mix, 32%)));\n --_neu-fg-muted: var(--neu-fg-muted, color-mix(in oklab, var(--_neu-bg), var(--_neu-ink, black) var(--_neu-fg-muted-mix, 18%)));\n --_neu-accent-soft: color-mix(in oklab, var(--_neu-accent-c) 25%, var(--_neu-bg));\n --_neu-focus: 0 0 0 3px color-mix(in oklab, var(--_neu-accent-c) 45%, transparent);\n --_neu-raised: var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) var(--_neu-sd), calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n --_neu-raised-sm: calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) var(--_neu-sd), calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);\n --_neu-inset: inset var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) var(--_neu-sd), inset calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n --_neu-inset-sm: inset calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) var(--_neu-sd), inset calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);\n font-family: var(--neu-font);\n color: var(--_neu-fg);\n -webkit-tap-highlight-color: transparent;\n}\n:host(:not(:defined)) { visibility: inherit !important; min-block-size: auto !important; }\n:host([hidden]) { display: none !important; }\n:host(:focus) { outline: none; }\n*, *::before, *::after { box-sizing: border-box; }\n\n:host { display: block; }\n:host(:disabled), :host([disabled]) { cursor: not-allowed; }\n.base { display: flex; flex-direction: column; }\n.label { font-size: 0.8125rem; font-weight: 600; line-height: 1rem; color: var(--_neu-fg-muted); }\n.label-text, ::slotted([slot='label']) { display: block; margin-block-end: 0.375rem; }\n.label-text:empty { display: none; }\n.control {\n display: flex; align-items: center; min-block-size: 2.75rem; padding-inline: 0.875rem;\n border-radius: var(--_neu-r); background: var(--_neu-bg); box-shadow: var(--_neu-inset);\n transition: box-shadow var(--_neu-dur) ease;\n}\n:host([size='sm']) .control { min-block-size: 2.25rem; padding-inline: 0.75rem; box-shadow: var(--_neu-inset-sm); }\n:host([size='lg']) .control { min-block-size: 3.25rem; padding-inline: 1rem; }\n:host(:focus-within) .control { box-shadow: var(--_neu-inset), var(--_neu-focus); }\n:host(:state(user-invalid)) .control {\n box-shadow:\n inset var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) color-mix(in oklab, var(--_neu-danger) 35%, var(--_neu-sd)),\n inset calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n}\n:host(:disabled) .control, :host([disabled]) .control { box-shadow: var(--_neu-inset-sm); opacity: 0.6; }\n.input {\n flex: 1; min-inline-size: 0; align-self: stretch; padding: 0; border: none; outline: none;\n background: transparent; color: inherit; font: inherit; font-size: 0.9375rem;\n}\n.input::placeholder { color: var(--_neu-fg-muted); opacity: 1; }\n.help { font-size: 0.75rem; color: var(--_neu-fg-muted); }\n.help-text, ::slotted([slot='help-text']) { display: block; margin-block-start: 0.375rem; }\n.help-text:empty { display: none; }\n.error-text { display: none; margin-block-start: 0.375rem; color: var(--_neu-danger); }\n:host(:state(user-invalid)) slot[name='help-text'] { display: none; }\n:host(:state(user-invalid)) .error-text:not(:empty) { display: block; }\n\n.prefix, .suffix { display: inline-flex; }\n::slotted([slot='prefix']) { margin-inline-end: 0.5rem; }\n::slotted([slot='suffix']) { margin-inline-start: 0.5rem; }\n";
@@ -1,39 +1,6 @@
1
- import { hostTheme } from '../../core/styles.js';
2
- export const textFieldStyles = `${hostTheme}
3
- :host { display: block; }
4
- :host(:disabled), :host([disabled]) { cursor: not-allowed; }
5
- .base { display: flex; flex-direction: column; }
6
- .label-text, ::slotted([slot='label']) {
7
- display: block; margin-block-end: 0.375rem;
8
- font-size: 0.8125rem; font-weight: 600; color: var(--_neu-fg-muted);
9
- }
10
- .label-text:empty { display: none; }
11
- .control {
12
- display: flex; align-items: center; min-block-size: 2.75rem; padding-inline: 0.875rem;
13
- border-radius: var(--_neu-r); background: var(--_neu-bg); box-shadow: var(--_neu-inset);
14
- transition: box-shadow var(--_neu-dur) ease;
15
- }
16
- :host([size='sm']) .control { min-block-size: 2.25rem; padding-inline: 0.75rem; box-shadow: var(--_neu-inset-sm); }
17
- :host([size='lg']) .control { min-block-size: 3.25rem; padding-inline: 1rem; }
18
- :host(:focus-within) .control { box-shadow: var(--_neu-inset), var(--_neu-focus); }
19
- :host(:state(user-invalid)) .control {
20
- box-shadow:
21
- inset var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) color-mix(in oklab, var(--_neu-danger) 35%, var(--_neu-sd)),
22
- inset calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);
23
- }
24
- :host(:disabled) .control, :host([disabled]) .control { box-shadow: var(--_neu-inset-sm); opacity: 0.6; }
25
- .input {
26
- flex: 1; min-inline-size: 0; align-self: stretch; padding: 0; border: none; outline: none;
27
- background: transparent; color: inherit; font: inherit; font-size: 0.9375rem;
28
- }
29
- .input::placeholder { color: var(--_neu-fg-muted); opacity: 1; }
1
+ import { fieldStyles } from '../shared/field.styles.js';
2
+ export const textFieldStyles = `${fieldStyles}
30
3
  .prefix, .suffix { display: inline-flex; }
31
4
  ::slotted([slot='prefix']) { margin-inline-end: 0.5rem; }
32
5
  ::slotted([slot='suffix']) { margin-inline-start: 0.5rem; }
33
- .help { font-size: 0.75rem; color: var(--_neu-fg-muted); }
34
- .help-text, ::slotted([slot='help-text']) { display: block; margin-block-start: 0.375rem; }
35
- .help-text:empty { display: none; }
36
- .error-text { display: none; margin-block-start: 0.375rem; color: var(--_neu-danger); }
37
- :host(:state(user-invalid)) slot[name='help-text'] { display: none; }
38
- :host(:state(user-invalid)) .error-text:not(:empty) { display: block; }
39
6
  `;