Skip to content

Commit

Permalink
Merge pull request #171 from omnifed/170-bug-cached-tabs-does-clears-…
Browse files Browse the repository at this point in the history
…state-before-ui-renders

170 bug cached tabs does clears state before UI renders
  • Loading branch information
caseybaggz committed Jun 14, 2024
2 parents f5a8b6a + 5fb3cef commit c5efd87
Show file tree
Hide file tree
Showing 8 changed files with 57 additions and 21 deletions.
2 changes: 1 addition & 1 deletion configs/src/versions.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const version = '0.3.1'
export const version = '0.3.2'
export const nextTag = 'next'

export const packages = ['panda-preset', 'icons', 'react', 'styled-system']
6 changes: 3 additions & 3 deletions docs/app/components/PageTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface TabProps {

export default function PageTabs(props: TabProps) {
return (
<Tabs active="overview" cache>
<Tabs active="overview" cache id="page-tabs">
<TabList
description={props.description}
className={css({
Expand All @@ -37,7 +37,7 @@ export default function PageTabs(props: TabProps) {
<GroupObjectsSave size={20} />
Guidelines
</Tab>
<Tab className={tabOverrideStyles} value="dev">
<Tab className={tabOverrideStyles} value="developers">
<IbmWatsonxCodeAssistantForZRefactor size={20} />
Dev
</Tab>
Expand All @@ -48,7 +48,7 @@ export default function PageTabs(props: TabProps) {
</TabList>
<TabPanel tab="overview">{props.overview}</TabPanel>
<TabPanel tab="guidelines">{props.guidelines}</TabPanel>
<TabPanel tab="dev">{props.dev}</TabPanel>
<TabPanel tab="developers">{props.dev}</TabPanel>
<TabPanel tab="a11y">{props.a11y}</TabPanel>
</Tabs>
)
Expand Down
2 changes: 1 addition & 1 deletion docs/app/react/tabs/components/tabs-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function BasicTabsPreview() {
export function CachedTabsPreview() {
return (
<div className={overrideStyles}>
<Tabs cache>
<Tabs cache id="tabs-cache-preview">
<TabList description="Button detail pages">
<Tab value="overview">Overview</Tab>
<Tab value="features">Features</Tab>
Expand Down
2 changes: 2 additions & 0 deletions docs/app/react/tabs/dev.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ function CustomTabsPreview() {
export interface TabsProps {
active?: string
cache?: boolean
id?: string
}

define function Tabs(props: TabsProps): ReactNode | null
Expand All @@ -176,6 +177,7 @@ The `Tabs` component accepts the following props:
| -------- | ------- | ------------------------------------------------------------- |
| active | | The `value` of the tab you want to be active by default. |
| cache | `false` | Whether to cache the active state of the tabs. |
| id | | A unique `id` attribute used when there are multiple Tabs on a single page. |

### TabList

Expand Down
11 changes: 9 additions & 2 deletions packages/react/src/components/Tab.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
'use client'

import { useMemo, type ButtonHTMLAttributes, type MouseEvent } from 'react'
import {
useMemo,
useTransition,
type ButtonHTMLAttributes,
type MouseEvent,
} from 'react'
import { useTabsContext } from '../context/tabs'
import { css, cx } from '@cerberus/styled-system/css'
import { useTabsKeyboardNavigation } from '../aria-helpers/tabs.aria'
Expand Down Expand Up @@ -29,19 +34,21 @@ export interface TabProps extends ButtonHTMLAttributes<HTMLButtonElement> {
export function Tab(props: TabProps) {
const { value, ...nativeProps } = props
const { active, onTabUpdate } = useTabsContext()
const [isPending, startTransition] = useTransition()
const { ref } = useTabsKeyboardNavigation()
const isActive = useMemo(() => active === value, [active, value])

function handleClick(e: MouseEvent<HTMLButtonElement>) {
props.onClick?.(e)
onTabUpdate(e.currentTarget.value)
startTransition(() => onTabUpdate(e.currentTarget.value))
}

return (
<button
{...nativeProps}
{...(!isActive && { tabIndex: -1 })}
aria-controls={`panel:${value}`}
aria-busy={isPending}
aria-selected={isActive}
id={value}
className={cx(nativeProps.className, btnStyles)}
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/components/TabList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
'use client'

import { cx } from '@cerberus/styled-system/css'
import { hstack } from '@cerberus/styled-system/patterns'
import type { HTMLAttributes, PropsWithChildren } from 'react'
import { useTabsContext } from '../context/tabs'

/**
* This module provides a TabList component.
Expand All @@ -24,6 +27,8 @@ export interface TabListProps extends HTMLAttributes<HTMLDivElement> {
*/
export function TabList(props: PropsWithChildren<TabListProps>) {
const { description, ...nativeProps } = props
const { id } = useTabsContext()

return (
<div
{...nativeProps}
Expand All @@ -37,6 +42,7 @@ export function TabList(props: PropsWithChildren<TabListProps>) {
w: 'full',
}),
)}
id={id ?? nativeProps.id}
/>
)
}
36 changes: 23 additions & 13 deletions packages/react/src/context/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@ import {
*/

export interface TabsContextValue {
active: string
tabs: MutableRefObject<HTMLButtonElement[]>
id: string
active: string
onTabUpdate: (active: string) => void
}

export const TabsContext = createContext<TabsContextValue | null>(null)

export interface TabsProps {
id?: string
active?: string
cache?: boolean
}
Expand All @@ -48,31 +50,39 @@ export interface TabsProps {
* ```
*/
export function Tabs(props: PropsWithChildren<TabsProps>): JSX.Element {
const { cache } = props
const [active, setActive] = useState(() => (cache ? '' : props.active ?? ''))
const { cache, active, id } = props
const [activeTab, setActiveTab] = useState(() => (cache ? '' : active ?? ''))
const tabs = useRef<HTMLButtonElement[]>([])
const uuid = useMemo(() => {
return id ? `cerberus-tabs-${id}` : 'cerberus-tabs'
}, [id])

const value = useMemo(
() => ({
active,
tabs,
onTabUpdate: setActive,
id: uuid,
active: activeTab,
onTabUpdate: setActiveTab,
}),
[active, setActive],
[activeTab, setActiveTab, uuid, tabs],
)

// Get the active tab from local storage
useEffect(() => {
const cachedTab = window.localStorage.getItem('cerberus-tabs')
if (cache && cachedTab) {
setActive(cachedTab)
if (cache) {
const cachedTab = window.localStorage.getItem(uuid)
setActiveTab(
cache ? cachedTab || (props.active ?? '') : props.active ?? '',
)
}
}, [cache])
}, [cache, active, uuid])

// Update the active tab in local storage
useEffect(() => {
if (cache) {
window.localStorage.setItem('cerberus-tabs', active)
if (cache && activeTab) {
window.localStorage.setItem(uuid, activeTab)
}
}, [active, cache])
}, [activeTab, cache])

return (
<TabsContext.Provider value={value}>{props.children}</TabsContext.Provider>
Expand Down
13 changes: 12 additions & 1 deletion tests/react/context/tabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ describe('Tabs Family & useTabsContext', () => {
<TestTabs />
</Tabs>,
)
expect(window.localStorage.getItem('cerberus-tabs')).toBe('')
expect(window.localStorage.getItem('cerberus-tabs')).toBeNull()
await user.click(screen.getByRole('tab', { name: /tab2/i }))
expect(window.localStorage.getItem('cerberus-tabs')).toBe('tab2')
})
Expand Down Expand Up @@ -201,4 +201,15 @@ describe('Tabs Family & useTabsContext', () => {
await user.type(tab1, END)
expect(tab3.focus).toBeTruthy()
})

test('should cache a uuid if provided and id prop', async () => {
render(
<Tabs id="unique-id" cache>
<TestTabs />
</Tabs>,
)
expect(window.localStorage.getItem('cerberus-tabs-unique-id')).toBeNull()
await user.click(screen.getByRole('tab', { name: /tab2/i }))
expect(window.localStorage.getItem('cerberus-tabs-unique-id')).toBe('tab2')
})
})

0 comments on commit c5efd87

Please sign in to comment.