CaptainCodeman / svelte-headlessui

HeadlessUI components for Svelte
https://captaincodeman.github.io/svelte-headlessui/
MIT License
560 stars 26 forks source link

Mobile Tabs #97

Open quangdaon opened 3 months ago

quangdaon commented 3 months ago

Is there a recommended approach to dynamically switching the selector style across multiple breakpoints? Specifically, I'd like to use the "Tabs" component while in desktop but switch to a "Listbox" on mobile.

I originally used both createListbox and createTabs to manage separate components, and used a writable store to try to synchronize the selected value, but that didn't work. I also tried to keep the selected value synchronized through event listeners, which worked for updating the listbox when the tabs is changed, but not for the reverse. What ultimately ended up working was using a single createListbox to control both elements and just styling the desktop view to look like tabs, but this seems hacky.

<script lang="ts">
    import { createEventDispatcher } from 'svelte';
    import { createListbox } from 'svelte-headlessui';

    const dispatch = createEventDispatcher();

    const tabOptions: Record<string, string> = {
        option1: 'Option 1',
        option2: 'Option 2',
        option3: 'Option 3'
    };

    const keys = Object.keys(tabOptions);

    const listbox = createListbox({ label: 'Post Type', selected: keys[0] });

    const handleListboxChange = (e: Event) => {
        dispatch('changed', (e as CustomEvent).detail.selected);
    };

    const select = (value: string) => {
        listbox.set({ selected: value });
    };
</script>

<div class="hidden md:flex w-full flex-col">
    <div
        use:listbox.button
        class="flex w-dull rounded-md bg-white divide-x divide-gray-200 overflow-clip"
    >
        {#each keys as value}
            {@const active = $listbox.active === value}
            {@const selected = $listbox.selected === value}
            <button
                use:listbox.item={{ value }}
                on:click={() => select(value)}
                class="w-full font-medium m-0 focus:outline-none"
            >
                <span
                    class="block py-4 text-sm border-x-0 border-b-4 border-transparent {selected
                        ? 'border-b-blue-500 font-bold'
                        : active
                            ? ''
                            : 'hover:border-b-gray-200 hover:bg-gray-50'}"
                >
                    {tabOptions[value]}
                </span>
            </button>
        {/each}
    </div>
</div>

<div class="block relative md:hidden px-4">
    <button
        use:listbox.button
        on:change={handleListboxChange}
        class="relative w-full flex justify-between items-center cursor-default rounded-md bg-white py-2 px-3 text-left text-sm shadow-sm focus:outline-none"
    >
        <span class="block truncate">{tabOptions[$listbox.selected]}</span>
    </button>

    {#if $listbox.expanded}
        <ul
            use:listbox.items
            class="absolute mt-1 left-4 right-4 overflow-auto rounded-md bg-white py-1 text-sm shadow-sm focus:outline-none"
        >
            {#each keys as value}
                {@const selected = $listbox.selected === value}
                <li
                    class="relative cursor-default select-none py-2 px-4 {selected ? 'bg-gray-50' : ''}"
                    use:listbox.item={{ value }}
                >
                    <span class="block truncate {selected ? 'font-bold' : 'font-normal'}">
                        {tabOptions[value]}
                    </span>
                </li>
            {/each}
        </ul>
    {/if}
</div>
CaptainCodeman commented 1 month ago

Yeah, when you have two completely different UI implementations the easiest approach is to show one or the other based on the breakpoint, but you'd want to have the selected item kept in sync. I'll try and come up with an example based on your