@djcali570/component-lib 0.1.96 → 0.1.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/DropDown5.svelte +116 -100
  2. package/package.json +1 -1
@@ -1,17 +1,18 @@
1
1
  <script lang="ts">
2
2
  import { BROWSER } from 'esm-env';
3
- import { onDestroy, onMount } from 'svelte';
3
+ import { onMount } from 'svelte';
4
4
  import { fly } from 'svelte/transition';
5
5
  import type { DropDown5ColorScheme, DropDownItem } from './types.js';
6
6
 
7
+ // Props
7
8
  let {
8
9
  colorScheme: partialColorScheme = {},
9
10
  name = 'dropdown',
10
11
  title = 'Title',
11
- value = $bindable(),
12
- valueKey = $bindable(),
13
- extraValue = $bindable(),
14
- onUpdate = (key, v, ev) => {},
12
+ value = $bindable<any>(),
13
+ valueKey = $bindable<string | null | undefined>(),
14
+ extraValue = $bindable<string | null | undefined>(),
15
+ onUpdate = () => {},
15
16
  disabled = false,
16
17
  dropdownItems = [] as DropDownItem[]
17
18
  }: {
@@ -26,6 +27,7 @@
26
27
  dropdownItems?: DropDownItem[];
27
28
  } = $props();
28
29
 
30
+ // Default color scheme
29
31
  const defaultColorScheme: DropDown5ColorScheme = {
30
32
  textColor: '#D6D6D6',
31
33
  bgColor: '#121212',
@@ -38,112 +40,134 @@
38
40
  itemHoverTextColor: '#121212'
39
41
  };
40
42
 
41
- // Merge partial colorScheme with defaults
42
- const colorScheme = { ...defaultColorScheme, ...partialColorScheme };
43
+ // Merge partial scheme with defaults
44
+ const colorScheme = $derived({ ...defaultColorScheme, ...partialColorScheme });
43
45
 
44
- const id = generateRandomString();
45
- let showDropdown = $state(false);
46
+ // Refs & state
47
+ let containerRef: HTMLDivElement | null = $state(null);
46
48
  let dropdownRef: HTMLDivElement | null = $state(null);
49
+ let showDropdown = $state(false);
47
50
  let filteredItems: DropDownItem[] = $state([]);
51
+ let highlightedIndex = $state(-1);
52
+ const id = generateRandomString();
48
53
 
