Skip to content

Commit

Permalink
docs: story page to check color contrasts
Browse files Browse the repository at this point in the history
  • Loading branch information
Powerplex committed Oct 8, 2024
1 parent 12a0943 commit 9a8bf97
Show file tree
Hide file tree
Showing 6 changed files with 445 additions and 12 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ module.exports = {
// NX
'@nx/enforce-module-boundaries': 2,
// Prettier
"prettier/prettier": [1, { "endOfLine": "auto" }],
'prettier/prettier': [1, { endOfLine: 'auto' }],
// Typescript
'@typescript-eslint/consistent-type-definitions': [1, 'interface'],
'@typescript-eslint/array-type': [2, { default: 'array', readonly: 'array' }],
Expand Down
73 changes: 73 additions & 0 deletions packages/utils/theme/src/contrastCheck.doc.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Canvas, Meta } from '@storybook/addon-docs'
import * as ContrastCheck from './contrastCheck.stories'

<Meta of={ContrastCheck} />

# Contrast check

## Usage

```typescript
import { getThemeContrastReport, checkColorContrast } from '@spark-ui/theme-utils'
```

### getThemeContrastReport

Use `getThemeContrastReport(theme)` to get a detailed report of the contrast ratio for each pair of colors in the theme.

For exemple `main` colored background is supposed to have `onMain` colored text. The pair `main/onMain` must provide sufficient contrast ratio to pass [SC 1.4.3 - Contrast (Minimum) (Level AA)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html).
Same goes for every other color pair in a Spark theme.

Each pair is tested for normal text size and larger text size, as they both have different contrast requirements.

Score can be: `Failed`, `AA` or `AAA`.

```typescript
import { checkColorContrast } from '@spark-ui/theme-utils'

const report = getThemeContrastReport(theme)
```

Report looks like this:

```json
// Exemple for the main color
{
"main": {
"previewStyles": "bg-main text-on-main",
"small": {
"contrastRatio": "3.48",
"score": "Failed",
"textSize": "Small",
"colors": ["#EC5A13", "#FFFFFF"]
},
"large": {
"contrastRatio": "3.48",
"score": "Failed",
"textSize": "Small",
"colors": ["#EC5A13", "#FFFFFF"]
}
}
// other colors...
}
```

### checkColorContrast

Use `checkColorContrast` to compare two colors given a font-size.

```typescript
import { checkColorContrast } from '@spark-ui/theme-utils'

const LARGE_FONT_SIZE = 24

checkColorContrast('#EC5A13', '#FFFFFF', LARGE_FONT_SIZE) // { "contrastRatio": "3.48", "score": "Failed", "textSize": "Large", "colors": ["#EC5A13", "#FFFFFF"] }
```

## Spark theme contrasts

This section displays card with all color schemes offered by Spark themes (default and dark).

It can be used by Axe dev tools or Playwright for an a11y color check.

<Canvas of={ContrastCheck.Default} />
94 changes: 94 additions & 0 deletions packages/utils/theme/src/contrastCheck.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Icon } from '@spark-ui/icon'
import { Block } from '@spark-ui/icons/dist/icons/Block'
import { Check } from '@spark-ui/icons/dist/icons/Check'
import { Tabs } from '@spark-ui/tabs'
import { Tag } from '@spark-ui/tag'
import { Meta, StoryFn } from '@storybook/react'
import { cx } from 'class-variance-authority'
import { ReactNode } from 'react'

import { getThemeContrastReport } from './contrastCheck'
import { defaultTheme } from './defaultTheme'
import { defaultThemeDark } from './defaultThemeDark'
import { type Theme } from './types'

const meta: Meta = {
title: 'Utils/theme-utils/contrast check',
}

export default meta

const ScoreTag = ({ score }: { score: string }) => {
const isSuccess = score.includes('AA')

return (
<Tag design="tinted" intent={isSuccess ? 'success' : 'danger'}>
{score}
<Icon>{isSuccess ? <Check /> : <Block />}</Icon>
</Tag>
)
}

const Cell = ({ className, children }: { className?: string; children: ReactNode }) => {
return <td className={cx('border-sm border-outline p-md', className)}>{children}</td>
}

const ThemeReport = ({ theme, ...rest }: { theme: Theme }) => {
const report = getThemeContrastReport(theme)

return (
<table className="table-auto bg-surface text-on-surface" {...rest}>
<thead className="sticky">
<tr>
<th className="p-sm text-left">Color</th>
<th className="p-sm text-left">Preview</th>
<th className="p-sm text-left">Ratio</th>
<th className="p-sm text-left">Score (small text)</th>
<th className="p-sm text-left">Score (large text)</th>
</tr>
</thead>
<tbody>
{Object.entries(report).map(([color, result]) => {
return (
<tr>
<Cell>{color}</Cell>
<Cell className={cx('border-current px-lg', result.previewStyles)}>
<span className="text-body-1">small, </span>
<span className="text-headline-1">large</span>
</Cell>
<Cell>{result.small.contrastRatio}</Cell>
<Cell>
<ScoreTag score={result.small.score} />
</Cell>
<Cell>
<ScoreTag score={result.large.score} />
</Cell>
</tr>
)
})}
</tbody>
</table>
)
}

export const Default: StoryFn = _args => (
<div className="flex">
<Tabs defaultValue="tab1">
<Tabs.List>
<Tabs.Trigger value="tab1">
<span>Default theme</span>
</Tabs.Trigger>
<Tabs.Trigger value="tab2">
<span>Dark theme</span>
</Tabs.Trigger>
</Tabs.List>

<Tabs.Content value="tab1" data-theme="default">
<ThemeReport theme={defaultTheme} />
</Tabs.Content>
<Tabs.Content value="tab2" data-theme="dark" className="bg-surface">
<ThemeReport theme={defaultThemeDark} />
</Tabs.Content>
</Tabs>
</div>
)
Loading

0 comments on commit 9a8bf97

Please sign in to comment.