Skip to content

Commit 159f26c

Browse files
authored
feat(storybook): restore a11y (and other) badges in toolbar (#1042)
* test(storybook): add badge parameter resolver * feat(storybook): render badges in toolbar
1 parent f2ebe94 commit 159f26c

6 files changed

Lines changed: 201 additions & 1 deletion

File tree

.storybook/badges/BadgesTool.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React, { memo, useMemo } from 'react'
2+
import type { CSSProperties } from 'react'
3+
import { useParameter } from 'storybook/manager-api'
4+
5+
import { resolveBadges } from './badges'
6+
import { BADGES_CONFIG_PARAMETER, BADGES_PARAMETER, TOOL_ID } from './constants'
7+
8+
const containerStyle: CSSProperties = {
9+
alignItems: 'center',
10+
display: 'inline-flex',
11+
gap: '4px',
12+
marginInline: '6px',
13+
}
14+
15+
const badgeStyle: CSSProperties = {
16+
alignItems: 'center',
17+
border: '1px solid currentColor',
18+
borderRadius: '3px',
19+
boxSizing: 'border-box',
20+
display: 'inline-flex',
21+
fontWeight: 600,
22+
minHeight: '20px',
23+
padding: '2px 6px',
24+
whiteSpace: 'nowrap',
25+
}
26+
27+
const emptyBadges: unknown[] = []
28+
const emptyBadgesConfig: Record<string, unknown> = {}
29+
30+
export const BadgesTool = memo(function BadgesTool() {
31+
const badgesParameter = useParameter(BADGES_PARAMETER, emptyBadges)
32+
const badgesConfigParameter = useParameter(BADGES_CONFIG_PARAMETER, emptyBadgesConfig)
33+
34+
const badges = useMemo(
35+
() => resolveBadges(badgesParameter, badgesConfigParameter),
36+
[badgesParameter, badgesConfigParameter],
37+
)
38+
39+
if (badges.length === 0) {
40+
return null
41+
}
42+
43+
return (
44+
<div
45+
key={TOOL_ID}
46+
aria-label="Story badges"
47+
style={containerStyle}
48+
title={badges.map(({ title }) => title).join(', ')}
49+
>
50+
{badges.map(({ id, title, styles }, index) => (
51+
<span key={`${id}-${index}`} style={{ ...badgeStyle, ...styles }}>
52+
{title}
53+
</span>
54+
))}
55+
</div>
56+
)
57+
})

.storybook/badges/badges.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { resolveBadges } from './badges'
2+
3+
describe('resolveBadges', () => {
4+
it('resolves configured badges in story order', () => {
5+
expect(
6+
resolveBadges(['accessible', 'deprecated'], {
7+
accessible: {
8+
title: 'Accessible',
9+
styles: { color: 'green' },
10+
},
11+
deprecated: {
12+
title: 'Deprecated',
13+
styles: { color: 'red' },
14+
},
15+
}),
16+
).toEqual([
17+
{
18+
id: 'accessible',
19+
title: 'Accessible',
20+
styles: { color: 'green' },
21+
},
22+
{
23+
id: 'deprecated',
24+
title: 'Deprecated',
25+
styles: { color: 'red' },
26+
},
27+
])
28+
})
29+
30+
it('filters unknown badges and invalid badge entries', () => {
31+
expect(
32+
resolveBadges(['accessible', 'missing', 42, 'untitled'], {
33+
accessible: {
34+
title: 'Accessible',
35+
},
36+
untitled: {
37+
styles: { color: 'orange' },
38+
},
39+
}),
40+
).toEqual([
41+
{
42+
id: 'accessible',
43+
title: 'Accessible',
44+
styles: undefined,
45+
},
46+
])
47+
})
48+
49+
it('treats invalid parameter shapes as empty', () => {
50+
expect(resolveBadges(undefined, {})).toEqual([])
51+
expect(resolveBadges('accessible', {})).toEqual([])
52+
expect(resolveBadges(['accessible'], undefined)).toEqual([])
53+
expect(resolveBadges(['accessible'], [])).toEqual([])
54+
})
55+
56+
it('ignores non-object styles', () => {
57+
expect(
58+
resolveBadges(['accessible'], {
59+
accessible: {
60+
title: 'Accessible',
61+
styles: 'color: green',
62+
},
63+
}),
64+
).toEqual([
65+
{
66+
id: 'accessible',
67+
title: 'Accessible',
68+
styles: undefined,
69+
},
70+
])
71+
})
72+
})

.storybook/badges/badges.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { CSSProperties } from 'react'
2+
3+
type UnknownRecord = Record<string, unknown>
4+
5+
type BadgeConfig = {
6+
title?: unknown
7+
styles?: unknown
8+
}
9+
10+
type ResolvedBadge = {
11+
id: string
12+
title: string
13+
styles: CSSProperties | undefined
14+
}
15+
16+
function isRecord(value: unknown): value is UnknownRecord {
17+
return typeof value === 'object' && value !== null && !Array.isArray(value)
18+
}
19+
20+
function isBadgeConfig(value: unknown): value is BadgeConfig {
21+
return isRecord(value)
22+
}
23+
24+
function resolveStyles(styles: unknown): CSSProperties | undefined {
25+
return isRecord(styles) ? (styles as CSSProperties) : undefined
26+
}
27+
28+
function resolveBadges(badges: unknown, badgesConfig: unknown): ResolvedBadge[] {
29+
if (!Array.isArray(badges) || !isRecord(badgesConfig)) {
30+
return []
31+
}
32+
33+
return badges.flatMap((badge) => {
34+
if (typeof badge !== 'string') {
35+
return []
36+
}
37+
38+
const badgeConfig = badgesConfig[badge]
39+
40+
if (!isBadgeConfig(badgeConfig) || typeof badgeConfig.title !== 'string') {
41+
return []
42+
}
43+
44+
return [
45+
{
46+
id: badge,
47+
title: badgeConfig.title,
48+
styles: resolveStyles(badgeConfig.styles),
49+
},
50+
]
51+
})
52+
}
53+
54+
export { resolveBadges }
55+
export type { ResolvedBadge }

.storybook/badges/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const ADDON_ID = 'reactist/badges'
2+
export const TOOL_ID = `${ADDON_ID}/tool`
3+
export const BADGES_PARAMETER = 'badges'
4+
export const BADGES_CONFIG_PARAMETER = 'badgesConfig'

.storybook/badges/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { BadgesTool } from './BadgesTool'
2+
export { ADDON_ID, TOOL_ID } from './constants'

.storybook/manager.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
1-
import { addons } from 'storybook/manager-api'
1+
import { addons, types } from 'storybook/manager-api'
2+
import { ADDON_ID, BadgesTool, TOOL_ID } from './badges'
23
import theme from './theme'
34

45
addons.setConfig({
56
theme,
67
})
8+
9+
addons.register(ADDON_ID, () => {
10+
addons.add(TOOL_ID, {
11+
title: 'Badges',
12+
type: types.TOOL,
13+
match: ({ viewMode }) => viewMode === 'story' || viewMode === 'docs',
14+
render: BadgesTool,
15+
})
16+
})

0 commit comments

Comments
 (0)