Skip to content

Commit 869ad37

Browse files
Fix SelectField menu icons & toggle collapsing btn
Fix SelectField toggle button not collapsing.
1 parent f3bda28 commit 869ad37

File tree

6 files changed

+71
-44
lines changed

6 files changed

+71
-44
lines changed

.changeset/bright-donuts-appear.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte-ux': patch
3+
---
4+
5+
Fix SelectField MenuOption icons no longer being passed to MenuItems. Fix SelectField toggle button not collapsing. Add `element` and `iconSpan` exports to `Button` so that the elements can be directly accessed from ancestor contexts. Fix SelectField `fieldClasses` not being incorporated into the inner `TextField` properly. Update SelectField docs page with colored example that differentiates icon & input color, both in the field and the menu items.

packages/svelte-ux/src/lib/components/Button.svelte

+6-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
export let iconOnly = icon !== undefined && $$slots.default !== true;
2121
export let actions: Actions<HTMLAnchorElement | HTMLButtonElement> | undefined = undefined;
2222
23+
export let element: HTMLAnchorElement | HTMLButtonElement | undefined | null = undefined;
24+
export let iconSpan: HTMLSpanElement | undefined | null = undefined;
25+
2326
export let loading: boolean = false;
2427
export let disabled: boolean = false;
2528
export let rounded: boolean | 'full' | undefined = undefined; // default in reactive groupContext below
@@ -240,6 +243,7 @@
240243
<!-- svelte-ignore a11y-no-static-element-interactions -->
241244
<svelte:element
242245
this={href ? 'a' : 'button'}
246+
bind:this={element}
243247
{href}
244248
{target}
245249
{type}
@@ -255,11 +259,11 @@
255259
on:blur
256260
>
257261
{#if loading}
258-
<span transition:slide={{ axis: 'x', duration: 200 }}>
262+
<span bind:this={iconSpan} transition:slide={{ axis: 'x', duration: 200 }}>
259263
<ProgressCircle size={16} width={2} class={cls(theme.loading, classes.loading)} />
260264
</span>
261265
{:else if icon}
262-
<span in:slide={{ axis: 'x', duration: 200 }}>
266+
<span bind:this={iconSpan} in:slide={{ axis: 'x', duration: 200 }}>
263267
{#if typeof icon === 'string' || 'icon' in icon}
264268
<!-- font path/url/etc or font-awesome IconDefinition -->
265269
<Icon data={asIconData(icon)} class={cls('pointer-events-none', theme.icon, classes.icon)} />

packages/svelte-ux/src/lib/components/SelectField.svelte

+56-22
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
import type { MenuOption } from '$lib/types/options';
2020
import type { ScrollIntoViewOptions } from '$lib/actions';
2121
22+
type LogReason<T extends Event = any> = {
23+
reason: string,
24+
event?: T
25+
}
26+
2227
const dispatch = createEventDispatcher<{
2328
change: { value: any; option: any };
2429
inputChange: string;
@@ -51,14 +56,16 @@
5156
: undefined;
5257
5358
let originalIcon = icon;
59+
let toggleButtonElement: ComponentProps<Button>['element'] = undefined;
60+
let toggleButtonIconSpan: ComponentProps<Button>['iconElement'] = undefined;
5461
5562
export let scrollIntoView: Partial<ScrollIntoViewOptions> = {};
5663
5764
export let classes: {
5865
root?: string;
5966
field?: string | ComponentProps<TextField>['classes'];
6067
options?: string;
61-
option?: string;
68+
option?: string | ComponentProps<MenuItem>['classes'];
6269
selected?: string;
6370
group?: string;
6471
empty?: string;
@@ -68,6 +75,9 @@
6875
let fieldClasses: ComponentProps<TextField>['classes'];
6976
$: fieldClasses = typeof(classes.field) === "string" ? { root: classes.field } : classes.field;
7077
78+
let optionClasses: ComponentProps<MenuItem>['classes'];
79+
$: optionClasses = typeof(classes.option) === "string" ? { root: classes.option } : classes.option;
80+
7181
// Menu props
7282
export let placement: Placement = 'bottom-start';
7383
export let autoPlacement = true;
@@ -194,17 +204,27 @@
194204
});
195205
}
196206
207+
function isToggleButtonClicked(ev: MouseEvent) {
208+
return toggleButtonIconSpan && toggleButtonIconSpan === ev.target;
209+
}
210+
211+
function isToggleButtonRelated(ev: MouseEvent|FocusEvent) {
212+
return toggleButtonElement && toggleButtonElement === ev.relatedTarget;
213+
}
214+
197215
function onChange(e: ComponentEvents<TextField>['change']) {
198216
logger.debug('onChange');
199217
200218
searchText = e.detail.inputValue as string;
201219
dispatch('inputChange', searchText);
202-
show();
220+
show({ reason: "onChange", event: e });
203221
}
204222
205-
function onFocus() {
206-
logger.debug('onFocus');
207-
show();
223+
function onFocus(event: FocusEvent) {
224+
if (isToggleButtonRelated(event)) {
225+
return;
226+
}
227+
show({ reason: "onFocus", event });
208228
}
209229
210230
function onBlur(e: FocusEvent|CustomEvent<any>) {
@@ -216,9 +236,10 @@
216236
fe.relatedTarget instanceof HTMLElement &&
217237
!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"
218238
fe.relatedTarget !== menuOptionsEl?.offsetParent && // click on scroll bar
219-
!fe.relatedTarget.closest('menu > [slot=actions]') // click on action item
239+
!fe.relatedTarget.closest('menu > [slot=actions]') && // click on action item
240+
!isToggleButtonRelated(fe) // click on toggle button
220241
) {
221-
hide('blur');
242+
hide({ reason: 'blur', event: e });
222243
} else {
223244
logger.debug('ignoring blur');
224245
}
@@ -237,7 +258,7 @@
237258
break;
238259
239260
case 'ArrowDown':
240-
show();
261+
show({ reason: `onKeyDown: '${e.key}'`, event: e });
241262
if (highlightIndex < filteredOptions.length - 1) {
242263
highlightIndex++;
243264
} else {
@@ -247,7 +268,7 @@
247268
break;
248269
249270
case 'ArrowUp':
250-
show();
271+
show({ reason: `onKeyDown: '${e.key}'`, event: e });
251272
if (highlightIndex > 0) {
252273
highlightIndex--;
253274
} else {
@@ -259,7 +280,7 @@
259280
case 'Escape':
260281
if (open) {
261282
inputEl?.focus();
262-
hide('escape');
283+
hide({ reason: 'escape', event: e });
263284
}
264285
break;
265286
}
@@ -274,15 +295,18 @@
274295
}
275296
}
276297
277-
function onClick() {
278-
logger.debug('onClick');
279-
show();
298+
function onClick(event: MouseEvent) {
299+
if (isToggleButtonClicked(event) || isToggleButtonRelated(event)) {
300+
return;
301+
}
302+
show({ reason: 'onClick', event });
280303
}
281304
282-
function show() {
283-
logger.debug('show');
305+
function show<T extends LogReason = any>(reason: string|T = '') {
306+
const doShow = !disabled && !readonly;
307+
logger.debug('show', { ...(typeof(reason) === "string" ? { reason } : reason), openBefore: open, openAfter: doShow });
284308
285-
if (!disabled && !readonly) {
309+
if (doShow) {
286310
if (open === false && clearSearchOnOpen) {
287311
searchText = ''; // Show all options on open
288312
}
@@ -291,8 +315,8 @@
291315
}
292316
}
293317
294-
function hide(reason = '') {
295-
logger.debug('hide', { reason });
318+
function hide<T extends LogReason = any>(reason: string|T = '') {
319+
logger.debug('hide', { ...(typeof(reason) === "string" ? { reason } : reason), openBefore: open, openAfter: false });
296320
open = false;
297321
highlightIndex = -1;
298322
}
@@ -384,8 +408,8 @@
384408
on:keydown={onKeyDown}
385409
on:keypress={onKeyPress}
386410
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)}
411+
classes={{ ...(fieldClasses ?? {}), container: inlineOptions ? 'border-none shadow-none hover:shadow-none group-focus-within:shadow-none' : undefined }}
412+
class={cls('h-full', theme.field)}
389413
role="combobox"
390414
aria-expanded={open ? "true" : "false"}
391415
aria-autocomplete={!inlineOptions ? "list" : undefined}
@@ -417,7 +441,13 @@
417441
icon={toggleIcon}
418442
class="text-black/50 p-1 transform {open ? 'rotate-180' : ''}"
419443
tabindex="-1"
420-
on:click={() => {logger.debug("toggleIcon clicked")}}
444+
bind:element={toggleButtonElement}
445+
bind:iconElement={toggleButtonIconSpan}
446+
on:click={(e) => {
447+
logger.debug("toggleIcon clicked", { event: e, open })
448+
const func = !open ? show : hide;
449+
func({ reason: "toggleIcon", event: e });
450+
}}
421451
/>
422452
{/if}
423453
</span>
@@ -434,7 +464,7 @@
434464
{disableTransition}
435465
moveFocus={false}
436466
bind:open
437-
on:close={() => hide('menu on:close')}
467+
on:close={e => hide({ reason: 'menu on:close', event: e})}
438468
{...menuProps}
439469
>
440470
<!-- TODO: Rework into hierarchy of snippets in v2.0 -->
@@ -447,13 +477,15 @@
447477
<svelte:fragment slot="option" let:option let:index>
448478
<slot name="option" {option} {index} {selected} {value} {highlightIndex}>
449479
<MenuItem
480+
classes={optionClasses}
450481
class={cls(
451482
index === highlightIndex && '[:not(.group:hover)>&]:bg-black/5',
452483
option === selected && (classes.selected || 'font-semibold'),
453484
option.group ? 'px-4' : 'px-2',
454485
theme.option,
455486
classes.option
456487
)}
488+
icon={option.icon}
457489
scrollIntoView={{ condition: index === highlightIndex, onlyIfNeeded: inlineOptions, ...scrollIntoView }}
458490
role="option"
459491
aria-selected={option === selected ? "true" : "false"}
@@ -485,13 +517,15 @@
485517
<svelte:fragment slot="option" let:option let:index>
486518
<slot name="option" {option} {index} {selected} {value} {highlightIndex}>
487519
<MenuItem
520+
classes={optionClasses}
488521
class={cls(
489522
index === highlightIndex && '[:not(.group:hover)>&]:bg-black/5',
490523
option === selected && (classes.selected || 'font-semibold'),
491524
option.group ? 'px-4' : 'px-2',
492525
theme.option,
493526
classes.option
494527
)}
528+
icon={option.icon}
495529
scrollIntoView={{ condition: index === highlightIndex, onlyIfNeeded: inlineOptions, ...scrollIntoView }}
496530
role="option"
497531
aria-selected={option === selected ? "true" : "false"}

packages/svelte-ux/src/lib/components/TextField.svelte

+2-1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
error?: string;
6666
prepend?: string;
6767
append?: string;
68+
icon?: string;
6869
} = {};
6970
const theme = getComponentTheme('TextField');
7071
@@ -248,7 +249,7 @@
248249
<slot name="prepend" />
249250
{#if icon}
250251
<span class="mr-3">
251-
<Icon data={asIconData(icon)} class="text-black/50" />
252+
<Icon data={asIconData(icon)} class={cls("text-black/50", classes.icon)} />
252253
</span>
253254
{/if}
254255
</div>

packages/svelte-ux/src/lib/components/_SelectListOptions.svelte

-3
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@
2424
2525
export let classes: {
2626
root?: string;
27-
option?: string;
28-
selected?: string;
2927
group?: string;
30-
empty?: string;
3128
} = {};
3229
3330
const theme = getComponentTheme('SelectField');

packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte

+2-16
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
1414
import { delay } from '$lib/utils/promise';
1515
import { cls } from '$lib/utils/styles';
16-
import Icon from '$lib/components/Icon.svelte';
1716
import type { MenuOption } from '$lib/types/options';
1817
1918
let options: MenuOption[] = [
@@ -176,22 +175,9 @@
176175
{options}
177176
bind:value
178177
activeOptionIcon={true}
178+
classes={{ field: { icon: 'text-pink-300', input: 'text-blue-600' }, option: { root: 'text-blue-600', icon: 'text-pink-300' } }}
179179
on:change={(e) => console.log('on:change', e.detail)}
180-
>
181-
<div slot="option" let:option let:index let:selected let:highlightIndex>
182-
<MenuItem
183-
class={cls(
184-
index === highlightIndex && 'bg-black/5',
185-
option === selected && 'font-semibold',
186-
option.group ? 'px-4' : 'px-2'
187-
)}
188-
scrollIntoView={index === highlightIndex}
189-
icon={{ data: option.icon, style: 'color: #0000FF;' }}
190-
>
191-
{option.label}
192-
</MenuItem>
193-
</div>
194-
</SelectField>
180+
/>
195181
</Preview>
196182

197183
<h2>option with action</h2>

0 commit comments

Comments
 (0)