176 lines
3.8 KiB
Svelte
176 lines
3.8 KiB
Svelte
<script lang="ts">
|
|
import type { Snippet } from 'svelte';
|
|
import type { ClassValue, HTMLButtonAttributes, MouseEventHandler } from 'svelte/elements';
|
|
import Spinner from './Spinner.svelte';
|
|
import { defaultIconProps, type IconDef } from './util';
|
|
|
|
interface Props extends Omit<HTMLButtonAttributes, 'class'> {
|
|
icon?: IconDef;
|
|
animate?: boolean;
|
|
loading?: boolean;
|
|
class?: ClassValue | null | undefined;
|
|
ref?: HTMLButtonElement | null;
|
|
children: Snippet;
|
|
onclick?: MouseEventHandler<HTMLButtonElement>;
|
|
}
|
|
|
|
let {
|
|
type = 'button',
|
|
icon,
|
|
animate = true,
|
|
loading,
|
|
disabled,
|
|
class: classValue,
|
|
ref = $bindable<HTMLButtonElement | null>(null),
|
|
children,
|
|
onclick,
|
|
...others
|
|
}: Props = $props();
|
|
|
|
type SVGInHTML = HTMLElement & SVGElement;
|
|
let iconElement = $state<SVGInHTML | null>(null);
|
|
$effect(() => {
|
|
if (icon && ref) {
|
|
iconElement = ref.querySelector('svg') as SVGInHTML | null;
|
|
}
|
|
});
|
|
|
|
const handleButtonClick: MouseEventHandler<HTMLButtonElement> = (event) => {
|
|
if (animate && !loading && !disabled) {
|
|
animateLoop();
|
|
animateRipple(event);
|
|
}
|
|
|
|
if (loading || disabled) return;
|
|
|
|
onclick?.(event);
|
|
};
|
|
|
|
const triggerAnimation = (className: string) => {
|
|
if (icon && iconElement) {
|
|
iconElement.classList.remove(className);
|
|
|
|
// make sure we have DOM reflow
|
|
requestAnimationFrame(() => {
|
|
iconElement?.classList.add(className);
|
|
});
|
|
}
|
|
};
|
|
|
|
export const animateLoop = () => triggerAnimation('animate');
|
|
export const animateBounce = () => triggerAnimation('bounce');
|
|
export const animateRipple: MouseEventHandler<HTMLButtonElement> = (event) => {
|
|
const button = event.currentTarget;
|
|
const circle = document.createElement('span');
|
|
const diameter = Math.max(button.clientWidth, button.clientHeight);
|
|
const radius = diameter / 2;
|
|
|
|
const rect = button.getBoundingClientRect();
|
|
const x = event.clientX - rect.left - radius;
|
|
const y = event.clientY - rect.top - radius;
|
|
|
|
circle.style.width = circle.style.height = `${diameter}px`;
|
|
circle.style.left = `${x}px`;
|
|
circle.style.top = `${y}px`;
|
|
circle.classList.add('ripple');
|
|
|
|
const ripples = button.getElementsByClassName('ripple');
|
|
for (let i = 0; i < ripples.length; i++) {
|
|
ripples[i].remove();
|
|
}
|
|
|
|
button.appendChild(circle);
|
|
};
|
|
</script>
|
|
|
|
<button
|
|
{type}
|
|
bind:this={ref}
|
|
class={[
|
|
'button group relative flex items-center gap-3 overflow-hidden rounded-sm px-5',
|
|
'text-sui-background py-3 font-medium transition-colors',
|
|
!loading && !disabled
|
|
? ' bg-sui-primary hover:bg-sui-secondary cursor-pointer'
|
|
: 'cursor-not-allowed opacity-55',
|
|
classValue
|
|
]}
|
|
onclick={handleButtonClick}
|
|
{...others}
|
|
>
|
|
{@render children()}
|
|
|
|
{#if icon && !loading}
|
|
<icon.component {...icon.props || defaultIconProps} />
|
|
{/if}
|
|
|
|
{#if loading}
|
|
<Spinner class="-mr-1 ml-0.5" size="1.3rem" />
|
|
{/if}
|
|
</button>
|
|
|
|
<style>
|
|
@keyframes loop {
|
|
0% {
|
|
transform: translateX(0);
|
|
opacity: 100%;
|
|
}
|
|
40% {
|
|
opacity: 100%;
|
|
}
|
|
50% {
|
|
transform: translateX(3rem);
|
|
opacity: 0%;
|
|
}
|
|
51% {
|
|
transform: translateX(-2rem);
|
|
opacity: 0%;
|
|
}
|
|
100% {
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
@keyframes bounce {
|
|
0% {
|
|
transform: translateX(0);
|
|
}
|
|
25% {
|
|
transform: translateX(-0.25rem);
|
|
}
|
|
75% {
|
|
transform: translateX(0.25rem);
|
|
}
|
|
100% {
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
@keyframes ripple {
|
|
to {
|
|
transform: scale(4);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
:global(button svg.animate) {
|
|
animation-name: loop;
|
|
animation-duration: 0.5s;
|
|
}
|
|
|
|
:global(button svg.bounce) {
|
|
animation-name: bounce;
|
|
animation-duration: 180ms;
|
|
animation-timing-function: ease-in-out;
|
|
animation-iteration-count: 3;
|
|
}
|
|
|
|
:global(span.ripple) {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
background-color: rgba(255, 255, 255, 0.3);
|
|
animation: ripple 0.5s linear;
|
|
transform: scale(0);
|
|
pointer-events: none;
|
|
}
|
|
</style>
|