@itenthusiasm/custom-elements 0.0.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ select-enhancer {
4
4
  position: relative;
5
5
  display: inline-block;
6
6
  box-sizing: border-box;
7
- width: inherit;
7
+ width: min(100%, 200px);
8
8
  height: inherit;
9
9
 
10
10
  @media only screen and (min-width: 600px) {
@@ -26,7 +26,7 @@ select-enhancer {
26
26
 
27
27
  cursor: pointer;
28
28
  white-space: pre;
29
- text-align: center;
29
+ text-align: left;
30
30
  font-size: inherit;
31
31
  font-family: inherit;
32
32
  color: currentcolor;
@@ -64,15 +64,16 @@ select-enhancer {
64
64
  }
65
65
 
66
66
  & > [role="option"],
67
- &:where([role="combobox"][data-bad-filter] + [role="listbox"])::after {
67
+ &:where([role="combobox"][data-bad-filter] + [role="listbox"])::before {
68
68
  display: block;
69
69
  box-sizing: border-box;
70
70
  height: var(--option-height);
71
71
  padding: var(--option-padding);
72
+ align-content: center;
72
73
  cursor: pointer;
73
74
 
74
75
  &[data-active="true"]:not([aria-selected="true"]) {
75
- background-color: #bddaff; /* `background-color` for `selected` items, brightened by 70% */
76
+ background-color: #bddaff; /* This color is the `background-color` for `selected` items, brightened by 70% */
76
77
  }
77
78
 
78
79
  &[aria-selected="true"] {
@@ -85,13 +86,13 @@ select-enhancer {
85
86
  visibility: hidden; /* Needed to hide filtered-out `option`s in Safari + VoiceOver (Bug in Browser) */
86
87
  }
87
88
 
88
- /* TODO: Add clearer `disabled` styles. */
89
+ /* TODO: Add clearer/better `disabled` styles. */
89
90
  &[aria-disabled="true"] {
90
- cursor: auto;
91
+ cursor: not-allowed;
91
92
  }
92
93
  }
93
94
 
94
- &:where([role="combobox"][data-bad-filter] + [role="listbox"])::after {
95
+ &:where([role="combobox"][data-bad-filter] + [role="listbox"])::before {
95
96
  content: attr(nomatchesmessage, "No options found") / "";
96
97
  cursor: auto;
97
98
  }
@@ -71,8 +71,8 @@ declare class ComboboxField extends HTMLElement implements ExposedInternals, Fie
71
71
  * @returns {void}
72
72
  */
73
73
  attributeChangedCallback(name: (typeof ComboboxField.observedAttributes)[number], oldValue: string | null, newValue: string | null): void;
74
- /** @param {string} v */
75
- set value(v: string);
74
+ /** @param {string} V */
75
+ set value(V: string);
76
76
  /** Sets or retrieves the `value` of the `combobox` @returns {string | null} */
77
77
  get value(): string | null;
78
78
  /** "On Mount" for Custom Elements @returns {void} */
@@ -178,10 +178,7 @@ declare class ComboboxField extends HTMLElement implements ExposedInternals, Fie
178
178
  * - `anyvalue`: The field's `value` can be any string, and it will automatically be set to
179
179
  * whatever value the user types. (Requires enabling `filter` mode.)
180
180
  *
181
- * <!--
182
- * TODO: Link to Documentation for More Details (like TS does for MDN). The deeper details of the behavior
183
- * are too sophisticated to place them all in a JSDoc, which should be [sufficiently] clear and succinct
184
- * -->
181
+ * [API Reference](https://github.com/ITenthusiasm/custom-elements/blob/main/src/Combobox/docs/combobox-field.md#attributes-valueis)
185
182
  *
186
183
  * @returns {"unclearable" | "clearable" | "anyvalue"}
187
184
  */
@@ -196,7 +193,7 @@ declare class ComboboxField extends HTMLElement implements ExposedInternals, Fie
196
193
  * Returns the `option` whose `label` matches the user's most recent filter input (if one exists).
197
194
  *
198
195
  * Value will be `null` if:
199
- * - The user's filter didn't match any `option`s
196
+ * - The user's filter didn't match any (enabled) `option`s
200
197
  * - The `combobox`'s text content was altered by a `value` change
201
198
  * - The `combobox` was just recently expanded
202
199
  * @returns {ComboboxOption | null}
@@ -31,10 +31,6 @@ const attrs = Object.freeze({
31
31
  * @typedef {Pick<HTMLInputElement, "name" | "required" | "disabled" | "setCustomValidity">} FieldPropertiesAndMethods
32
32
  */
33
33
 
34
- /*
35
- * TODO: Some of our functionality requires (or recommends) CSS to be properly implemented (e.g., hiding `listbox`,
36
- * properly showing white spaces, etc.). We should probably explain all such things to developers.
37
- */
38
34
  /** @implements {ExposedInternals} @implements {FieldPropertiesAndMethods} */
39
35
  class ComboboxField extends HTMLElement {
40
36
  /* ------------------------------ Custom Element Settings ------------------------------ */
@@ -95,7 +91,7 @@ class ComboboxField extends HTMLElement {
95
91
  * @returns {void}
96
92
  */
97
93
  attributeChangedCallback(name, oldValue, newValue) {
98
- if (name === "id" && newValue !== oldValue) {
94
+ if (name === "id" && this.#mounted && newValue !== oldValue) {
99
95
  this.listbox.id = `${this.id}-listbox`;
100
96
  for (let option = this.listbox.firstElementChild; option; option = /** @type {any} */ (option.nextElementSibling))
101
97
  option.id = `${this.id}-option-${option.value}`;
@@ -122,16 +118,19 @@ class ComboboxField extends HTMLElement {
122
118
  oldValue === "anyvalue" || oldValue === "unclearable" ? oldValue : "clearable";
123
119
  if (trueNewValue === trueOldValue && !filterModeIsBeingDisabled) return;
124
120
 
121
+ const hasOptions = this.listbox.children.length !== 0;
122
+
125
123
  // `anyvalue` activated
126
124
  if (trueNewValue === "anyvalue" && !filterModeIsBeingDisabled) {
127
125
  if (this.text.data === "") return this.forceEmptyValue();
128
- if (this.getAttribute(attrs["aria-expanded"]) !== String(true)) return; // A valid value should already exist
126
+ if (this.getAttribute(attrs["aria-expanded"]) !== String(true) && hasOptions) return; // A valid value should already exist
129
127
 
130
128
  if (this.#autoselectableOption) this.value = this.#autoselectableOption.value;
131
129
  else this.value = this.text.data;
132
130
  }
133
131
  // `clearable` activated (default when `filter` mode is ON)
134
132
  else if (trueNewValue === "clearable" && !filterModeIsBeingDisabled) {
133
+ if (!hasOptions && trueOldValue === "anyvalue") return this.#forceNullValue();
135
134
  if (this.text.data === "") return this.forceEmptyValue();
136
135
  if (trueOldValue !== "anyvalue") return; // A valid value should already exist
137
136
 
@@ -140,6 +139,7 @@ class ComboboxField extends HTMLElement {
140
139
  }
141
140
  // `unclearable` activated (default when `filter` mode is OFF)
142
141
  else {
142
+ if (!hasOptions && (trueOldValue === "anyvalue" || filterModeIsBeingDisabled)) return this.#forceNullValue();
143
143
  /** @type {ComboboxOption | null | undefined} */ let option;
144
144
 
145
145
  if (trueOldValue !== "unclearable" && this.text.data === "") option = this.getOptionByValue("");
@@ -184,6 +184,10 @@ class ComboboxField extends HTMLElement {
184
184
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- This is due to our own TS Types. :\
185
185
  this.#matchingOptions ??= Array.from(this.listbox?.children ?? []);
186
186
 
187
+ if (this.#value === null && this.valueIs === "anyvalue") {
188
+ this.attributeChangedCallback("valueis", /** @satisfies {this["valueIs"]} */ ("unclearable"), this.valueIs);
189
+ }
190
+
187
191
  if (this.isConnected) {
188
192
  if (/** @type {Document | ShadowRoot} */ (this.getRootNode()).activeElement === this) {
189
193
  this.ownerDocument.getSelection()?.setBaseAndExtent(this.text, 0, this.text, this.text.length);
@@ -227,7 +231,11 @@ class ComboboxField extends HTMLElement {
227
231
  // Setup Mutation Observers
228
232
  this.#optionNodesObserver.observe(this.listbox, { childList: true });
229
233
  this.#textNodeObserver.observe(this, { childList: true });
230
- this.#expansionObserver.observe(this, { attributes: true, attributeFilter: [attrs["aria-expanded"]] });
234
+ this.#expansionObserver.observe(this, {
235
+ attributes: true,
236
+ attributeFilter: [attrs["aria-expanded"]],
237
+ attributeOldValue: true,
238
+ });
231
239
  this.#activeDescendantObserver.observe(this, {
232
240
  attributes: true,
233
241
  attributeFilter: [attrs["aria-activedescendant"]],
@@ -283,7 +291,6 @@ class ComboboxField extends HTMLElement {
283
291
  #handleTypeahead = (event) => {
284
292
  const combobox = /** @type {ComboboxField} */ (event.currentTarget);
285
293
  const { listbox } = combobox;
286
- // TODO: This will probably be faster with getElementById?
287
294
  const activeOption = listbox.querySelector(":scope [role='option'][data-active='true']");
288
295
 
289
296
  if (event.key.length === 1 && !event.altKey && !event.ctrlKey && !event.metaKey) {
@@ -373,7 +380,6 @@ class ComboboxField extends HTMLElement {
373
380
  if (prevOption?.selected) prevOption.selected = false;
374
381
  this.#validateRequiredConstraint();
375
382
 
376
- // TODO: We might want to document that this `InputEvent` is not cancelable (and why)
377
383
  combobox[editingKey] = true;
378
384
  combobox.dispatchEvent(
379
385
  new InputEvent("input", {
@@ -418,7 +424,7 @@ class ComboboxField extends HTMLElement {
418
424
  for (let option = this.listbox.firstElementChild; option; option = /** @type {any} */ (option.nextElementSibling)) {
419
425
  if (!this.optionMatchesFilter(option)) option.filteredOut = true;
420
426
  else {
421
- if (option.textContent === search) autoselectableOption = option;
427
+ if (option.textContent === search && !option.disabled) autoselectableOption = option;
422
428
 
423
429
  option.filteredOut = false;
424
430
  this.#matchingOptions[matches++] = option;
@@ -462,6 +468,12 @@ class ComboboxField extends HTMLElement {
462
468
  * @returns {void}
463
469
  */
464
470
  #resetOptions() {
471
+ /*
472
+ * TODO: If we ever decide to create a public, overridable `getResetOptions()` method, we should consider
473
+ * using this private internal method to initialize the component's `option`s onMount if no `#matchingOptions`
474
+ * exist yet. The reason is that doing so will GUARANTEE that the developer's `matchingOption`s will be kept
475
+ * in sync with ours from the get go.
476
+ */
465
477
  let i = 0;
466
478
  for (let option = this.listbox.firstElementChild; option; option = /** @type {any} */ (option.nextElementSibling)) {
467
479
  option.filteredOut = false;
@@ -482,8 +494,9 @@ class ComboboxField extends HTMLElement {
482
494
  return this.#value;
483
495
  }
484
496
 
485
- /** @param {string} v */
486
- set value(v) {
497
+ /** @param {string} V */
498
+ set value(V) {
499
+ const v = typeof V === "string" ? V : String(V);
487
500
  const newOption = this.getOptionByValue(v);
488
501
  if (v === this.#value && newOption?.selected === true) return;
489
502
 
@@ -517,9 +530,8 @@ class ComboboxField extends HTMLElement {
517
530
  if (this.valueIs !== "anyvalue" && this.valueIs !== "clearable") {
518
531
  throw new TypeError('Method requires `filter` mode to be on and `valueis` to be "anyvalue" or "clearable"');
519
532
  }
520
- if (this.valueIs === "clearable" && this.#value === null) {
521
- throw new TypeError('Cannot coerce value to `""` for a `clearable` `combobox` that owns no `option`s');
522
- }
533
+ // Cannot coerce value to `""` for a `clearable` `combobox` that owns no `option`s
534
+ if (this.valueIs === "clearable" && this.#value === null) return;
523
535
 
524
536
  const prevOption = this.#value == null ? null : this.getOptionByValue(this.#value);
525
537
 
@@ -531,6 +543,25 @@ class ComboboxField extends HTMLElement {
531
543
  this.#validateRequiredConstraint();
532
544
  }
533
545
 
546
+ /**
547
+ * Coerces the `combobox`'s value to `null` and empties its text content.
548
+ *
549
+ * **NOTE: This method should only be called when an `(un)clearable` `combobox` is found to have no `option`s during
550
+ * a non-trivial state transition.**
551
+ * @returns {void}
552
+ */
553
+ #forceNullValue() {
554
+ // NOTE: This error is only intended to help with internal development. If it causes problems for devs, remove it.
555
+ if (this.listbox.children.length || this.valueIs === "anyvalue") {
556
+ throw new TypeError("Method can only be called when an `(un)clearable` `combobox` has no `option`s");
557
+ }
558
+
559
+ this.#value = null;
560
+ this.#internals.setFormValue(null);
561
+ this.text.data = "";
562
+ this.#validateRequiredConstraint();
563
+ }
564
+
534
565
  /**
535
566
  * Retrieves the `option` with the provided `value` (if it exists)
536
567
  * @param {string} value
@@ -627,10 +658,7 @@ class ComboboxField extends HTMLElement {
627
658
  * - `anyvalue`: The field's `value` can be any string, and it will automatically be set to
628
659
  * whatever value the user types. (Requires enabling `filter` mode.)
629
660
  *
630
- * <!--
631
- * TODO: Link to Documentation for More Details (like TS does for MDN). The deeper details of the behavior
632
- * are too sophisticated to place them all in a JSDoc, which should be [sufficiently] clear and succinct
633
- * -->
661
+ * [API Reference](https://github.com/ITenthusiasm/custom-elements/blob/main/src/Combobox/docs/combobox-field.md#attributes-valueis)
634
662
  *
635
663
  * @returns {"unclearable" | "clearable" | "anyvalue"}
636
664
  */
@@ -663,7 +691,7 @@ class ComboboxField extends HTMLElement {
663
691
  * Returns the `option` whose `label` matches the user's most recent filter input (if one exists).
664
692
  *
665
693
  * Value will be `null` if:
666
- * - The user's filter didn't match any `option`s
694
+ * - The user's filter didn't match any (enabled) `option`s
667
695
  * - The `combobox`'s text content was altered by a `value` change
668
696
  * - The `combobox` was just recently expanded
669
697
  * @returns {ComboboxOption | null}
@@ -763,7 +791,7 @@ class ComboboxField extends HTMLElement {
763
791
  const defaultOption = listbox.querySelector(":scope [role='option']:nth-last-child(1 of [selected])");
764
792
 
765
793
  if (defaultOption) this.value = defaultOption.value;
766
- else if (this.valueIs === "anyvalue" || this.valueIs === "clearable") this.value = "";
794
+ else if (this.valueIs === "anyvalue" || this.valueIs === "clearable") this.forceEmptyValue();
767
795
  else if (listbox.firstElementChild) this.value = listbox.firstElementChild.value;
768
796
  }
769
797
 
@@ -924,6 +952,19 @@ class ComboboxField extends HTMLElement {
924
952
  }
925
953
 
926
954
  if (event.key === " ") {
955
+ /*
956
+ * TODO: Right now, we only support blocking `SpaceBar` and `Enter`. Should we support blocking ALL event keys?
957
+ * Doing so would require us to allow blocking the `typeahead` functionality as well (to keep things consistent).
958
+ * (Note: Filtering can already be blocked today because `beforeinput` handlers don't run if the `keydown` event
959
+ * is prevented.)
960
+ *
961
+ * If we decide to support blocking ALL keys, then we'll have to refactor the way we're using `#handleTypeahead`.
962
+ * More than likely, we'd have to call it inside `#handleKeydown` to make sure that SpaceBar searching doesn't
963
+ * break when `event.preventDefault()` is called from within the `#handleKeydown` event handler.
964
+ *
965
+ * But before we start rearranging method calls, we need to know if devs actually care about this kind of feature.
966
+ */
967
+ if (event.defaultPrevented) return;
927
968
  if (combobox.filter) return; // Defer to `#handleSearch` instead
928
969
  event.preventDefault(); // Don't scroll
929
970
 
@@ -936,6 +977,7 @@ class ComboboxField extends HTMLElement {
936
977
  }
937
978
 
938
979
  if (event.key === "Enter") {
980
+ if (event.defaultPrevented) return;
939
981
  // Prevent `#handleSearch` from triggering
940
982
  if (combobox.filter) event.preventDefault();
941
983
 
@@ -1019,59 +1061,69 @@ class ComboboxField extends HTMLElement {
1019
1061
  * @returns {void}
1020
1062
  */
1021
1063
  #watchExpansion(mutations) {
1022
- for (let i = 0; i < mutations.length; i++) {
1023
- const mutation = mutations[i];
1024
- const combobox = /** @type {ComboboxField} */ (mutation.target);
1025
- const expanded = combobox.getAttribute(attrs["aria-expanded"]) === String(true);
1026
-
1027
- // Open Combobox
1028
- if (expanded) {
1029
- /*
1030
- * NOTE: If the user opens the `combobox` with search/typeahead, then `aria-activedescendant` will already
1031
- * exist and this expansion logic will be irrelevant. Remember that `MutationObserver` callbacks are run
1032
- * asynchronously, so this check would happen AFTER the search/typeahead handler completed. It's also
1033
- * possible for this condition to be met if we redundantly set `aria-expanded`. Although we should be
1034
- * be able to avoid that, we can't prevent Developers from accidentally doing that themselves.
1035
- */
1036
- if (combobox.getAttribute(attrs["aria-activedescendant"]) !== "") return;
1037
-
1038
- /** @type {ComboboxOption | null} */
1039
- const selectedOption = combobox.value == null ? null : combobox.getOptionByValue(combobox.value);
1040
- let activeOption = selectedOption ?? combobox.listbox.firstElementChild;
1041
- if (combobox.filter && activeOption?.filteredOut) [activeOption] = this.#matchingOptions;
1042
-
1043
- if (combobox.filter) {
1044
- this.#autoselectableOption = null;
1045
- this.#activeIndex = activeOption ? this.#matchingOptions.indexOf(activeOption) : -1;
1046
- }
1064
+ const combobox = /** @type {ComboboxField} */ (mutations[0].target);
1065
+ const ariaExpanded = combobox.getAttribute(attrs["aria-expanded"]);
1047
1066
 
1048
- if (activeOption) combobox.setAttribute(attrs["aria-activedescendant"], activeOption.id);
1049
- }
1050
- // Close Combobox
1051
- else {
1052
- combobox.setAttribute(attrs["aria-activedescendant"], "");
1053
- this.#searchString = "";
1067
+ const oldState = mutations[0].oldValue === String(true) ? "open" : "closed";
1068
+ const newState = ariaExpanded === String(true) ? "open" : "closed";
1069
+ const event = new ToggleEvent("toggle", { newState, oldState });
1054
1070
 
1055
- // See if logic _exclusive_ to `filter`ed `combobox`es needs to be run
1056
- if (!combobox.filter || combobox.value == null) return;
1057
- this.#resetOptions();
1058
-
1059
- // Reset `combobox` display if needed
1060
- // NOTE: `option` CAN be `null` or unselected if `combobox` is `clearable`, empty, and `collapsed` with a non-empty filter
1061
- const textNode = combobox.text;
1062
- if (!combobox.acceptsValue(textNode.data)) {
1063
- const option = combobox.getOptionByValue(combobox.value);
1064
- if (combobox.valueIs === "clearable" && !combobox.value && !option?.selected) textNode.data = "";
1065
- else if (textNode.data !== option?.textContent) textNode.data = /** @type {string} */ (option?.textContent);
1066
- }
1071
+ if (newState === oldState) {
1072
+ if (mutations.some((m) => m.oldValue !== ariaExpanded)) combobox.dispatchEvent(event);
1073
+ return;
1074
+ }
1067
1075
 
1068
- // Reset cursor if `combobox` is still `:focus`ed
1069
- if (/** @type {Document | ShadowRoot} */ (combobox.getRootNode()).activeElement !== combobox) return;
1076
+ // Open Combobox
1077
+ /* eslint-disable no-labels, no-restricted-syntax */
1078
+ $: if (newState === "open") {
1079
+ /*
1080
+ * NOTE: If the user opens the `combobox` with search/typeahead, then `aria-activedescendant` will already
1081
+ * exist and this expansion logic will be irrelevant. Remember that `MutationObserver` callbacks are run
1082
+ * asynchronously, so this check would happen AFTER the search/typeahead handler completed. It's also
1083
+ * possible for this condition to be met if we redundantly set `aria-expanded`. Although we should be
1084
+ * be able to avoid that, we can't prevent Developers from accidentally doing that themselves.
1085
+ */
1086
+ if (combobox.getAttribute(attrs["aria-activedescendant"]) !== "") break $;
1087
+
1088
+ /** @type {ComboboxOption | null} */
1089
+ const selectedOption = combobox.value == null ? null : combobox.getOptionByValue(combobox.value);
1090
+ let activeOption = selectedOption ?? combobox.listbox.firstElementChild;
1091
+ if (combobox.filter && activeOption?.filteredOut) [activeOption] = this.#matchingOptions;
1070
1092
 
1071
- const selection = /** @type {Selection} */ (combobox.ownerDocument.getSelection());
1072
- selection.setBaseAndExtent(textNode, textNode.length, textNode, textNode.length);
1093
+ if (combobox.filter) {
1094
+ this.#autoselectableOption = null;
1095
+ this.#activeIndex = activeOption ? this.#matchingOptions.indexOf(activeOption) : -1;
1096
+ }
1097
+
1098
+ if (activeOption) combobox.setAttribute(attrs["aria-activedescendant"], activeOption.id);
1099
+ }
1100
+ // Close Combobox
1101
+ else {
1102
+ combobox.setAttribute(attrs["aria-activedescendant"], "");
1103
+ this.#searchString = "";
1104
+
1105
+ // See if logic _exclusive_ to `filter`ed `combobox`es needs to be run
1106
+ if (!combobox.filter || combobox.value == null) break $;
1107
+ this.#resetOptions();
1108
+
1109
+ // Reset `combobox` display if needed
1110
+ // NOTE: `option` CAN be `null` or unselected if `combobox` is `clearable`, empty, and `collapsed` with a non-empty filter
1111
+ const textNode = combobox.text;
1112
+ if (!combobox.acceptsValue(textNode.data)) {
1113
+ const option = combobox.getOptionByValue(combobox.value);
1114
+ if (combobox.valueIs === "clearable" && !combobox.value && !option?.selected) textNode.data = "";
1115
+ else if (textNode.data !== option?.textContent) textNode.data = /** @type {string} */ (option?.textContent);
1073
1116
  }
1117
+
1118
+ // Reset cursor if `combobox` is still `:focus`ed
1119
+ if (/** @type {Document | ShadowRoot} */ (combobox.getRootNode()).activeElement !== combobox) break $;
1120
+
1121
+ const selection = /** @type {Selection} */ (combobox.ownerDocument.getSelection());
1122
+ selection.setBaseAndExtent(textNode, textNode.length, textNode, textNode.length);
1074
1123
  }
1124
+ /* eslint-enable no-labels, no-restricted-syntax */
1125
+
1126
+ combobox.dispatchEvent(event);
1075
1127
  }
1076
1128
 
1077
1129
  /**
@@ -1132,12 +1184,7 @@ class ComboboxField extends HTMLElement {
1132
1184
 
1133
1185
  if (!this.listbox.children.length) {
1134
1186
  if (!nullable) this.value = textNode.data;
1135
- else {
1136
- this.#value = null;
1137
- this.#internals.setFormValue(null);
1138
- textNode.data = "";
1139
- this.#validateRequiredConstraint();
1140
- }
1187
+ else this.#forceNullValue();
1141
1188
 
1142
1189
  if (this.filter) this.#filterOptions(); // Clean up internal data and show "No Matches" Message
1143
1190
  return;
@@ -1155,13 +1202,13 @@ class ComboboxField extends HTMLElement {
1155
1202
  if (this.valueIs !== "clearable") this.value = node.value;
1156
1203
  else {
1157
1204
  this.#value = "";
1158
- this.forceEmptyValue();
1205
+ this.#internals.setFormValue("");
1159
1206
  }
1160
1207
  }
1161
1208
  });
1162
1209
 
1163
1210
  mutation.removedNodes.forEach((node) => {
1164
- if (!(node instanceof ComboboxOption)) return;
1211
+ if (!(node instanceof ComboboxOption) || this.listbox.contains(node)) return;
1165
1212
  if (this.#autoselectableOption === node) this.#autoselectableOption = null;
1166
1213
  if (node.selected) {
1167
1214
  if (nullable) this.formResetCallback();
@@ -1,17 +1,268 @@
1
1
  # Combobox
2
2
 
3
- A robust, accessible and stylable [`Combobox`](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) [Web Component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) whose functionality can be extended or customized with ease.
3
+ A group of robust, extendable and stylable [Web Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) which work together to function as an accessible [`combobox`](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/).
4
4
 
5
5
  ## Features and Benefits
6
6
 
7
- - **Framework Agnostic**: Because the `combobox` component is just a custom `HTMLElement`, it works seamlessly in all JS Frameworks (and in pure-JS applications if that's what you fancy).
8
- - **Integrates with Native Web Forms**: This `combobox` integrates with the web's native `<form>` element, meaning that its value will be seen in the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) and will be automatically [sent to the server](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Sending_and_retrieving_form_data) when the form is submitted -- all without writing a single line of JS.
9
- - **Works with Various Form Libraries**: The `combobox` component emits standard DOM events like [`input`](https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event) and [`change`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event), enabling it to work naturally with reputable form libraries (e.g., the [`Form Observer`](https://github.com/enthusiastic-js/form-observer), [`Conform`](https://conform.guide/), and [`React Hook Form`](https://react-hook-form.com/)).
10
- - **Progressive Enhacement**: When used in `Select Enhacing Mode`, the component will fallback to a regular `<select>` element if JS is disabled or unavailable for your users. This means your forms will _always_ be fully usable and accessible.
11
- - **Highly Customizable**: The `combobox` component is flexible enough to work with whatever CSS you provide, and its functionality can be enhanced or overriden by [extending](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/extends) it.
12
- - **Performant**: Unlike many other alternatives, the `combobox` component has been cleverly designed to work without complex state management tools or aggressive DOM Tree manipulation. This makes it a fast and memory-efficient solution.
13
- - **No Dependencies**: The `combobox` component is built on the native web platform instead of extending other frameworks or libraries, guaranteeing your bundle size remains as small as possible.
7
+ - **Framework Agnostic**: Because this component simply uses custom `HTMLElement`s, it works seamlessly in all JS Frameworks (and in pure-JS applications if that's what you fancy).
8
+ - **Integrates with Native Web Forms**: This component [integrates](https://web.dev/articles/more-capable-form-controls) with the web's native `<form>` element, meaning that its value will be seen in the form's [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) and will be automatically [sent to the server](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Sending_and_retrieving_form_data) when the form is submitted &mdash; all without writing a single line of JS.
9
+ - **Works with Various Form Libraries**: The component emits standard DOM events like [`input`](https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event) and [`change`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event), enabling it to work naturally with reputable form libraries (e.g., the [`Form Observer`](https://github.com/enthusiastic-js/form-observer), [`Conform`](https://conform.guide/), and [`React Hook Form`](https://react-hook-form.com/)).
10
+ - **Progressive Enhacement**: When used in [`Select Enhacing Mode`](#select-enhancing-mode), the component will fallback to a regular `<select>` element if JS is disabled or unavailable for your users. This means your forms will _always_ be fully usable and accessible.
11
+ - **Highly Customizable**: The component is flexible enough to work with whatever CSS you provide, and its functionality can be enhanced or overriden through [class extension](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/extends).
12
+ - **Performant**: Unlike many other alternatives, this component has been cleverly designed to work without complex state management tools or aggressive DOM Tree manipulation. This makes it a fast and memory-efficient solution.
13
+ - **No Dependencies**: The component is built on the native web platform instead of extending other frameworks or libraries, guaranteeing your bundle size remains as small as possible.
14
14
 
15
15
  <!-- TODO: Link to article explaining how progressively-enhanced Form Controls _greatly_ simplify frontend code. -->
16
16
 
17
+ ## Install
18
+
19
+ ```
20
+ npm install @itenthusiasm/custom-elements
21
+ ```
22
+
23
+ ## Quickstart
24
+
25
+ ```html
26
+ <!-- HTML -->
27
+ <form>
28
+ <label for="rating">Rating</label>
29
+ <select-enhancer>
30
+ <combobox-field id="rating" name="rating"></combobox-field>
31
+ <combobox-listbox>
32
+ <combobox-option>1</combobox-option>
33
+ <combobox-option>2</combobox-option>
34
+ <combobox-option>3</combobox-option>
35
+ <combobox-option>4</combobox-option>
36
+ <combobox-option selected>5</combobox-option>
37
+ </combobox-listbox>
38
+ </select-enhancer>
39
+ </form>
40
+ ```
41
+
42
+ ```js
43
+ /* JavaScript */
44
+ import { SelectEnhancer, ComboboxField, ComboboxListbox, ComboboxOption } from "@itenthusiasm/custom-elements";
45
+ // or import { SelectEnhancer, ComboboxField, ComboboxListbox, ComboboxOption } from "@itenthusiasm/custom-elements/Combobox";
46
+
47
+ // NOTE: The order in which these Custom Elements are registered is important
48
+ customElements.define("combobox-listbox", ComboboxListbox);
49
+ customElements.define("combobox-field", ComboboxField);
50
+ customElements.define("combobox-option", ComboboxOption);
51
+ customElements.define("select-enhancer", SelectEnhancer);
52
+
53
+ // Retrieve some info about the `combobox`
54
+ const form = document.querySelector("form");
55
+ const formData = new FormData(form);
56
+ console.log(formData.get("rating")); // 5
57
+
58
+ const combobox = document.querySelector("combobox-field");
59
+ console.log(combobox.form === form); // true
60
+ console.log(combobox.value); // 5
61
+ ```
62
+
63
+ To use the component's built-in styles, you can use our `Combobox.css` file. If you're using a bundler like [Vite](https://vite.dev/), you can just import this directly into one of your JS files.
64
+
65
+ ```js
66
+ /* JavaScript */
67
+ import { SelectEnhancer, ComboboxField, ComboboxListbox, ComboboxOption } from "@itenthusiasm/custom-elements";
68
+ import "@itenthusiasm/custom-elements/Combobox/Combobox.css";
69
+
70
+ // ...
71
+ ```
72
+
73
+ If you prefer to load the CSS in a [`<link>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/link) tag (which tends to be more efficient), you can copy the `Combobox.css` file to your project, modify it to look the way you want, and then load the CSS file the regular way in your HTML.
74
+
75
+ ```html
76
+ <html>
77
+ <head>
78
+ <link rel="preload" as="style" href="/path/to/my/copy/of/Combobox.css" />
79
+ <!-- ... -->
80
+ </head>
81
+
82
+ <!-- ... -->
83
+ </html>
84
+ ```
85
+
86
+ In most cases, the `ComboboxField` custom element shown above behaves like an _enhanced_ drop-in replacement for the native [`HTMLSelectElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement). For example, it exposes a `value` property that can be used to get or set the value of the component. The `ComboboxField`'s value will always be synchronized with the state of its `ComboboxOption`s. (This matches the behavior of the native `<select>` element.) For more details on the component's attributes and properties, see the [documentation](https://github.com/ITenthusiasm/custom-elements/tree/main/src/Combobox/docs/combobox-field.md).
87
+
88
+ ### Select Enhancing Mode
89
+
90
+ The example HTML displayed at the [beginning](#quickstart) of this document showed the `Combobox` component being used in `Manual Setup Mode`. In `Manual Setup Mode`, the Custom Elements which make up the `Combobox` component are rendered directly to the DOM. This is recommended for SPAs.
91
+
92
+ If your application is server-rendered and you would like the component to be [progressively enhanced](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement), you can use it in `Select Enhancing Mode` instead. This mode is engaged by wrapping a regular [`<select>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/select) element with the `<select-enhancer>`:
93
+
94
+ ```html
95
+ <!-- HTML -->
96
+ <select-enhancer>
97
+ <select id="rank" name="rank" filter>
98
+ <option value="1">First</option>
99
+ <option value="2">Second</option>
100
+ <option value="3">Third</option>
101
+ <option value="4">Fourth</option>
102
+ <option value="5" selected>Fifth</option>
103
+ </select>
104
+ </select-enhancer>
105
+ ```
106
+
107
+ In this case, the `<select-enhancer>` will swap the `<select>` and `<option>` elements with the appropriate `<combobox-field>`, `<combobox-listbox>`, and `<combobox-option>` elements when the page loads, copying over all relevant attributes, values, and form states in the process.
108
+
109
+ ```html
110
+ <!-- HTML -->
111
+ <select-enhancer>
112
+ <combobox-field id="rank" name="rank" filter></combobox-field>
113
+ <combobox-listbox>
114
+ <combobox-option value="1">First</combobox-option>
115
+ <combobox-option value="2">Second</combobox-option>
116
+ <combobox-option value="3">Third</combobox-option>
117
+ <combobox-option value="4">Fourth</combobox-option>
118
+ <combobox-option value="5" selected>Fifth</combobox-option>
119
+ </combobox-listbox>
120
+ </select-enhancer>
121
+ ```
122
+
123
+ > If the elements are created _outside_ of the DOM, then the `<select-enhancer>` will perform this action when the elements are first mounted to the DOM instead.
124
+
125
+ `Select Enhancing Mode` is great for situations where you want your form to remain usable for people who have JS Disabled (or who fail to download it for any other reason).
126
+
127
+ Note that because `Select Enhancing Mode` manipulates the DOM to provide progressive enhancement, you should be careful when using this mode in JS Frameworks that expect to have _full_ control over the DOM. In general, if your list of `option`s is static, then `Select Enhancing Mode` should be safe to use regardless of which JS Framework you're using. However, if your `option`s are _dynamic_, then you should consider rendering a regular `<select>` element on initial render, then a `Manual Setup Mode` Combobox component after initial render. Examples of this can be found in the [documentation](https://github.com/ITenthusiasm/custom-elements/tree/main/src/Combobox/docs/guides/select-enhancement-in-js-frameworks.md).
128
+
129
+ ## Tips
130
+
131
+ Below are some quick tips for using the `Combobox` component. If you want to dive deeper into the component's features, we've laid out all the details of the component's attributes, properties, methods, and more in our [documentation](https://github.com/ITenthusiasm/custom-elements/tree/main/src/Combobox/docs). We've also provided a demo of the component on [`StackBlitz`](https://stackblitz.com/edit/custom-elements-combobox?file=index.html,src%2Fmain.ts).
132
+
133
+ ### Filter Mode
134
+
135
+ You can use the [`filter`](https://github.com/ITenthusiasm/custom-elements/tree/main/src/Combobox/docs/combobox-field.md#attributes-filter) attribute to enable users to filter the available list of options in a searchbox.
136
+
137
+ ```html
138
+ <!-- Manual Setup Mode -->
139
+ <select-enhancer>
140
+ <combobox-field filter></combobox-field>
141
+ <combobox-listbox>
142
+ <combobox-option value="1">One</combobox-option>
143
+ <combobox-option value="2">Two</combobox-option>
144
+ <combobox-option value="3">Three</combobox-option>
145
+ </combobox-listbox>
146
+ </select-enhancer>
147
+
148
+ <!-- Select Enhancing Mode -->
149
+ <select-enhancer>
150
+ <select filter>
151
+ <option value="1">One</option>
152
+ <option value="2">Two</option>
153
+ <option value="3">Three</option>
154
+ </select>
155
+ </select-enhancer>
156
+ ```
157
+
158
+ Remember that in `Select Enhancing Mode`, all attributes/states will be copied from the `<select>`/`<option>` elements to the `<combobox-field>`/`<combobox-option>` elements on mount / page load.
159
+
160
+ ### Configuring the Allowed Values
161
+
162
+ When the `combobox` is in `filter` mode, its allowed values can be configured with the [`valueis`](https://github.com/ITenthusiasm/custom-elements/tree/main/src/Combobox/docs/combobox-field.md#attributes-valueis) attribute. (This attribute does nothing if the component is not in `filter` mode.) There are 3 allowed values:
163
+
164
+ <dl>
165
+ <dt><code>unclearable</code></dt>
166
+ <dd>
167
+ The <code>Combobox</code> component can only be given a value that matches one of its <code>option</code>s. If the user starts filtering the <code>option</code>s and leaves the <code>combobox</code> without selecting an <code>option</code>, then the searchbox text will be reset to the label of the currently-selected <code>option</code>.
168
+ </dd>
169
+ <dt><code>clearable</code> (Default)</dt>
170
+ <dd>
171
+ Same as <code>unclearable</code>, except that the component's value can be cleared. This can happen if the user empties the searchbox, or if the developer sets the <a href="https://github.com/ITenthusiasm/custom-elements/tree/main/src/Combobox/docs/combobox-field.md#properties-value"><code>ComboboxField.value</code></a> property to an empty string. When the component's value is cleared, the previously-selected <code>option</code> will be deselected (if one exists).
172
+ </dd>
173
+ <dt><code>anyvalue</code></dt>
174
+ <dd>
175
+ The <code>Combobox</code> component accepts any value, even if there is no matching <code>option</code>. If the user types into the <code>combobox</code> and leaves it without selecting an <code>option</code>, then searchbox text will be left alone, and the component will adopt the value of the search text. The component's value can also be set programmatically to any string.
176
+ </dd>
177
+ </dl>
178
+
179
+ ```html
180
+ <!-- Manual Setup Mode -->
181
+ <select-enhancer>
182
+ <combobox-field filter valueis="anyvalue"></combobox-field>
183
+ <combobox-listbox>
184
+ <combobox-option value="1">One</combobox-option>
185
+ <combobox-option value="2">Two</combobox-option>
186
+ <combobox-option value="3">Three</combobox-option>
187
+ </combobox-listbox>
188
+ </select-enhancer>
189
+
190
+ <!-- Select Enhancing Mode -->
191
+ <select-enhancer>
192
+ <select filter valueis="anyvalue">
193
+ <option value="1">One</option>
194
+ <option value="2">Two</option>
195
+ <option value="3">Three</option>
196
+ </select>
197
+ </select-enhancer>
198
+ ```
199
+
200
+ Remember that in `Select Enhancing Mode`, all attributes/states will be copied from the `<select>`/`<option>` elements to the `<combobox-field>`/`<combobox-option>` elements on mount / page load.
201
+
202
+ ### TS Usage in Other Frameworks
203
+
204
+ Many JS frameworks, such as Svelte and React, often define their own "Element Namespaces". Because of this, most frameworks are not able (on their own) to recognize the correct attributes, properties, and event listeners that belong to the Custom Elements which you use. Thankfully, our library ships with TypeScript types that tell the various JS Frameworks about the existence and shape of our Custom Elements. (This also includes types for _enhanced_ elements like the enhanced `<select>` element.) To define _all_ of our library's Custom Elements within a Framework's "Element Namespace", simply import the appropriate type definition file:
205
+
206
+ ```ts
207
+ import type {} from "@itenthusiasm/custom-elements/types/react";
208
+ // For Svelte: import type {} from "@itenthusiasm/custom-elements/types/svelte";
209
+ // For Vue: import type {} from "@itenthusiasm/custom-elements/types/vue";
210
+ // etc. ...
211
+ ```
212
+
213
+ If you only intend to use _some_ of the Custom Elements provided by this library, then you should only import the types for those components.
214
+
215
+ ```ts
216
+ // Define ONLY the `Combobox` component's types in the framework's "Element Namespace"
217
+ import type {} from "@itenthusiasm/custom-elements/Combobox/types/react";
218
+ // For Svelte: import type {} from "@itenthusiasm/custom-elements/Combobox/types/svelte";
219
+ // For Vue: import type {} from "@itenthusiasm/custom-elements/Combobox/types/vue";
220
+ // etc. ...
221
+ ```
222
+
223
+ ### Restyling the Component
224
+
225
+ For simplicity, the `Combobox` component ships with its own default styles via the `@itenthusiasm/custom-elements/Combobox/Combobox.css` file. You're welcome to use this file directly in your application if you like. However, if you intend to modify _any_ of the styles to fit your own needs, then we recommend creating your own CSS file for the component instead, using our styles as an initial template.
226
+
227
+ There are some minor nuances that come with restyling the component. You can read about them in our [documentation](https://github.com/ITenthusiasm/custom-elements/tree/main/src/Combobox/docs/guides/styling-the-combobox.md).
228
+
229
+ ### Adding Icons / Buttons
230
+
231
+ Oftentimes, people prefer to add items like caret icons to their `combobox`es. The `Combobox` component allows you to do this by simply appending or prepending elements within the `<select-enhancer>`:
232
+
233
+ ```html
234
+ <!-- Manual Setup Mode -->
235
+ <select-enhancer>
236
+ <combobox-field name="numbers"></combobox-field>
237
+ <combobox-listbox>
238
+ <combobox-option>1</combobox-option>
239
+ <combobox-option>2</combobox-option>
240
+ <combobox-option>3</combobox-option>
241
+ </combobox-listbox>
242
+
243
+ <svg viewBox="0 0 100 100"><!-- Caret Icon SVG Elements ... --></svg>
244
+ </select-enhancer>
245
+
246
+ <!-- Select Enhancing Mode -->
247
+ <select-enhancer>
248
+ <select name="numbers">
249
+ <option>1</option>
250
+ <option>2</option>
251
+ <option>3</option>
252
+ </select>
253
+
254
+ <svg viewBox="0 0 100 100"><!-- Caret Icon SVG Elements ... --></svg>
255
+ </select-enhancer>
256
+ ```
257
+
258
+ Additional examples of customizing the `Combobox` component's appearance can be found in our [guides](https://github.com/ITenthusiasm/custom-elements/tree/main/src/Combobox/docs/guides/styling-the-combobox.md).
259
+
260
+ ## What's Next?
261
+
262
+ To learn more about all that you can accomplish with the `Combobox` component, visit our [documentation](https://github.com/ITenthusiasm/custom-elements/tree/main/src/Combobox/docs). Trust us, you've only seen the beginning of what can be done with this component!
263
+
264
+ If you're too excited to sit and read, you can also play with our [`StackBlitz` Demo](https://stackblitz.com/edit/custom-elements-combobox?file=index.html,src%2Fmain.ts) to see the `Combobox` component in action.
265
+
17
266
  <!-- TODO: Link to example of styling our `combobox` to look like GitHub's or ShadcnUI's. Probably put it alongside an example of another styling approach. -->
267
+
268
+ <!-- TODO: Maybe also add some example styles for making the `<select>` look like the `<combobox-field>`? -->
@@ -37,7 +37,7 @@ declare module "preact" {
37
37
  defaultSelected?: Signalish<ComboboxOption["defaultSelected"] | undefined>;
38
38
  disabled?: Signalish<ComboboxOption["disabled"] | undefined>;
39
39
  selected?: Signalish<ComboboxOption["selected"] | undefined>;
40
- value?: Signalish<ComboboxOption["value"] | undefined>;
40
+ value?: Signalish<ComboboxOption["value"] | number | undefined>;
41
41
  }
42
42
  }
43
43
  }
@@ -31,7 +31,7 @@ declare module "react" {
31
31
  defaultSelected?: ComboboxOption["defaultSelected"];
32
32
  disabled?: ComboboxOption["disabled"];
33
33
  selected?: ComboboxOption["selected"];
34
- value?: ComboboxOption["value"];
34
+ value?: ComboboxOption["value"] | number;
35
35
  }
36
36
 
37
37
  // eslint-disable-next-line @typescript-eslint/no-namespace -- Necessary for type declaration merging
@@ -20,12 +20,16 @@ declare module "solid-js" {
20
20
  disabled?: ComboboxField["disabled"];
21
21
  filter?: ComboboxField["filter"];
22
22
  filtermethod?: ComboboxField["filterMethod"];
23
+ "attr:filtermethod"?: ComboboxField["filterMethod"];
23
24
  form?: string;
24
25
  name?: ComboboxField["name"];
25
26
  nomatchesmessage?: ComboboxField["noMatchesMessage"];
27
+ "attr:nomatchesmessage"?: ComboboxField["noMatchesMessage"];
26
28
  required?: ComboboxField["required"];
27
29
  valueis?: ComboboxField["valueIs"];
30
+ "attr:valueis"?: ComboboxField["valueIs"];
28
31
  valuemissingerror?: ComboboxField["valueMissingError"];
32
+ "attr:valuemissingerror"?: ComboboxField["valueMissingError"];
29
33
 
30
34
  onFilterchange?: EventHandlerUnion<T, Event>;
31
35
  onfilterchange?: EventHandlerUnion<T, Event>;
@@ -38,8 +42,15 @@ declare module "solid-js" {
38
42
 
39
43
  interface ComboboxOptionHTMLAttributes<T> extends HTMLAttributes<T> {
40
44
  disabled?: ComboboxOption["disabled"];
41
- selected?: ComboboxOption["selected"];
42
- value?: ComboboxOption["value"];
45
+ selected?: ComboboxOption["defaultSelected"];
46
+ value?: ComboboxOption["value"] | number;
47
+ }
48
+
49
+ interface ExplicitBoolAttributes {
50
+ filter?: boolean;
51
+ disabled?: boolean;
52
+ required?: boolean;
53
+ selected?: boolean;
43
54
  }
44
55
  }
45
56
  }
@@ -15,7 +15,7 @@ declare module "svelte/elements" {
15
15
  optiontag?: SelectEnhancer["optionTag"] | null;
16
16
  }
17
17
 
18
- interface HTMLComboboxFieldAttributes<T extends EventTarget> extends HTMLAttributes<T> {
18
+ interface HTMLComboboxFieldAttributes<T extends EventTarget = ComboboxField> extends HTMLAttributes<T> {
19
19
  disabled?: ComboboxField["disabled"] | null;
20
20
  filter?: ComboboxField["filter"] | null;
21
21
  filtermethod?: ComboboxField["filterMethod"] | null;
@@ -38,6 +38,6 @@ declare module "svelte/elements" {
38
38
  defaultSelected?: ComboboxOption["defaultSelected"] | null;
39
39
  disabled?: ComboboxOption["disabled"] | null;
40
40
  selected?: ComboboxOption["selected"] | null;
41
- value?: ComboboxOption["value"] | null;
41
+ value?: ComboboxOption["value"] | number | null;
42
42
  }
43
43
  }
@@ -65,7 +65,7 @@ declare module "vue" {
65
65
  defaultSelected?: ComboboxOption["defaultSelected"];
66
66
  disabled?: ComboboxOption["disabled"];
67
67
  selected?: ComboboxOption["selected"];
68
- value?: ComboboxOption["value"];
68
+ value?: ComboboxOption["value"] | number;
69
69
  }
70
70
 
71
71
  interface ComboboxOptionVueSFCType extends ComboboxOption {
package/README.md CHANGED
@@ -1,3 +1,27 @@
1
1
  # @itenthusiasm/custom-elements
2
2
 
3
- Robust, accessible, and progressively-enhanceable Web Components for common developer needs.
3
+ Robust, accessible, and progressively-enhanceable [Web Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) for common developer needs. Each component integrates seamlessly into your web applications, whether you use pure HTML/CSS/JS or you use a JS Framework.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ npm install @itenthusiasm/custom-elements
9
+ ```
10
+
11
+ ## Components
12
+
13
+ Below are the components that this library currently provides. Each component has its own `README` which you can view to learn more about how the component operates.
14
+
15
+ <dl>
16
+ <dt id="components-combobox">
17
+ <a href="./Combobox"><code>Combobox</code></a>
18
+ </dt>
19
+ <dd>
20
+ <p>
21
+ Progressively enhances the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/select"><code>&lt;select&gt;</code></a> element, transforming it into a stylable, (optionally) filterable <a href="https://www.w3.org/TR/wai-aria-1.2/#combobox"><code>combobox</code></a> which meets WAI-ARIA's <a href="https://www.w3.org/WAI/ARIA/apg/patterns/combobox/">accessibility requirements</a>. The <code>Combobox</code> component behaves just like the native form controls, meaning that it dispatches the standard <code>input</code>/<code>change</code> events, is <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements">associated</a> with its owning form, and automatically participates in all form activity, <a href="https://web.dev/articles/more-capable-form-controls">including form submission</a>.
22
+ </p>
23
+ <p>
24
+ <a href="https://stackblitz.com/edit/custom-elements-combobox?file=index.html,src%2Fmain.ts">Stackblitz Form Integration Demo</a>
25
+ </p>
26
+ </dd>
27
+ </dl>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@itenthusiasm/custom-elements",
3
3
  "type": "module",
4
- "version": "0.0.2",
4
+ "version": "0.7.0",
5
5
  "sideEffects": false,
6
6
  "license": "MIT",
7
7
  "description": "Robust, accessible, and progressively-enhanceable Web Components for common developer needs",
@@ -33,11 +33,12 @@
33
33
  "progressive-enhancement"
34
34
  ],
35
35
  "files": [
36
- "./**/README.md",
36
+ "./*/README.md",
37
37
  "./**/*.{js,css,d.ts}"
38
38
  ],
39
39
  "exports": {
40
40
  "./*.js": "./*.js",
41
+ "./*.css": "./*.css",
41
42
  "./*.d.ts": {
42
43
  "types": "./*.d.ts"
43
44
  },