Skip to content

Commit 2f58ae2

Browse files
authored
feat(theme): adding theme support per component (#500)
* feat(theme): accordion theme component * feat(theme): alert theme component * feat(theme): badge theme component * feat(theme): avatar theme component * feat(theme): avartargroup and avatargroupcounter theme component * fix(avatar): fix avatar* theme * feat(theme): breadcrumb theme component * fix(avatargroup): remove mb-5 closes #467 * feat(theme): card theme component * test(avatar): adding missing unit tests
1 parent d2206b5 commit 2f58ae2

22 files changed

+430
-213
lines changed

src/docs/pages/ThemePage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import { Flowbite } from '../../lib/components';
88
import type { CustomFlowbiteTheme } from '../../lib/components/Flowbite/FlowbiteTheme';
99

1010
const ThemePage: FC = () => {
11-
const theme: CustomFlowbiteTheme = { alert: { color: { primary: 'bg-primary' } } };
11+
const theme: CustomFlowbiteTheme = { alert: { root: { color: { primary: 'bg-primary' } } } };
1212

1313
return (
14-
<div className="mx-auto flex max-w-4xl flex-col gap-8 dark:text-white">
14+
<div className="flex flex-col max-w-4xl gap-8 mx-auto dark:text-white">
1515
<div className="flex flex-col gap-2">
1616
<span className="text-2xl font-bold">Theme</span>
1717
<div className="py-4">

src/lib/components/Accordion/Accordion.spec.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ describe('Components / Accordion', () => {
115115
it('should use custom `base` classes', () => {
116116
const theme = {
117117
accordion: {
118-
base: 'text-4xl',
118+
root: {
119+
base: 'text-4xl',
120+
},
119121
},
120122
};
121123

@@ -131,9 +133,11 @@ describe('Components / Accordion', () => {
131133
it('should use custom `flush` classes', () => {
132134
const theme = {
133135
accordion: {
134-
flush: {
135-
off: 'text-4xl',
136-
on: 'text-3xl',
136+
root: {
137+
flush: {
138+
off: 'text-4xl',
139+
on: 'text-3xl',
140+
},
137141
},
138142
},
139143
};

src/lib/components/Accordion/Accordion.tsx

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,24 @@ import classNames from 'classnames';
22
import type { ComponentProps, FC, PropsWithChildren, ReactElement } from 'react';
33
import { Children, cloneElement, useMemo, useState } from 'react';
44
import { HiChevronDown } from 'react-icons/hi';
5+
import { DeepPartial } from '..';
6+
import { mergeDeep } from '../../helpers/mergeDeep';
57
import { FlowbiteBoolean } from '../Flowbite/FlowbiteTheme';
68
import { useTheme } from '../Flowbite/ThemeContext';
7-
import { AccordionContent } from './AccordionContent';
9+
import { AccordionContent, FlowbiteAccordionComponentTheme } from './AccordionContent';
810
import type { AccordionPanelProps } from './AccordionPanel';
911
import { AccordionPanel } from './AccordionPanel';
10-
import { AccordionTitle } from './AccordionTitle';
12+
import { AccordionTitle, FlowbiteAccordionTitleTheme } from './AccordionTitle';
1113

1214
export interface FlowbiteAccordionTheme {
15+
root: FlowbiteAccordionRootTheme;
16+
content: FlowbiteAccordionComponentTheme;
17+
title: FlowbiteAccordionTitleTheme;
18+
}
19+
20+
export interface FlowbiteAccordionRootTheme {
1321
base: string;
14-
content: {
15-
base: string;
16-
};
1722
flush: FlowbiteBoolean;
18-
title: {
19-
arrow: {
20-
base: string;
21-
open: {
22-
off: string;
23-
on: string;
24-
};
25-
};
26-
base: string;
27-
flush: FlowbiteBoolean;
28-
heading: string;
29-
open: FlowbiteBoolean;
30-
};
3123
}
3224

3325
export interface AccordionProps extends PropsWithChildren<ComponentProps<'div'>> {
@@ -36,6 +28,7 @@ export interface AccordionProps extends PropsWithChildren<ComponentProps<'div'>>
3628
children: ReactElement<AccordionPanelProps> | ReactElement<AccordionPanelProps>[];
3729
flush?: boolean;
3830
collapseAll?: boolean;
31+
theme?: DeepPartial<FlowbiteAccordionRootTheme>;
3932
}
4033

4134
const AccordionComponent: FC<AccordionProps> = ({
@@ -45,6 +38,7 @@ const AccordionComponent: FC<AccordionProps> = ({
4538
flush = false,
4639
collapseAll = false,
4740
className,
41+
theme: customTheme = {},
4842
...props
4943
}): JSX.Element => {
5044
const [isOpen, setOpen] = useState(collapseAll ? -1 : 0);
@@ -55,7 +49,8 @@ const AccordionComponent: FC<AccordionProps> = ({
5549
),
5650
[alwaysOpen, arrowIcon, children, flush, isOpen],
5751
);
58-
const theme = useTheme().theme.accordion;
52+
53+
const theme = mergeDeep(useTheme().theme.accordion.root, customTheme);
5954

6055
return (
6156
<div

src/lib/components/Accordion/AccordionContent.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
11
import classNames from 'classnames';
2-
import type { ComponentProps, FC } from 'react';
2+
import type { ComponentProps, FC, PropsWithChildren } from 'react';
3+
import { DeepPartial } from '..';
4+
import { mergeDeep } from '../../helpers/mergeDeep';
35
import { useTheme } from '../Flowbite/ThemeContext';
46
import { useAccordionContext } from './AccordionPanelContext';
57

6-
export const AccordionContent: FC<ComponentProps<'div'>> = ({ children, className, ...props }): JSX.Element => {
8+
export interface FlowbiteAccordionComponentTheme {
9+
base: string;
10+
}
11+
12+
export interface AccordionContentProps extends PropsWithChildren<ComponentProps<'div'>> {
13+
theme?: DeepPartial<FlowbiteAccordionComponentTheme>;
14+
}
15+
16+
export const AccordionContent: FC<AccordionContentProps> = ({
17+
children,
18+
className,
19+
theme: customTheme = {},
20+
...props
21+
}): JSX.Element => {
722
const { isOpen } = useAccordionContext();
8-
const theme = useTheme().theme.accordion.content;
23+
24+
const theme = mergeDeep(useTheme().theme.accordion.content, customTheme);
925

1026
return (
1127
<div

src/lib/components/Accordion/AccordionTitle.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,43 @@
11
import classNames from 'classnames';
22
import type { ComponentProps, FC } from 'react';
3-
import type { FlowbiteHeadingLevel } from '../Flowbite/FlowbiteTheme';
3+
import { DeepPartial } from '..';
4+
import { mergeDeep } from '../../helpers/mergeDeep';
5+
import type { FlowbiteBoolean, FlowbiteHeadingLevel } from '../Flowbite/FlowbiteTheme';
46
import { useTheme } from '../Flowbite/ThemeContext';
57
import { useAccordionContext } from './AccordionPanelContext';
68

9+
export interface FlowbiteAccordionTitleTheme {
10+
arrow: {
11+
base: string;
12+
open: {
13+
off: string;
14+
on: string;
15+
};
16+
};
17+
base: string;
18+
flush: FlowbiteBoolean;
19+
heading: string;
20+
open: FlowbiteBoolean;
21+
}
22+
723
export interface AccordionTitleProps extends ComponentProps<'button'> {
824
arrowIcon?: FC<ComponentProps<'svg'>>;
925
as?: FlowbiteHeadingLevel;
26+
theme?: DeepPartial<FlowbiteAccordionTitleTheme>;
1027
}
1128

1229
export const AccordionTitle: FC<AccordionTitleProps> = ({
1330
as: Heading = 'h2',
1431
children,
1532
className,
33+
theme: customTheme = {},
1634
...props
1735
}): JSX.Element => {
1836
const { arrowIcon: ArrowIcon, flush, isOpen, setOpen } = useAccordionContext();
19-
const theme = useTheme().theme.accordion.title;
20-
2137
const onClick = () => typeof setOpen !== 'undefined' && setOpen();
2238

39+
const theme = mergeDeep(useTheme().theme.accordion.title, customTheme);
40+
2341
return (
2442
<button
2543
className={classNames(

src/lib/components/Alert/Alert.tsx

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
11
import classNames from 'classnames';
22
import type { ComponentProps, FC, PropsWithChildren, ReactNode } from 'react';
33
import { HiX } from 'react-icons/hi';
4+
import { DeepPartial } from '..';
5+
import { mergeDeep } from '../../helpers/mergeDeep';
46
import type { FlowbiteColors } from '../Flowbite/FlowbiteTheme';
57
import { useTheme } from '../Flowbite/ThemeContext';
68

79
export interface FlowbiteAlertTheme {
10+
root: FlowbiteAlertRootTheme;
11+
closeButton: FlowbiteAlertCloseButtonTheme;
12+
}
13+
14+
export interface FlowbiteAlertRootTheme {
815
base: string;
916
borderAccent: string;
1017
wrapper: string;
11-
closeButton: {
12-
base: string;
13-
icon: string;
14-
color: AlertColors;
15-
};
1618
color: AlertColors;
1719
icon: string;
1820
rounded: string;
1921
}
2022

23+
export interface FlowbiteAlertCloseButtonTheme {
24+
base: string;
25+
icon: string;
26+
color: AlertColors;
27+
}
28+
2129
export interface AlertColors extends Pick<FlowbiteColors, 'failure' | 'gray' | 'info' | 'success' | 'warning'> {
2230
[key: string]: string;
2331
}
@@ -29,6 +37,7 @@ export interface AlertProps extends PropsWithChildren<Omit<ComponentProps<'div'>
2937
onDismiss?: boolean | (() => void);
3038
rounded?: boolean;
3139
withBorderAccent?: boolean;
40+
theme?: DeepPartial<FlowbiteAlertTheme>;
3241
}
3342

3443
export const Alert: FC<AlertProps> = ({
@@ -40,22 +49,23 @@ export const Alert: FC<AlertProps> = ({
4049
rounded = true,
4150
withBorderAccent,
4251
className,
52+
theme: customTheme = {},
4353
}) => {
44-
const theme = useTheme().theme.alert;
54+
const theme = mergeDeep(useTheme().theme.alert, customTheme);
4555

4656
return (
4757
<div
4858
className={classNames(
49-
theme.base,
50-
theme.color[color],
51-
rounded && theme.rounded,
52-
withBorderAccent && theme.borderAccent,
59+
theme.root.base,
60+
theme.root.color[color],
61+
rounded && theme.root.rounded,
62+
withBorderAccent && theme.root.borderAccent,
5363
className,
5464
)}
5565
role="alert"
5666
>
57-
<div className={theme.wrapper}>
58-
{Icon && <Icon className={theme.icon} />}
67+
<div className={theme.root.wrapper}>
68+
{Icon && <Icon className={theme.root.icon} />}
5969
<div>{children}</div>
6070
{typeof onDismiss === 'function' && (
6171
<button

src/lib/components/Avatar/Avatar.spec.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ describe('Components / Avatar', () => {
99
it('should use custom sizes', () => {
1010
const theme: CustomFlowbiteTheme = {
1111
avatar: {
12-
size: {
13-
xxl: 'h-64 w-64',
12+
root: {
13+
size: {
14+
xxl: 'h-64 w-64',
15+
},
1416
},
1517
},
1618
};
@@ -26,8 +28,10 @@ describe('Components / Avatar', () => {
2628
it('should use custom colors', () => {
2729
const theme: CustomFlowbiteTheme = {
2830
avatar: {
29-
color: {
30-
rose: 'ring-rose-500 dark:ring-rose-400',
31+
root: {
32+
color: {
33+
rose: 'ring-rose-500 dark:ring-rose-400',
34+
},
3135
},
3236
},
3337
};
@@ -77,8 +81,20 @@ describe('Components / Avatar', () => {
7781
expect(img()).toHaveAttribute('referrerpolicy', 'no-referrer');
7882
});
7983
});
84+
describe('Status', () => {
85+
it('should have online status indicator', () => {
86+
render(
87+
<Flowbite>
88+
<Avatar status="online" />
89+
</Flowbite>,
90+
);
91+
92+
expect(status()).toHaveClass('bg-green-400');
93+
});
94+
});
8095
});
8196

97+
const status = () => screen.getByTestId('flowbite-avatar-status');
8298
const img = () => screen.getByTestId('flowbite-avatar-img');
8399
const initialsPlaceholder = () => screen.getByTestId('flowbite-avatar-initials-placeholder');
84100
const initialsPlaceholderText = () => screen.getByTestId('flowbite-avatar-initials-placeholder-text');

0 commit comments

Comments
 (0)