Skip to content

Commit 3687fd3

Browse files
committed
feat: component optimizations, add custom classes support
1 parent f6a86ab commit 3687fd3

File tree

5 files changed

+174
-92
lines changed

5 files changed

+174
-92
lines changed

src/UsageBar.stories.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import * as React from "react"
22
import UsageBar from "./UsageBar"
33

4-
export default {
4+
export default {
55
title: "Usage Bar",
66
component: UsageBar,
7-
parameters: {}
7+
parameters: {},
88
}
99

1010
const items = [
@@ -33,13 +33,21 @@ const items = [
3333

3434
export const lightMode = () => <UsageBar items={items} total={100} />
3535

36-
export const withoutLabels = () => <UsageBar removeLabels items={items} total={100} />
36+
export const withoutLabels = () => (
37+
<UsageBar showLabels={false} showFallbackColors items={items} total={100} />
38+
)
3739

38-
export const withPercentages = () => <UsageBar showPercentage items={items} total={100} />
40+
export const withPercentages = () => (
41+
<UsageBar showPercentage showFallbackColors items={items} total={100} />
42+
)
3943

40-
export const compactLayout = () => <UsageBar showPercentage compactLayout items={items} total={100} />
44+
export const compactLayout = () => (
45+
<UsageBar showPercentage compactLayout items={items} total={100} />
46+
)
4147

42-
export const compactLayoutWithoutLabels = () => <UsageBar removeLabels compactLayout items={items} total={100} />
48+
export const compactLayoutWithoutLabels = () => (
49+
<UsageBar showLabels={false} compactLayout items={items} total={100} />
50+
)
4351

4452
export const error = () => (
4553
<>

src/UsageBar.tsx

Lines changed: 145 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import "./index.css"
33

44
interface Item {
55
color?: string
6+
className?: string
67
value: number
78
name: string
89
}
@@ -11,9 +12,17 @@ interface Props {
1112
items: Item[]
1213
total: number
1314
darkMode?: boolean
14-
removeLabels?: boolean
15+
showLabels?: boolean
1516
showPercentage?: boolean
1617
compactLayout?: boolean
18+
showFallbackColors?: boolean
19+
errorMessage?: string
20+
21+
usageBarContainerClassName?: string
22+
usageBarClassName?: string
23+
tooltipClassName?: string
24+
dotElementClassName?: string
25+
errorMessageClassName?: string
1726
}
1827

1928
const lightColors: string[] = [
@@ -44,16 +53,39 @@ const darkColors: string[] = [
4453
const getPercentageValue = (value: number, total: number): string =>
4554
`${((value / total) * 100).toFixed(0)}%`
4655

56+
const shuffleArray = (a: string[]) => {
57+
let j, x, i
58+
for (i = a.length - 1; i > 0; i--) {
59+
j = Math.floor(Math.random() * (i + 1))
60+
x = a[i]
61+
a[i] = a[j]
62+
a[j] = x
63+
}
64+
return a
65+
}
66+
67+
const appendCustomClass = (customClass?: string) => {
68+
if (customClass) {
69+
return customClass[0] === " " ? customClass : ` ${customClass}`
70+
}
71+
return ""
72+
}
73+
4774
const UsageBar: React.FC<Props> = ({
4875
darkMode = false,
49-
removeLabels = false,
76+
showLabels = true,
5077
showPercentage = false,
5178
compactLayout = false,
79+
showFallbackColors = false,
5280
total,
5381
items,
82+
errorMessage = "ERROR: Total elements values exceed 100%.",
83+
usageBarContainerClassName,
84+
usageBarClassName,
85+
tooltipClassName,
86+
dotElementClassName,
87+
errorMessageClassName,
5488
}) => {
55-
const [formattedItems, setFormattedItems] = React.useState<Item[]>([])
56-
5789
/**
5890
* Checks if the total value is equal or greater than the sum of all the elements values.
5991
*/
@@ -65,82 +97,84 @@ const UsageBar: React.FC<Props> = ({
6597
)
6698

6799
/**
68-
* Formats the items prop array providing a color to
69-
* elements without a defined one.
100+
* Returns an array of colors based on the value of `darkMode`.
101+
* The colors are either from the `darkColors` array or the `lightColors` array.
102+
* The chosen array is shuffled before being returned.
70103
*/
71-
const formatItemsArray = React.useCallback(() => {
72-
const selectedColors: string[] = []
104+
const fallbackColors = React.useMemo(() => {
73105
const colorsToPickFrom = darkMode ? [...darkColors] : [...lightColors]
106+
shuffleArray(colorsToPickFrom)
107+
return colorsToPickFrom
108+
}, [])
74109

75-
// For each element a random index is generated and then used to pick a value
76-
// from the colorsToPickFrom array; the selected value is removed by its original array
77-
// and it's pushed into the selectedColors one.
78-
for (let i = 0; i < items.length; i++) {
79-
const randIndex = Math.floor(Math.random() * colorsToPickFrom.length)
80-
const color = colorsToPickFrom[randIndex]
81-
selectedColors.push(color)
82-
colorsToPickFrom.splice(randIndex, 1)
83-
}
84-
85-
// Each element from the items array is formatted correctly
86-
// with a defined and valid color property.
87-
setFormattedItems(
88-
items.map((item: Item, index: number) => {
89-
return item.color ? item : { ...item, color: selectedColors[index] }
90-
})
91-
)
92-
}, [items, darkMode])
93-
94-
React.useEffect(() => {
95-
if (itemsValuesAreCorrect) {
96-
formatItemsArray()
97-
}
98-
}, [itemsValuesAreCorrect, formatItemsArray])
110+
/**
111+
* Returns the color of an element based on certain conditions.
112+
*
113+
* @param element - The item for which the color needs to be determined.
114+
* @param index - The index of the item in the list.
115+
* @returns The color of the element, either from the `color` property or a fallback
116+
* color from the `fallbackColors` array. If the `element` does not have a `color`
117+
* property and `showFallbackColors` is false, null is returned.
118+
*/
119+
const getElementColor = React.useCallback(
120+
(element: Item, index: number) => {
121+
if (element.color) return element.color
122+
return showFallbackColors
123+
? fallbackColors[index % fallbackColors.length]
124+
: null
125+
},
126+
[showFallbackColors, fallbackColors]
127+
)
99128

100129
if (!itemsValuesAreCorrect)
101130
return (
102-
<span className="u-UsageBar__error">
103-
ERROR: Elements values exceed the total.
131+
<span
132+
className={`u-UsageBar__error${appendCustomClass(
133+
errorMessageClassName
134+
)}`}
135+
>
136+
{errorMessage}
104137
</span>
105138
)
106139

107-
if (formattedItems.length === 0) return null
140+
if (items.length === 0) return null
108141

109142
if (compactLayout) {
110143
return (
111144
<div
112145
className={`c-UsageBar c-UsageBar__compact ${
113146
darkMode ? "u-UsageBar-dark" : "u-UsageBar-light"
114-
}`}
147+
}${appendCustomClass(usageBarContainerClassName)}`}
115148
>
116-
<div className="o-UsageBar__bar o-UsageBar__compact__bar">
117-
{formattedItems.map((element: Item, index: number) => {
118-
return (
119-
<div
120-
key={index}
121-
className="o-UsageBar__bar__element"
122-
style={{
123-
width: getPercentageValue(element.value, total),
124-
backgroundColor: element.color,
125-
}}
126-
/>
127-
)
128-
})}
149+
<div
150+
className={`o-UsageBar__bar o-UsageBar__compact__bar${appendCustomClass(
151+
usageBarClassName
152+
)}`}
153+
>
154+
{items.map((element: Item, index: number) => (
155+
<UsageBarElement
156+
element={element}
157+
color={getElementColor(element, index)}
158+
total={total}
159+
key={index}
160+
/>
161+
))}
129162
</div>
130-
{!removeLabels && (
163+
{showLabels && (
131164
<div className="o-UsageBar__bar__elements__labels__container">
132-
{formattedItems.map((element: Item, index: number) => {
165+
{items.map((element: Item, index: number) => {
166+
const color = getElementColor(element, index)
133167
return (
134168
<div key={index} className="o-UsageBar__bar__elements__label">
135169
<div
136-
className="o-UsageBar__bar__elements__label--dot"
137-
style={{ backgroundColor: element.color }}
170+
className={`o-UsageBar__bar__elements__label--dot ${appendCustomClass(
171+
dotElementClassName
172+
)}`}
173+
style={color ? { backgroundColor: color } : {}}
138174
/>
139175
<span>{element.name}</span>
140176
{showPercentage && (
141-
<span className="o-UsageBar__bar__tooltip__percentage">
142-
{getPercentageValue(element.value, total)}
143-
</span>
177+
<UsageBarPercentageLabel element={element} total={total} />
144178
)}
145179
</div>
146180
)
@@ -155,35 +189,65 @@ const UsageBar: React.FC<Props> = ({
155189
<div
156190
className={`c-UsageBar ${
157191
darkMode ? "u-UsageBar-dark" : "u-UsageBar-light"
158-
}`}
192+
}${appendCustomClass(usageBarContainerClassName)}`}
159193
>
160-
<div className="o-UsageBar__bar">
161-
{formattedItems.map((element: Item, index: number) => {
162-
return (
163-
<div
164-
key={index}
165-
className="o-UsageBar__bar__element"
166-
style={{
167-
width: getPercentageValue(element.value, total),
168-
backgroundColor: element.color,
169-
}}
170-
>
171-
{!removeLabels && (
172-
<div className="o-UsageBar__bar__tooltip">
173-
<span>{element.name}</span>
174-
{showPercentage && (
175-
<span className="o-UsageBar__bar__tooltip__percentage">
176-
{getPercentageValue(element.value, total)}
177-
</span>
178-
)}
179-
</div>
180-
)}
181-
</div>
182-
)
183-
})}
194+
<div className={`o-UsageBar__bar${appendCustomClass(usageBarClassName)}`}>
195+
{items.map((element: Item, index: number) => (
196+
<UsageBarElement
197+
element={element}
198+
color={getElementColor(element, index)}
199+
total={total}
200+
key={index}
201+
>
202+
{showLabels && (
203+
<div
204+
className={`o-UsageBar__bar__tooltip${appendCustomClass(
205+
tooltipClassName
206+
)}`}
207+
>
208+
<span>{element.name}</span>
209+
{showPercentage && (
210+
<UsageBarPercentageLabel element={element} total={total} />
211+
)}
212+
</div>
213+
)}
214+
</UsageBarElement>
215+
))}
184216
</div>
185217
</div>
186218
)
187219
}
188220

221+
const UsageBarElement: React.FC<{
222+
element: Item
223+
color: string | null
224+
total: number
225+
children?: React.ReactNode
226+
}> = ({ element, color, total, children }) => {
227+
return (
228+
<div
229+
className={`o-UsageBar__bar__element${appendCustomClass(
230+
element.className
231+
)}`}
232+
style={{
233+
width: getPercentageValue(element.value, total),
234+
...(color ? { backgroundColor: color } : null),
235+
}}
236+
>
237+
{children}
238+
</div>
239+
)
240+
}
241+
242+
const UsageBarPercentageLabel: React.FC<{ element: Item; total: number }> = ({
243+
element,
244+
total,
245+
}) => {
246+
return (
247+
<span className="o-UsageBar__bar__tooltip__percentage">
248+
{getPercentageValue(element.value, total)}
249+
</span>
250+
)
251+
}
252+
189253
export default UsageBar

src/UsageBarDark.stories.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const darkMode = () => (
4141

4242
export const darkModeWithoutLabels = () => (
4343
<div style={mainDarkModeContainerStyle}>
44-
<UsageBar removeLabels darkMode items={items} total={100} />
44+
<UsageBar showLabels={false} darkMode items={items} total={100} />
4545
</div>
4646
)
4747

@@ -59,7 +59,13 @@ export const darkModeCompact = () => (
5959

6060
export const darkModeCompactWithoutLabels = () => (
6161
<div style={mainDarkModeContainerStyle}>
62-
<UsageBar removeLabels compactLayout darkMode items={items} total={100} />
62+
<UsageBar
63+
showLabels={false}
64+
compactLayout
65+
darkMode
66+
items={items}
67+
total={100}
68+
/>
6369
</div>
6470
)
6571

src/index.css

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
}
104104
.o-UsageBar__bar__tooltip__percentage {
105105
opacity: 0.78;
106-
font-size: 0.9em;
106+
font-size: 0.85em;
107107
margin: 0 0 0 3pt;
108108
}
109109
.o-UsageBar__bar__tooltip::after {
@@ -128,20 +128,22 @@
128128
.o-UsageBar__bar__elements__label {
129129
display: flex;
130130
justify-content: flex-start;
131-
align-items: center;
132131
flex-direction: row;
133132
}
134133
.o-UsageBar__bar__elements__labels__container {
135134
margin-top: 12pt;
136135
position: relative;
137136
width: 100%;
138137
max-width: 480px;
138+
align-items: center;
139139
flex-wrap: wrap;
140140
gap: 12pt;
141141
font-size: 0.9em;
142142
}
143143
.o-UsageBar__bar__elements__label {
144+
align-items: flex-end;
144145
flex-wrap: nowrap;
146+
line-height: 1;
145147
}
146148
.o-UsageBar__bar__elements__label > span,
147149
.o-UsageBar__bar__elements__label > .o-UsageBar__bar__tooltip__percentage {
@@ -152,4 +154,5 @@
152154
margin-right: 6pt;
153155
height: 8pt;
154156
width: 8pt;
157+
border: 1px solid var(--background-bar-color);
155158
}

0 commit comments

Comments
 (0)