54
+ // Effects
49
55
  $effect(() => {
50
56
  if (!BROWSER || !showDropdown || !dropdownRef) return;
57
+
51
58
  setTimeout(() => {
52
59
  if (!dropdownRef) return;
53
- const dropdownRect = dropdownRef.getBoundingClientRect();
60
+
61
+ const rect = dropdownRef.getBoundingClientRect();
54
62
  const viewportHeight = window.innerHeight;
55
- if (dropdownRect.bottom > viewportHeight) {
56
- const scrollY = window.scrollY + (dropdownRect.bottom - viewportHeight) + 10;
63
+
64
+ if (rect.bottom > viewportHeight) {
65
+ const scrollY = window.scrollY + (rect.bottom - viewportHeight) + 10;
57
66
  window.scrollTo({ top: scrollY, behavior: 'smooth' });
58
67
  }
59
68
  }, 0);
60
69
  });
61
70
 
62
- onMount(() => {
63
- document.addEventListener('click', closeDropdown);
64
- });
71
+ $effect(() => {
72
+ if (!showDropdown || !dropdownRef || highlightedIndex < 0) return;
65
73
 
66
- onDestroy(() => {
67
- try {
68
- document.removeEventListener('click', closeDropdown);
69
- } catch (error) {}
74
+ const items = dropdownRef.querySelectorAll<HTMLButtonElement>('button');
75
+ items[highlightedIndex]?.scrollIntoView({ block: 'nearest' });
70
76
  });
71
77
 
72
- /**
73
- * Generate a random string so that each
74
- * input will have a unique id in the dom
75
- */
76
- function generateRandomString() {
77
- const length = 6;
78
- const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
79
-
80
- let result = '';
81
- for (let i = 0; i < length; i++) {
82
- const randomIndex = Math.floor(Math.random() * characters.length);
83
- result += characters.charAt(randomIndex);
84
- }
78
+ // Outside click
79
+ function setupOutsideClick() {
80
+ if (!BROWSER) return;
81
+
82
+ const handler = (event: Event) => {
83
+ if (!containerRef?.contains(event.target as Node)) {
84
+ showDropdown = false;
85
+ validateInput();
86
+ }
87
+ };
85
88
 
86
- return result;
89
+ document.addEventListener('pointerdown', handler);
90
+ return () => document.removeEventListener('pointerdown', handler);
91
+ }
92
+
93
+ onMount(() => setupOutsideClick());
94
+
95
+ // Helpers
96
+ function generateRandomString(length = 6) {
97
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
98
+ return Array.from({ length }, () =>
99
+ chars.charAt(Math.floor(Math.random() * chars.length))
100
+ ).join('');
101
+ }
102
+
103
+ function handleBlur() {
104
+ setTimeout(() => {
105
+ if (!showDropdown) validateInput();
106
+ }, 100);
87
107
  }
88
108
 
89
- /**
90
- * Show the dropdown list
91
- */
92
109
  function showDropdownList() {
93
110
  showDropdown = true;
94
111
  filteredItems = dropdownItems;
112
+ highlightedIndex = filteredItems.length > 0 ? 0 : -1;
95
113
  }
96
114
 
97
- function closeDropdown(event: Event) {
98
- const target = event.target as HTMLElement;
99
-
100
- if (target.id !== `dropdown-${id}`) {
101
- showDropdown = false;
102
- validateInput();
103
- }
104
- }
105
115
  function filterItems() {
106
- filteredItems = dropdownItems.filter((item) =>
107
- item.value.toLowerCase().includes(value.toLowerCase())
108
- );
116
+ const search = (value ?? '').toLowerCase();
117
+ filteredItems = dropdownItems.filter((item) => item.value.toLowerCase().includes(search));
118
+ highlightedIndex = filteredItems.length > 0 ? 0 : -1;
109
119
  }
120
+
110
121
  function selectItem(item: DropDownItem) {
111
- // Set all bound values
112
122
  value = item.value;
113
123
  valueKey = item.id ?? '';
114
124
  extraValue = item.extraValue ?? '';
115
-
116
- // Notify parent
117
125
  onUpdate(valueKey, value, extraValue);
118
-
119
- // Close dropdown
120
126
  showDropdown = false;
127
+ highlightedIndex = -1;
121
128
  }
122
129
 
123
130
  function validateInput() {
124
- const matchedItem = dropdownItems.find(
125
- (item) => item.value.toLowerCase() === value.toLowerCase()
126
- );
127
-
128
- if (matchedItem) {
129
- // Valid match: sync values
130
- value = matchedItem.value;
131
- valueKey = matchedItem.id ?? '';
132
- extraValue = matchedItem.extraValue ?? '';
133
- } else {
134
- // No match: clear all
131
+ const search = (value ?? '').toLowerCase();
132
+ const match = dropdownItems.find((item) => item.value.toLowerCase() === search);
133
+
134
+ if (match) selectItem(match);
135
+ else {
135
136
  value = '';
136
137
  valueKey = '';
137
138
  extraValue = '';
139
+ onUpdate(valueKey, value, extraValue);
138
140
  }
141
+ }
139
142
 
140
- // Always notify parent of the current state
141
- onUpdate(valueKey, value, extraValue);
143
+ function handleKeydown(e: KeyboardEvent) {
144
+ if (!showDropdown) return;
145
+
146
+ if (e.key === 'ArrowDown') {
147
+ e.preventDefault();
148
+ highlightedIndex = Math.min(highlightedIndex + 1, filteredItems.length - 1);
149
+ }
150
+
151
+ if (e.key === 'ArrowUp') {
152
+ e.preventDefault();
153
+ highlightedIndex = Math.max(highlightedIndex - 1, 0);
154
+ }
155
+
156
+ if (e.key === 'Enter' && highlightedIndex >= 0) {
157
+ e.preventDefault();
158
+ selectItem(filteredItems[highlightedIndex]);
159
+ }
160
+
161
+ if (e.key === 'Escape') {
162
+ showDropdown = false;
163
+ highlightedIndex = -1;
164
+ }
142
165
  }
143
166
  </script>
144
167
 
145
168
  <div
146
169
  class="dropdown5__container"
170
+ bind:this={containerRef}
147
171
  style="
148
172
  --dropdown5__textColor: {colorScheme.textColor};
149
173
  --dropdown5__mainBgColor: {colorScheme.bgColor};
@@ -166,9 +190,10 @@
166
190
  aria-haspopup="listbox"
167
191
  {name}
168
192
  bind:value
193
+ onkeydown={handleKeydown}
169
194
  oninput={filterItems}
170
195
  onfocus={showDropdownList}
171
- onblur={validateInput}
196
+ onblur={handleBlur}
172
197
  autocomplete="off"
173
198
  {disabled}
174
199
  />
@@ -188,24 +213,22 @@
188
213
  role="listbox"
189
214
  aria-labelledby="dropdown-{id}-title"
190
215
  bind:this={dropdownRef}
191
- oninput={filterItems}
192
- transition:fly={{
193
- y: 10
194
- }}
216
+ transition:fly={{ y: 10 }}
195
217
  >
196
218
  <div style="height:100%;">
197
219
  {#each filteredItems as item, index}
198
220
  <button
199
221
  type="button"
200
222
  class="dropdown5__list__item"
223
+ class:selected={index === highlightedIndex}
201
224
  role="option"
202
225
  aria-selected={item.value === value ? 'true' : 'false'}
203
- onclick={() => selectItem(item)}
226
+ onmousedown={() => selectItem(item)}
204
227
  >
205
- {#if item.value === value}
206
- <div class="fic">
207
- <div style="padding-right: 0.5rem;">
208
- <div class="fca" style="width: 1rem; height: 1rem;">
228
+ <div class="fic">
229
+ <div style="padding-right: 0.5rem;">
230
+ <div class="fca" style="width: 1rem; height: 1rem;">
231
+ {#if item.value === value}
209
232
  <svg
210
233
  viewBox="0 0 12 12"
211
234
  xmlns="http://www.w3.org/2000/svg"
@@ -213,36 +236,24 @@
213
236
  role="presentation"
214
237
  focusable="false"
215
238
  style="display: block; height: 12px; width: 12px; fill: currentcolor;"
216
- ><path
217
- d="m10.5 1.939 1.061 1.061-7.061 7.061-.53-.531-3-3-.531-.53 1.061-1.061 3 3 5.47-5.469z"
218
- ></path></svg
219
239
  >
220
- </div>
240
+ <path
241
+ d="m10.5 1.939 1.061 1.061-7.061 7.061-.53-.531-3-3-.531-.53 1.061-1.061 3 3 5.47-5.469z"
242
+ ></path>
243
+ </svg>
244
+ {/if}
221
245
  </div>
222
- {#if item.component}
223
- {@const Component = item.component}
224
- <div class="component" style={item.componentStyles}>
225
- <Component {...item.props} />
226
- </div>
227
- {/if}
228
- <div>{item.value}</div>
229
246
  </div>
230
- {:else}
231
- <div class="fic">
232
- <div style="padding-right: 0.5rem;">
233
- <div style="width: 1rem; height: 1rem;"></div>
234
- </div>
235
247
 
236
- {#if item.component}
237
- {@const Component = item.component}
238
- <div class="component" style={item.componentStyles}>
239
- <Component {...item.props} />
240
- </div>
241
- {/if}
248
+ {#if item.component}
249
+ {@const Component = item.component}
250
+ <div class="component" style={item.componentStyles}>
251
+ <Component {...item.props} />
252
+ </div>
253
+ {/if}
242
254
 
243
- <div>{item.value}</div>
244
- </div>
245
- {/if}
255
+ <div>{item.value}</div>
256
+ </div>
246
257
  </button>
247
258
  {/each}
248
259
  </div>
@@ -332,4 +343,9 @@
332
343
  width: 2rem;
333
344
  height: 2rem;
334
345
  }
346
+
347
+ .selected {
348
+ background-color: var(--dropdown5__itemHoverBgColor);
349
+ color: var(--dropdown5__itemHoverTextColor);
350
+ }
335
351
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djcali570/component-lib",
3
- "version": "0.1.96",
3
+ "version": "0.1.97",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.test.*",