From 3687fd313801849d19ba14c926f12cd7e55be26e Mon Sep 17 00:00:00 2001 From: ChrisUser Date: Sat, 25 Nov 2023 09:55:06 +0100 Subject: [PATCH] feat: component optimizations, add custom classes support --- src/UsageBar.stories.tsx | 20 +++- src/UsageBar.tsx | 226 ++++++++++++++++++++++------------- src/UsageBarDark.stories.tsx | 10 +- src/index.css | 7 +- tsconfig.json | 3 +- 5 files changed, 174 insertions(+), 92 deletions(-) diff --git a/src/UsageBar.stories.tsx b/src/UsageBar.stories.tsx index 0e3187d..5adc59b 100644 --- a/src/UsageBar.stories.tsx +++ b/src/UsageBar.stories.tsx @@ -1,10 +1,10 @@ import * as React from "react" import UsageBar from "./UsageBar" -export default { +export default { title: "Usage Bar", component: UsageBar, - parameters: {} + parameters: {}, } const items = [ @@ -33,13 +33,21 @@ const items = [ export const lightMode = () => -export const withoutLabels = () => +export const withoutLabels = () => ( + +) -export const withPercentages = () => +export const withPercentages = () => ( + +) -export const compactLayout = () => +export const compactLayout = () => ( + +) -export const compactLayoutWithoutLabels = () => +export const compactLayoutWithoutLabels = () => ( + +) export const error = () => ( <> diff --git a/src/UsageBar.tsx b/src/UsageBar.tsx index 6a7223b..9ba0778 100644 --- a/src/UsageBar.tsx +++ b/src/UsageBar.tsx @@ -3,6 +3,7 @@ import "./index.css" interface Item { color?: string + className?: string value: number name: string } @@ -11,9 +12,17 @@ interface Props { items: Item[] total: number darkMode?: boolean - removeLabels?: boolean + showLabels?: boolean showPercentage?: boolean compactLayout?: boolean + showFallbackColors?: boolean + errorMessage?: string + + usageBarContainerClassName?: string + usageBarClassName?: string + tooltipClassName?: string + dotElementClassName?: string + errorMessageClassName?: string } const lightColors: string[] = [ @@ -44,16 +53,39 @@ const darkColors: string[] = [ const getPercentageValue = (value: number, total: number): string => `${((value / total) * 100).toFixed(0)}%` +const shuffleArray = (a: string[]) => { + let j, x, i + for (i = a.length - 1; i > 0; i--) { + j = Math.floor(Math.random() * (i + 1)) + x = a[i] + a[i] = a[j] + a[j] = x + } + return a +} + +const appendCustomClass = (customClass?: string) => { + if (customClass) { + return customClass[0] === " " ? customClass : ` ${customClass}` + } + return "" +} + const UsageBar: React.FC = ({ darkMode = false, - removeLabels = false, + showLabels = true, showPercentage = false, compactLayout = false, + showFallbackColors = false, total, items, + errorMessage = "ERROR: Total elements values exceed 100%.", + usageBarContainerClassName, + usageBarClassName, + tooltipClassName, + dotElementClassName, + errorMessageClassName, }) => { - const [formattedItems, setFormattedItems] = React.useState([]) - /** * Checks if the total value is equal or greater than the sum of all the elements values. */ @@ -65,82 +97,84 @@ const UsageBar: React.FC = ({ ) /** - * Formats the items prop array providing a color to - * elements without a defined one. + * Returns an array of colors based on the value of `darkMode`. + * The colors are either from the `darkColors` array or the `lightColors` array. + * The chosen array is shuffled before being returned. */ - const formatItemsArray = React.useCallback(() => { - const selectedColors: string[] = [] + const fallbackColors = React.useMemo(() => { const colorsToPickFrom = darkMode ? [...darkColors] : [...lightColors] + shuffleArray(colorsToPickFrom) + return colorsToPickFrom + }, []) - // For each element a random index is generated and then used to pick a value - // from the colorsToPickFrom array; the selected value is removed by its original array - // and it's pushed into the selectedColors one. - for (let i = 0; i < items.length; i++) { - const randIndex = Math.floor(Math.random() * colorsToPickFrom.length) - const color = colorsToPickFrom[randIndex] - selectedColors.push(color) - colorsToPickFrom.splice(randIndex, 1) - } - - // Each element from the items array is formatted correctly - // with a defined and valid color property. - setFormattedItems( - items.map((item: Item, index: number) => { - return item.color ? item : { ...item, color: selectedColors[index] } - }) - ) - }, [items, darkMode]) - - React.useEffect(() => { - if (itemsValuesAreCorrect) { - formatItemsArray() - } - }, [itemsValuesAreCorrect, formatItemsArray]) + /** + * Returns the color of an element based on certain conditions. + * + * @param element - The item for which the color needs to be determined. + * @param index - The index of the item in the list. + * @returns The color of the element, either from the `color` property or a fallback + * color from the `fallbackColors` array. If the `element` does not have a `color` + * property and `showFallbackColors` is false, null is returned. + */ + const getElementColor = React.useCallback( + (element: Item, index: number) => { + if (element.color) return element.color + return showFallbackColors + ? fallbackColors[index % fallbackColors.length] + : null + }, + [showFallbackColors, fallbackColors] + ) if (!itemsValuesAreCorrect) return ( - - ERROR: Elements values exceed the total. + + {errorMessage} ) - if (formattedItems.length === 0) return null + if (items.length === 0) return null if (compactLayout) { return (
-
- {formattedItems.map((element: Item, index: number) => { - return ( -
- ) - })} +
+ {items.map((element: Item, index: number) => ( + + ))}
- {!removeLabels && ( + {showLabels && (
- {formattedItems.map((element: Item, index: number) => { + {items.map((element: Item, index: number) => { + const color = getElementColor(element, index) return (
{element.name} {showPercentage && ( - - {getPercentageValue(element.value, total)} - + )}
) @@ -155,35 +189,65 @@ const UsageBar: React.FC = ({
-
- {formattedItems.map((element: Item, index: number) => { - return ( -
- {!removeLabels && ( -
- {element.name} - {showPercentage && ( - - {getPercentageValue(element.value, total)} - - )} -
- )} -
- ) - })} +
+ {items.map((element: Item, index: number) => ( + + {showLabels && ( +
+ {element.name} + {showPercentage && ( + + )} +
+ )} +
+ ))}
) } +const UsageBarElement: React.FC<{ + element: Item + color: string | null + total: number + children?: React.ReactNode +}> = ({ element, color, total, children }) => { + return ( +
+ {children} +
+ ) +} + +const UsageBarPercentageLabel: React.FC<{ element: Item; total: number }> = ({ + element, + total, +}) => { + return ( + + {getPercentageValue(element.value, total)} + + ) +} + export default UsageBar diff --git a/src/UsageBarDark.stories.tsx b/src/UsageBarDark.stories.tsx index f65b58e..502ffbd 100644 --- a/src/UsageBarDark.stories.tsx +++ b/src/UsageBarDark.stories.tsx @@ -41,7 +41,7 @@ export const darkMode = () => ( export const darkModeWithoutLabels = () => (
- +
) @@ -59,7 +59,13 @@ export const darkModeCompact = () => ( export const darkModeCompactWithoutLabels = () => (
- +
) diff --git a/src/index.css b/src/index.css index 74c820e..383c595 100644 --- a/src/index.css +++ b/src/index.css @@ -103,7 +103,7 @@ } .o-UsageBar__bar__tooltip__percentage { opacity: 0.78; - font-size: 0.9em; + font-size: 0.85em; margin: 0 0 0 3pt; } .o-UsageBar__bar__tooltip::after { @@ -128,7 +128,6 @@ .o-UsageBar__bar__elements__label { display: flex; justify-content: flex-start; - align-items: center; flex-direction: row; } .o-UsageBar__bar__elements__labels__container { @@ -136,12 +135,15 @@ position: relative; width: 100%; max-width: 480px; + align-items: center; flex-wrap: wrap; gap: 12pt; font-size: 0.9em; } .o-UsageBar__bar__elements__label { + align-items: flex-end; flex-wrap: nowrap; + line-height: 1; } .o-UsageBar__bar__elements__label > span, .o-UsageBar__bar__elements__label > .o-UsageBar__bar__tooltip__percentage { @@ -152,4 +154,5 @@ margin-right: 6pt; height: 8pt; width: 8pt; + border: 1px solid var(--background-bar-color); } diff --git a/tsconfig.json b/tsconfig.json index fa64dcc..f517a46 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "compilerOptions": { "jsx": "react", "declaration": true, - "declarationDir": "./build" + "declarationDir": "./build", + "strict": true } }