@openvoxproject/voxblocks 0.12.0 → 0.15.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.
@@ -1,5 +1,6 @@
1
- import { html, css } from 'lit';
1
+ import { html, css, nothing } from 'lit';
2
2
  import { customElement, property, query } from 'lit/decorators.js';
3
+ import { ifDefined } from 'lit/directives/if-defined.js';
3
4
  import { VoxFieldElement, fieldStyles } from '../../internal/field.js';
4
5
 
5
6
  /**
@@ -12,11 +13,23 @@ import { VoxFieldElement, fieldStyles } from '../../internal/field.js';
12
13
  * <option value="rpm">RHEL</option>
13
14
  * </vox-select>
14
15
  * ```
16
+ *
17
+ * Add `multiple` for a list that accepts more than one selection; read the
18
+ * result from `values` rather than `value`.
15
19
  */
16
20
  @customElement('vox-select')
17
21
  export class VoxSelect extends VoxFieldElement {
18
22
  @property() value = '';
19
23
 
24
+ /** Accept more than one selection, rendering as a scrolling list box. */
25
+ @property({ type: Boolean, reflect: true }) multiple = false;
26
+
27
+ /** Rows shown when `multiple` is set. */
28
+ @property({ type: Number }) size?: number;
29
+
30
+ /** Selected values. Only meaningful when `multiple` is set. */
31
+ @property({ type: Array }) values: string[] = [];
32
+
20
33
  @query('select') private selectEl!: HTMLSelectElement;
21
34
 
22
35
  static styles = [
@@ -30,19 +43,48 @@ export class VoxSelect extends VoxFieldElement {
30
43
  background-position: right var(--vox-space-3) center;
31
44
  cursor: pointer;
32
45
  }
46
+
47
+ /* A list box has no collapsed affordance, so drop the chevron. */
48
+ :host([multiple]) select.control {
49
+ appearance: none;
50
+ padding-right: var(--vox-space-3);
51
+ background-image: none;
52
+ cursor: default;
53
+ }
54
+
55
+ :host([multiple]) select.control option {
56
+ padding: var(--vox-space-1) var(--vox-space-2);
57
+ }
33
58
  `,
34
59
  ];
35
60
 
36
61
  formResetCallback() {
37
62
  this.value = '';
63
+ this.values = [];
38
64
  this.syncOptions();
39
65
  }
40
66
 
41
67
  updated() {
42
- this.internals.setFormValue(this.value);
68
+ this.internals.setFormValue(this.multiple ? this.formData() : this.value);
43
69
  if (this.selectEl) this.syncValidity(this.selectEl);
44
70
  }
45
71
 
72
+ focus(options?: FocusOptions) {
73
+ this.selectEl?.focus(options);
74
+ }
75
+
76
+ /**
77
+ * A multi-select submits one entry per selection, which only FormData can
78
+ * express. Without a `name` there is nothing to key them on, so submit
79
+ * nothing — matching a native select with no name.
80
+ */
81
+ private formData(): FormData | null {
82
+ if (!this.name) return null;
83
+ const data = new FormData();
84
+ for (const value of this.values) data.append(this.name, value);
85
+ return data;
86
+ }
87
+
46
88
  private syncOptions() {
47
89
  if (!this.selectEl) return;
48
90
  const slot = this.renderRoot.querySelector('slot');
@@ -53,14 +95,34 @@ export class VoxSelect extends VoxFieldElement {
53
95
  .filter((el) => el instanceof HTMLOptionElement || el instanceof HTMLOptGroupElement)
54
96
  .map((el) => el.cloneNode(true)),
55
97
  );
98
+
99
+ if (this.multiple) {
100
+ const wanted = new Set(this.values);
101
+ for (const option of this.selectEl.options) {
102
+ option.selected = wanted.has(option.value);
103
+ }
104
+ this.values = this.selected();
105
+ return;
106
+ }
107
+
56
108
  if (this.value) {
57
109
  this.selectEl.value = this.value;
58
110
  }
59
111
  this.value = this.selectEl.value;
60
112
  }
61
113
 
114
+ private selected(): string[] {
115
+ return [...this.selectEl.selectedOptions].map((option) => option.value);
116
+ }
117
+
62
118
  private handleChange() {
63
- this.value = this.selectEl.value;
119
+ if (this.multiple) {
120
+ this.values = this.selected();
121
+ // Keep `value` meaningful as the first selection, as the native API does.
122
+ this.value = this.values[0] ?? '';
123
+ } else {
124
+ this.value = this.selectEl.value;
125
+ }
64
126
  this.dispatchEvent(new Event('change', { bubbles: true }));
65
127
  }
66
128
 
@@ -71,8 +133,11 @@ export class VoxSelect extends VoxFieldElement {
71
133
  <select
72
134
  id="select"
73
135
  class="control"
136
+ ?multiple=${this.multiple}
137
+ size=${ifDefined(this.multiple ? (this.size ?? 4) : undefined)}
74
138
  ?required=${this.required}
75
139
  ?disabled=${this.disabled}
140
+ aria-label=${this.label ? nothing : 'options'}
76
141
  @change=${this.handleChange}
77
142
  ></select>
78
143
  ${this.renderNote()}
package/src/index.ts CHANGED
@@ -12,11 +12,13 @@ export { VoxIcon } from './components/icon/vox-icon.js';
12
12
 
13
13
  // Forms
14
14
  export { VoxCheckbox } from './components/checkbox/vox-checkbox.js';
15
+ export { VoxCombobox } from './components/combobox/vox-combobox.js';
15
16
  export { VoxFileInput } from './components/file-input/vox-file-input.js';
16
17
  export { VoxInput } from './components/input/vox-input.js';
17
18
  export { VoxInputGroup } from './components/input-group/vox-input-group.js';
18
19
  export { VoxRadio } from './components/radio/vox-radio.js';
19
20
  export { VoxRadioGroup } from './components/radio/vox-radio-group.js';
21
+ export { VoxRange } from './components/range/vox-range.js';
20
22
  export { VoxSelect } from './components/select/vox-select.js';
21
23
  export { VoxSwitch } from './components/switch/vox-switch.js';
22
24
  export { VoxTextarea } from './components/textarea/vox-textarea.js';