Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions packages/ui/src/components/text-shimmer-core.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, test } from "bun:test"
import { buildTextShimmerStyle } from "./text-shimmer-core"
import { TextShimmerConfig } from "./text-shimmer"
import { TextShimmerV2Config } from "../v2/components/text-shimmer-v2"

describe("buildTextShimmerStyle", () => {
test("swap/index vars are mapped", () => {
expect(buildTextShimmerStyle("--text-shimmer-swap",
"--text-shimmer-index", 220, 5)).toEqual({
"--text-shimmer-swap": "220ms",
"--text-shimmer-index": "5",
})
})
test("swap/index vars are mapped for v2", () => {
expect(buildTextShimmerStyle("--_swap", "--_index", 220, 0)).toEqual({
"--_swap": "220ms",
"--_index": "0",
})
})
test("helper keys match config", () => {
expect(Object.keys(buildTextShimmerStyle(TextShimmerConfig.swapVar,
TextShimmerConfig.indexVar, 220, 1))).toEqual(
[TextShimmerConfig.swapVar, TextShimmerConfig.indexVar]
)
expect(Object.keys(buildTextShimmerStyle(TextShimmerV2Config.swapVar,
TextShimmerV2Config.indexVar, 220, 1))).toEqual(
[TextShimmerV2Config.swapVar, TextShimmerV2Config.indexVar]
)
})
})

describe("Configurations of shimmers", () => {
test("original selectors of v1 is kept", () => {
expect(TextShimmerConfig).toEqual({
component: "text-shimmer",
charSlot: "text-shimmer-char",
baseSlot: "text-shimmer-char-base",
shimmerSlot: "text-shimmer-char-shimmer",
swapVar: "--text-shimmer-swap",
indexVar: "--text-shimmer-index",
})
})
test("original selectos of v2 is kept", () => {
expect(TextShimmerV2Config).toEqual({
component: "text-shimmer-v2",
charSlot: "text-shimmer-v2-char",
baseSlot: "text-shimmer-v2-base",
shimmerSlot: "text-shimmer-v2-shimmer",
swapVar: "--_swap",
indexVar: "--_index",
})
})
test("v1 & v2 are different", () => {
expect(TextShimmerConfig.component).not.toBe(TextShimmerV2Config.component)
expect(TextShimmerConfig.charSlot).not.toBe(TextShimmerV2Config.charSlot)
})
})
74 changes: 74 additions & 0 deletions packages/ui/src/components/text-shimmer-core.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { createEffect, createMemo, createSignal, onCleanup, type JSX, type ValidComponent } from "solid-js"
import { Dynamic } from "solid-js/web"

export type TextShimmerCoreProps<T extends ValidComponent = "span"> = {
text: string
class?: string
as?: T
active?: boolean
offset?: number
component: string
charSlot: string
baseSlot: string
shimmerSlot: string
swapVar: string
indexVar: string
}

export const TextShimmerCore = <T extends ValidComponent = "span">(
props: TextShimmerCoreProps<T>
) => {
const text = createMemo(() => props.text ?? "")
const active = createMemo(() => props.active ?? true)
const offset = createMemo(() => props.offset ?? 0)
const [run, setRun] = createSignal(active())
const swap = 220
let timer: ReturnType<typeof setTimeout> | undefined

createEffect(() => {
if (timer) {
clearTimeout(timer)
timer = undefined
}

if (active()) {
setRun(true)
return
}

timer = setTimeout(() => {
timer = undefined
setRun(false)
}, swap)
})

onCleanup(() => {
if (!timer) return
clearTimeout(timer)
})

return (
<Dynamic
component={props.as ?? "span"}
data-component={props.component}
data-active={active() ? "true" : "false"}
class={props.class}
aria-label={text()}
style={buildTextShimmerStyle(props.swapVar, props.indexVar, swap, offset())}
>
<span data-slot={props.charSlot}>
<span data-slot={props.baseSlot} aria-hidden="true">
{text()}
</span>
<span data-slot={props.shimmerSlot} data-run={run() ? "true" : "false"} aria-hidden="true">
{text()}
</span>
</span>
</Dynamic>
)
}

