Skip to main content

Actions

Button Side Expand code

Copy the implementation or inspect every file that belongs to this component unit.

resources/views/components/ui/button-side-expand.blade.php

@props([
    'icon' => 'plus',
    'openIcon' => 'x',
    'text' => null,
    'iconTrailing' => 'caret-right',
    'label',
    'side' => 'right',
    'variant' => 'primary',
    'size' => 'base',
])

@php
    /*
    | Compact trigger that expands horizontally into a button bar. Click the
    | trigger to reveal slot content; click again or press Escape to collapse.
    |
    | Pass `text` for a labeled button trigger (e.g. "Actions" with a chevron).
    | Omit `text` for a compact icon-only trigger.
    |
    | side: right (default) — grows to the right; left — grows to the left
    |
    | Usage:
    |   <x-ui.button-side-expand text="Actions" label="Open actions" variant="outline">
    |       <x-ui.icon-button icon="pencil-simple" label="Edit" variant="ghost" size="sm" />
    |   </x-ui.button-side-expand>
    |
    |   <x-ui.button-side-expand icon="plus" label="Open actions">
    |       <x-ui.icon-button icon="copy" label="Copy" variant="ghost" size="sm" />
    |   </x-ui.button-side-expand>
    */
    $isTextTrigger = filled($text);
    $accessibleLabel = $label ?? $text;

    $iconTriggerSizes = [
        'sm' => 'h-8 w-8',
        'base' => 'h-9 w-9',
        'lg' => 'h-11 w-11',
    ];

    $textTriggerSizes = [
        'sm' => 'gap-1.5 px-3 py-2 text-sm',
        'base' => 'gap-2 px-4 py-2.5 text-sm',
        'lg' => 'gap-2 px-5 py-3 text-base',
    ];

    $iconSizes = [
        'sm' => 'sm',
        'base' => 'base',
        'lg' => 'lg',
    ];

    $triggerVariants = [
        'primary' => 'bg-brand-600 text-white hover:bg-brand-500 focus:ring-brand-500',
        'secondary' => 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-400 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700',
        'outline' => 'border border-brand-300 text-brand-700 hover:bg-brand-50 focus:ring-brand-500 dark:border-brand-700 dark:text-brand-300 dark:hover:bg-brand-950/40',
        'ghost' => 'text-gray-600 hover:bg-gray-100 focus:ring-gray-400 dark:text-gray-300 dark:hover:bg-gray-800',
        'accent' => 'bg-accent-600 text-white hover:bg-accent-500 focus:ring-accent-500',
    ];

    $iconSize = $iconSizes[$size] ?? $iconSizes['base'];
    $triggerVariantClass = $triggerVariants[$variant] ?? $triggerVariants['primary'];
    $expandDirection = $side === 'left' ? 'flex-row-reverse' : 'flex-row';
    $containerRadius = $isTextTrigger ? 'rounded-lg' : 'rounded-full';
    $triggerRadius = $isTextTrigger ? 'rounded-md' : 'rounded-full';
    $trailingIcon = $side === 'left' && $isTextTrigger ? 'caret-left' : $iconTrailing;
@endphp

<div
    x-data="{ open: false }"
    @click.outside="open = false"
    @keydown.escape.window="open = false"
    {{ $attributes->class(['inline-flex']) }}
>
    <div @class([
        'inline-flex items-center overflow-hidden border border-gray-200 bg-white p-1 shadow-sm dark:border-gray-800 dark:bg-gray-900',
        $containerRadius,
        $expandDirection,
    ])>
        <button
            type="button"
            @click="open = ! open"
            :aria-expanded="open.toString()"
            aria-label="{{ $accessibleLabel }}"
            @class([
                'inline-flex shrink-0 items-center justify-center font-semibold transition-[background-color,color,transform] duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-900',
                $triggerRadius,
                $triggerVariantClass,
                $isTextTrigger
                    ? ($textTriggerSizes[$size] ?? $textTriggerSizes['base'])
                    : ($iconTriggerSizes[$size] ?? $iconTriggerSizes['base']),
            ])
        >
            @if ($isTextTrigger)
                <span>{{ $text }}</span>
                <x-ui.icon
                    :name="$trailingIcon"
                    :size="$iconSize"
                    weight="bold"
                    ::class="open ? '{{ $side === 'left' ? '-rotate-90' : 'rotate-90' }}' : ''"
                    class="transition-transform duration-200"
                />
            @else
                <x-ui.icon :name="$icon" :size="$iconSize" weight="bold" x-show="! open" />
                <x-ui.icon :name="$openIcon" :size="$iconSize" weight="bold" x-show="open" x-cloak />
            @endif
        </button>

        <div
            class="flex items-center gap-0.5 overflow-hidden whitespace-nowrap transition-[max-width,opacity,margin] duration-300 ease-out"
            :class="open
                ? '{{ $side === 'left' ? 'max-w-96 opacity-100 me-0.5' : 'max-w-96 opacity-100 ms-0.5' }}'
                : 'max-w-0 opacity-0 {{ $side === 'left' ? 'me-0' : 'ms-0' }} pointer-events-none'"
            x-cloak
        >
            <span
                class="h-5 w-px shrink-0 bg-gray-200 dark:bg-gray-700"
                x-show="open"
                x-cloak
                aria-hidden="true"
            ></span>

            <div class="flex items-center gap-0.5 ps-0.5">
                {{ $slot }}
            </div>
        </div>
    </div>
</div>