@intechstudio/grid-uikit 1.20260622.1304 → 1.20260623.833

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.
@@ -39,7 +39,17 @@
39
39
  </script>
40
40
 
41
41
  <label class:checkbox-box={style === "box"} class:disabled>
42
- <button {...$root} use:root class:disabled>
42
+ <button
43
+ {...$root}
44
+ use:root
45
+ class:disabled
46
+ on:keydown={(e) => {
47
+ if (e.key === "Enter") {
48
+ e.preventDefault();
49
+ e.currentTarget.click();
50
+ }
51
+ }}
52
+ >
43
53
  <div class="checkbox-outer" class:disabled>
44
54
  <div
45
55
  style:display={target ? "block" : "none"}
@@ -121,6 +131,10 @@
121
131
  cursor: pointer;
122
132
  font-size: inherit;
123
133
  }
134
+ button:focus {
135
+ outline: var(--focus-outline);
136
+ outline-offset: var(--focus-offset);
137
+ }
124
138
 
125
139
  button.disabled {
126
140
  cursor: default;
@@ -20,7 +20,7 @@
20
20
  </script>
21
21
 
22
22
  <script lang="ts">
23
- import { createEventDispatcher } from "svelte";
23
+ import { createEventDispatcher, tick } from "svelte";
24
24
  import { createPopover } from "@melt-ui/svelte";
25
25
  import { fade } from "svelte/transition";
26
26
  import { writable, type Writable } from "svelte/store";
@@ -53,6 +53,16 @@
53
53
  let selected: Writable<MeltComboSuggestion> = writable();
54
54
 
55
55
  let inputElement: any;
56
+ let highlightedIndex = -1;
57
+
58
+ $: if (!$open) {
59
+ highlightedIndex = -1;
60
+ } else if ($open && filteredSuggestions.length > 0) {
61
+ const match = filteredSuggestions.findIndex(
62
+ (s) => String(s.value).trim() === String(inputValue).trim(),
63
+ );
64
+ highlightedIndex = match >= 0 ? match : 0;
65
+ }
56
66
 
57
67
  const open = writable(false);
58
68
 
@@ -80,7 +90,7 @@
80
90
  $: handleSelectionChange($selected);
81
91
  $: handleInputChange(inputValue);
82
92
 
83
- $: filteredSuggestions = searchable
93
+ $: baseList = searchable
84
94
  ? suggestions.filter(
85
95
  (e) =>
86
96
  e.info.toLowerCase().includes(inputValue?.toLowerCase()) ||
@@ -88,6 +98,17 @@
88
98
  )
89
99
  : suggestions;
90
100
 
101
+ $: filteredSuggestions =
102
+ inputValue.trim() &&
103
+ !suggestions.some(
104
+ (s) => String(s.value).trim() === String(inputValue).trim(),
105
+ )
106
+ ? [
107
+ { info: "Manual entry: " + inputValue, value: inputValue },
108
+ ...baseList,
109
+ ]
110
+ : baseList;
111
+
91
112
  $: infoValue =
92
113
  suggestions.find((s) => String(s.value).trim() == String(inputValue).trim())
93
114
  ?.info || "";
@@ -147,16 +168,60 @@
147
168
  }
148
169
  }
149
170
 
