@studiocms/ui 0.1.0 → 0.3.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.
@@ -3,21 +3,63 @@ import Icon from '../utils/Icon.astro';
3
3
  import { generateID } from '../utils/generateID';
4
4
  import Input from './Input.astro';
5
5
 
6
+ /**
7
+ * An option in the select dropdown
8
+ */
6
9
  interface Option {
10
+ /**
11
+ * The label of the option.
12
+ */
7
13
  label: string;
14
+ /**
15
+ * The value of the option.
16
+ */
8
17
  value: string;
18
+ /**
19
+ * Whether the option is disabled.
20
+ */
9
21
  disabled?: boolean;
10
22
  }
11
23
 
24
+ /**
25
+ * The props for the search select component.
26
+ */
12
27
  interface Props {
28
+ /**
29
+ * The label of the search select.
30
+ */
13
31
  label?: string;
32
+ /**
33
+ * The default value of the search select. Needs to be one of the values in the options.
34
+ */
14
35
  defaultValue?: string;
36
+ /**
37
+ * Additional classes to apply to the search select.
38
+ */
15
39
  class?: string;
40
+ /**
41
+ * The name of the search select.
42
+ */
16
43
  name?: string;
44
+ /**
45
+ * Whether the search select is required. Defaults to `false`.
46
+ */
17
47
  isRequired?: boolean;
48
+ /**
49
+ * The options to display in the search select.
50
+ */
18
51
  options: Option[];
52
+ /**
53
+ * Whether the search select is disabled. Defaults to `false`.
54
+ */
19
55
  disabled?: boolean;
56
+ /**
57
+ * Whether the search select should take up the full width of its container.
58
+ */
20
59
  fullWidth?: boolean;
60
+ /**
61
+ * The placeholder of the search select.
62
+ */
21
63
  placeholder?: string;
22
64
  }
23
65
 
@@ -38,6 +80,8 @@ const {
38
80
  id={`${name}-container`}
39
81
  class="sui-search-select-label"
40
82
  class:list={[disabled && "disabled", className, fullWidth && "full"]}
83
+ data-options={JSON.stringify(options)}
84
+ data-id={name}
41
85
  >
42
86
  <div class="sui-search-input-wrapper" id={`${name}-search-input-wrapper`}>
43
87
  <Input
@@ -70,7 +114,7 @@ const {
70
114
  ))
71
115
  }
72
116
  </ul>
73
- <select class="sui-hidden-select" id={name} name={name} required={isRequired}>
117
+ <select class="sui-hidden-select" id={name} name={name} required={isRequired} hidden tabindex="-1">
74
118
  <option value={""}> Select </option>
