Skip to content

Commit

Permalink
WIP something.
Browse files Browse the repository at this point in the history
  • Loading branch information
willnationsdev committed Dec 10, 2023
1 parent f48b6f0 commit 19f76c2
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 86 deletions.
2 changes: 1 addition & 1 deletion packages/svelte-ux/src/lib/components/QuickSearch.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
icon={mdiMagnify}
placeholder="Search..."
hideToggleIcon={true}
optionsMode="list"
inlineOptions={true}
{options}
{fieldActions}
on:change
Expand Down
168 changes: 86 additions & 82 deletions packages/svelte-ux/src/lib/components/SelectField.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import { createEventDispatcher, type ComponentProps, type ComponentEvents } from 'svelte';
import type { Placement } from '@floating-ui/dom';
//import { destyleButtonClass } from '../utils/styles';
import { mdiChevronDown, mdiClose } from '@mdi/js';
Expand All @@ -12,9 +13,9 @@
import ProgressCircle from './ProgressCircle.svelte';
import Menu from './Menu.svelte';
import MenuItem from './MenuItem.svelte';
import SelectListOptions from './_SelectListOptions.svelte';
import TextField from './TextField.svelte';
import { getComponentTheme } from './theme';
import Maybe from './Maybe.svelte';
import type { IconInput } from '$lib/utils/icons';
const dispatch = createEventDispatcher<{
Expand Down Expand Up @@ -346,17 +347,13 @@
logger.info('clear');
selectOption(null);
filteredOptions = options;
//inputEl?.focus();
}
</script>

<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
role="button"
{tabindex}
aria-pressed={open ? "true" : "false"}
<button
aria-haspopup={!inlineOptions ? "listbox" : undefined}
class={cls('SelectField', theme.root, classes.root, $$props.class)}
class={cls('SelectField block w-full cursor-default', theme.root, classes.root, $$props.class)}
on:click={onClick}>

<TextField
Expand All @@ -375,6 +372,7 @@
on:keydown={onKeyDown}
on:keypress={onKeyPress}
actions={fieldActions}
classes={{ container: inlineOptions ? 'border-none shadow-none hover:shadow-none group-focus-within:shadow-none' : undefined }}
class={cls('h-full', theme.field, fieldClasses)}
role="combobox"
aria-expanded={open ? "true" : "false"}
Expand All @@ -398,6 +396,7 @@
class="text-black/50 p-1"
on:click={(e) => {
e.stopPropagation();
logger.debug("closeIcon clicked");
clear();
}}
/>
Expand All @@ -406,92 +405,97 @@
icon={toggleIcon}
class="text-black/50 p-1 transform {open ? 'rotate-180' : ''}"
tabindex="-1"
on:click={() => {logger.debug("toggleIcon clicked")}}
/>
{/if}
</span>
</TextField>