export function buildTextShimmerStyle(swapVar: string, indexVar: string,
swap: number, offset: number) {
return {[swapVar]: `${swap}ms`, [indexVar]: `${offset}` } as JSX.CSSProperties
}
65 changes: 12 additions & 53 deletions packages/ui/src/components/text-shimmer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { createEffect, createMemo, createSignal, onCleanup, type ValidComponent } from "solid-js"
import { Dynamic } from "solid-js/web"
import type { ValidComponent } from "solid-js"
import { TextShimmerCore } from "./text-shimmer-core"

export const TextShimmerConfig = {
component: "text-shimmer",
charSlot: "text-shimmer-char",
baseSlot: "text-shimmer-char-base",
shimmerSlot: "text-shimmer-char-shimmer",
swapVar: "--text-shimmer-swap",
indexVar: "--text-shimmer-index"
} as const

export const TextShimmer = <T extends ValidComponent = "span">(props: {
text: string
Expand All @@ -8,55 +17,5 @@ export const TextShimmer = <T extends ValidComponent = "span">(props: {
active?: boolean
offset?: number
}) => {
const text = createMemo(() => props.text ?? "")
const active = createMemo(() => props.active ?? true)
const offset = createMemo(() => props.offset ?? 0)
const [run, setRun] = createSignal(active())
const swap = 220
let timer: ReturnType<typeof setTimeout> | undefined

createEffect(() => {
if (timer) {
clearTimeout(timer)
timer = undefined
}

if (active()) {
setRun(true)
return
}

timer = setTimeout(() => {
timer = undefined
setRun(false)
}, swap)
})

onCleanup(() => {
if (!timer) return
clearTimeout(timer)
})

return (
<Dynamic
component={props.as ?? "span"}
data-component="text-shimmer"
data-active={active() ? "true" : "false"}
class={props.class}
aria-label={text()}
style={{
"--text-shimmer-swap": `${swap}ms`,
"--text-shimmer-index": `${offset()}`,
}}
>
<span data-slot="text-shimmer-char">
<span data-slot="text-shimmer-char-base" aria-hidden="true">
{text()}
</span>
<span data-slot="text-shimmer-char-shimmer" data-run={run() ? "true" : "false"} aria-hidden="true">
{text()}
</span>
</span>
</Dynamic>
)
return <TextShimmerCore {...props} {...TextShimmerConfig} />
}
65 changes: 12 additions & 53 deletions packages/ui/src/v2/components/text-shimmer-v2.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,22 @@
import { createEffect, createMemo, createSignal, onCleanup, type ValidComponent } from "solid-js"
import { Dynamic } from "solid-js/web"
import type { ValidComponent } from "solid-js"
import { TextShimmerCore } from "../../components/text-shimmer-core"
import "./text-shimmer-v2.css"

export const TextShimmerV2Config = {
component: "text-shimmer-v2",
charSlot: "text-shimmer-v2-char",
baseSlot: "text-shimmer-v2-base",
shimmerSlot: "text-shimmer-v2-shimmer",
swapVar: "--_swap",
indexVar: "--_index"
} as const

export const TextShimmerV2 = <T extends ValidComponent = "span">(props: {
text: string
class?: string
as?: T
active?: boolean
offset?: number
}) => {
const text = createMemo(() => props.text ?? "")
const active = createMemo(() => props.active ?? true)
const offset = createMemo(() => props.offset ?? 0)
const [run, setRun] = createSignal(active())
const swap = 220
let timer: ReturnType<typeof setTimeout> | undefined

createEffect(() => {
if (timer) {
clearTimeout(timer)
timer = undefined
}

if (active()) {
setRun(true)
return
}

timer = setTimeout(() => {
timer = undefined
setRun(false)
}, swap)
})

onCleanup(() => {
if (!timer) return
clearTimeout(timer)
})

return (
<Dynamic
component={props.as ?? "span"}
data-component="text-shimmer-v2"
data-active={active() ? "true" : "false"}
class={props.class}
aria-label={text()}
style={{
"--_swap": `${swap}ms`,
"--_index": `${offset()}`,
}}
>
<span data-slot="text-shimmer-v2-char">
<span data-slot="text-shimmer-v2-base" aria-hidden="true">
{text()}
</span>
<span data-slot="text-shimmer-v2-shimmer" data-run={run() ? "true" : "false"} aria-hidden="true">
{text()}
</span>
</span>
</Dynamic>
)
return <TextShimmerCore {...props} {...TextShimmerV2Config} />
}
Loading