diff --git a/package.json b/package.json index 5da609a..60be73c 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" } } diff --git a/src/components/LabelCustomColor/LabelCustomColor.stories.mdx b/src/components/LabelCustomColor/LabelCustomColor.stories.mdx new file mode 100644 index 0000000..709505b --- /dev/null +++ b/src/components/LabelCustomColor/LabelCustomColor.stories.mdx @@ -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'; + + + +# LabelCustomColor + +A wrapper for PatternFly's Label 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. + + + + + +### Color Examples + + + + + +### 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. + + + + + +## Props + + + + diff --git a/src/components/LabelCustomColor/LabelCustomColor.stories.tsx b/src/components/LabelCustomColor/LabelCustomColor.stories.tsx new file mode 100644 index 0000000..134582e --- /dev/null +++ b/src/components/LabelCustomColor/LabelCustomColor.stories.tsx @@ -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 ( + <> + Label Text Here +
+
+ 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) => ( + + {color} + + ))} + +); + +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) => ( + + {color} + + ))} + +); diff --git a/src/components/LabelCustomColor/LabelCustomColor.tsx b/src/components/LabelCustomColor/LabelCustomColor.tsx new file mode 100644 index 0000000..2a4860c --- /dev/null +++ b/src/components/LabelCustomColor/LabelCustomColor.tsx @@ -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 { + color: string; +} + +const globalColorCache: Record< + string, + { borderColor: string; backgroundColor: string; textColor: string } +> = {}; + +export const LabelCustomColor: React.FC = ({ 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 ( +