Skip to content

Commit 1bd8889

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

File tree

6 files changed

+75
-45
lines changed

6 files changed

+75
-45
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

+60-23
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,6 +56,8 @@
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
@@ -59,14 +66,20 @@
5966
field?: string | ComponentProps<TextField>['classes'];
6067
options?: string;
6168
option?: string;
69+
optionIcon?: string;
70+
optionLoading?: string;
6271
selected?: string;
6372
group?: string;
6473
empty?: string;
6574
} = {};
6675
const theme = getComponentTheme('SelectField');
6776
68-
let fieldClasses: ComponentProps<TextField>['classes'];
69-
$: fieldClasses = typeof(classes.field) === "string" ? { root: classes.field } : classes.field;
77+
let fieldClasses: Partial<ComponentProps<TextField>['classes']>;
78+
$: {
79+
fieldClasses = (typeof(classes.field) === "string" || classes.field == null) ? { root: classes.field } : classes.field;
80+
if (activeOptionIcon)
81+
console.log(fieldClasses);
82+
}
7083
7184
// Menu props
7285
export let placement: Placement = 'bottom-start';
@@ -194,17 +207,27 @@
194207
});
195208
}
196209
210+
function isToggleButtonClicked(ev: MouseEvent) {
211+
return toggleButtonIconSpan && toggleButtonIconSpan === ev.target;
212+
}
213+
214+
function isToggleButtonRelated(ev: MouseEvent|FocusEvent) {
215+
return toggleButtonElement && toggleButtonElement === ev.relatedTarget;
216+
}
217+
197218
function onChange(e: ComponentEvents<TextField>['change']) {
198219
logger.debug('onChange');
199220
200221
searchText = e.detail.inputValue as string;
201222
dispatch('inputChange', searchText);
202-
show();
223+
show({ reason: "onChange", event: e });
203224
}
204225
205-
function onFocus() {
206-
logger.debug('onFocus');
207-
show();
226+
function onFocus(event: FocusEvent) {
227+
if (isToggleButtonRelated(event)) {
228+
return;
229+
}
230+
show({ reason: "onFocus", event });
208231
}
209232
210233
function onBlur(e: FocusEvent|CustomEvent<any>) {
@@ -216,9 +239,10 @@
216239
fe.relatedTarget instanceof HTMLElement &&
217240
!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"
218241
fe.relatedTarget !== menuOptionsEl?.offsetParent && // click on scroll bar
219-
!fe.relatedTarget.closest('menu > [slot=actions]') // click on action item
242+
!fe.relatedTarget.closest('menu > [slot=actions]') && // click on action item
243+
!isToggleButtonRelated(fe) // click on toggle button
220244
) {
221-
hide('blur');
245+
hide({ reason: 'blur', event: e });
222246
} else {
223247
logger.debug('ignoring blur');
224248
}
@@ -237,7 +261,7 @@
237261
break;
238262
239263
case 'ArrowDown':
240-
show();
264+
show({ reason: `onKeyDown: '${e.key}'`, event: e });
241265
if (highlightIndex < filteredOptions.length - 1) {
242266
highlightIndex++;
243267
} else {
@@ -247,7 +271,7 @@
247271
break;
248272
249273
case 'ArrowUp':
250-
show();
274+
show({ reason: `onKeyDown: '${e.key}'`, event: e });
251275
if (highlightIndex > 0) {
252276
highlightIndex--;
253277
} else {
@@ -259,7 +283,7 @@
259283
case 'Escape':
260284
if (open) {
261285
inputEl?.focus();
262-
hide('escape');
286+
hide({ reason: 'escape', event: e });
263287
}
264288
break;
265289
}
@@ -274,15 +298,18 @@
274298
}
275299
}
276300
277-
function onClick() {
278-
logger.debug('onClick');
279-
show();
301+
function onClick(event: MouseEvent) {
302+
if (isToggleButtonClicked(event) || isToggleButtonRelated(event)) {
303+
return;
304+
}
305+
show({ reason: 'onClick', event });
280306
}
281307
282-
function show() {
283-
logger.debug('show');
308+
function show<T extends LogReason = any>(reason: string|T = '') {
309+
const doShow = !disabled && !readonly;
310+
logger.debug('show', { ...(typeof(reason) === "string" ? { reason } : reason), openBefore: open, openAfter: doShow });
284311
285-
if (!disabled && !readonly) {
312+
if (doShow) {
286313
if (open === false && clearSearchOnOpen) {
287314
searchText = ''; // Show all options on open
288315
}
@@ -291,8 +318,8 @@
291318
}
292319
}
293320
294-
function hide(reason = '') {
295-
logger.debug('hide', { reason });
321+
function hide<T extends LogReason = any>(reason: string|T = '') {
322+
logger.debug('hide', { ...(typeof(reason) === "string" ? { reason } : reason), openBefore: open, openAfter: false });
296323
open = false;
297324
highlightIndex = -1;
298325
}
@@ -384,8 +411,8 @@
384411
on:keydown={onKeyDown}
385412
on:keypress={onKeyPress}
386413
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)}
414+
classes={{ ...(fieldClasses ?? {}), container: inlineOptions ? 'border-none shadow-none hover:shadow-none group-focus-within:shadow-none' : undefined }}
415+
class={cls('h-full', theme.field)}
389416
role="combobox"
390417
aria-expanded={open ? "true" : "false"}
391418
aria-autocomplete={!inlineOptions ? "list" : undefined}
@@ -417,7 +444,13 @@
417444
icon={toggleIcon}
418445
class="text-black/50 p-1 transform {open ? 'rotate-180' : ''}"
419446
tabindex="-1"
420-
on:click={() => {logger.debug("toggleIcon clicked")}}
447+
bind:element={toggleButtonElement}
448+
bind:iconElement={toggleButtonIconSpan}
449+
on:click={(e) => {
450+
logger.debug("toggleIcon clicked", { event: e, open })
451+
const func = !open ? show : hide;
452+
func({ reason: "toggleIcon", event: e });
453+
}}
421454
/>
422455
{/if}
423456
</span>
@@ -434,7 +467,7 @@
434467
{disableTransition}
435468
moveFocus={false}
436469
bind:open
437-
on:close={() => hide('menu on:close')}
470+
on:close={e => hide({ reason: 'menu on:close', event: e})}
438471
{...menuProps}
439472
>
440473
<!-- TODO: Rework into hierarchy of snippets in v2.0 -->
@@ -447,13 +480,15 @@
447480
<svelte:fragment slot="option" let:option let:index>
448481
<slot name="option" {option} {index} {selected} {value} {highlightIndex}>
449482
<MenuItem
483+
classes={{ icon: classes.optionIcon, loading: classes.optionLoading }}
450484
class={cls(
451485
index === highlightIndex && '[:not(.group:hover)>&]:bg-black/5',
452486
option === selected && (classes.selected || 'font-semibold'),
453487
option.group ? 'px-4' : 'px-2',
454488
theme.option,
455489
classes.option
456490
)}
491+
icon={option.icon}
457492
scrollIntoView={{ condition: index === highlightIndex, onlyIfNeeded: inlineOptions, ...scrollIntoView }}
458493
role="option"
459494
aria-selected={option === selected ? "true" : "false"}
@@ -485,13 +520,15 @@
485520
<svelte:fragment slot="option" let:option let:index>
486521
<slot name="option" {option} {index} {selected} {value} {highlightIndex}>
487522
<MenuItem
523+
classes={{ icon: classes.optionIcon, loading: classes.optionLoading }}
488524
class={cls(
489525
index === highlightIndex && '[:not(.group:hover)>&]:bg-black/5',
490526
option === selected && (classes.selected || 'font-semibold'),
491527
option.group ? 'px-4' : 'px-2',
492528
theme.option,
493529
classes.option
494530
)}
531+
icon={option.icon}
495532
scrollIntoView={{ condition: index === highlightIndex, onlyIfNeeded: inlineOptions, ...scrollIntoView }}
496533
role="option"
497534
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: 'text-blue-600', optionIcon: '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)