75
119
  {
76
120
  options.map((x) => (
@@ -85,245 +129,249 @@ const {
85
129
  }
86
130
  </select>
87
131
  </div>
88
- <script is:inline define:vars={{ id: name, options, defaultValue }}>
89
- const container = document.getElementById(`${id}-container`);
90
- const hiddenSelect = document.getElementById(id);
91
- const searchWrapper = document.getElementById(`${id}-search-input-wrapper`);
92
- const searchInput = searchWrapper.querySelector('input');
93
- const valueSpan = document.getElementById(`${id}-value-span`);
94
- const dropdown = document.getElementById(`${id}-dropdown`);
95
- let optionElements = container.querySelectorAll("li");
96
-
97
- let active = false;
98
-
99
- let filteredOptions = options;
100
-
101
- searchWrapper.addEventListener("click", () => {
102
- const { bottom, left, right, width, x, y, height } = searchWrapper.getBoundingClientRect();
103
-
104
- const optionHeight = 36;
105
- const totalBorderSize = 2;
106
- const margin = 4;
107
-
108
- const dropdownHeight = options.length * optionHeight + totalBorderSize + margin;
109
-
110
- const CustomRect = {
111
- top: bottom + margin,
112
- left,
113
- right,
114
- bottom: bottom + margin + dropdownHeight,
115
- width,
116
- height: dropdownHeight,
117
- x,
118
- y: y + height + margin,
119
- };
120
-
121
- if (active) {
122
- searchInput.ariaExpanded = false;
123
- dropdown.classList.remove("active", "above");
124
- active = false;
125
- return;
126
- }
132
+ <script>
133
+ interface Option {
134
+ label: string;
135
+ value: string;
136
+ disabled?: boolean;
137
+ }
138
+ // id: name, options, defaultValue
139
+ const searchSelects = document.querySelectorAll<HTMLDivElement>('.sui-search-select-label');
140
+
141
+ for (const container of searchSelects) {
142
+ const hiddenSelect = container.querySelector<HTMLSelectElement>('select')!;
143
+ const searchWrapper = container.querySelector<HTMLDivElement>(`.sui-search-input-wrapper`)!;
144
+ const searchInput = searchWrapper.querySelector('input')!;
145
+ const dropdown = container.querySelector(`.sui-search-select-dropdown`)!;
146
+ let optionElements = container.querySelectorAll("li");
147
+
148
+ let active = false;
149
+
150
+ const options = JSON.parse(container.dataset.options!) as Option[];
151
+ const id = container.dataset.id!;
152
+ let filteredOptions = options;
153
+
154
+ searchWrapper.addEventListener("click", () => {
155
+ const { bottom, left, right, width, x, y, height } = searchWrapper.getBoundingClientRect();
156
+
157
+ const optionHeight = 36;
158
+ const totalBorderSize = 2;
159
+ const margin = 4;
160
+
161
+ const dropdownHeight = options.length * optionHeight + totalBorderSize + margin;
162
+
163
+ const CustomRect = {
164
+ top: bottom + margin,
165
+ left,
166
+ right,
167
+ bottom: bottom + margin + dropdownHeight,
168
+ width,
169
+ height: dropdownHeight,
170
+ x,
171
+ y: y + height + margin,
172
+ };
173
+
174
+ if (active) {
175
+ searchInput.ariaExpanded = 'false';
176
+ dropdown.classList.remove("active", "above");
177
+ active = false;
178
+ return;
179
+ }
127
180
 
128
- active = true;
129
- searchInput.ariaExpanded = true;
130
-
131
- if (
132
- CustomRect.top >= 0 &&
133
- CustomRect.left >= 0 &&
134
- CustomRect.bottom <=
135
- (window.innerHeight || document.documentElement.clientHeight) &&
136
- CustomRect.right <=
137
- (window.innerWidth || document.documentElement.clientWidth)
138
- ) {
139
- dropdown.classList.add("active");
140
- } else {
141
- dropdown.classList.add("active", "above");
142
- }
143
- });
181
+ active = true;
182
+ searchInput.ariaExpanded = 'true';
144
183
 
145
- const handleSelection = (e, option) => {
146
- e.stopImmediatePropagation();
147
-
148
- if (option.id === `${id}-selected` || !id) return;
184
+ if (
185
+ CustomRect.top >= 0 &&
186
+ CustomRect.left >= 0 &&
187
+ CustomRect.bottom <=
188
+ (window.innerHeight || document.documentElement.clientHeight) &&
189
+ CustomRect.right <=
190
+ (window.innerWidth || document.documentElement.clientWidth)
191
+ ) {
192
+ dropdown.classList.add("active");
193
+ } else {
194
+ dropdown.classList.add("active", "above");
195
+ }
196
+ });
149
197
 
150
- const currentlySelected = document.getElementById(`${id}-selected`);
198
+ const handleSelection = (e: MouseEvent, option: HTMLLIElement) => {
199
+ e.stopImmediatePropagation();
200
+
201
+ if (option.id === `${id}-selected` || !id) return;
151
202
 
152
- if (currentlySelected) {
153
- currentlySelected.classList.remove("selected");
154
- currentlySelected.id = "";
155
- }
203
+ const currentlySelected = document.getElementById(`${id}-selected`);
156
204
 
157
- option.id = `${id}-selected`;
158
- option.classList.add("selected");
205
+ if (currentlySelected) {
206
+ currentlySelected.classList.remove("selected");
207
+ currentlySelected.id = "";
208
+ }
159
209
 
160
- const index = options.findIndex((x) => x.value === option.dataset.value);
161
- focusIndex = index;
210
+ option.id = `${id}-selected`;
211
+ option.classList.add("selected");
162
212
 
163
- const opt = options[index];
164
- hiddenSelect.value = opt.value;
213
+ const index = options.findIndex((x) => x.value === option.dataset.value);
214
+ focusIndex = index;
165
215
 
166
- searchInput.placeholder = opt.label;
167
- dropdown.classList.remove("active", "above");
168
- searchInput.blur();
216
+ const opt = options[index]!;
217
+ hiddenSelect.value = opt.value;
169
218
 
170
- searchInput.value = "";
171
- filteredOptions = options;
172
- constructOptionsBasedOnOptions(options);
219
+ searchInput.placeholder = opt.label;
220
+ dropdown.classList.remove("active", "above");
221
+ // searchInput.blur();
173
222
 
174
- active = false;
175
- }
223
+ searchInput.value = "";
224
+ filteredOptions = options;
225
+ constructOptionsBasedOnOptions(options);
176
226
 
177
- optionElements.forEach((option) => {
178
- option.addEventListener("click", (e) => handleSelection(e, option));
179
- });
227
+ active = false;
228
+ }
180
229
 
181
- window.addEventListener("scroll", () => {
182
- dropdown.classList.remove("active", "above");
183
- active = false;
184
- });
230
+ optionElements.forEach((option) => {
231
+ option.addEventListener("click", (e) => handleSelection(e, option));
232
+ });
185
233
 
186
- hideOnClickOutside(container);
234
+ window.addEventListener("scroll", () => {
235
+ dropdown.classList.remove("active", "above");
236
+ active = false;
237
+ });
238
+
239
+ hideOnClickOutside(container);
240
+
241
+ function hideOnClickOutside(element: HTMLElement) {
242
+ const outsideClickListener = (event: MouseEvent) => {
243
+ if (
244
+ !element.contains(event.target! as Element) &&
245
+ isVisible(element) &&
246
+ active === true
247
+ ) {
248
+ // or use: event.target.closest(selector) === null
249
+ dropdown.classList.remove("active", "above");
250
+ active = false;
251
+ }
252
+ };
187
253
 
188
- function hideOnClickOutside(element) {
189
- const outsideClickListener = (event) => {
190
- if (
191
- !element.contains(event.target) &&
192
- isVisible(element) &&
193
- active === true
194
- ) {
195
- // or use: event.target.closest(selector) === null
196
- dropdown.classList.remove("active", "above");
197
- active = false;
198
- }
199
- };
254
+ document.addEventListener("click", outsideClickListener);
255
+ }
200
256
 
201
- const removeClickListener = () => {
202
- document.removeEventListener("click", outsideClickListener);
203
- };
257
+ // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js
258
+ const isVisible = (elem: HTMLElement) =>
259
+ !!elem &&
260
+ !!(
261
+ elem.offsetWidth ||
262
+ elem.offsetHeight ||
263
+ elem.getClientRects().length
264
+ );
204
265
 
205
- document.addEventListener("click", outsideClickListener);
206
- }
266
+ let focusIndex = 0;
207
267
 
208
- // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js
209
- const isVisible = (elem) =>
210
- !!elem &&
211
- !!(
212
- elem.offsetWidth ||
213
- elem.offsetHeight ||
214
- elem.getClientRects().length
215
- );
216
-
217
- let focusIndex = 0;
218
-
219
- const recomputeOptions = () => {
220
- for (const entry of optionElements) {
221
- if (entry.dataset.optionIndex == focusIndex) {
222
- entry.classList.add('focused');
223
- } else {
224
- entry.classList.remove('focused');
268
+ const recomputeOptions = () => {
269
+ for (const entry of optionElements) {
270
+ if (Number.parseInt(entry.dataset.optionIndex!) == focusIndex) {
271
+ entry.classList.add('focused');
272
+ } else {
273
+ entry.classList.remove('focused');
274
+ }
225
275
  }
226
276
  }
227
- }
228
277
 
229
- searchInput.addEventListener('keydown', (e) => {
230
- if (e.key === "Escape") {
231
- e.preventDefault();
232
- e.stopImmediatePropagation();
233
-
234
- active = false;
235
- dropdown.classList.remove("active", "above");
236
- searchInput.blur();
237
-
238
- return;
239
- }
278
+ searchInput.addEventListener('keydown', (e) => {
279
+ if (e.key === "Escape") {
280
+ e.preventDefault();
281
+ e.stopImmediatePropagation();
240
282
 
241
- if (e.key === "ArrowUp" && focusIndex > 0) {
242
- e.preventDefault();
243
- e.stopImmediatePropagation();
283
+ active = false;
284
+ dropdown.classList.remove("active", "above");
285
+ searchInput.blur();
286
+
287
+ return;
288
+ }
244
289
 
245
- focusIndex--;
246
- recomputeOptions();
290
+ if (e.key === "ArrowUp" && focusIndex > 0) {
291
+ e.preventDefault();
292
+ e.stopImmediatePropagation();
247
293
 
248
- return;
249
- }
294
+ focusIndex--;
295
+ recomputeOptions();
250
296
 
251
- if (e.key === "ArrowDown" && focusIndex + 1 < filteredOptions.filter(x => !x.disabled).length) {
252
- e.preventDefault();
253
- e.stopImmediatePropagation();
297
+ return;
298
+ }
254
299
 
255
- focusIndex++;
256
- recomputeOptions();
300
+ if (e.key === "ArrowDown" && focusIndex + 1 < filteredOptions.filter(x => !x.disabled).length) {
301
+ e.preventDefault();
302
+ e.stopImmediatePropagation();
257
303
 
258
- return;
259
- }
304
+ focusIndex++;
305
+ recomputeOptions();
260
306
 
261
- if (e.key === "Enter") {
262
- e.preventDefault();
263
- e.stopImmediatePropagation();
307
+ return;
308
+ }
264
309
 
265
- console.log(optionElements, focusIndex);
310
+ if (e.key === "Enter") {
311
+ e.preventDefault();
312
+ e.stopImmediatePropagation();
266
313
 
267
- for (const entry of optionElements) {
268
- if (entry.dataset.optionIndex == focusIndex) {
269
- entry.click();
314
+ for (const entry of optionElements) {
315
+ if (Number.parseInt(entry.dataset.optionIndex!) === focusIndex) {
316
+ entry.click();
317
+ }
270
318
  }
319
+
320
+ return;
271
321
  }
322
+ });
272
323
 
273
- return;
274
- }
275
- });
324
+ searchInput.addEventListener('keyup', (e) => {
325
+ if (["Enter", "ArrowUp", "ArrowDown"].includes(e.key)) return;
276
326
 
277
- searchInput.addEventListener('keyup', (e) => {
278
- if (["Enter", "ArrowUp", "ArrowDown"].includes(e.key)) return;
327
+ if (searchInput.value.trim().length === 0) {
328
+ constructOptionsBasedOnOptions(options);
329
+ filteredOptions = options;
330
+ return;
331
+ };
279
332
 
280
- if (searchInput.value.trim().length === 0) {
281
- constructOptionsBasedOnOptions(options);
282
- filteredOptions = options;
283
- return;
284
- };
333
+ filteredOptions = options.filter(x => x.label.includes(searchInput.value));
334
+ focusIndex = 0;
285
335
 
286
- filteredOptions = options.filter(x => x.label.includes(searchInput.value));
287
- focusIndex = 0;
336
+ constructOptionsBasedOnOptions(filteredOptions);
337
+ });
288
338
 
289
- constructOptionsBasedOnOptions(filteredOptions);
290
- });
339
+ function constructOptionsBasedOnOptions(options: Option[]) {
340
+ dropdown.innerHTML = '';
291
341
 
292
- function constructOptionsBasedOnOptions(options) {
293
- dropdown.innerHTML = '';
342
+ if (options.length === 0) {
343
+ const element = document.createElement('li');
344
+ element.classList.add('empty-search-results');
345
+ element.textContent = "No results found.";
294
346
 
295
- if (options.length === 0) {
296
- const element = document.createElement('li');
297
- element.classList.add('empty-search-results');
298
- element.textContent = "No results found.";
347
+ dropdown.appendChild(element);
348
+ }
299
349
 
300
- dropdown.appendChild(element);
301
- }
350
+ let i = 0;
302
351
 
303
- let i = 0;
352
+ for (const option of options) {
353
+ const element = document.createElement('li');
354
+ element.classList.add(...[
355
+ 'sui-search-select-option',
356
+ option.disabled && "disabled",
357
+ focusIndex === i && 'focused',
358
+ ].filter((x) => typeof x === 'string'));
359
+ element.role = "option";
360
+ element.value = Number.parseInt(option.value);
361
+ element.id = "";
362
+ element.dataset.optionIndex = i.toString();
363
+ element.dataset.value = option.value;
364
+ element.textContent = option.label;
304
365
 
305
- for (const option of options) {
306
- const element = document.createElement('li');
307
- element.classList.add(...[
308
- 'sui-search-select-option',
309
- option.disabled && "disabled",
310
- focusIndex === i && 'focused',
311
- ].filter(Boolean));
312
- element.role = "option";
313
- element.value = option.value;
314
- element.id = "";
315
- element.dataset.optionIndex = i;
316
- element.dataset.value = option.value;
317
- element.textContent = option.label;
366
+ element.addEventListener("click", (e) => handleSelection(e, element));
318
367
 
319
- element.addEventListener("click", (e) => handleSelection(e, element));
368
+ dropdown.appendChild(element);
320
369
 
321
- dropdown.appendChild(element);
370
+ i++;
371
+ }
322
372
 
323
- i++;
373
+ optionElements = container.querySelectorAll("li");
324
374
  }
325
-
326
- optionElements = container.querySelectorAll("li");
327
375
  }
328
376
  </script>
329
377
  <style is:global>
@@ -418,10 +466,14 @@ const {
418
466
  cursor: pointer;
419
467
  }
420
468
 
469
+ .sui-search-input-wrapper input {
470
+ padding-right: 2.5rem;
471
+ }
472
+
421
473
  .sui-search-select-indicator {
422
474
  position: absolute;
423
- bottom: .75rem;
424
- right: .75rem;
475
+ bottom: .675rem;
476
+ right: .675rem;
425
477
  }
426
478
 
427
479
  .sui-search-input-wrapper:has(input:focus) + .sui-search-select-dropdown {