Skip to content

Commit

Permalink
Merge pull request #740 from omnifed/739-feat-add-refetch-helper-to-u…
Browse files Browse the repository at this point in the history
…serootcolors

feat(react): add refetch helper to useRootColors
  • Loading branch information
caseybaggz authored Nov 22, 2024
2 parents 394d7c4 + 83d5343 commit b2ca1e0
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useEffect, useRef } from 'react'
const colorList = ['dataViz.diverging.50', 'dataViz.diverging.200']

export default function CanvasPreview() {
const colors = useRootColors(colorList)
const { colors } = useRootColors(colorList)
const canvasRef = useRef<HTMLCanvasElement>(null)

useEffect(() => {
Expand Down
83 changes: 70 additions & 13 deletions docs/app/react/use-root-colors/doc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ a11y: 'utilities'
---

import CodePreview from '@/app/components/CodePreview'
import {
WhenToUseAdmonition
} from '@/app/components/Admonition'
import CanvasPreview from './components/canvas-preview'

<WhenToUseAdmonition description="When you want to use Cerberus colors within a `canvas` element." />

```ts
import { useRootColors } from '@cerberus/react'
```

This hook allows you to access Cerberus colors from the document root which is useful when you want to use Cerberus colors within a `canvas` element.

Expand All @@ -24,7 +24,7 @@ This hook allows you to access Cerberus colors from the document root which is u
'use client'

import { useRootColors } from '@cerberus/react'
import { useEffect, useState, useRef } from 'react'
import { useEffect, useState } from 'react'

// This is a list of Cerberus colors to use in the chart
const colorList = [
Expand All @@ -38,11 +38,10 @@ const chartSettings = {}

function SomeChart() {
const [settings, setSettings] = useState(chartSettings)
const colors = useRootColors(colorList)
const hasSetColors = useRef<boolean>(false)
const { colors } = useRootColors(colorList)

useEffect(() => {
if (Object.keys(colors).length && !hasSetColors.current) {
if (Object.keys(colors).length === colorList.length) {
setSettings({
...settings,
something: {
Expand All @@ -63,10 +62,64 @@ function SomeChart() {
```
</CodePreview>

## Advanced Usage

If you need to get the latest colors when a user changes the color mode or theme, you can use the `refetch` function to get the latest colors from the document root.

```tsx title="some-chart.tsx" {18}
'use client'

import { useRootColors, useTheme } from '@cerberus/react'
import { useEffect, useState } from 'react'

const initialSettings = {...stuffFromXChartLibrary}

const colorList = [
'dataViz.diverging.50',
'dataViz.diverging.100',
'dataViz.diverging.200',
]

function SomeChart() {
const [settings, setOptions] = useState(initialSettings)

const { theme } = useTheme()
const { colors, refetch } = useRootColors(colorList)

useEffect(() => {
if (window && theme) {
// We need to wait for the theme to be applied to the root element
setTimeout(async () => {
await refetch()
}, 10)
}
}, [theme, refetch])

useEffect(() => {
const start = colors[colorList[0]]
setOptions((prev) => ({
...prev,
background: {
image: `radial-gradient(75% 82% at 52% 100%, ${start}40 0%, transparent 100%)`,
},
}))
}, [colors])

return (
<SomeChartLib settings={settings} />
)
}
```

## API

```ts showLineNumbers=false
define function useRootColors(colors: string[] = []): Record<string, string>
export interface RootColorsResult {
colors: Record<string, string>
refetch: () => void
}

define function useRootColors(colors: string[] = []): RootColorsResult
```

### Arguments
Expand All @@ -75,16 +128,20 @@ The `useRootColors` hook accepts the following optional arguments:

| Name | Default | Description |
| ------------ | ---------- | ------------------------------------------------ |
| `colors` | `[]` | An array of Cerberus color keys to retrieve. |
| colors | | An array of Cerberus color keys to retrieve. |
| refetch | | A function to refetch the colors from the root. |

### Return

The `useRootColors` hook returns a memoized object with the same properties as the options object passed in.

```ts
{
'dataViz.diverging.50': '#F7F7F7',
'dataViz.diverging.100': '#E5E5E5',
'dataViz.diverging.200': '#C6C6C6',
colors: {
'dataViz.diverging.50': '#F7F7F7',
'dataViz.diverging.100': '#E5E5E5',
'dataViz.diverging.200': '#C6C6C6',
},
refetch: () => void
}
```
32 changes: 18 additions & 14 deletions docs/app/scene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'
import Particles, { initParticlesEngine } from '@tsparticles/react'
import { type ISourceOptions } from '@tsparticles/engine'
import { loadSlim } from '@tsparticles/slim'
import { useThemeContext } from '@cerberus-design/react'
import { useThemeContext, useRootColors } from '@cerberus-design/react'

const fire = {
fpsLimit: 40,
Expand Down Expand Up @@ -56,10 +56,13 @@ const fire = {
},
} as ISourceOptions

const colorList = ['action-bg-initial']

export function Scene() {
const [init, setInit] = useState<boolean>(false)
const [options, setOptions] = useState<ISourceOptions>(fire)
const { theme } = useThemeContext()
const { colors, refetch } = useRootColors(colorList)

// this should be run only once per application lifetime
useEffect(() => {
Expand All @@ -72,21 +75,22 @@ export function Scene() {

useEffect(() => {
if (window && theme) {
// We need to wait for the theme to be applied
setTimeout(() => {
const rootStyle = window.getComputedStyle(document.body)
const start = rootStyle.getPropertyValue(
'--cerberus-colors-action-bg-initial',
)
setOptions((prev) => ({
...prev,
background: {
image: `radial-gradient(75% 82% at 52% 100%, ${start}40 0%, transparent 100%)`,
},
}))
// We need to wait for the theme to be applied to the root element
setTimeout(async () => {
await refetch()
}, 10)
}
}, [theme])
}, [theme, refetch])

useEffect(() => {
const start = colors[colorList[0]]
setOptions((prev) => ({
...prev,
background: {
image: `radial-gradient(75% 82% at 52% 100%, ${start}40 0%, transparent 100%)`,
},
}))
}, [colors])

if (init) {
return <Particles id="tsparticles" options={options} />
Expand Down
48 changes: 36 additions & 12 deletions packages/react/src/hooks/useRootColors.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
'use client'

import { useEffect, useReducer } from 'react'
import { useCallback, useEffect, useMemo, useReducer } from 'react'

/**
* This module provides a hook to get Cerberus colors from the document root.
* @module useRootColors
*/

export type RootColorsResult = Record<string, string>
export interface RootColorsResult {
/**
* A record of Cerberus colors where the key is the token name provided and the value is the color hex.
*/
colors: Record<string, string>
/**
* A function to refetch the Cerberus colors from the document root. Useful when you need the latest colors after a theme/mode change.
*/
refetch: () => Promise<void>
}

/**
* This hook returns a record of Cerberus colors from the document root.
Expand All @@ -19,26 +28,41 @@ export type RootColorsResult = Record<string, string>
export function useRootColors(colors: string[] = []): RootColorsResult {
const [state, dispatch] = useReducer(rootColorsReducer, {})

const handleRefetch = useCallback(() => {
return new Promise<void>((resolve) => {
dispatch(formatColors(colors))
resolve()
})
}, [])

useEffect(() => {
if (Object.keys(state).length === colors.length) return
dispatch(formatColors(colors))
console.log('updating colors in root hook')
}, [colors])

// reducer is already memoized
return useMemo(
() => ({ colors: state, refetch: handleRefetch }),
[state, handleRefetch],
)
}

const rootStyles = getComputedStyle(document.body)
const rootColors = colors.reduce((acc, color) => {
function formatColors(colors: string[]): Record<string, string> {
const rootStyles = getComputedStyle(document.body)
return colors.reduce(
(acc, color) => {
const formattedColor = color
.replace(/([a-z])([A-Z])/g, '$1-$2')
.toLowerCase()
.replaceAll('.', '-')
acc[color as keyof typeof acc] = rootStyles
acc[color] = rootStyles
.getPropertyValue(`--cerberus-colors-${formattedColor}`)
.trim()
return acc
}, {} as RootColorsResult)

dispatch(rootColors)
}, [colors])

// reducer is already memoized
return state
},
{} as Record<string, string>,
)
}

function rootColorsReducer(
Expand Down
15 changes: 13 additions & 2 deletions tests/react/hooks/useRootColors.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { describe, test, expect, afterEach } from 'bun:test'
import { render, screen, cleanup } from '@testing-library/react'
import { useRootColors } from '@cerberus-design/react'
import { setupStrictMode } from '@/utils'
import { setupStrictMode, user } from '@/utils'

describe('useRootColors', () => {
setupStrictMode()
afterEach(cleanup)

function Test() {
const colors = useRootColors([
const { colors, refetch } = useRootColors([
'dataViz.diverging.50',
'dataViz.diverging.200',
])
Expand All @@ -20,6 +20,8 @@ describe('useRootColors', () => {
<span>{color}</span>
</div>
))}

<button onClick={refetch}>refetch</button>
</div>
)
}
Expand All @@ -31,4 +33,13 @@ describe('useRootColors', () => {
expect(screen.getByText(/dataViz.diverging.50/i)).toBeTruthy()
expect(screen.getByText(/dataViz.diverging.200/i)).toBeTruthy()
})

test('should refetch colors', async () => {
render(<Test />)
// There's no document to store styles in a test environment, so we can't
// test this hook to the full extent.
await user.click(screen.getByText(/refetch/i))
expect(screen.getByText(/dataViz.diverging.50/i)).toBeTruthy()
expect(screen.getByText(/dataViz.diverging.200/i)).toBeTruthy()
})
})

0 comments on commit b2ca1e0

Please sign in to comment.