@c2n/select 0.0.6 → 0.0.8

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/dist/select.js CHANGED
@@ -1,5 +1,6 @@
1
- import { LitElement, html, svg, unsafeCSS } from "lit";
2
- import { customElement, property, query, state } from "lit/decorators.js";
1
+ import { LitElement, html, nothing, svg, unsafeCSS } from "lit";
2
+ import { property, query, state } from "lit/decorators.js";
3
+ import { customElement } from "@c2n/core/element-helper.js";
3
4
  import { classMap } from "lit/directives/class-map.js";
4
5
  import { ListItem } from "@c2n/list-item";
5
6
  import { arrayPropertyConverter } from "@c2n/core/lit-helper.js";
@@ -7,7 +8,7 @@ import { redispatchEvent } from "@c2n/core/dom-helper.js";
7
8
  import "@c2n/overlay";
8
9
  import "@c2n/list";
9
10
  //#region src/select.scss?inline
10
- var select_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: inline-flex;\n vertical-align: middle;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.c2-select {\n display: contents;\n}\n\n.button {\n display: flex;\n flex: 1;\n position: relative;\n box-sizing: border-box;\n width: 100%;\n min-width: 100%;\n max-width: 100%;\n min-height: var(--c2-select__button--min-height,36px);\n margin: 0;\n align-items: center;\n justify-content: space-between;\n appearance: none;\n user-select: none;\n cursor: pointer;\n text-transform: none;\n vertical-align: middle;\n outline: none;\n font-size: var(--c2-select__button--font-size,14px);\n font-weight: var(--c2-select__button--font-weight);\n font-style: var(--c2-select__button--font-style,normal);\n font-family: var(--c2-select__button--font-family);\n line-height: 1.4;\n background: var(--c2-select__button--background,#ffffff);\n gap: var(--c2-select__button--gap,8px);\n border-top: var(--c2-select__button--border-top,1px solid #bcbcc6);\n border-bottom: var(--c2-select__button--border-bottom,1px solid #bcbcc6);\n border-right: var(--c2-select__button--border-right,1px solid #bcbcc6);\n border-left: var(--c2-select__button--border-left,1px solid #bcbcc6);\n color: var(--c2-select__button--color,#18181b);\n padding: var(--c2-select__button--padding,6px 10px 6px 12px);\n border-top-left-radius: var(--c2-select__button--border-top-left-radius,6px);\n border-top-right-radius: var(--c2-select__button--border-top-right-radius,6px);\n border-bottom-left-radius: var(--c2-select__button--border-bottom-left-radius,6px);\n border-bottom-right-radius: var(--c2-select__button--border-bottom-right-radius,6px);\n will-change: background, color, border;\n transition: background 250ms cubic-bezier(0.2, 0, 0, 1), color 250ms cubic-bezier(0.2, 0, 0, 1), border 250ms cubic-bezier(0.2, 0, 0, 1);\n}\n.button:hover {\n border-top: var(--c2-select__button__hover--border-top,1px solid #a1a1aa);\n border-bottom: var(--c2-select__button__hover--border-bottom,1px solid #a1a1aa);\n border-right: var(--c2-select__button__hover--border-right,1px solid #a1a1aa);\n border-left: var(--c2-select__button__hover--border-left,1px solid #a1a1aa);\n background: var(--c2-select__button__hover--background,#fafafa);\n color: var(--c2-select__button__hover--color,#18181b);\n}\n.button:focus-visible {\n outline: var(--c2-select__button__focus--outline,2px solid rgba(71, 110, 249, 0.4));\n outline-offset: var(--c2-select__button__focus--outline-offset,1px);\n}\n.button:disabled {\n cursor: default;\n pointer-events: none;\n opacity: var(--c2-select__button__disabled--opacity,0.38);\n}\n.button ::slotted([slot=button-prefix-icon]) {\n flex: none;\n width: var(--c2-select__button__prefix-icon--width,16px);\n height: var(--c2-select__button__prefix-icon--height,16px);\n color: var(--c2-select__button__prefix-icon--color);\n --c2-feather-icon--size: var(--c2-select__button__prefix-icon--width,16px);\n --c2-mat-icon--font-size: var(--c2-select__button__prefix-icon--width,16px);\n}\n.button ::slotted([slot=button-suffix-icon]),\n.button .default-icon {\n flex: none;\n width: var(--c2-select__button__suffix-icon--width,16px);\n height: var(--c2-select__button__suffix-icon--height,16px);\n color: var(--c2-select__button__suffix-icon--color,#71717a);\n --c2-feather-icon--size: var(--c2-select__button__suffix-icon--width,16px);\n --c2-mat-icon--font-size: var(--c2-select__button__suffix-icon--width,16px);\n}\n.button .default-icon {\n transition: transform 250ms cubic-bezier(0.2, 0, 0, 1);\n}\n.button .placeholder {\n font-weight: var(--c2-select__placeholder--font-weight);\n font-style: var(--c2-select__placeholder--font-style);\n color: var(--c2-select__placeholder--color,#71717a);\n opacity: var(--c2-select__placeholder--opacity,1);\n}\n.button .button-content {\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n text-align: start;\n flex: 1;\n min-width: 0;\n}\n\n:host([open]) .button {\n border-top: var(--c2-select__button__open--border-top,1px solid #476ef9);\n border-bottom: var(--c2-select__button__open--border-bottom,1px solid #476ef9);\n border-right: var(--c2-select__button__open--border-right,1px solid #476ef9);\n border-left: var(--c2-select__button__open--border-left,1px solid #476ef9);\n}\n:host([open]) .button .default-icon {\n transform: rotate(180deg);\n}\n\n:host([readonly]) .button {\n cursor: default;\n}\n\n.list {\n --c2-list--background: var(--c2-select__list--background,#ffffff);\n --c2-list--border-top: var(--c2-select__list--border-top,1px solid #e4e4e7);\n --c2-list--border-right: var(--c2-select__list--border-right,1px solid #e4e4e7);\n --c2-list--border-bottom: var(--c2-select__list--border-bottom,1px solid #e4e4e7);\n --c2-list--border-left: var(--c2-select__list--border-left,1px solid #e4e4e7);\n --c2-list--border-top-left-radius: var(--c2-select__list--border-top-left-radius,8px);\n --c2-list--border-top-right-radius: var(--c2-select__list--border-top-right-radius,8px);\n --c2-list--border-bottom-left-radius: var(--c2-select__list--border-bottom-left-radius,8px);\n --c2-list--border-bottom-right-radius: var(--c2-select__list--border-bottom-right-radius,8px);\n --c2-list--box-shadow: var(--c2-select__list--box-shadow,0 8px 24px rgba(24, 24, 27, 0.08));\n --c2-list--padding-top: var(--c2-select__list--padding-top,4px);\n --c2-list--padding-right: var(--c2-select__list--padding-right,4px);\n --c2-list--padding-bottom: var(--c2-select__list--padding-bottom,4px);\n --c2-list--padding-left: var(--c2-select__list--padding-left,4px);\n --c2-list--max-height: var(--c2-select__list--max-height,280px);\n}";
11
+ var select_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: inline-flex;\n vertical-align: middle;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.c2-select {\n display: contents;\n}\n\n.button {\n display: flex;\n flex: 1;\n position: relative;\n box-sizing: border-box;\n width: 100%;\n min-width: 100%;\n max-width: 100%;\n min-height: var(--c2-select__button--min-height,36px);\n margin: 0;\n align-items: center;\n justify-content: space-between;\n appearance: none;\n user-select: none;\n cursor: pointer;\n text-transform: none;\n vertical-align: middle;\n outline: none;\n font-size: var(--c2-select__button--font-size,14px);\n font-weight: var(--c2-select__button--font-weight);\n font-style: var(--c2-select__button--font-style,normal);\n font-family: var(--c2-select__button--font-family);\n line-height: 1.4;\n background: var(--c2-select__button--background,#ffffff);\n gap: var(--c2-select__button--gap,8px);\n border-top: var(--c2-select__button--border-top,1px solid #bcbcc6);\n border-bottom: var(--c2-select__button--border-bottom,1px solid #bcbcc6);\n border-right: var(--c2-select__button--border-right,1px solid #bcbcc6);\n border-left: var(--c2-select__button--border-left,1px solid #bcbcc6);\n color: var(--c2-select__button--color,#18181b);\n padding: var(--c2-select__button--padding,6px 10px 6px 12px);\n border-top-left-radius: var(--c2-select__button--border-top-left-radius,6px);\n border-top-right-radius: var(--c2-select__button--border-top-right-radius,6px);\n border-bottom-left-radius: var(--c2-select__button--border-bottom-left-radius,6px);\n border-bottom-right-radius: var(--c2-select__button--border-bottom-right-radius,6px);\n will-change: background, color, border;\n transition: background 250ms cubic-bezier(0.2, 0, 0, 1), color 250ms cubic-bezier(0.2, 0, 0, 1), border 250ms cubic-bezier(0.2, 0, 0, 1);\n}\n.button:hover {\n border-top: var(--c2-select__button__hover--border-top,1px solid #a1a1aa);\n border-bottom: var(--c2-select__button__hover--border-bottom,1px solid #a1a1aa);\n border-right: var(--c2-select__button__hover--border-right,1px solid #a1a1aa);\n border-left: var(--c2-select__button__hover--border-left,1px solid #a1a1aa);\n background: var(--c2-select__button__hover--background,#fafafa);\n color: var(--c2-select__button__hover--color,#18181b);\n}\n.button:focus-visible {\n outline: var(--c2-select__button__focus--outline,2px solid rgba(71, 110, 249, 0.4));\n outline-offset: var(--c2-select__button__focus--outline-offset,0px);\n}\n.button:disabled {\n cursor: default;\n pointer-events: none;\n opacity: var(--c2-select__button__disabled--opacity,0.38);\n}\n.button ::slotted([slot=button-prefix-icon]) {\n flex: none;\n width: var(--c2-select__button__prefix-icon--width,16px);\n height: var(--c2-select__button__prefix-icon--height,16px);\n color: var(--c2-select__button__prefix-icon--color);\n --c2-feather-icon--size: var(--c2-select__button__prefix-icon--width,16px);\n --c2-mat-icon--font-size: var(--c2-select__button__prefix-icon--width,16px);\n}\n.button ::slotted([slot=button-suffix-icon]),\n.button .default-icon {\n flex: none;\n width: var(--c2-select__button__suffix-icon--width,16px);\n height: var(--c2-select__button__suffix-icon--height,16px);\n color: var(--c2-select__button__suffix-icon--color,#71717a);\n --c2-feather-icon--size: var(--c2-select__button__suffix-icon--width,16px);\n --c2-mat-icon--font-size: var(--c2-select__button__suffix-icon--width,16px);\n}\n.button .default-icon {\n transition: transform 250ms cubic-bezier(0.2, 0, 0, 1);\n}\n.button .placeholder {\n font-weight: var(--c2-select__placeholder--font-weight);\n font-style: var(--c2-select__placeholder--font-style);\n color: var(--c2-select__placeholder--color,#71717a);\n opacity: var(--c2-select__placeholder--opacity,1);\n}\n.button .button-content {\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n text-align: start;\n flex: 1;\n min-width: 0;\n}\n\n:host([open]) .button {\n border-top: var(--c2-select__button__open--border-top,1px solid #476ef9);\n border-bottom: var(--c2-select__button__open--border-bottom,1px solid #476ef9);\n border-right: var(--c2-select__button__open--border-right,1px solid #476ef9);\n border-left: var(--c2-select__button__open--border-left,1px solid #476ef9);\n}\n:host([open]) .button .default-icon {\n transform: rotate(180deg);\n}\n\n:host([readonly]) .button {\n cursor: default;\n}\n\n.list {\n min-width: var(--c2-select__list--min-width,auto);\n --c2-list--background: var(--c2-select__list--background,#ffffff);\n --c2-list--border-top: var(--c2-select__list--border-top,1px solid #e4e4e7);\n --c2-list--border-right: var(--c2-select__list--border-right,1px solid #e4e4e7);\n --c2-list--border-bottom: var(--c2-select__list--border-bottom,1px solid #e4e4e7);\n --c2-list--border-left: var(--c2-select__list--border-left,1px solid #e4e4e7);\n --c2-list--border-top-left-radius: var(--c2-select__list--border-top-left-radius,8px);\n --c2-list--border-top-right-radius: var(--c2-select__list--border-top-right-radius,8px);\n --c2-list--border-bottom-left-radius: var(--c2-select__list--border-bottom-left-radius,8px);\n --c2-list--border-bottom-right-radius: var(--c2-select__list--border-bottom-right-radius,8px);\n --c2-list--box-shadow: var(--c2-select__list--box-shadow,0 8px 24px rgba(24, 24, 27, 0.08));\n --c2-list--padding-top: var(--c2-select__list--padding-top,4px);\n --c2-list--padding-right: var(--c2-select__list--padding-right,4px);\n --c2-list--padding-bottom: var(--c2-select__list--padding-bottom,4px);\n --c2-list--padding-left: var(--c2-select__list--padding-left,4px);\n --c2-list--max-height: var(--c2-select__list--max-height,280px);\n}";
11
12
  //#endregion
12
13
  //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
13
14
  function __decorate(decorators, target, key, desc) {
@@ -18,17 +19,28 @@ function __decorate(decorators, target, key, desc) {
18
19
  }
19
20
  //#endregion
20
21
  //#region src/select.ts
22
+ /** Once per page: the cause is always the same, and one select with the problem usually means all of them. */
23
+ var warnedUnupgraded = false;
21
24
  var Select = class Select extends LitElement {
22
25
  constructor(..._args) {
23
26
  super(..._args);
27
+ this.internals = this.attachInternals();
28
+ this.customValidityMessage = "";
29
+ this.disabledByForm = false;
30
+ this.defaultValue = [];
31
+ this.defaultValueCaptured = false;
24
32
  this.open = false;
25
33
  this.readonly = false;
26
34
  this.disabled = false;
27
35
  this.focused = false;
28
36
  this.fitSize = false;
29
37
  this.placeholder = "";
38
+ this.ariaLabel = null;
39
+ this.ariaLabelledByAttr = null;
40
+ this.labelText = "";
30
41
  this.multiple = false;
31
42
  this.required = false;
43
+ this.name = "";
32
44
  this.value = [];
33
45
  this.displayText = "";
34
46
  this.data = [];
@@ -39,6 +51,9 @@ var Select = class Select extends LitElement {
39
51
  this.toggle(true);
40
52
  };
41
53
  }
54
+ static {
55
+ this.formAssociated = true;
56
+ }
42
57
  static {
43
58
  this.styles = unsafeCSS(select_default);
44
59
  }
@@ -48,12 +63,66 @@ var Select = class Select extends LitElement {
48
63
  delegatesFocus: true
49
64
  };
50
65
  }
66
+ /** Scalar convenience API for a single-select. Setting it replaces the current selection. */
67
+ get selectedValue() {
68
+ return this.value[0] ?? "";
69
+ }
70
+ set selectedValue(value) {
71
+ this.value = value ? [value] : [];
72
+ }
73
+ /** The containing form, when this control is associated with one. */
74
+ get form() {
75
+ return this.internals.form;
76
+ }
77
+ /** Labels associated with this form control. */
78
+ get labels() {
79
+ return this.internals.labels;
80
+ }
81
+ get validity() {
82
+ return this.internals.validity;
83
+ }
84
+ get validationMessage() {
85
+ return this.internals.validationMessage;
86
+ }
87
+ get willValidate() {
88
+ return this.internals.willValidate;
89
+ }
90
+ connectedCallback() {
91
+ super.connectedCallback();
92
+ queueMicrotask(() => this.isConnected && this.syncLabelText());
93
+ if (!this.defaultValueCaptured) {
94
+ this.defaultValue = arrayPropertyConverter.fromAttribute(this.getAttribute("value") ?? "");
95
+ this.defaultValueCaptured = true;
96
+ }
97
+ }
98
+ formResetCallback() {
99
+ this.value = [...this.defaultValue];
100
+ }
101
+ formDisabledCallback(disabled) {
102
+ this.disabledByForm = disabled && !this.hasAttribute("disabled");
103
+ }
104
+ formStateRestoreCallback(state) {
105
+ if (typeof state === "string") this.value = arrayPropertyConverter.fromAttribute(state);
106
+ }
107
+ checkValidity() {
108
+ return this.internals.checkValidity();
109
+ }
110
+ reportValidity() {
111
+ return this.internals.reportValidity();
112
+ }
113
+ setCustomValidity(message) {
114
+ this.customValidityMessage = message;
115
+ this.syncFormState();
116
+ }
51
117
  onButtonBlur() {
52
118
  this.focused = false;
53
119
  this.button.removeEventListener("keydown", this.onKeydown);
54
120
  }
55
121
  toggle(value) {
56
- if (this.readonly) return;
122
+ if (this.readonly || this.effectiveDisabled) {
123
+ if (this.menu.matches(":popover-open")) this.menu.hidePopover();
124
+ return;
125
+ }
57
126
  if (typeof value == "undefined") {
58
127
  this.menu.togglePopover();
59
128
  return;
@@ -76,9 +145,16 @@ var Select = class Select extends LitElement {
76
145
  this.prepareGetDisplayText();
77
146
  await this.updateComplete;
78
147
  const displayText = [];
148
+ let unupgraded = 0;
79
149
  if (this.listItemSlot) for (const slotItem of this.listItemSlot.assignedElements({ flatten: true })) {
80
150
  const listItem = slotItem instanceof ListItem ? slotItem : slotItem.firstChild;
81
- if (listItem instanceof ListItem && this.value.includes(listItem.value)) displayText.push(listItem.displayText);
151
+ if (listItem instanceof ListItem) {
152
+ if (this.value.includes(listItem.value)) displayText.push(listItem.displayText);
153
+ } else if (slotItem.localName === "c2-list-item" || slotItem.firstElementChild?.localName === "c2-list-item") unupgraded += 1;
154
+ }
155
+ if (unupgraded > 0 && !warnedUnupgraded) {
156
+ warnedUnupgraded = true;
157
+ console.warn(`<c2-select> has ${unupgraded} c2-list-item child(ren) that have not been upgraded, so it cannot read a label off them and its trigger will render empty. Import '@c2n/list-item' on the client, and make sure the options reach the DOM as plain tags rather than as server-rendered, never-hydrated elements.`);
82
158
  }
83
159
  this.displayText = displayText.join(", ");
84
160
  }
@@ -94,6 +170,14 @@ var Select = class Select extends LitElement {
94
170
  this.data = event.detail.data;
95
171
  if (!this.multiple) this.toggle(false);
96
172
  redispatchEvent(this, event);
173
+ this.dispatchEvent(new Event("input", {
174
+ bubbles: true,
175
+ composed: true
176
+ }));
177
+ this.dispatchEvent(new Event("change", {
178
+ bubbles: true,
179
+ composed: true
180
+ }));
97
181
  }
98
182
  handleOverlayToggle(event) {
99
183
  const toggleEvent = event;
@@ -103,8 +187,24 @@ var Select = class Select extends LitElement {
103
187
  if (changedProperties.has("value") && typeof this.value === "string") this.value = arrayPropertyConverter.fromAttribute(this.value);
104
188
  }
105
189
  updated(_changedProperties) {
106
- if (_changedProperties.has("open")) this.toggle(this.open);
190
+ if (_changedProperties.has("ariaLabelledByAttr")) this.syncLabelText();
191
+ if (_changedProperties.has("open") || _changedProperties.has("readonly") || _changedProperties.has("disabled")) this.toggle(this.open);
107
192
  if (_changedProperties.has("value")) this.updateDisplayText();
193
+ this.syncFormState();
194
+ }
195
+ syncFormState() {
196
+ if (this.multiple && this.name) {
197
+ const formData = new FormData();
198
+ for (const value of this.value) formData.append(this.name, value);
199
+ this.internals.setFormValue(formData, this.value.join(";"));
200
+ } else this.internals.setFormValue(this.selectedValue || null, this.value.join(";"));
201
+ const missing = this.required && this.value.length === 0;
202
+ const flags = this.customValidityMessage ? { customError: true } : missing ? { valueMissing: true } : {};
203
+ const message = this.customValidityMessage || (missing ? "Please select an option." : "");
204
+ this.internals.setValidity(flags, message, this.button);
205
+ }
206
+ get effectiveDisabled() {
207
+ return this.disabled || this.disabledByForm;
108
208
  }
109
209
  async getUpdateComplete() {
110
210
  const complete = await super.getUpdateComplete();
@@ -114,18 +214,37 @@ var Select = class Select extends LitElement {
114
214
  handleSlotChange() {
115
215
  this.updateDisplayText();
116
216
  }
217
+ /**
218
+ * The accessible name for the trigger.
219
+ *
220
+ * The focusable control is a `<button>` inside the shadow root, and neither of the two ways an author
221
+ * names this element reaches it on its own: `aria-labelledby` on the host points at an id in the light DOM,
222
+ * which ARIA cannot resolve across the shadow boundary, and a wrapping `<label>` names the host rather than
223
+ * the button. So the label's *text* is copied onto the button instead.
224
+ */
225
+ get triggerLabel() {
226
+ return this.ariaLabel || this.labelText;
227
+ }
228
+ /** Reads the text of whatever labels the host: `aria-labelledby` first, then any associated `<label>`. */
229
+ syncLabelText() {
230
+ const root = this.getRootNode();
231
+ const fromIds = (this.ariaLabelledByAttr ?? "").split(/\s+/).filter(Boolean).map((id) => root.getElementById?.(id)?.textContent?.trim()).filter(Boolean);
232
+ const text = fromIds.length > 0 ? fromIds.join(" ") : [...this.internals.labels].map((label) => label.textContent?.trim()).filter(Boolean).join(" ");
233
+ this.labelText = text ?? "";
234
+ }
117
235
  render() {
118
236
  return html`<div class="c2-select">
119
237
  <button
120
238
  type="button"
239
+ aria-label=${this.triggerLabel || nothing}
121
240
  aria-haspopup="listbox"
122
241
  aria-expanded=${this.open ? "true" : "false"}
123
242
  id="button"
124
- popovertarget="menu-overlay"
243
+ popovertarget=${this.readonly || this.effectiveDisabled ? nothing : "menu-overlay"}
125
244
  class="button"
126
245
  @blur=${this.onButtonBlur}
127
246
  @focus=${this.onButtonFocus}
128
- ?disabled=${this.disabled}
247
+ ?disabled=${this.effectiveDisabled}
129
248
  >
130
249
  <slot name="button-prefix-icon"></slot>
131
250
  <slot name="button-content">${this.renderButtonContent()}</slot>
@@ -134,6 +253,7 @@ var Select = class Select extends LitElement {
134
253
  <c2-overlay id="menu-overlay" popover @toggle=${this.handleOverlayToggle} ?fit-anchor=${this.fitSize}>
135
254
  <c2-list
136
255
  id="list"
256
+ aria-labelledby="button"
137
257
  .value=${this.value}
138
258
  class="list"
139
259
  ?multiple=${this.multiple}
@@ -146,6 +266,7 @@ var Select = class Select extends LitElement {
146
266
  </div> `;
147
267
  }
148
268
  };
269
+ __decorate([state()], Select.prototype, "disabledByForm", void 0);
149
270
  __decorate([property({
150
271
  type: Boolean,
151
272
  reflect: true
@@ -167,8 +288,12 @@ __decorate([property({
167
288
  attribute: "fit-size"
168
289
  })], Select.prototype, "fitSize", void 0);
169
290
  __decorate([property({ type: String })], Select.prototype, "placeholder", void 0);
291
+ __decorate([property({ attribute: "aria-label" })], Select.prototype, "ariaLabel", void 0);
292
+ __decorate([property({ attribute: "aria-labelledby" })], Select.prototype, "ariaLabelledByAttr", void 0);
293
+ __decorate([state()], Select.prototype, "labelText", void 0);
170
294
  __decorate([property({ type: Boolean })], Select.prototype, "multiple", void 0);
171
295
  __decorate([property({ type: Boolean })], Select.prototype, "required", void 0);
296
+ __decorate([property({ type: String })], Select.prototype, "name", void 0);
172
297
  __decorate([property({
173
298
  converter: arrayPropertyConverter,
174
299
  reflect: true
package/package.json CHANGED
@@ -1,18 +1,30 @@
1
1
  {
2
2
  "name": "@c2n/select",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "main": "dist/select.js",
6
6
  "exports": {
7
7
  ".": {
8
- "default": "./dist/select.js",
9
- "types": "./types/src/select.d.ts"
8
+ "types": "./types/src/select.d.ts",
9
+ "default": "./dist/select.js"
10
+ },
11
+ "./react": {
12
+ "types": "./react.d.ts",
13
+ "default": "./react.js"
14
+ },
15
+ "./vue": {
16
+ "types": "./vue.d.ts",
17
+ "default": "./vue.js"
10
18
  },
11
19
  "./custom-elements.json": "./custom-elements.json"
12
20
  },
13
21
  "files": [
14
22
  "dist",
15
- "types"
23
+ "types",
24
+ "react.d.ts",
25
+ "react.js",
26
+ "vue.d.ts",
27
+ "vue.js"
16
28
  ],
17
29
  "keywords": [
18
30
  "select",
@@ -50,15 +62,15 @@
50
62
  }
51
63
  },
52
64
  "dependencies": {
53
- "@c2n/core": "0.0.6",
54
- "@c2n/list": "0.0.6",
55
- "@c2n/list-item": "0.0.6",
56
- "@c2n/overlay": "0.0.6",
65
+ "@c2n/core": "0.0.8",
66
+ "@c2n/list": "0.0.8",
67
+ "@c2n/list-item": "0.0.8",
68
+ "@c2n/overlay": "0.0.8",
57
69
  "lit": "3.3.3"
58
70
  },
59
71
  "devDependencies": {
60
72
  "@c2n/config": "*"
61
73
  },
62
74
  "customElements": "custom-elements.json",
63
- "gitHead": "2921054bc75299da66dad11c1e374246ff88a51e"
75
+ "gitHead": "be59cbccee1d33ca248e39f69b05c0b595ec6c6d"
64
76
  }
package/react.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ // GENERATED by @c2n/framework-types. Do not edit by hand.
2
+ //
3
+ // JSX types for the custom elements of @c2n/select. Import once, anywhere in the program:
4
+ //
5
+ // import '@c2n/select/react'
6
+ //
7
+ // Register the elements at module scope before React renders, or React writes an object prop as an
8
+ // attribute and it stringifies.
9
+
10
+ import type { DetailedHTMLProps, HTMLAttributes } from 'react'
11
+ import type { Select } from '@c2n/select'
12
+
13
+ /** Standard React host-element attributes plus the element's own public properties. */
14
+ type C2Props<T> = DetailedHTMLProps<HTMLAttributes<T>, T> & Partial<Omit<T, keyof HTMLElement>>
15
+
16
+ declare module 'react' {
17
+ namespace JSX {
18
+ interface IntrinsicElements {
19
+ 'c2-select': C2Props<Select>
20
+ }
21
+ }
22
+ }
23
+
24
+ export {}
package/react.js ADDED
@@ -0,0 +1,3 @@
1
+ // GENERATED by @c2n/framework-types. Do not edit by hand.
2
+ // Types only — see the matching .d.ts.
3
+ export {}
@@ -1,6 +1,18 @@
1
1
  import { LitElement, type PropertyValueMap } from 'lit';
2
+ import type { TypedAddEventListener, TypedRemoveEventListener } from '@c2n/core/event-helper.js';
3
+ import type { SelectionChangeEventDetail } from '@c2n/list';
2
4
  import '@c2n/overlay';
3
5
  import '@c2n/list';
6
+ /** Events fired by {@link Select}, keyed for `addEventListener`. */
7
+ export interface SelectEventMap {
8
+ 'selection-change': CustomEvent<SelectionChangeEventDetail>;
9
+ input: Event;
10
+ change: Event;
11
+ }
12
+ export interface Select {
13
+ addEventListener: TypedAddEventListener<Select, SelectEventMap>;
14
+ removeEventListener: TypedRemoveEventListener<Select, SelectEventMap>;
15
+ }
4
16
  /**
5
17
  * A dropdown built from a trigger button and a `c2-list` of `c2-list-item` rows inside a `c2-overlay` popover. The
6
18
  * rows are the light-DOM children of the select, so they are themed with the `--c2-list-item--*` variables directly on
@@ -15,7 +27,9 @@ import '@c2n/list';
15
27
  * @slot button-suffix-icon - Icon shown at the end of the trigger. Defaults to a chevron that flips while open.
16
28
  * @slot button-content - Replaces the trigger text entirely (selected labels or placeholder) with custom markup.
17
29
  *
18
- * @event {CustomEvent<SelectionChangeEventDetail>} selection-change - Fired after the user picks an option. `detail.value` is the array of selected values (one entry unless `multiple`), `detail.data` the `data` of each selected row.
30
+ * @event {CustomEvent<SelectionChangeEventDetail>} selection-change - Fired after the user picks an option. `detail.value` is the array of selected values (one entry unless `multiple`), `detail.data` the `data` of each selected row. Does not bubble: several components fire `selection-change`, so a listener belongs on the element itself rather than on an ancestor.
31
+ * @event {Event} input - Fired with `selection-change`, after `value` is updated. Carries no detail: it exists so the select behaves like the other form controls, which is what `v-model`, `ngModel` and any generic form binding listen for.
32
+ * @event {Event} change - Fired with `input`. A select commits on every pick, so the two always fire together.
19
33
  *
20
34
  * @cssproperty {pixel} [--c2-select__button--min-height=36px]
21
35
  * @cssproperty {padding} [--c2-select__button--padding=6px 10px 6px 12px]
@@ -51,7 +65,7 @@ import '@c2n/list';
51
65
  * @cssproperty {border} [--c2-select__button__open--border-left=1px solid #476ef9]
52
66
  *
53
67
  * @cssproperty {outline} [--c2-select__button__focus--outline=2px solid rgba(71, 110, 249, 0.4)]
54
- * @cssproperty {pixel} [--c2-select__button__focus--outline-offset=1px]
68
+ * @cssproperty {pixel} [--c2-select__button__focus--outline-offset=0px] - Flush by default: `[open]` already turns the border accent, so an offset ring reads as a second border.
55
69
  *
56
70
  * @cssproperty {opacity} [--c2-select__button__disabled--opacity=0.38]
57
71
  *
@@ -82,6 +96,7 @@ import '@c2n/list';
82
96
  * @cssproperty {padding} [--c2-select__list--padding-right=4px]
83
97
  * @cssproperty {padding} [--c2-select__list--padding-bottom=4px]
84
98
  * @cssproperty {padding} [--c2-select__list--padding-left=4px]
99
+ * @cssproperty {pixel} [--c2-select__list--min-width=auto] - Floor for the dropdown's width; it is never narrower than the trigger.
85
100
  * @cssproperty {pixel} [--c2-select__list--max-height=280px] - The dropdown scrolls past this height.
86
101
  *
87
102
  * @internalcomponent c2-list
@@ -90,6 +105,7 @@ import '@c2n/list';
90
105
  * @slotcomponent c2-list-item
91
106
  */
92
107
  export declare class Select extends LitElement {
108
+ static formAssociated: boolean;
93
109
  static styles: import("lit").CSSResult;
94
110
  static shadowRootOptions: {
95
111
  delegatesFocus: boolean;
@@ -99,6 +115,11 @@ export declare class Select extends LitElement {
99
115
  serializable?: boolean;
100
116
  slotAssignment?: SlotAssignmentMode;
101
117
  };
118
+ private readonly internals;
119
+ private customValidityMessage;
120
+ private disabledByForm;
121
+ private defaultValue;
122
+ private defaultValueCaptured;
102
123
  /** Whether the dropdown is showing. Reflects the popover state; set it to open or close programmatically. */
103
124
  open: boolean;
104
125
  /** Shows the current value but never opens the dropdown. */
@@ -111,18 +132,46 @@ export declare class Select extends LitElement {
111
132
  fitSize: boolean;
112
133
  /** Text shown in the trigger while nothing is selected. */
113
134
  placeholder: string;
135
+ /** Accessible name of the trigger and option list. */
136
+ ariaLabel: string | null;
137
+ /**
138
+ * Mirrors the host's `aria-labelledby` so the trigger re-renders when a label claims the element — a
139
+ * `c2-label[for]` sets it after both have upgraded.
140
+ */
141
+ ariaLabelledByAttr: string | null;
142
+ /** Text of whatever labels the host, resolved once the DOM settles; see {@link triggerLabel}. */
143
+ private labelText;
114
144
  /** Allow several options to be selected; the dropdown stays open after a pick and the trigger lists every label. */
115
145
  multiple: boolean;
116
146
  /** Keep at least one option selected: picking the only selected option again does not clear it. */
117
147
  required: boolean;
148
+ /** Name used when the select participates in a form. */
149
+ name: string;
118
150
  /** Selected values. Written as `value="a;b"` in markup, read as `['a', 'b']` from the property. */
119
151
  value: string[];
152
+ /** Scalar convenience API for a single-select. Setting it replaces the current selection. */
153
+ get selectedValue(): string;
154
+ set selectedValue(value: string);
155
+ /** The containing form, when this control is associated with one. */
156
+ get form(): HTMLFormElement | null;
157
+ /** Labels associated with this form control. */
158
+ get labels(): NodeList;
159
+ get validity(): ValidityState;
160
+ get validationMessage(): string;
161
+ get willValidate(): boolean;
120
162
  button: HTMLButtonElement;
121
163
  menu: HTMLElement;
122
164
  listItemSlot?: HTMLSlotElement;
123
165
  private displayText;
124
166
  protected childItemsUpdated: Promise<unknown[]>;
125
167
  data: unknown[];
168
+ connectedCallback(): void;
169
+ formResetCallback(): void;
170
+ formDisabledCallback(disabled: boolean): void;
171
+ formStateRestoreCallback(state: string | File | FormData | null): void;
172
+ checkValidity(): boolean;
173
+ reportValidity(): boolean;
174
+ setCustomValidity(message: string): void;
126
175
  private onButtonBlur;
127
176
  private onKeydown;
128
177
  toggle(value?: boolean): void;
@@ -135,8 +184,21 @@ export declare class Select extends LitElement {
135
184
  private handleOverlayToggle;
136
185
  protected willUpdate(changedProperties: PropertyValueMap<this>): void;
137
186
  protected updated(_changedProperties: PropertyValueMap<this>): void;
187
+ private syncFormState;
188
+ private get effectiveDisabled();
138
189
  protected getUpdateComplete(): Promise<boolean>;
139
190
  private handleSlotChange;
191
+ /**
192
+ * The accessible name for the trigger.
193
+ *
194
+ * The focusable control is a `<button>` inside the shadow root, and neither of the two ways an author
195
+ * names this element reaches it on its own: `aria-labelledby` on the host points at an id in the light DOM,
196
+ * which ARIA cannot resolve across the shadow boundary, and a wrapping `<label>` names the host rather than
197
+ * the button. So the label's *text* is copied onto the button instead.
198
+ */
199
+ private get triggerLabel();
200
+ /** Reads the text of whatever labels the host: `aria-labelledby` first, then any associated `<label>`. */
201
+ private syncLabelText;
140
202
  render(): import("lit-html").TemplateResult<1>;
141
203
  }
142
204
  declare global {
@@ -1 +1 @@
1
- {"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../src/select.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAwB,KAAK,gBAAgB,EAAE,MAAM,KAAK,CAAA;AAS7E,OAAO,cAAc,CAAA;AACrB,OAAO,WAAW,CAAA;AAElB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuFG;AACH,qBACa,MAAO,SAAQ,UAAU;IACpC,OAAgB,MAAM,0BAAoB;IAE1C,OAAgB,iBAAiB;;;;;;;MAA4D;IAE7F,6GAA6G;IACjE,IAAI,UAAQ;IAExD,4DAA4D;IAChB,QAAQ,UAAQ;IAE5D,gDAAgD;IACJ,QAAQ,UAAQ;IAE5D,iDAAiD;IACL,OAAO,UAAQ;IAE3D,oFAAoF;IAChC,OAAO,UAAQ;IAEnE,2DAA2D;IAC/B,WAAW,SAAK;IAE5C,oHAAoH;IACvF,QAAQ,EAAE,OAAO,CAAQ;IAEtD,mGAAmG;IACtE,QAAQ,EAAE,OAAO,CAAQ;IAEtD,mGAAmG;IAKnG,KAAK,EAAE,MAAM,EAAE,CAAK;IAEW,MAAM,EAAG,iBAAiB,CAAA;IACpB,IAAI,EAAG,WAAW,CAAA;IACf,YAAY,CAAC,EAAE,eAAe,CAAA;IAE7D,OAAO,CAAC,WAAW,CAAK;IAEjC,SAAS,CAAC,iBAAiB,EAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;IAEhD,IAAI,EAAE,OAAO,EAAE,CAAK;IAEpB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,SAAS,CAOhB;IAEM,MAAM,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI;IAmBpC,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,qBAAqB;YAaf,iBAAiB;IAe/B,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,sBAAsB;IAI9B,OAAO,CAAC,qBAAqB;IAU7B,OAAO,CAAC,mBAAmB;cAKR,UAAU,CAAC,iBAAiB,EAAE,gBAAgB,CAAC,IAAI,CAAC,GAAG,IAAI;cAO3D,OAAO,CAAC,kBAAkB,EAAE,gBAAgB,CAAC,IAAI,CAAC,GAAG,IAAI;cASnD,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC;IAM9D,OAAO,CAAC,gBAAgB;IAIf,MAAM;CA+BhB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,WAAW,EAAE,MAAM,CAAA;KACpB;CACF"}
1
+ {"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../src/select.ts"],"names":[],"mappings":"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"}
package/vue.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ // GENERATED by @c2n/framework-types. Do not edit by hand.
2
+ //
3
+ // Vue template types for the custom elements of @c2n/select. Import once, anywhere in the program:
4
+ //
5
+ // import '@c2n/select/vue'
6
+ //
7
+ // Tell the compiler about the tags as well, or every c2-* tag is resolved as a Vue component and renders
8
+ // nothing: template.compilerOptions.isCustomElement = (tag) => tag.startsWith('c2-') in vite.config.ts.
9
+
10
+ import type { DefineComponent, HTMLAttributes } from 'vue'
11
+ import type { Select, SelectEventMap } from '@c2n/select'
12
+
13
+ /** The element's own public properties, plus every attribute Vue understands on a host element. */
14
+ type C2Props<T> = Partial<Omit<T, keyof HTMLElement>> & HTMLAttributes
15
+
16
+ declare module 'vue' {
17
+ interface GlobalComponents {
18
+ 'c2-select': DefineComponent<
19
+ C2Props<Select> & {
20
+ 'fit-size'?: unknown
21
+ 'aria-label'?: unknown
22
+ 'aria-labelledby'?: unknown
23
+ onInput?: (event: SelectEventMap['input']) => void
24
+ onChange?: (event: SelectEventMap['change']) => void
25
+ onSelectionChange?: (event: SelectEventMap['selection-change']) => void
26
+ }
27
+ >
28
+ }
29
+ }
30
+
31
+ declare module '@vue/runtime-dom' {
32
+ interface HTMLAttributes {
33
+ /** Vue's own definition omits it, and slotting a plain element into a component needs it. */
34
+ slot?: string
35
+ }
36
+ }
37
+
38
+ export {}
package/vue.js ADDED
@@ -0,0 +1,3 @@
1
+ // GENERATED by @c2n/framework-types. Do not edit by hand.
2
+ // Types only — see the matching .d.ts.
3
+ export {}