From 891f518f5dba8276c6e4378bc1cde66efd1fac64 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 2 Oct 2023 15:03:40 -0400 Subject: [PATCH 1/2] feat(useStorage): Add isEnabled option to useStorage/useLocalStorage/useSessionStorage via refactor to use options objects (#140) * feat(useStorage): Add isEnabled option to useStorage/useLocalStorage/useSessionStorage via refactor to use options objects BREAKING CHANGE: useLocalStorage and useSessionStorage (via useStorage) require an object of named options instead of multiple function arguments Signed-off-by: Mike Turley * Fix broken storybook example Signed-off-by: Mike Turley --------- Signed-off-by: Mike Turley --- .../useStorage/useLocalStorage.stories.mdx | 2 +- .../useStorage/useLocalStorage.stories.tsx | 31 ++++++++++---- .../useStorage/useSessionStorage.stories.mdx | 2 +- .../useStorage/useSessionStorage.stories.tsx | 6 +-- src/hooks/useStorage/useStorage.ts | 41 +++++++++++-------- 5 files changed, 52 insertions(+), 30 deletions(-) diff --git a/src/hooks/useStorage/useLocalStorage.stories.mdx b/src/hooks/useStorage/useLocalStorage.stories.mdx index 1557d4f..c2b4793 100644 --- a/src/hooks/useStorage/useLocalStorage.stories.mdx +++ b/src/hooks/useStorage/useLocalStorage.stories.mdx @@ -33,7 +33,7 @@ and it supports TypeScript [generics](https://www.typescriptlang.org/docs/handbo infer its return types based on the `defaultValue`. ```ts -const [value, setValue] = useLocalStorage(key: string, defaultValue: T); +const [value, setValue] = useLocalStorage({ key: string, defaultValue: T }); ``` ## Notes diff --git a/src/hooks/useStorage/useLocalStorage.stories.tsx b/src/hooks/useStorage/useLocalStorage.stories.tsx index f54d461..a42a68d 100644 --- a/src/hooks/useStorage/useLocalStorage.stories.tsx +++ b/src/hooks/useStorage/useLocalStorage.stories.tsx @@ -20,7 +20,7 @@ import { ValidatedTextInput } from '../../components/ValidatedTextInput'; import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; export const PersistentCounterExample: React.FunctionComponent = () => { - const [count, setCount] = useLocalStorage('exampleCounter', 0); + const [count, setCount] = useLocalStorage({ key: 'exampleCounter', defaultValue: 0 }); return ( { }; export const PersistentTextFieldExample: React.FunctionComponent = () => { - const [value, setValue] = useLocalStorage('exampleTextField', ''); + const [value, setValue] = useLocalStorage({ key: 'exampleTextField', defaultValue: '' }); return ( { }; export const PersistentCheckboxExample: React.FunctionComponent = () => { - const [isChecked, setIsChecked] = useLocalStorage('exampleCheckboxChecked', false); + const [isChecked, setIsChecked] = useLocalStorage({ + key: 'exampleCheckboxChecked', + defaultValue: false, + }); return ( { export const WelcomeModalExample: React.FunctionComponent = () => { const ExamplePage: React.FunctionComponent = () => { - const [isModalDisabled, setIsModalDisabled] = useLocalStorage('welcomeModalDisabled', false); + const [isModalDisabled, setIsModalDisabled] = useLocalStorage({ + key: 'welcomeModalDisabled', + defaultValue: false, + }); const [isModalOpen, setIsModalOpen] = React.useState(!isModalDisabled); return ( <> @@ -121,7 +127,10 @@ export const WelcomeModalExample: React.FunctionComponent = () => { export const ReusedKeyExample: React.FunctionComponent = () => { // In a real app each of these components would be in separate files. const ComponentA: React.FunctionComponent = () => { - const [value, setValue] = useLocalStorage('exampleReusedKey', 'default value here'); + const [value, setValue] = useLocalStorage({ + key: 'exampleReusedKey', + defaultValue: 'default value here', + }); return (
@@ -136,7 +145,10 @@ export const ReusedKeyExample: React.FunctionComponent = () => { ); }; const ComponentB: React.FunctionComponent = () => { - const [value] = useLocalStorage('exampleReusedKey', 'default value here'); + const [value] = useLocalStorage({ + key: 'exampleReusedKey', + defaultValue: 'default value here', + }); return (
@@ -157,7 +169,8 @@ export const ReusedKeyExample: React.FunctionComponent = () => { export const CustomHookExample: React.FunctionComponent = () => { // This could be exported from its own file and imported in multiple component files. - const useMyStoredValue = () => useLocalStorage('myStoredValue', 'default defined once'); + const useMyStoredValue = () => + useLocalStorage({ key: 'myStoredValue', defaultValue: 'default defined once' }); // In a real app each of these components would be in separate files. const ComponentA: React.FunctionComponent = () => { @@ -176,7 +189,7 @@ export const CustomHookExample: React.FunctionComponent = () => { ); }; const ComponentB: React.FunctionComponent = () => { - const [value] = useLocalStorage('exampleReusedKey', 'default value here'); + const [value] = useMyStoredValue(); return (
@@ -197,7 +210,7 @@ export const CustomHookExample: React.FunctionComponent = () => { export const ComplexValueExample: React.FunctionComponent = () => { type Item = { name: string; description?: string }; - const [items, setItems] = useLocalStorage('exampleArray', []); + const [items, setItems] = useLocalStorage({ key: 'exampleArray', defaultValue: [] }); const addForm = useFormState({ name: useFormField('', yup.string().required().label('Name')), diff --git a/src/hooks/useStorage/useSessionStorage.stories.mdx b/src/hooks/useStorage/useSessionStorage.stories.mdx index 37f0306..aba4e79 100644 --- a/src/hooks/useStorage/useSessionStorage.stories.mdx +++ b/src/hooks/useStorage/useSessionStorage.stories.mdx @@ -28,7 +28,7 @@ and it supports TypeScript [generics](https://www.typescriptlang.org/docs/handbo infer its return types based on the `defaultValue`. ```ts -const [value, setValue] = useSessionStorage(key: string, defaultValue: T); +const [value, setValue] = useSessionStorage({ key: string, defaultValue: T }); ``` ## Notes diff --git a/src/hooks/useStorage/useSessionStorage.stories.tsx b/src/hooks/useStorage/useSessionStorage.stories.tsx index 1809ff8..b33a815 100644 --- a/src/hooks/useStorage/useSessionStorage.stories.tsx +++ b/src/hooks/useStorage/useSessionStorage.stories.tsx @@ -18,7 +18,7 @@ import { ValidatedTextInput } from '../../components/ValidatedTextInput'; import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; export const PersistentCounterExample: React.FunctionComponent = () => { - const [count, setCount] = useSessionStorage('exampleCounter', 0); + const [count, setCount] = useSessionStorage({ key: 'exampleCounter', defaultValue: 0 }); return ( { }; export const PersistentTextFieldExample: React.FunctionComponent = () => { - const [value, setValue] = useSessionStorage('exampleTextField', ''); + const [value, setValue] = useSessionStorage({ key: 'exampleTextField', defaultValue: '' }); return ( { export const ComplexValueExample: React.FunctionComponent = () => { type Item = { name: string; description?: string }; - const [items, setItems] = useSessionStorage('exampleArray', []); + const [items, setItems] = useSessionStorage({ key: 'exampleArray', defaultValue: [] }); const addForm = useFormState({ name: useFormField('', yup.string().required().label('Name')), diff --git a/src/hooks/useStorage/useStorage.ts b/src/hooks/useStorage/useStorage.ts index 7e4805b..06e8152 100644 --- a/src/hooks/useStorage/useStorage.ts +++ b/src/hooks/useStorage/useStorage.ts @@ -35,30 +35,38 @@ const setValueInStorage = (storageType: StorageType, key: string, newValue: T } }; -const useStorage = ( - storageType: StorageType, - key: string, - defaultValue: T -): [T, React.Dispatch>] => { +interface IUseStorageOptions { + isEnabled?: boolean; + type: StorageType; + key: string; + defaultValue: T; +} + +const useStorage = ({ + isEnabled = true, + type, + key, + defaultValue, +}: IUseStorageOptions): [T, React.Dispatch>] => { const [cachedValue, setCachedValue] = React.useState( - getValueFromStorage(storageType, key, defaultValue) + getValueFromStorage(type, key, defaultValue) ); - const usingStorageEvents = storageType === 'localStorage' && typeof window !== 'undefined'; + const usingStorageEvents = type === 'localStorage' && typeof window !== 'undefined' && isEnabled; const setValue: React.Dispatch> = React.useCallback( (newValueOrFn: T | ((prevState: T) => T)) => { const newValue = newValueOrFn instanceof Function - ? newValueOrFn(getValueFromStorage(storageType, key, defaultValue)) + ? newValueOrFn(getValueFromStorage(type, key, defaultValue)) : newValueOrFn; - setValueInStorage(storageType, key, newValue); + setValueInStorage(type, key, newValue); if (!usingStorageEvents) { // The cache won't update automatically if there is no StorageEvent dispatched. setCachedValue(newValue); } }, - [storageType, key, defaultValue, usingStorageEvents] + [type, key, defaultValue, usingStorageEvents] ); React.useEffect(() => { @@ -77,12 +85,13 @@ const useStorage = ( return [cachedValue, setValue]; }; +export type UseStorageTypeOptions = Omit, 'type'>; + export const useLocalStorage = ( - key: string, - defaultValue: T -): [T, React.Dispatch>] => useStorage('localStorage', key, defaultValue); + options: UseStorageTypeOptions +): [T, React.Dispatch>] => useStorage({ ...options, type: 'localStorage' }); export const useSessionStorage = ( - key: string, - defaultValue: T -): [T, React.Dispatch>] => useStorage('sessionStorage', key, defaultValue); + options: UseStorageTypeOptions +): [T, React.Dispatch>] => + useStorage({ ...options, type: 'sessionStorage' }); From 0cb74ce6d4de236abe1b7d20bbe747ceca392f28 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 4 Oct 2023 20:31:10 -0400 Subject: [PATCH 2/2] refactor(labelcustomcolor): remove LabelCustomColor (moving it to tackle2-ui) (#141) --- .../LabelCustomColor.stories.mdx | 56 ------------- .../LabelCustomColor.stories.tsx | 79 ------------------- .../LabelCustomColor/LabelCustomColor.tsx | 71 ----------------- src/components/LabelCustomColor/index.ts | 1 - src/index.ts | 1 - 5 files changed, 208 deletions(-) delete mode 100644 src/components/LabelCustomColor/LabelCustomColor.stories.mdx delete mode 100644 src/components/LabelCustomColor/LabelCustomColor.stories.tsx delete mode 100644 src/components/LabelCustomColor/LabelCustomColor.tsx delete mode 100644 src/components/LabelCustomColor/index.ts diff --git a/src/components/LabelCustomColor/LabelCustomColor.stories.mdx b/src/components/LabelCustomColor/LabelCustomColor.stories.mdx deleted file mode 100644 index 709505b..0000000 --- a/src/components/LabelCustomColor/LabelCustomColor.stories.mdx +++ /dev/null @@ -1,56 +0,0 @@ -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 deleted file mode 100644 index 8389a83..0000000 --- a/src/components/LabelCustomColor/LabelCustomColor.stories.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import * as React from 'react'; -import { SketchPicker } from 'react-color'; -import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing'; -import { - global_palette_black_1000 as black, - global_palette_black_500 as gray, - global_palette_blue_300 as blue, - global_palette_green_300 as green, - global_palette_cyan_300 as cyan, - global_palette_purple_600 as purple, - global_palette_gold_300 as gold, - global_palette_orange_300 as orange, -} from '@patternfly/react-tokens'; - -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://sashamaps.net/docs/resources/20-colors/ with some colors removed for being too bright -// and PF global pallete colors used in place of their closest counterparts -const EXAMPLE_COLORS = [ - '#D95F55', // Red (PF red is weird because 100 is too close to Maroon and 50 is too bright) - green.value, // Green - gold.value, // Gold - blue.value, // Blue - orange.value, // Orange - purple.value, // Purple - cyan.value, // Cyan - '#F032E6', // Magenta - '#BFEF45', // Lime - '#469990', // Teal - '#9A6324', // Brown - '#800000', // Maroon - '#808000', // Olive - '#000075', // Navy - gray.value, // Gray - black.value, // Black -]; - -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 deleted file mode 100644 index fe53ff2..0000000 --- a/src/components/LabelCustomColor/LabelCustomColor.tsx +++ /dev/null @@ -1,71 +0,0 @@ -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 30%, and lighten it further if necessary until it can support readable text - const bgColorObj = tinycolor(color).lighten(30); - 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 ( -