150
- function handleFocus() {
151
- filteredSuggestions = suggestions;
152
- if (searchable) {
153
- inputElement.select();
171
+ function handleBlur() {
172
+ open.set(false);
173
+ }
174
+
175
+ function handleKeydown(e: KeyboardEvent) {
176
+ if (e.key === "ArrowDown") {
177
+ e.preventDefault();
178
+ if (filteredSuggestions.length === 0) return;
179
+ if (!$open) {
180
+ open.set(true);
181
+ } else {
182
+ highlightedIndex =
183
+ highlightedIndex < filteredSuggestions.length - 1
184
+ ? highlightedIndex + 1
185
+ : 0;
186
+ scrollIntoView();
187
+ }
188
+ } else if (e.key === "ArrowUp") {
189
+ e.preventDefault();
190
+ if (filteredSuggestions.length === 0) return;
191
+ if (!$open) {
192
+ open.set(true);
193
+ } else {
194
+ highlightedIndex =
195
+ highlightedIndex > 0
196
+ ? highlightedIndex - 1
197
+ : filteredSuggestions.length - 1;
198
+ scrollIntoView();
199
+ }
200
+ } else if (e.key === "Enter") {
201
+ if (
202
+ highlightedIndex >= 0 &&
203
+ highlightedIndex < filteredSuggestions.length
204
+ ) {
205
+ e.preventDefault();
206
+ selected.set(filteredSuggestions[highlightedIndex]);
207
+ highlightedIndex = -1;
208
+ } else if (!$open) {
209
+ e.preventDefault();
210
+ open.set(true);
211
+ }
212
+ } else if (e.key === "Escape") {
213
+ highlightedIndex = -1;
214
+ open.set(false);
154
215
  }
155
- open.set(true);
156
216
  }
157
217
 
158
- function handleBlur() {
159
- open.set(false);
218
+ function scrollIntoView() {
219
+ tick().then(() => {
220
+ const el = document.querySelector(
221
+ `[data-combo-index="${highlightedIndex}"]`,
222
+ );
223
+ el?.scrollIntoView({ block: "nearest" });
224
+ });
160
225
  }
161
226
 
162
227
  let menuWidth;
@@ -166,7 +231,7 @@
166
231
  <div class="content">
167
232
  {#if title?.length > 0}
168
233
  <label>
169
- {title}
234
+ <span class="label-text">{title}</span>
170
235
  <div bind:clientWidth={menuWidth}>
171
236
  <input
172
237
  bind:this={inputElement}
@@ -175,11 +240,11 @@
175
240
  use:trigger
176
241
  bind:value={inputValue}
177
242
  on:change={handleChange}
178
- on:focus={handleFocus}
179
243
  on:blur={handleBlur}
180
244
  on:m-keydown={(e) => {
181
245
  e.preventDefault();
182
246
  }}
247
+ on:keydown={handleKeydown}
183
248
  on:click={() => {
184
249
  open.set(true);
185
250
  }}
@@ -199,11 +264,11 @@
199
264
  use:trigger
200
265
  bind:value={inputValue}
201
266
  on:change={handleChange}
202
- on:focus={handleFocus}
203
267
  on:blur={handleBlur}
204
268
  on:m-keydown={(e) => {
205
269
  e.preventDefault();
206
270
  }}
271
+ on:keydown={handleKeydown}
207
272
  on:click={() => {
208
273
  open.set(true);
209
274
  }}
@@ -216,7 +281,7 @@
216
281
  {/if}
217
282
  </div>
218
283
 
219
- {#if $open && !disabled && suggestions.length > 0}
284
+ {#if $open && !disabled && filteredSuggestions.length > 0}
220
285
  <!-- svelte-ignore a11y-no-static-element-interactions -->
221
286
  <div
222
287
  {...$content}
@@ -227,12 +292,17 @@
227
292
  style="min-width: {menuWidth}px;"
228
293
  >
229
294
  <div>
230
- {#each filteredSuggestions as suggestion}
295
+ {#each filteredSuggestions as suggestion, i}
231
296
  <option
232
297
  {...$close}
233
298
  use:close
234
299
  class="suggestion"
235
- on:click={() => selected.set(suggestion)}>{suggestion.info}</option
300
+ data-combo-index={i}
301
+ on:click={() => selected.set(suggestion)}
302
+ on:mouseenter={() => (highlightedIndex = i)}
303
+ style={highlightedIndex === i
304
+ ? "background-color: var(--popover-selection); color: var(--foreground);"
305
+ : ""}>{suggestion.info}</option
236
306
  >
237
307
  {/each}
238
308
  </div>
@@ -264,18 +334,19 @@
264
334
  color: var(--foreground);
265
335
  font-size: 0.875em;
266
336
  line-height: 1.25em;
337
+ align-items: center;
338
+ }
339
+ .label-text {
267
340
  overflow: hidden;
268
341
  text-overflow: ellipsis;
269
342
  white-space: nowrap;
270
- align-items: center;
343
+ display: block;
271
344
  }
272
345
  input {
273
346
  cursor: auto;
274
- outline: 2px solid transparent;
275
- outline-offset: 2px;
347
+ outline: none;
276
348
  width: 100%;
277
- display: flex;
278
- flex-direction: row;
349
+ box-sizing: border-box;
279
350
  border: 1px solid var(--background-soft);
280
351
  padding: 0.5em;
281
352
  color: var(--foreground);
@@ -283,6 +354,10 @@
283
354
  margin: 0.25em 0em 0em;
284
355
  font-size: inherit;
285
356
  }
357
+ input:focus {
358
+ outline: var(--focus-outline);
359
+ outline-offset: var(--focus-offset);
360
+ }
286
361
  input.error {
287
362
  border-color: var(--error);
288
363
  }
@@ -325,6 +400,7 @@
325
400
  color: var(--foreground-soft);
326
401
  font-size: 0.875em;
327
402
  line-height: 1.25em;
403
+ margin-top: 0.25em;
328
404
  overflow: hidden;
329
405
  text-overflow: ellipsis;
330
406
  white-space: nowrap;
@@ -84,8 +84,8 @@
84
84
  class:disabled
85
85
  />
86
86
  </div>
87
+ <span class:disabled>{title}</span>
87
88
  </button>
88
- <span class:disabled>{title}</span>
89
89
  {/if}
90
90
  {#if style === "button"}
91
91
  <button
@@ -176,6 +176,12 @@
176
176
  padding: 0; /* 3 */
177
177
  background-color: transparent; /* 2 */
178
178
  cursor: pointer;
179
+ display: inline-flex;
180
+ align-items: center;
181
+ }
182
+ button:focus {
183
+ outline: var(--focus-outline);
184
+ outline-offset: var(--focus-offset);
179
185
  }
180
186
  button.style-button {
181
187
  position: relative;
@@ -183,6 +189,7 @@
183
189
  width: 100%;
184
190
  border-radius: 0.25em;
185
191
  border: 1px solid var(--background-soft);
192
+ justify-content: center;
186
193
  }
187
194
  button.style-button:hover {
188
195
  background-color: var(--background-muted);
@@ -120,6 +120,10 @@
120
120
  background-color: var(--background-muted);
121
121
  cursor: pointer;
122
122
  }
123
+ button.select:focus {
124
+ outline: var(--focus-outline);
125
+ outline-offset: var(--focus-offset);
126
+ }
123
127
  button.disabled {
124
128
  color: var(--foreground-disabled);
125
129
  cursor: default;
@@ -103,6 +103,10 @@
103
103
  border-radius: 9999px;
104
104
  background-color: var(--foreground-muted);
105
105
  }
106
+ span.thumb:focus {
107
+ outline: var(--focus-outline);
108
+ outline-offset: var(--focus-offset);
109
+ }
106
110
 
107
111
  span.container.disabled {
108
112
  pointer-events: none;
@@ -101,8 +101,8 @@
101
101
  font-size: inherit;
102
102
  }
103
103
  input:focus {
104
- outline: 2px solid transparent;
105
- outline-offset: 2px;
104
+ outline: var(--focus-outline);
105
+ outline-offset: var(--focus-offset);
106
106
  }
107
107
  input:disabled {
108
108
  cursor: default;
@@ -143,8 +143,8 @@
143
143
  }
144
144
 
145
145
  button:focus {
146
- outline: 2px solid transparent;
147
- outline-offset: 2px;
146
+ outline: var(--focus-outline);
147
+ outline-offset: var(--focus-offset);
148
148
  }
149
149
 
150
150
  button.grouped {
@@ -185,8 +185,8 @@
185
185
  }
186
186
 
187
187
  .dropdown-button:focus {
188
- outline: 2px solid transparent;
189
- outline-offset: 2px;
188
+ outline: var(--focus-outline);
189
+ outline-offset: var(--focus-offset);
190
190
  }
191
191
 
192
192
  .dropdown-button.grouped {
@@ -20,11 +20,13 @@
20
20
 
21
21
  let showbuttons = $state(false);
22
22
  let showTooltip = $state(false);
23
+ let tooltipKey = $state(0);
23
24
 
24
25
  let tooltipElement: HTMLDivElement | undefined = $state();
25
26
  let previousFocusedElement: HTMLElement | null = null;
26
27
  let closeTimeout: any;
27
28
  let openTimeout: any;
29
+ let isRestoringFocus = false;
28
30
 
29
31
  function handleClick(e: any) {
30
32
  //handleReferenceElementClick(e);
@@ -49,12 +51,11 @@
49
51
  }
50
52
  }
51
53
  if (triggerEvents.includes("show-buttons")) {
52
- if (!showbuttons) {
53
- clearTimeout(openTimeout);
54
- showTooltip = true;
55
- if (buttons.length > 0) {
56
- showbuttons = true;
57
- }
54
+ clearTimeout(openTimeout);
55
+ tooltipKey++;
56
+ showTooltip = true;
57
+ if (buttons.length > 0) {
58
+ showbuttons = true;
58
59
  }
59
60
  }
60
61
  if (triggerEvents.includes("hover") && !showbuttons) {
@@ -85,14 +86,14 @@
85
86
  clearTimeout(openTimeout);
86
87
  closeTimeout = setTimeout(
87
88
  () => {
88
- showTooltip = false;
89
+ close();
89
90
  },
90
91
  instant ? 0 : 100,
91
92
  );
92
93
  }
93
94
  if (triggerEvents.includes("click")) {
94
95
  closeTimeout = setTimeout(() => {
95
- showTooltip = false;
96
+ close();
96
97
  }, 100);
97
98
  }
98
99
  //e.stopPropagation();
@@ -100,16 +101,21 @@
100
101
 
101
102
  function handleReferenceElementFocus(e: any) {
102
103
  if (triggerEvents.includes("focus")) {
104
+ if (isRestoringFocus) return;
103
105
  clearTimeout(closeTimeout);
104
106
  showTooltip = true;
105
107
  }
106
108
  //e.stopPropagation();
107
109
  }
108
110
 
109
- function handleReferenceElementBlur(e: any) {
111
+ function handleReferenceElementBlur(e: FocusEvent) {
110
112
  if (triggerEvents.includes("focus")) {
113
+ const relatedTarget = e.relatedTarget as Node | null;
114
+ if (relatedTarget && tooltipElement?.contains(relatedTarget)) {
115
+ return;
116
+ }
111
117
  closeTimeout = setTimeout(() => {
112
- showTooltip = false;
118
+ close();
113
119
  }, 100);
114
120
  }
115
121
  //e.stopPropagation();
@@ -147,6 +153,17 @@
147
153
  if (tooltipElement) {
148
154
  previousFocusedElement = document.activeElement as HTMLElement | null;
149
155
  tooltipElement.focus();
156
+ const handleFocusOut = (e: FocusEvent) => {
157
+ const relatedTarget = e.relatedTarget as Node | null;
158
+ if (relatedTarget && tooltipElement?.contains(relatedTarget)) {
159
+ return;
160
+ }
161
+ close();
162
+ };
163
+ tooltipElement.addEventListener("focusout", handleFocusOut);
164
+ return () => {
165
+ tooltipElement?.removeEventListener("focusout", handleFocusOut);
166
+ };
150
167
  }
151
168
  });
152
169
 
@@ -204,90 +221,96 @@
204
221
  );
205
222
  }
206
223
 
207
- function close() {
224
+ function close(restoreFocus = true) {
208
225
  clearTimeout(openTimeout);
209
226
  clearTimeout(closeTimeout);
210
227
  showTooltip = false;
211
228
  showbuttons = false;
212
- if (previousFocusedElement) {
229
+ if (restoreFocus && previousFocusedElement) {
230
+ isRestoringFocus = true;
213
231
  previousFocusedElement.focus();
214
232
  previousFocusedElement = null;
233
+ requestAnimationFrame(() => {
234
+ isRestoringFocus = false;
235
+ });
215
236
  }
216
237
  }
217
238
  </script>
218
239
 
219
- <Popover
220
- bind:isOpen={showTooltip}
221
- triggerEvents={["manual"]}
222
- {referenceElement}
223
- bind:placement
224
- spaceAway={10}
225
- >
226
- <!-- svelte-ignore a11y-click-events-have-key-events -->
227
- <!-- svelte-ignore a11y-no-static-element-interactions -->
228
- <div
229
- on:mouseenter={handleMouseEnter}
230
- on:mouseleave={handleMouseLeave}
231
- on:click={handleClick}
232
- bind:this={tooltipElement}
233
- tabindex="-1"
234
- class="{className} tooltip-container"
235
- transition:fade|global={{
236
- duration: instant ? 0 : duration, //Make it instant when explicitly clicked
237
- }}
240
+ {#key tooltipKey}
241
+ <Popover
242
+ isOpen={showTooltip}
243
+ triggerEvents={["manual"]}
244
+ {referenceElement}
245
+ bind:placement
246
+ spaceAway={10}
238
247
  >
248
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
249
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
239
250
  <div
240
- class="tooltip-container-content"
241
- class:tooltip-gap-2={buttons.length > 0}
251
+ on:mouseenter={handleMouseEnter}
252
+ on:mouseleave={handleMouseLeave}
253
+ on:click={handleClick}
254
+ bind:this={tooltipElement}
255
+ tabindex="-1"
256
+ class="{className} tooltip-container"
257
+ transition:fade|global={{
258
+ duration: instant ? 0 : duration, //Make it instant when explicitly clicked
259
+ }}
242
260
  >
243
- {#if typeof component === "undefined"}
244
- <div
245
- class="tooltip-container-text"
246
- class:tooltip-whitespace-nowrap={nowrap}
247
- >
248
- {text}
249
- </div>
250
- {:else}
251
- <svelte:component
252
- this={component.object}
253
- {...component.props}
254
- class="tooltip-container-component"
255
- on:event={interceptEvent}
256
- />
257
- {/if}
258
-
259
- {#if showbuttons}
260
- <div
261
- transition:slide|global={{ duration: instant ? 0 : 100 }}
262
- class="tooltip-container-buttons"
263
- >
264
- {#each buttons as button}
265
- <MoltenPushButton
266
- text={button.label}
267
- snap={"full"}
268
- click={() => {
269
- if (typeof button.handler !== "undefined") {
270
- button.handler();
271
- }
272
- close();
273
- }}
274
- />
275
- {/each}
276
- </div>
277
- {/if}
261
+ <div
262
+ class="tooltip-container-content"
263
+ class:tooltip-gap-2={buttons.length > 0}
264
+ >
265
+ {#if typeof component === "undefined"}
266
+ <div
267
+ class="tooltip-container-text"
268
+ class:tooltip-whitespace-nowrap={nowrap}
269
+ >
270
+ {text}
271
+ </div>
272
+ {:else}
273
+ <svelte:component
274
+ this={component.object}
275
+ {...component.props}
276
+ class="tooltip-container-component"
277
+ on:event={interceptEvent}
278
+ />
279
+ {/if}
280
+
281
+ {#if showbuttons}
282
+ <div
283
+ transition:slide|global={{ duration: instant ? 0 : 100 }}
284
+ class="tooltip-container-buttons"
285
+ >
286
+ {#each buttons as button}
287
+ <MoltenPushButton
288
+ text={button.label}
289
+ snap={"full"}
290
+ click={() => {
291
+ if (typeof button.handler !== "undefined") {
292
+ button.handler();
293
+ }
294
+ close();
295
+ }}
296
+ />
297
+ {/each}
298
+ </div>
299
+ {/if}
300
+ </div>
278
301
  </div>
279
- </div>
280
- <div
281
- transition:fade|global={{
282
- duration: instant ? 0 : duration,
283
- }}
284
- class="tooltip-absolute"
285
- id="arrow"
286
- data-popper-arrow
287
- >
288
- <div class="tooltip-absolute" id="arrow_face" />
289
- </div>
290
- </Popover>
302
+ <div
303
+ transition:fade|global={{
304
+ duration: instant ? 0 : duration,
305
+ }}
306
+ class="tooltip-absolute"
307
+ id="arrow"
308
+ data-popper-arrow
309
+ >
310
+ <div class="tooltip-absolute" id="arrow_face" />
311
+ </div>
312
+ </Popover>
313
+ {/key}
291
314
 
292
315
  <style global>
293
316
  div.tooltip-container {
@@ -299,6 +322,11 @@
299
322
  border-radius: 0.375rem;
300
323
  z-index: 99;
301
324
  padding: 0.25rem;
325
+ outline: 1px dashed transparent;
326
+ }
327
+ div.tooltip-container:focus {
328
+ outline: var(--focus-outline);
329
+ outline-offset: var(--focus-offset);
302
330
  }
303
331
 
304
332
  div.tooltip-container-content {
@@ -26,6 +26,13 @@
26
26
  on:change={handleChange}
27
27
  class="{$$props.class} toggle"
28
28
  {disabled}
29
+ on:keydown={(e) => {
30
+ if (e.key === "Enter") {
31
+ e.preventDefault();
32
+ value = !value;
33
+ handleChange();
34
+ }
35
+ }}
29
36
  />
30
37
  </div>
31
38
 
@@ -49,7 +56,8 @@
49
56
  }
50
57
 
51
58
  input[type="checkbox"]:focus {
52
- outline: 0;
59
+ outline: var(--focus-outline);
60
+ outline-offset: var(--focus-offset);
53
61
  }
54
62
 
55
63
  .toggle {
package/dist/theme.css CHANGED
@@ -26,6 +26,10 @@
26
26
  --accent-muted: color-mix(in srgb, var(--accent), var(--shadow) 30%);
27
27
  --accent-soft: color-mix(in srgb, var(--accent), var(--shadow) 50%);
28
28
 
29
+ --focus: #0000ff;
30
+ --focus-outline: 1px solid var(--focus);
31
+ --focus-offset: 1px;
32
+
29
33
  --error: #dc2626;
30
34
 
31
35
  --popover-background: color-mix(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intechstudio/grid-uikit",
3
- "version": "1.20260622.1304",
3
+ "version": "1.20260623.833",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/intechstudio/grid-uikit.git"
package/src/lib/theme.css CHANGED
@@ -26,6 +26,10 @@
26
26
  --accent-muted: color-mix(in srgb, var(--accent), var(--shadow) 30%);
27
27
  --accent-soft: color-mix(in srgb, var(--accent), var(--shadow) 50%);
28
28
 
29
+ --focus: #0000ff;
30
+ --focus-outline: 1px solid var(--focus);
31
+ --focus-offset: 1px;
32
+
29
33
  --error: #dc2626;
30
34
 
31
35
  --popover-background: color-mix(