Skip to content

Commit

Permalink
feat: Add LabelCustomColor component (#125)
Browse files Browse the repository at this point in the history
* Initial implementation

Signed-off-by: Mike Turley <[email protected]>

* All in on tinycolor2 instead of linear-gradient

Signed-off-by: Mike Turley <[email protected]>

* Tuning brightness

Signed-off-by: Mike Turley <[email protected]>

* Remove @types/react-color because it does something weird to the built-in ReactNode type

Signed-off-by: Mike Turley <[email protected]>

* Tuning brightness

Signed-off-by: Mike Turley <[email protected]>

* Tuning brightness some more

Signed-off-by: Mike Turley <[email protected]>

* Properly export

Signed-off-by: Mike Turley <[email protected]>

* Handle extra props, add examples from Tackle

Signed-off-by: Mike Turley <[email protected]>

* Update docs paragraph

Signed-off-by: Mike Turley <[email protected]>

* Better margins in second example

Signed-off-by: Mike Turley <[email protected]>

* Use the readability functions in tinycolor2

Signed-off-by: Mike Turley <[email protected]>

* Nits

Signed-off-by: Mike Turley <[email protected]>

* Cleanup

Signed-off-by: Mike Turley <[email protected]>

* Introduce a cache, perf test example, and better description

Signed-off-by: Mike Turley <[email protected]>

* More notes for the doc

Signed-off-by: Mike Turley <[email protected]>

* Fix WCAG URL

Signed-off-by: Mike Turley <[email protected]>

---------

Signed-off-by: Mike Turley <[email protected]>
  • Loading branch information
mturley authored Mar 22, 2023
1 parent 1f6e29f commit 9817e08
Show file tree
Hide file tree
Showing 7 changed files with 2,222 additions and 1,967 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"postcss": "^8.2.13",
"prettier": "^2.2.1",
"react": "^17.0.1",
"react-color": "^2.19.3",
"react-dom": "^17.0.1",
"react-is": "^17.0.1",
"rimraf": "^3.0.2",
Expand All @@ -94,6 +95,7 @@
"dependencies": {
"@tanstack/react-query": "^4.26.1",
"fast-deep-equal": "^3.1.3",
"tinycolor2": "^1.6.0",
"yup": "^0.32.9"
}
}
56 changes: 56 additions & 0 deletions src/components/LabelCustomColor/LabelCustomColor.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs/blocks';
import { LabelCustomColor } from './LabelCustomColor';
import {
LabelCustomColorPicker,
LabelCustomColorExamples,
LabelCustomColorPerfTest,
} from './LabelCustomColor.stories.tsx';
import GithubLink from '../../../.storybook/helpers/GithubLink';

<Meta title="Components/LabelCustomColor" component={LabelCustomColor} />

# LabelCustomColor

A wrapper for PatternFly's <a href="https://www.patternfly.org/v4/components/label">Label</a> component that supports
arbitrary custom CSS colors (e.g. hexadecimal) and ensures text will always be readable.

