|
19 | 19 | import type { MenuOption } from '$lib/types/options'; |
20 | 20 | import type { ScrollIntoViewOptions } from '$lib/actions'; |
21 | 21 |
|
| 22 | + type LogReason<T extends Event = any> = { |
| 23 | + reason: string, |
| 24 | + event?: T |
| 25 | + } |
| 26 | +
|
22 | 27 | const dispatch = createEventDispatcher<{ |
23 | 28 | change: { value: any; option: any }; |
24 | 29 | inputChange: string; |
|
58 | 63 | root?: string; |
59 | 64 | field?: string | ComponentProps<TextField>['classes']; |
60 | 65 | options?: string; |
61 | | - option?: string; |
| 66 | + option?: string | ComponentProps<MenuItem>['classes']; |
62 | 67 | selected?: string; |
63 | 68 | group?: string; |
64 | 69 | empty?: string; |
|
68 | 73 | let fieldClasses: ComponentProps<TextField>['classes']; |
69 | 74 | $: fieldClasses = typeof(classes.field) === "string" ? { root: classes.field } : classes.field; |
70 | 75 |
|
| 76 | + let optionClasses: ComponentProps<MenuItem>['classes']; |
| 77 | + $: optionClasses = typeof(classes.option) === "string" ? { root: classes.option } : classes.option; |
| 78 | +
|
71 | 79 | // Menu props |
72 | 80 | export let placement: Placement = 'bottom-start'; |
73 | 81 | export let autoPlacement = true; |
|
199 | 207 |
|
200 | 208 | searchText = e.detail.inputValue as string; |
201 | 209 | dispatch('inputChange', searchText); |
202 | | - show(); |
| 210 | + show({ reason: "onChange", event: e }); |
203 | 211 | } |
204 | 212 |
|
205 | | - function onFocus() { |
206 | | - logger.debug('onFocus'); |
207 | | - show(); |
| 213 | + function onFocus(event: FocusEvent) { |
| 214 | + show({ reason: "onFocus", event }); |
208 | 215 | } |
209 | 216 |
|
210 | 217 | function onBlur(e: FocusEvent|CustomEvent<any>) { |
|
218 | 225 | fe.relatedTarget !== menuOptionsEl?.offsetParent && // click on scroll bar |
219 | 226 | !fe.relatedTarget.closest('menu > [slot=actions]') // click on action item |
220 | 227 | ) { |
221 | | - hide('blur'); |
| 228 | + hide({ reason: 'blur', event: e }); |
222 | 229 | } else { |
223 | 230 | logger.debug('ignoring blur'); |
224 | 231 | } |
|
237 | 244 | break; |
238 | 245 |
|
239 | 246 | case 'ArrowDown': |
240 | | - show(); |
| 247 | + show({ reason: `onKeyDown: '${e.key}'`, event: e }); |
241 | 248 | if (highlightIndex < filteredOptions.length - 1) { |
242 | 249 | highlightIndex++; |
243 | 250 | } else { |
|
247 | 254 | break; |
248 | 255 |
|
249 | 256 | case 'ArrowUp': |
250 | | - show(); |
| 257 | + show({ reason: `onKeyDown: '${e.key}'`, event: e }); |
251 | 258 | if (highlightIndex > 0) { |
252 | 259 | highlightIndex--; |
253 | 260 | } else { |
|
259 | 266 | case 'Escape': |
260 | 267 | if (open) { |
261 | 268 | inputEl?.focus(); |
262 | | - hide('escape'); |
| 269 | + hide({ reason: 'escape', event: e }); |
263 | 270 | } |
264 | 271 | break; |
265 | 272 | } |
|
274 | 281 | } |
275 | 282 | } |
276 | 283 |
|
277 | | - function onClick() { |
278 | | - logger.debug('onClick'); |
279 | | - show(); |
| 284 | + function onClick(event: MouseEvent) { |
| 285 | + show({ reason: 'onClick', event }); |
280 | 286 | } |
281 | 287 |
|
282 | | - function show() { |
283 | | - logger.debug('show'); |
| 288 | + function show<T extends LogReason = any>(reason: string|T = '') { |
| 289 | + const doShow = !disabled && !readonly; |
| 290 | + logger.debug('show', { ...(typeof(reason) === "string" ? { reason } : reason), openBefore: open, openAfter: doShow }); |
284 | 291 |
|
285 | | - if (!disabled && !readonly) { |
| 292 | + if (doShow) { |
286 | 293 | if (open === false && clearSearchOnOpen) { |
287 | 294 | searchText = ''; // Show all options on open |
288 | 295 | } |
|
291 | 298 | } |
292 | 299 | } |
293 | 300 |
|
294 | | - function hide(reason = '') { |
295 | | - logger.debug('hide', { reason }); |
| 301 | + function hide<T extends LogReason = any>(reason: string|T = '') { |
| 302 | + logger.debug('hide', { ...(typeof(reason) === "string" ? { reason } : reason), openBefore: open, openAfter: false }); |
296 | 303 | open = false; |
297 | 304 | highlightIndex = -1; |
298 | 305 | } |
|
384 | 391 | on:keydown={onKeyDown} |
385 | 392 | on:keypress={onKeyPress} |
386 | 393 | actions={fieldActions} |
387 | | - classes={{ container: inlineOptions ? 'border-none shadow-none hover:shadow-none group-focus-within:shadow-none' : undefined }} |
388 | | - class={cls('h-full', theme.field, fieldClasses)} |
| 394 | + classes={{ ...(fieldClasses ?? {}), container: inlineOptions ? 'border-none shadow-none hover:shadow-none group-focus-within:shadow-none' : undefined }} |
| 395 | + class={cls('h-full', theme.field)} |
389 | 396 | role="combobox" |
390 | 397 | aria-expanded={open ? "true" : "false"} |
391 | 398 | aria-autocomplete={!inlineOptions ? "list" : undefined} |
|
417 | 424 | icon={toggleIcon} |
418 | 425 | class="text-black/50 p-1 transform {open ? 'rotate-180' : ''}" |
419 | 426 | tabindex="-1" |
420 | | - on:click={() => {logger.debug("toggleIcon clicked")}} |
| 427 | + on:click={(e) => { |
| 428 | + logger.debug("toggleIcon clicked", { event: e, open }) |
| 429 | + const func = !open ? show : hide; |
| 430 | + func({ reason: "toggleIcon", event: e }); |
| 431 | + }} |
421 | 432 | /> |
422 | 433 | {/if} |
423 | 434 | </span> |
|
434 | 445 | {disableTransition} |
435 | 446 | moveFocus={false} |
436 | 447 | bind:open |
437 | | - on:close={() => hide('menu on:close')} |
| 448 | + on:close={e => hide({ reason: 'menu on:close', event: e})} |
438 | 449 | {...menuProps} |
439 | 450 | > |
440 | 451 | <!-- TODO: Rework into hierarchy of snippets in v2.0 --> |
|
447 | 458 | <svelte:fragment slot="option" let:option let:index> |
448 | 459 | <slot name="option" {option} {index} {selected} {value} {highlightIndex}> |
449 | 460 | <MenuItem |
| 461 | + classes={optionClasses} |
450 | 462 | class={cls( |
451 | 463 | index === highlightIndex && '[:not(.group:hover)>&]:bg-black/5', |
452 | 464 | option === selected && (classes.selected || 'font-semibold'), |
453 | 465 | option.group ? 'px-4' : 'px-2', |
454 | 466 | theme.option, |
455 | 467 | classes.option |
456 | 468 | )} |
| 469 | + icon={option.icon} |
457 | 470 | scrollIntoView={{ condition: index === highlightIndex, onlyIfNeeded: inlineOptions, ...scrollIntoView }} |
458 | 471 | role="option" |
459 | 472 | aria-selected={option === selected ? "true" : "false"} |
|
485 | 498 | <svelte:fragment slot="option" let:option let:index> |
486 | 499 | <slot name="option" {option} {index} {selected} {value} {highlightIndex}> |
487 | 500 | <MenuItem |
| 501 | + classes={optionClasses} |
488 | 502 | class={cls( |
489 | 503 | index === highlightIndex && '[:not(.group:hover)>&]:bg-black/5', |
490 | 504 | option === selected && (classes.selected || 'font-semibold'), |
491 | 505 | option.group ? 'px-4' : 'px-2', |
492 | 506 | theme.option, |
493 | 507 | classes.option |
494 | 508 | )} |
| 509 | + icon={option.icon} |
495 | 510 | scrollIntoView={{ condition: index === highlightIndex, onlyIfNeeded: inlineOptions, ...scrollIntoView }} |
496 | 511 | role="option" |
497 | 512 | aria-selected={option === selected ? "true" : "false"} |
|
0 commit comments