|
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; |
|
51 | 56 | : undefined; |
52 | 57 |
|
53 | 58 | let originalIcon = icon; |
| 59 | + let toggleButtonElement: ComponentProps<Button>['element'] = undefined; |
| 60 | + let toggleButtonIconSpan: ComponentProps<Button>['iconElement'] = undefined; |
54 | 61 |
|
55 | 62 | export let scrollIntoView: Partial<ScrollIntoViewOptions> = {}; |
56 | 63 |
|
|
194 | 201 | }); |
195 | 202 | } |
196 | 203 |
|
| 204 | + function isToggleButtonClicked(ev: MouseEvent) { |
| 205 | + return toggleButtonIconSpan && toggleButtonIconSpan === ev.target; |
| 206 | + } |
| 207 | +
|
| 208 | + function isToggleButtonRelated(ev: MouseEvent|FocusEvent) { |
| 209 | + return toggleButtonElement && toggleButtonElement === ev.relatedTarget; |
| 210 | + } |
| 211 | +
|
197 | 212 | function onChange(e: ComponentEvents<TextField>['change']) { |
198 | 213 | logger.debug('onChange'); |
199 | 214 |
|
200 | 215 | searchText = e.detail.inputValue as string; |
201 | 216 | dispatch('inputChange', searchText); |
202 | | - show(); |
| 217 | + show({ reason: "onChange", event: e }); |
203 | 218 | } |
204 | 219 |
|
205 | | - function onFocus() { |
206 | | - logger.debug('onFocus'); |
207 | | - show(); |
| 220 | + function onFocus(event: FocusEvent) { |
| 221 | + if (isToggleButtonRelated(event)) { |
| 222 | + return; |
| 223 | + } |
| 224 | + show({ reason: "onFocus", event }); |
208 | 225 | } |
209 | 226 |
|
210 | 227 | function onBlur(e: FocusEvent|CustomEvent<any>) { |
|
216 | 233 | fe.relatedTarget instanceof HTMLElement && |
217 | 234 | !menuOptionsEl?.contains(fe.relatedTarget) && // TODO: Oddly Safari does not set `relatedTarget` to the clicked on menu option (like Chrome and Firefox) but instead appears to take `tabindex` into consideration. Currently resolves to `.options` after setting `tabindex="-1" |
218 | 235 | fe.relatedTarget !== menuOptionsEl?.offsetParent && // click on scroll bar |
219 | | - !fe.relatedTarget.closest('menu > [slot=actions]') // click on action item |
| 236 | + !fe.relatedTarget.closest('menu > [slot=actions]') && // click on action item |
| 237 | + !isToggleButtonRelated(fe) // click on toggle button |
220 | 238 | ) { |
221 | | - hide('blur'); |
| 239 | + hide({ reason: 'blur', event: e }); |
222 | 240 | } else { |
223 | 241 | logger.debug('ignoring blur'); |
224 | 242 | } |
|
237 | 255 | break; |
238 | 256 |
|
239 | 257 | case 'ArrowDown': |
240 | | - show(); |
| 258 | + show({ reason: `onKeyDown: '${e.key}'`, event: e }); |
241 | 259 | if (highlightIndex < filteredOptions.length - 1) { |
242 | 260 | highlightIndex++; |
243 | 261 | } else { |
|
247 | 265 | break; |
248 | 266 |
|
249 | 267 | case 'ArrowUp': |
250 | | - show(); |
| 268 | + show({ reason: `onKeyDown: '${e.key}'`, event: e }); |
251 | 269 | if (highlightIndex > 0) { |
252 | 270 | highlightIndex--; |
253 | 271 | } else { |
|
259 | 277 | case 'Escape': |
260 | 278 | if (open) { |
261 | 279 | inputEl?.focus(); |
262 | | - hide('escape'); |
| 280 | + hide({ reason: 'escape', event: e }); |
263 | 281 | } |
264 | 282 | break; |
265 | 283 | } |
|
274 | 292 | } |
275 | 293 | } |
276 | 294 |
|
277 | | - function onClick() { |
278 | | - logger.debug('onClick'); |
279 | | - show(); |
| 295 | + function onClick(event: MouseEvent) { |
| 296 | + if (isToggleButtonClicked(event) || isToggleButtonRelated(event)) { |
| 297 | + return; |
| 298 | + } |
| 299 | + show({ reason: 'onClick', event }); |
280 | 300 | } |
281 | 301 |
|
282 | | - function show() { |
283 | | - logger.debug('show'); |
| 302 | + function show<T extends LogReason = any>(reason: string|T = '') { |
| 303 | + const doShow = !disabled && !readonly; |
| 304 | + logger.debug('show', { ...(typeof(reason) === "string" ? { reason } : reason), openBefore: open, openAfter: doShow }); |
284 | 305 |
|
285 | | - if (!disabled && !readonly) { |
| 306 | + if (doShow) { |
286 | 307 | if (open === false && clearSearchOnOpen) { |
287 | 308 | searchText = ''; // Show all options on open |
288 | 309 | } |
|
291 | 312 | } |
292 | 313 | } |
293 | 314 |
|
294 | | - function hide(reason = '') { |
295 | | - logger.debug('hide', { reason }); |
| 315 | + function hide<T extends LogReason = any>(reason: string|T = '') { |
| 316 | + logger.debug('hide', { ...(typeof(reason) === "string" ? { reason } : reason), openBefore: open, openAfter: false }); |
296 | 317 | open = false; |
297 | 318 | highlightIndex = -1; |
298 | 319 | } |
|
417 | 438 | icon={toggleIcon} |
418 | 439 | class="text-black/50 p-1 transform {open ? 'rotate-180' : ''}" |
419 | 440 | tabindex="-1" |
420 | | - on:click={() => {logger.debug("toggleIcon clicked")}} |
| 441 | + bind:element={toggleButtonElement} |
| 442 | + bind:iconElement={toggleButtonIconSpan} |
| 443 | + on:click={(e) => { |
| 444 | + logger.debug("toggleIcon clicked", { event: e, open }) |
| 445 | + const func = !open ? show : hide; |
| 446 | + func({ reason: "toggleIcon", event: e }); |
| 447 | + }} |
421 | 448 | /> |
422 | 449 | {/if} |
423 | 450 | </span> |
|
434 | 461 | {disableTransition} |
435 | 462 | moveFocus={false} |
436 | 463 | bind:open |
437 | | - on:close={() => hide('menu on:close')} |
| 464 | + on:close={e => hide({ reason: 'menu on:close', event: e})} |
438 | 465 | {...menuProps} |
439 | 466 | > |
440 | 467 | <!-- TODO: Rework into hierarchy of snippets in v2.0 --> |
|
492 | 519 | theme.option, |
493 | 520 | classes.option |
494 | 521 | )} |
| 522 | + icon={option.icon} |
495 | 523 | scrollIntoView={{ condition: index === highlightIndex, onlyIfNeeded: inlineOptions, ...scrollIntoView }} |
496 | 524 | role="option" |
497 | 525 | aria-selected={option === selected ? "true" : "false"} |
|
0 commit comments