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

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 (102) hide show
  1. package/README.md +12 -1
  2. package/cdn/neu.min.js +184 -51
  3. package/custom-elements.json +2232 -282
  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/button/button.d.ts +0 -2
  9. package/dist/components/button/button.js +3 -36
  10. package/dist/components/checkbox/checkbox.d.ts +44 -0
  11. package/dist/components/checkbox/checkbox.def.d.ts +31 -0
  12. package/dist/components/checkbox/checkbox.def.js +37 -0
  13. package/dist/components/checkbox/checkbox.js +40 -0
  14. package/dist/components/checkbox/checkbox.styles.d.ts +1 -0
  15. package/dist/components/checkbox/checkbox.styles.js +28 -0
  16. package/dist/components/icon/system-icons.d.ts +10 -0
  17. package/dist/components/icon/system-icons.js +22 -11
  18. package/dist/components/number-field/number-field.d.ts +84 -0
  19. package/dist/components/number-field/number-field.def.d.ts +125 -0
  20. package/dist/components/number-field/number-field.def.js +62 -0
  21. package/dist/components/number-field/number-field.js +121 -0
  22. package/dist/components/number-field/number-field.styles.d.ts +1 -0
  23. package/dist/components/number-field/number-field.styles.js +15 -0
  24. package/dist/components/option/option-data.d.ts +8 -0
  25. package/dist/components/option/option-data.js +12 -0
  26. package/dist/components/option/option.d.ts +19 -0
  27. package/dist/components/option/option.def.d.ts +10 -0
  28. package/dist/components/option/option.def.js +19 -0
  29. package/dist/components/option/option.js +17 -0
  30. package/dist/components/option/option.styles.d.ts +2 -0
  31. package/dist/components/option/option.styles.js +5 -0
  32. package/dist/components/radio/radio-link.d.ts +11 -0
  33. package/dist/components/radio/radio-link.js +5 -0
  34. package/dist/components/radio/radio.d.ts +29 -0
  35. package/dist/components/radio/radio.def.d.ts +11 -0
  36. package/dist/components/radio/radio.def.js +25 -0
  37. package/dist/components/radio/radio.js +43 -0
  38. package/dist/components/radio/radio.styles.d.ts +1 -0
  39. package/dist/components/radio/radio.styles.js +21 -0
  40. package/dist/components/radio-group/radio-group.d.ts +77 -0
  41. package/dist/components/radio-group/radio-group.def.d.ts +40 -0
  42. package/dist/components/radio-group/radio-group.def.js +55 -0
  43. package/dist/components/radio-group/radio-group.js +184 -0
  44. package/dist/components/radio-group/radio-group.styles.d.ts +1 -0
  45. package/dist/components/radio-group/radio-group.styles.js +15 -0
  46. package/dist/components/select/select.d.ts +119 -0
  47. package/dist/components/select/select.def.d.ts +38 -0
  48. package/dist/components/select/select.def.js +59 -0
  49. package/dist/components/select/select.js +385 -0
  50. package/dist/components/select/select.styles.d.ts +1 -0
  51. package/dist/components/select/select.styles.js +39 -0
  52. package/dist/components/shared/checkable.d.ts +45 -0
  53. package/dist/components/shared/checkable.js +108 -0
  54. package/dist/components/shared/field.styles.d.ts +1 -0
  55. package/dist/components/shared/field.styles.js +36 -0
  56. package/dist/components/shared/press.d.ts +7 -0
  57. package/dist/components/shared/press.js +42 -0
  58. package/dist/components/shared/step.d.ts +2 -0
  59. package/dist/components/shared/step.js +2 -0
  60. package/dist/components/shared/text-control.d.ts +72 -0
  61. package/dist/components/shared/text-control.js +191 -0
  62. package/dist/components/slider/slider.d.ts +69 -0
  63. package/dist/components/slider/slider.def.d.ts +39 -0
  64. package/dist/components/slider/slider.def.js +76 -0
  65. package/dist/components/slider/slider.js +170 -0
  66. package/dist/components/slider/slider.styles.d.ts +1 -0
  67. package/dist/components/slider/slider.styles.js +16 -0
  68. package/dist/components/switch/switch.d.ts +4 -30
  69. package/dist/components/switch/switch.js +5 -117
  70. package/dist/components/text-field/implicit-submit.d.ts +5 -0
  71. package/dist/components/text-field/implicit-submit.js +8 -3
  72. package/dist/components/text-field/text-field.d.ts +9 -36
  73. package/dist/components/text-field/text-field.js +21 -178
  74. package/dist/components/text-field/text-field.styles.d.ts +1 -1
  75. package/dist/components/text-field/text-field.styles.js +2 -35
  76. package/dist/components/textarea/textarea.d.ts +98 -0
  77. package/dist/components/textarea/textarea.def.d.ts +139 -0
  78. package/dist/components/textarea/textarea.def.js +65 -0
  79. package/dist/components/textarea/textarea.js +101 -0
  80. package/dist/components/textarea/textarea.styles.d.ts +1 -0
  81. package/dist/components/textarea/textarea.styles.js +9 -0
  82. package/dist/core/define.d.ts +16 -1
  83. package/dist/core/form-control.js +3 -1
  84. package/dist/core/validation.d.ts +6 -2
  85. package/dist/core/validation.js +15 -8
  86. package/dist/number-field.d.ts +3 -0
  87. package/dist/number-field.js +4 -0
  88. package/dist/option.d.ts +3 -0
  89. package/dist/option.js +4 -0
  90. package/dist/radio-group.d.ts +4 -0
  91. package/dist/radio-group.js +6 -0
  92. package/dist/radio.d.ts +3 -0
  93. package/dist/radio.js +4 -0
  94. package/dist/select.d.ts +3 -0
  95. package/dist/select.js +4 -0
  96. package/dist/slider.d.ts +3 -0
  97. package/dist/slider.js +4 -0
  98. package/dist/ssr.d.ts +9 -5
  99. package/dist/ssr.js +25 -5
  100. package/dist/textarea.d.ts +3 -0
  101. package/dist/textarea.js +4 -0
  102. package/package.json +41 -1