<!-- Improve initial open display, still needs work when switching from No options found (options.length === 0) -->
{#if options?.length > 0 || loading !== true}
<Maybe this={!inlineOptions ? Menu : undefined}
{placement}
{autoPlacement}
{matchWidth}
{resize}
{disableTransition}
moveFocus={false}
bind:open
on:close={() => hide('menu on:close')}
{...menuProps}
>
<div
role="listbox"
tabindex="-1"
aria-expanded={open ? "true" : "false"}
class={cls('options group p-1 focus:outline-none', theme.options, classes.options)}
class:opacity-50={loading}
bind:this={menuOptionsEl}
on:click|stopPropagation={(e) => {
logger.debug('options container clicked');

if (e.target instanceof HTMLElement) {
// Find slot parent of click target option, fallback to `e.target` if slot is not overridden
// Use `.options > ` in case slot is nested (ex. GraphQLSelect with slot)
const slotEl = e.target.closest('.options > [slot=option]') ?? e.target;
// Find the index of the clicked on element (ignoring group headers)
const optionIndex = slotEl
? [...menuOptionsEl.children]
.filter((el) => !el.classList.contains('group-header'))
.indexOf(slotEl)
: -1;
logger.debug({ slotEl, optionIndex });
// ignore clicks on group options
if (optionIndex !== -1) {
selectIndex(optionIndex);
}
}
}}
>
{#each filteredOptions ?? [] as option, index (optionValue(option))}
{@const previousOption = filteredOptions[index - 1]}
{#if option.group && option.group !== previousOption?.group}
<div
class={cls(
'group-header text-xs leading-8 tracking-widest text-black/50 px-2',
theme.group,
classes.group
)}
>
{option.group}
{#if !inlineOptions}
<Menu
{placement}
{autoPlacement}
{matchWidth}
{resize}
{disableTransition}
moveFocus={false}
bind:open
on:close={() => hide('menu on:close')}
{...menuProps}
>
<!-- TODO: Rework into hierarchy of snippets in v2.0 -->
<SelectListOptions
bind:menuOptionsEl
{open} {loading} {highlightIndex} {searchText} {filteredOptions}
classes={{...classes, root: classes.options}}
{optionText} {optionValue} {selectIndex} {selectOption} {onKeyPress} {onKeyDown}>

<svelte:fragment slot="option" let:option let:index>
<slot name="option" {option} {index} {selected} {value} {highlightIndex}>
<MenuItem
class={cls(
index === highlightIndex && '[:not(.group:hover)>&]:bg-black/5',
option === selected && (classes.selected || 'font-semibold'),
option.group ? 'px-4' : 'px-2',
theme.option,
classes.option
)}
scrollIntoView={index === highlightIndex}
role="option"
aria-selected={option === selected ? "true" : "false"}
aria-disabled={option?.disabled ? "true" : "false"}
>
{optionText(option)}
</MenuItem>
</slot>
</svelte:fragment>

<slot name="empty" slot="empty" let:loading>
<div class={cls('p-3 text-black/50 italic text-sm', theme.empty, classes.empty)}>
{loading ? 'Loading...' : 'No options found'}
</div>
{/if}

<slot name="option" {option} {index} {selected} {value} {highlightIndex}>
<MenuItem
class={cls(
index === highlightIndex && '[:not(.group:hover)>&]:bg-black/5',
option === selected && (classes.selected || 'font-semibold'),
option.group ? 'px-4' : 'px-2',
theme.option,
classes.option
)}
scrollIntoView={index === highlightIndex}
role="option"
aria-selected={option === selected ? "true" : "false"}
aria-disabled={option?.disabled ? "true" : "false"}
>
{optionText(option)}
</MenuItem>
</slot>
{:else}
<slot name="empty" {loading} {searchText}>
</SelectListOptions>

<slot name="actions" {hide} />
</Menu>
{:else}
<!-- TODO: Rework into hierarchy of snippets in v2.0. -->
<!-- This code must be identical to the above block -->
<SelectListOptions
bind:menuOptionsEl
{open} {loading} {highlightIndex} {searchText} {filteredOptions}
classes={{...classes, root: classes.options}}
{optionText} {optionValue} {selectIndex} {selectOption} {onKeyPress} {onKeyDown}>

<svelte:fragment slot="option" let:option let:index>
<slot name="option" {option} {index} {selected} {value} {highlightIndex}>
<MenuItem
class={cls(
index === highlightIndex && '[:not(.group:hover)>&]:bg-black/5',
option === selected && (classes.selected || 'font-semibold'),
option.group ? 'px-4' : 'px-2',
theme.option,
classes.option
)}
scrollIntoView={index === highlightIndex}
role="option"
aria-selected={option === selected ? "true" : "false"}
aria-disabled={option?.disabled ? "true" : "false"}
>
{optionText(option)}
</MenuItem>
</slot>
</svelte:fragment>

<slot name="empty" slot="empty" let:loading>
<div class={cls('p-3 text-black/50 italic text-sm', theme.empty, classes.empty)}>
{loading ? 'Loading...' : 'No options found'}
</div>
</slot>
{/each}
</div>
<slot name="actions" {hide} />
</Maybe>
</SelectListOptions>
{/if}
{/if}
</div>
</button>
87 changes: 87 additions & 0 deletions packages/svelte-ux/src/lib/components/_SelectListOptions.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<script lang='ts'>
import MenuItem from './MenuItem.svelte';
import Logger from '../utils/logger';
import { getComponentTheme } from './theme';
import { cls } from '../utils/styles';
const logger = new Logger('SelectListOptions');
export let optionText: (option: any) => string;
export let optionValue: (option: any) => any;
export let selectIndex: (index: number) => any;
export let selectOption: (x: any) => any;
export let onKeyDown: (x: KeyboardEvent) => void;
export let onKeyPress: (x: KeyboardEvent) => void;
export let open: boolean;
export let loading: boolean;
export let filteredOptions: any[];
export let value: any = undefined;
export let selected: any = undefined;
export let highlightIndex: number;
export let searchText: string;
export let classes: {
root?: string;
option?: string;
selected?: string;
group?: string;
empty?: string;
} = {};
const theme = getComponentTheme('SelectField');
export let menuOptionsEl: HTMLDivElement;
</script>

<div
role='listbox'
tabindex='-1'
aria-expanded={open ? 'true' : 'false'}
class={cls('_SelectListOptions options group p-1 focus:outline-none', theme.options, classes.root)}
class:opacity-50={loading}
bind:this={menuOptionsEl}
on:click|stopPropagation={(e) => {
logger.debug('options container clicked');

if (e.target instanceof HTMLElement) {
// Find slot parent of click target option, fallback to `e.target` if slot is not overridden
// Use `.options > ` in case slot is nested (ex. GraphQLSelect with slot)
const slotEl = e.target.closest('.options > [slot=option]') ?? e.target;
// Find the index of the clicked on element (ignoring group headers)
const optionIndex = slotEl
? [...menuOptionsEl.children]
.filter((el) => !el.classList.contains('group-header'))
.indexOf(slotEl)
: -1;
logger.debug({ slotEl, optionIndex });
// ignore clicks on group options
if (optionIndex !== -1) {
selectIndex(optionIndex);
}
}
}}
on:keydown={onKeyDown}
on:keypress={onKeyPress}
>
{#each filteredOptions ?? [] as option, index (optionValue(option))}
{@const previousOption = filteredOptions[index - 1]}
{#if option.group && option.group !== previousOption?.group}
<div
class={cls(
'group-header text-xs leading-8 tracking-widest text-black/50 px-2',
theme.group,
classes.group
)}
>
{option.group}
</div>
{/if}

<slot name="option" {option} {index}></slot>
{:else}
<slot name="empty" {loading} {searchText}></slot>
{/each}
</div>
10 changes: 7 additions & 3 deletions packages/svelte-ux/src/lib/utils/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ export function objectToString(styleObj: { [key: string]: string }) {
* Wrapper around `tailwind-merge` and `clsx`
*/
const twMerge = extendTailwindMerge({
classGroups: {
shadow: ['shadow-border-l', 'shadow-border-r', 'shadow-border-t', 'shadow-border-b'],
},
extend: {
classGroups: {
shadow: ['shadow-border-l', 'shadow-border-r', 'shadow-border-t', 'shadow-border-b'],
},
}
});

export const cls = (...inputs: ClassValue[]) => twMerge(clsx(...inputs));

//export const destyleButtonClass = 'bg-none bg-inherit text-inherit border-none border-0 p-0 cursor-pointer outline-inherit';

0 comments on commit 19f76c2

Please sign in to comment.