Applying an arbitrary color to a label presents the possibility of unreadable text due to insufficient color contrast.
This component solves the issue by applying the given color as a border color and using the
[tinycolor2](https://www.npmjs.com/package/tinycolor2) library to determine a lightened background color and darkened
text color (if necessary) in order to reach a color contrast ratio of at least 7:1. This ratio meets the "level AAA"
requirement of the [Web Content Accessibility Guidelines (WCAG)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-enhanced).

**Note: This adjustment means that multiple labels with very similar colors (especially dark colors) may be adjusted to look almost identical.**

All props of PatternFly's Label component are supported except the `variant` prop (only the default "filled" variant is supported).

## Examples

### Arbitrary Color Picker

Choose any color here to see how the readability adjustments apply to it.

<Canvas>
<Story story={LabelCustomColorPicker} />
</Canvas>

### Color Examples

<Canvas>
<Story story={LabelCustomColorExamples} />
</Canvas>

### Performance Test

The component maintains a global cache of the readability adjustments it makes for each color.
If labels of the same color are rendered multiple times on a page, each color only needs to be processed once.

<Canvas>
<Story story={LabelCustomColorPerfTest} />
</Canvas>

## Props

<ArgsTable of={LabelCustomColor} />

<GithubLink path="src/components/LabelCustomColor/LabelCustomColor.tsx" />
82 changes: 82 additions & 0 deletions src/components/LabelCustomColor/LabelCustomColor.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as React from 'react';
import { SketchPicker } from 'react-color';
import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing';
import { LabelCustomColor } from './LabelCustomColor';

export const LabelCustomColorPicker: React.FC = () => {
const [color, setColor] = React.useState('#4A90E2');
return (
<>
<LabelCustomColor color={color}>Label Text Here</LabelCustomColor>
<br />
<br />
<SketchPicker color={color} onChangeComplete={(newColor) => setColor(newColor.hex)} />
</>
);
};

// Colors from https://github.com/konveyor/tackle2-hub/blob/main/migration/v3/seed/main.go#L331-L766
const EXAMPLE_COLORS = [
'#773CF3',
'#74F33C',
'#F33CA9',
'#3CF342',
'#4EF33C',
'#F33CE6',
'#F3AC3C',
'#3CF367',
'#F3D23C',
'#B43CF3',
'#F3493C',
'#3C65F3',
'#3CF3E1',
'#3CF3A4',
'#F33C47',
'#F36F3C',
'#B1F33C',
'#F3E93C',
'#3C7CF3',
'#3C3FF3',
'#3CDFF3',
'#F33C6C',
'#D93CF3',
'#3CF37F',
'#3CF3CA',
'#F33CCF',
'#9AF33C',
'#F3953C',
'#D7F33C',
'#3CA2F3',
'#9C3CF3',
];

export const LabelCustomColorExamples: React.FC = () => (
<>
{EXAMPLE_COLORS.map((color) => (
<LabelCustomColor key={color} color={color} className={spacing.mXs}>
{color}
</LabelCustomColor>
))}
</>
);

export const LabelCustomColorPerfTest: React.FC = () => (
<>
{[
...EXAMPLE_COLORS,
...EXAMPLE_COLORS,
...EXAMPLE_COLORS,
...EXAMPLE_COLORS,
...EXAMPLE_COLORS,
...EXAMPLE_COLORS,
...EXAMPLE_COLORS,
...EXAMPLE_COLORS,
...EXAMPLE_COLORS,
...EXAMPLE_COLORS,
].map((color, index) => (
<LabelCustomColor key={`${index}-${color}`} color={color} className={spacing.mXs}>
{color}
</LabelCustomColor>
))}
</>
);
71 changes: 71 additions & 0 deletions src/components/LabelCustomColor/LabelCustomColor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import * as React from 'react';
import { Label, LabelProps } from '@patternfly/react-core';
import tinycolor from 'tinycolor2';

// Omit the variant prop, we won't support the outline variant
export interface ILabelCustomColorProps extends Omit<LabelProps, 'variant' | 'color'> {
color: string;
}

const globalColorCache: Record<
string,
{ borderColor: string; backgroundColor: string; textColor: string }
> = {};

export const LabelCustomColor: React.FC<ILabelCustomColorProps> = ({ color, ...props }) => {
const { borderColor, backgroundColor, textColor } = React.useMemo(() => {
if (globalColorCache[color]) return globalColorCache[color];
// Lighten the background 25%, and lighten it further if necessary until it can support readable text
const bgColorObj = tinycolor(color).lighten(25);
const blackTextReadability = () => tinycolor.readability(bgColorObj, '#000000');
const whiteTextReadability = () => tinycolor.readability(bgColorObj, '#FFFFFF');
while (blackTextReadability() < 9 && whiteTextReadability() < 9) {
bgColorObj.lighten(5);
}
// Darken or lighten the text color until it is sufficiently readable
const textColorObj = tinycolor(color);
while (tinycolor.readability(bgColorObj, textColorObj) < 7) {
if (blackTextReadability() > whiteTextReadability()) {
textColorObj.darken(5);
} else {
textColorObj.lighten(5);
}
}
globalColorCache[color] = {
borderColor: color,
backgroundColor: bgColorObj.toString(),
textColor: textColorObj.toString(),
};
return globalColorCache[color];
}, [color]);
return (
<Label
style={
{
'--pf-c-label__content--before--BorderColor': borderColor,
'--pf-c-label__content--link--hover--before--BorderColor': borderColor,
'--pf-c-label__content--link--focus--before--BorderColor': borderColor,
'--pf-c-label--BackgroundColor': backgroundColor,
'--pf-c-label__icon--Color': textColor,
'--pf-c-label__content--Color': textColor,
} as React.CSSProperties
}
{...props}
/>
);
};

/*
Note: if we were to support the outline variant of Label,
we would need to account for the following additional CSS variables:
--pf-c-label--m-outline__content--Color
--pf-c-label--m-outline__content--before--BorderColor
--pf-c-label--m-outline__content--link--hover--before--BorderColor
--pf-c-label--m-outline__content--link--focus--before--BorderColor
--pf-c-label--m-editable__content--before--BorderColor
--pf-c-label--m-editable__content--hover--before--BorderColor
--pf-c-label--m-editable__content--focus--before--BorderColor
*/
1 change: 1 addition & 0 deletions src/components/LabelCustomColor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './LabelCustomColor';
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './components/StatusIcon';
export * from './components/ValidatedTextInput';
export * from './components/ResolvedQuery';
export * from './components/LoadingEmptyState';
export * from './components/LabelCustomColor';

export * from './hooks/useSelectionState';
export * from './hooks/useFormState';
Expand Down
Loading

0 comments on commit 9817e08

Please sign in to comment.