@@ -0,0 +1,184 @@
1
+ import { dispatch, setText } from '../../core/dom.js';
2
+ import { FormControlElement } from '../../core/form-control.js';
3
+ import { requiredMessage } from '../../core/validation.js';
4
+ import { RESYNC, SET_CHECKED, SET_GROUP_DISABLED } from '../radio/radio-link.js';
5
+ import { markRule, radioGroupDef } from './radio-group.def.js';
6
+ /** Which way each arrow key moves through the radios. */
7
+ const STEP = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 };
8
+ /** A radio's value; without the attribute it is "on", like a native radio. */
9
+ const valueOf = (radio) => radio.getAttribute('value') ?? 'on';
10
+ export class NeuRadioGroup extends FormControlElement {
11
+ static def = radioGroupDef;
12
+ static extraProperties = ['defaultValue'];
13
+ /**
14
+ * The requested value, kept even while no radio matches it (plan decision 8); null requests
15
+ * nothing, so a radio whose value is "" is chosen only when asked for.
16
+ */
17
+ current;
18
+ dirty = false;
19
+ /** The radio that holds the Tab stop. */
20
+ stop;
21
+ /** The selected radio as of the last sync, which the form value and validity read. */
22
+ picked;
23
+ observer;
24
+ constructor() {
25
+ super();
26
+ this.current = this.getAttribute('value');
27
+ this.internals.role = 'radiogroup';
28
+ this.addEventListener('click', (event) => {
29
+ const radio = this.radioAt(event);
30
+ if (radio && !this.isDisabled && !radio.hasAttribute('disabled'))
31
+ this.choose(radio);
32
+ });
33
+ this.addEventListener('keydown', (event) => this.onKey(event));
34
+ }
35
+ get value() {
36
+ return this.selected() ? this.current : '';
37
+ }
38
+ set value(next) {
39
+ this.dirty = true;
40
+ this.current = next === null || next === undefined ? null : String(next);
41
+ this.sync();
42
+ }
43
+ get defaultValue() {
44
+ return String(this.props.value);
45
+ }
46
+ set defaultValue(next) {
47
+ this.setAttribute('value', String(next));
48
+ }
49
+ /** The radios of this group: those whose closest group is this one. */
50
+ radios() {
51
+ return [...this.querySelectorAll('neu-radio')].filter((radio) => radio.closest('neu-radio-group') === this);
52
+ }
53
+ /** The first radio of this group matching the requested value, if one is requested. */
54
+ selected(radios = this.radios()) {
55
+ return this.current === null
56
+ ? undefined
57
+ : radios.find((radio) => valueOf(radio) === this.current);
58
+ }
59
+ radioAt(event) {
60
+ const radio = event.target instanceof Element ? event.target.closest('neu-radio') : null;
61
+ return radio && radio.closest('neu-radio-group') === this ? radio : null;
62
+ }
63
+ onKey(event) {
64
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey)
65
+ return;
66
+ const radio = this.radioAt(event);
67
+ // A key pressed inside a disabled radio (say, a link in its label) is not a choice.
68
+ if (!radio || this.isDisabled || radio.hasAttribute('disabled'))
69
+ return;
70
+ if (event.key === ' ') {
71
+ event.preventDefault();
72
+ this.choose(radio);
73
+ return;
74
+ }
75
+ let step = STEP[event.key];
76
+ if (!step)
77
+ return;
78
+ // Like native radios, the horizontal arrows follow the reading direction.
79
+ if ((event.key === 'ArrowLeft' || event.key === 'ArrowRight') && this.matches(':dir(rtl)')) {
80
+ step = -step;
81
+ }
82
+ const on = this.radios().filter((candidate) => !candidate.hasAttribute('disabled'));
83
+ const next = on[(on.indexOf(radio) + step + on.length) % on.length];
84
+ if (!next)
85
+ return;
86
+ event.preventDefault();
87
+ next.focus();
88
+ this.choose(next);
89
+ }
90
+ /** A choice by the user: no event when the value does not change. */
91
+ choose(radio) {
92
+ const next = valueOf(radio);
93
+ if (next === this.current && this.selected())
94
+ return;
95
+ this.dirty = true;
96
+ this.current = next;
97
+ this.sync();
98
+ this.markInteracted();
99
+ dispatch(this, 'input');
100
+ dispatch(this, 'change');
101
+ }
102
+ [RESYNC]() {
103
+ this.sync();
104
+ }
105
+ /**
106
+ * Brings every radio in line with the group: checked state, disabling, the one Tab stop (the
107
+ * selected radio, else the first enabled one), the server-visible mark, and the form value.
108
+ * Radios that are not upgraded yet are skipped; they ask for a resync when they connect.
109
+ */
110
+ sync() {
111
+ const radios = this.radios();
112
+ const picked = (this.picked = this.selected(radios));
113
+ const off = this.isDisabled;
114
+ const ready = radios.filter((radio) => SET_CHECKED in radio);
115
+ for (const radio of ready) {
116
+ radio[SET_GROUP_DISABLED](off);
117
+ radio[SET_CHECKED](radio === picked);
118
+ }
119
+ const on = off ? [] : ready.filter((radio) => !radio.disabled);
120
+ this.stop = on.find((radio) => radio === picked) ?? on[0];
121
+ for (const radio of on) {
122
+ const tabindex = radio === this.stop ? '0' : '-1';
123
+ if (radio.getAttribute('tabindex') !== tabindex)
124
+ radio.setAttribute('tabindex', tabindex);
125
+ }
126
+ const rule = markRule(this.current ?? '');
127
+ setText(this.ref('mark'), rule === '' ? '' : rule.value);
128
+ this.commit();
129
+ }
130
+ willUpdate(changed) {
131
+ if (changed.has('value') && !this.dirty)
132
+ this.current = this.getAttribute('value');
133
+ }
134
+ update(changed) {
135
+ if (changed.has('label')) {
136
+ setText(this.ref('label'), String(this.props.label));
137
+ this.internals.ariaLabel = String(this.props.label) || null;
138
+ }
139
+ if (changed.has('required'))
140
+ this.internals.ariaRequired = this.props.required ? 'true' : null;
141
+ if (changed.has('disabled'))
142
+ this.internals.ariaDisabled = this.isDisabled ? 'true' : null;
143
+ if (changed.has('value') || changed.has('required') || changed.has('disabled'))
144
+ this.sync();
145
+ }
146
+ connected() {
147
+ this.observer ??= new MutationObserver(() => this.sync());
148
+ this.observer.observe(this, {
149
+ childList: true,
150
+ subtree: true,
151
+ attributes: true,
152
+ attributeFilter: ['disabled', 'value'],
153
+ });
154
+ this.sync();
155
+ }
156
+ disconnected() {
157
+ this.observer?.disconnect();
158
+ }
159
+ formValue() {
160
+ return this.picked ? this.current : null;
161
+ }
162
+ /** Nothing selected saves nothing, so a radio whose value is "" is not chosen on restore. */
163
+ formState() {
164
+ return this.formValue();
165
+ }
166
+ validate() {
167
+ if (this.props.required === true && !this.picked) {
168
+ this.internals.setValidity({ valueMissing: true }, requiredMessage('radio', 'Please select one of these options.'), this.stop);
169
+ }
170
+ else {
171
+ this.internals.setValidity({});
172
+ }
173
+ }
174
+ resetToDefault() {
175
+ this.dirty = false;
176
+ this.current = this.getAttribute('value');
177
+ this.sync();
178
+ }
179
+ restoreState(state) {
180
+ this.dirty = true;
181
+ this.current = state;
182
+ this.sync();
183
+ }
184
+ }
@@ -0,0 +1 @@
1
+ export declare const radioGroupStyles = "\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 {\n display: block; margin-block-end: 0.5rem;\n font-size: 0.8125rem; font-weight: 600; color: var(--_neu-fg-muted);\n}\n.label:empty { display: none; }\n.items { display: flex; flex-direction: column; align-items: flex-start; gap: 0.75rem; }\n:host([orientation='horizontal']) .items { flex-direction: row; flex-wrap: wrap; align-items: center; }\n:host(:disabled) .items, :host([disabled]) .items { opacity: 0.55; cursor: not-allowed; }\n:host(:state(user-invalid)) .label { color: var(--_neu-danger); }\n";
@@ -0,0 +1,15 @@
1
+ import { hostTheme } from '../../core/styles.js';
2
+ export const radioGroupStyles = `${hostTheme}
3
+ :host { display: block; }
4
+ :host(:disabled), :host([disabled]) { cursor: not-allowed; }
5
+ .base { display: flex; flex-direction: column; }
6
+ .label {
7
+ display: block; margin-block-end: 0.5rem;
8
+ font-size: 0.8125rem; font-weight: 600; color: var(--_neu-fg-muted);
9
+ }
10
+ .label:empty { display: none; }
11
+ .items { display: flex; flex-direction: column; align-items: flex-start; gap: 0.75rem; }
12
+ :host([orientation='horizontal']) .items { flex-direction: row; flex-wrap: wrap; align-items: center; }
13
+ :host(:disabled) .items, :host([disabled]) .items { opacity: 0.55; cursor: not-allowed; }
14
+ :host(:state(user-invalid)) .label { color: var(--_neu-danger); }
15
+ `;
@@ -0,0 +1,119 @@
1
+ import { FormControlElement } from '../../core/form-control.js';
2
+ export declare class NeuSelect extends FormControlElement {
3
+ static def: import("../../core/define.js").ComponentDef<{
4
+ readonly value: {
5
+ readonly type: "string";
6
+ };
7
+ readonly name: {
8
+ readonly type: "string";
9
+ readonly reflect: true;
10
+ };
11
+ readonly label: {
12
+ readonly type: "string";
13
+ };
14
+ readonly placeholder: {
15
+ readonly type: "string";
16
+ };
17
+ readonly helpText: {
18
+ readonly type: "string";
19
+ };
20
+ readonly errorText: {
21
+ readonly type: "string";
22
+ };
23
+ readonly required: {
24
+ readonly type: "boolean";
25
+ readonly reflect: true;
26
+ };
27
+ readonly disabled: {
28
+ readonly type: "boolean";
29
+ readonly reflect: true;
30
+ };
31
+ readonly size: {
32
+ readonly type: "string";
33
+ readonly default: "md";
34
+ readonly values: readonly ["sm", "md", "lg"];
35
+ readonly reflect: true;
36
+ };
37
+ }>;
38
+ static extraProperties: string[];
39
+ name: string;
40
+ label: string;
41
+ placeholder: string;
42
+ helpText: string;
43
+ errorText: string;
44
+ required: boolean;
45
+ disabled: boolean;
46
+ size: 'sm' | 'md' | 'lg';
47
+ /** The requested value, kept even while no option matches it (plan decision 8). */
48
+ private current;
49
+ private dirty;
50
+ /** The options as last read, which the listbox mirrors by index (`option-<index>`). */
51
+ private opts;
52
+ private observer;
53
+ private opened;
54
+ /** The light-DOM option behind the active one, so a rebuild keeps it active. */
55
+ private activeSource;
56
+ /** Whether the listbox was open when the pointer went down on the combobox. */
57
+ private pressedOpen;
58
+ private typed;
59
+ private typedTimer;
60
+ constructor();
61
+ get value(): string;
62
+ set value(next: string);
63
+ get defaultValue(): string;
64
+ set defaultValue(next: string);
65
+ /** Reads the neu-option children from their attributes and text; they need not be upgraded. */
66
+ private read;
67
+ /** The value as of the last read: the requested one while an option matches it, else ''. */
68
+ private chosen;
69
+ /**
70
+ * Reads the options again, then brings the display, an open listbox and the form in line.
71
+ * setText writes only when the text differs, so a server-rendered display is left alone.
72
+ */
73
+ private sync;
74
+ /** The first enabled option from `from` going `dir`, `from` included; -1 when there is none. */
75
+ private step;
76
+ /**
77
+ * The enabled option nearest to `from` (clamped to the list) going `dir`, else turning back:
78
+ * a move or jump past either end stops at the last enabled option that way.
79
+ */
80
+ private near;
81
+ private activeIndex;
82
+ /** Builds the listbox from the options and places it; the active option stays if it can. */
83
+ private render;
84
+ /** Makes option `i` the active one (none for -1) and scrolls it into view. */
85
+ private activate;
86
+ /** Puts the listbox under the combobox at its width, or above it when that has more room. */
87
+ private readonly place;
88
+ private open;
89
+ /**
90
+ * Focus that lands outside the select while the listbox is open. The combobox's focusout
91
+ * covers most moves; this covers focus set elsewhere after the window was left (the focusout
92
+ * then kept the listbox open, and Firefox sends no second one).
93
+ */
94
+ private readonly focusLanded;
95
+ /** The one way the listbox closes: by key, by choice, by light dismiss (toggle), by focus leaving or when disabled. */
96
+ private close;
97
+ /** A choice by the user: enabled options only, and events only when the value changes. */
98
+ private choose;
99
+ /**
100
+ * Jumps to the next option whose label starts with the characters typed within TYPEAHEAD_MS.
101
+ * A buffer of one letter (typed once or repeated) searches that letter from the option after
102
+ * the active one, so repeating it cycles; any other buffer refines the search and starts at the
103
+ * active option, which may still match.
104
+ */
105
+ private typeahead;
106
+ private onKey;
107
+ /** Index of the listbox option an event happened on, or -1. */
108
+ private optionAt;
109
+ protected rendered(): void;
110
+ protected willUpdate(changed: ReadonlySet<string>): void;
111
+ protected update(changed: ReadonlySet<string>): void;
112
+ protected connected(): void;
113
+ protected disconnected(): void;
114
+ /** Like a native select: an option whose value is "" submits name=; no option, nothing. */
115
+ protected formValue(): string | null;
116
+ protected validate(): void;
117
+ protected resetToDefault(): void;
118
+ protected restoreState(state: string): void;
119
+ }
@@ -0,0 +1,38 @@
1
+ import { type OptionData } from '../../core/define.js';
2
+ /** The option a select shows for `value`: the first one whose value matches, as in the client. */
3
+ export declare function pickOption<T extends OptionData>(options: readonly T[] | undefined, value: string): T | undefined;
4
+ export declare const selectDef: import("../../core/define.js").ComponentDef<{
5
+ readonly value: {
6
+ readonly type: "string";
7
+ };
8
+ readonly name: {
9
+ readonly type: "string";
10
+ readonly reflect: true;
11
+ };
12
+ readonly label: {
13
+ readonly type: "string";
14
+ };
15
+ readonly placeholder: {
16
+ readonly type: "string";
17
+ };
18
+ readonly helpText: {
19
+ readonly type: "string";
20
+ };
21
+ readonly errorText: {
22
+ readonly type: "string";
23
+ };
24
+ readonly required: {
25
+ readonly type: "boolean";
26
+ readonly reflect: true;
27
+ };
28
+ readonly disabled: {
29
+ readonly type: "boolean";
30
+ readonly reflect: true;
31
+ };
32
+ readonly size: {
33
+ readonly type: "string";
34
+ readonly default: "md";
35
+ readonly values: readonly ["sm", "md", "lg"];
36
+ readonly reflect: true;
37
+ };
38
+ }>;
@@ -0,0 +1,59 @@
1
+ import { defineComponent } from '../../core/define.js';
2
+ import { html, unsafeHTML } from '../../core/html.js';
3
+ import { themeCssProperties } from '../../core/styles.js';
4
+ import { chevronDownBody, svgWrap } from '../icon/system-icons.js';
5
+ import { selectStyles } from './select.styles.js';
6
+ /** The option a select shows for `value`: the first one whose value matches, as in the client. */
7
+ export function pickOption(options, value) {
8
+ return options?.find((option) => option.value === value);
9
+ }
10
+ const chevron = unsafeHTML(svgWrap(chevronDownBody));
11
+ export const selectDef = defineComponent({
12
+ tag: 'neu-select',
13
+ props: {
14
+ value: { type: 'string' },
15
+ name: { type: 'string', reflect: true },
16
+ label: { type: 'string' },
17
+ placeholder: { type: 'string' },
18
+ helpText: { type: 'string' },
19
+ errorText: { type: 'string' },
20
+ required: { type: 'boolean', reflect: true },
21
+ disabled: { type: 'boolean', reflect: true },
22
+ size: { type: 'string', default: 'md', values: ['sm', 'md', 'lg'], reflect: true },
23
+ },
24
+ delegatesFocus: true,
25
+ form: { valueProp: 'value' },
26
+ optionsFrom: 'neu-option',
27
+ styles: selectStyles,
28
+ // The client renders without ctx and update() fills the display, so both roots end up the same.
29
+ template: (p, ctx) => {
30
+ const picked = pickOption(ctx?.options, p.value);
31
+ // Options the server could not read (e.g. a custom component's children) leave a set value's
32
+ // display empty rather than showing the placeholder; update() fills it after hydration.
33
+ const unread = !picked && p.value !== '' && ctx?.options?.length === 0;
34
+ return html `<div part="base" class="base"><span part="label" class="label" id="label" data-ref="label">${p.label}</span><div part="combobox" class="combobox" id="combobox" role="combobox"${p.disabled ? html ` aria-disabled="true"` : html ` tabindex="0"`} aria-haspopup="listbox" aria-expanded="false" aria-controls="listbox" aria-labelledby="label" aria-describedby="help"${p.required ? html ` aria-required="true"` : ''} data-ref="combobox"><span part="display" class="display${picked || unread ? '' : ' placeholder'}" data-ref="display">${picked ? picked.label : unread ? '' : p.placeholder}</span><span part="chevron" class="chevron">${chevron}</span></div><div part="listbox" class="listbox" id="listbox" role="listbox" aria-labelledby="label" popover="auto" data-ref="listbox"></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>`;
35
+ },
36
+ docs: {
37
+ summary: 'A single-value select (the APG select-only combobox): a raised trigger and a popover listbox built from the neu-option children, which are data only. The value attribute is the default value.',
38
+ events: [
39
+ { name: 'input', description: 'Fired when the user chooses an option with a new value.' },
40
+ { name: 'change', description: 'Fired after input when the user chooses a new value.' },
41
+ { name: 'neu-open', description: 'Fired when the listbox opens.' },
42
+ { name: 'neu-close', description: 'Fired when the listbox closes.' },
43
+ ],
44
+ slots: [
45
+ { name: 'help-text', description: 'Rich help text; replaces the help-text attribute.' },
46
+ ],
47
+ parts: [
48
+ { name: 'base', description: 'Outer wrapper.' },
49
+ { name: 'label', description: 'The label text.' },
50
+ { name: 'combobox', description: 'The raised trigger that holds focus.' },
51
+ { name: 'display', description: 'The selected label, or the placeholder.' },
52
+ { name: 'chevron', description: 'The chevron icon.' },
53
+ { name: 'listbox', description: 'The popover list of options.' },
54
+ { name: 'option', description: 'One option in the listbox.' },
55
+ { name: 'help-text', description: 'Help or error text area.' },
56
+ ],
57
+ cssProperties: themeCssProperties,
58
+ },
59
+ });