Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(theme): adding theme support per component #500

Merged
merged 10 commits into from
Jan 11, 2023
4 changes: 2 additions & 2 deletions src/docs/pages/ThemePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import { Flowbite } from '../../lib/components';
import type { CustomFlowbiteTheme } from '../../lib/components/Flowbite/FlowbiteTheme';

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

return (
<div className="mx-auto flex max-w-4xl flex-col gap-8 dark:text-white">
<div className="flex flex-col max-w-4xl gap-8 mx-auto dark:text-white">
<div className="flex flex-col gap-2">
<span className="text-2xl font-bold">Theme</span>
<div className="py-4">
Expand Down
12 changes: 8 additions & 4 deletions src/lib/components/Accordion/Accordion.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ describe('Components / Accordion', () => {
it('should use custom `base` classes', () => {
const theme = {
accordion: {
base: 'text-4xl',
root: {
base: 'text-4xl',
},
},
};

Expand All @@ -131,9 +133,11 @@ describe('Components / Accordion', () => {
it('should use custom `flush` classes', () => {
const theme = {
accordion: {
flush: {
off: 'text-4xl',
on: 'text-3xl',
root: {
flush: {
off: 'text-4xl',
on: 'text-3xl',
},
},
},
};
Expand Down
33 changes: 14 additions & 19 deletions src/lib/components/Accordion/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,24 @@ import classNames from 'classnames';
import type { ComponentProps, FC, PropsWithChildren, ReactElement } from 'react';
import { Children, cloneElement, useMemo, useState } from 'react';
import { HiChevronDown } from 'react-icons/hi';
import { DeepPartial } from '..';
import { mergeDeep } from '../../helpers/mergeDeep';
import { FlowbiteBoolean } from '../Flowbite/FlowbiteTheme';
import { useTheme } from '../Flowbite/ThemeContext';
import { AccordionContent } from './AccordionContent';
import { AccordionContent, FlowbiteAccordionComponentTheme } from './AccordionContent';
import type { AccordionPanelProps } from './AccordionPanel';
import { AccordionPanel } from './AccordionPanel';
import { AccordionTitle } from './AccordionTitle';
import { AccordionTitle, FlowbiteAccordionTitleTheme } from './AccordionTitle';

export interface FlowbiteAccordionTheme {
root: FlowbiteAccordionRootTheme;
content: FlowbiteAccordionComponentTheme;
title: FlowbiteAccordionTitleTheme;
}

export interface FlowbiteAccordionRootTheme {
base: string;
content: {
base: string;
};
flush: FlowbiteBoolean;
title: {
arrow: {
base: string;
open: {
off: string;
on: string;
};
};
base: string;
flush: FlowbiteBoolean;
heading: string;
open: FlowbiteBoolean;
};
}

export interface AccordionProps extends PropsWithChildren<ComponentProps<'div'>> {
Expand All @@ -36,6 +28,7 @@ export interface AccordionProps extends PropsWithChildren<ComponentProps<'div'>>
children: ReactElement<AccordionPanelProps> | ReactElement<AccordionPanelProps>[];
flush?: boolean;
collapseAll?: boolean;
theme?: DeepPartial<FlowbiteAccordionRootTheme>;
}

const AccordionComponent: FC<AccordionProps> = ({
Expand All @@ -45,6 +38,7 @@ const AccordionComponent: FC<AccordionProps> = ({
flush = false,
collapseAll = false,
className,
theme: customTheme = {},
...props
}): JSX.Element => {
const [isOpen, setOpen] = useState(collapseAll ? -1 : 0);
Expand All @@ -55,7 +49,8 @@ const AccordionComponent: FC<AccordionProps> = ({
),
[alwaysOpen, arrowIcon, children, flush, isOpen],
);
const theme = useTheme().theme.accordion;

const theme = mergeDeep(useTheme().theme.accordion.root, customTheme);

return (
<div
Expand Down
22 changes: 19 additions & 3 deletions src/lib/components/Accordion/AccordionContent.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import classNames from 'classnames';
import type { ComponentProps, FC } from 'react';
import type { ComponentProps, FC, PropsWithChildren } from 'react';
import { DeepPartial } from '..';
import { mergeDeep } from '../../helpers/mergeDeep';
import { useTheme } from '../Flowbite/ThemeContext';
import { useAccordionContext } from './AccordionPanelContext';

export const AccordionContent: FC<ComponentProps<'div'>> = ({ children, className, ...props }): JSX.Element => {
export interface FlowbiteAccordionComponentTheme {
base: string;
}

export interface AccordionContentProps extends PropsWithChildren<ComponentProps<'div'>> {
theme?: DeepPartial<FlowbiteAccordionComponentTheme>;
}

export const AccordionContent: FC<AccordionContentProps> = ({
children,
className,
theme: customTheme = {},
...props
}): JSX.Element => {
const { isOpen } = useAccordionContext();
const theme = useTheme().theme.accordion.content;

const theme = mergeDeep(useTheme().theme.accordion.content, customTheme);

return (
<div
Expand Down
24 changes: 21 additions & 3 deletions src/lib/components/Accordion/AccordionTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
import classNames from 'classnames';
import type { ComponentProps, FC } from 'react';
import type { FlowbiteHeadingLevel } from '../Flowbite/FlowbiteTheme';
import { DeepPartial } from '..';
import { mergeDeep } from '../../helpers/mergeDeep';
import type { FlowbiteBoolean, FlowbiteHeadingLevel } from '../Flowbite/FlowbiteTheme';
import { useTheme } from '../Flowbite/ThemeContext';
import { useAccordionContext } from './AccordionPanelContext';

export interface FlowbiteAccordionTitleTheme {
arrow: {
base: string;
open: {
off: string;
on: string;
};
};
base: string;
flush: FlowbiteBoolean;
heading: string;
open: FlowbiteBoolean;
}

export interface AccordionTitleProps extends ComponentProps<'button'> {
arrowIcon?: FC<ComponentProps<'svg'>>;
as?: FlowbiteHeadingLevel;
theme?: DeepPartial<FlowbiteAccordionTitleTheme>;
}

export const AccordionTitle: FC<AccordionTitleProps> = ({
as: Heading = 'h2',
children,
className,
theme: customTheme = {},
...props
}): JSX.Element => {
const { arrowIcon: ArrowIcon, flush, isOpen, setOpen } = useAccordionContext();
const theme = useTheme().theme.accordion.title;

const onClick = () => typeof setOpen !== 'undefined' && setOpen();

const theme = mergeDeep(useTheme().theme.accordion.title, customTheme);

return (
<button
className={classNames(
Expand Down
34 changes: 22 additions & 12 deletions src/lib/components/Alert/Alert.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import classNames from 'classnames';
import type { ComponentProps, FC, PropsWithChildren, ReactNode } from 'react';
import { HiX } from 'react-icons/hi';
import { DeepPartial } from '..';
import { mergeDeep } from '../../helpers/mergeDeep';
import type { FlowbiteColors } from '../Flowbite/FlowbiteTheme';
import { useTheme } from '../Flowbite/ThemeContext';

export interface FlowbiteAlertTheme {
root: FlowbiteAlertRootTheme;
closeButton: FlowbiteAlertCloseButtonTheme;
}

export interface FlowbiteAlertRootTheme {
base: string;
borderAccent: string;
wrapper: string;
closeButton: {
base: string;
icon: string;
color: AlertColors;
};
color: AlertColors;
icon: string;
rounded: string;
}

export interface FlowbiteAlertCloseButtonTheme {
base: string;
icon: string;
color: AlertColors;
}

export interface AlertColors extends Pick<FlowbiteColors, 'failure' | 'gray' | 'info' | 'success' | 'warning'> {
[key: string]: string;
}
Expand All @@ -29,6 +37,7 @@ export interface AlertProps extends PropsWithChildren<Omit<ComponentProps<'div'>
onDismiss?: boolean | (() => void);
rounded?: boolean;
withBorderAccent?: boolean;
theme?: DeepPartial<FlowbiteAlertTheme>;
}

export const Alert: FC<AlertProps> = ({
Expand All @@ -40,22 +49,23 @@ export const Alert: FC<AlertProps> = ({
rounded = true,
withBorderAccent,
className,
theme: customTheme = {},
}) => {
const theme = useTheme().theme.alert;
const theme = mergeDeep(useTheme().theme.alert, customTheme);

return (
<div
className={classNames(
theme.base,
theme.color[color],
rounded && theme.rounded,
withBorderAccent && theme.borderAccent,
theme.root.base,
theme.root.color[color],
rounded && theme.root.rounded,
withBorderAccent && theme.root.borderAccent,
className,
)}
role="alert"
>
<div className={theme.wrapper}>
{Icon && <Icon className={theme.icon} />}
<div className={theme.root.wrapper}>
{Icon && <Icon className={theme.root.icon} />}
<div>{children}</div>
{typeof onDismiss === 'function' && (
<button
Expand Down
24 changes: 20 additions & 4 deletions src/lib/components/Avatar/Avatar.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ describe('Components / Avatar', () => {
it('should use custom sizes', () => {
const theme: CustomFlowbiteTheme = {
avatar: {
size: {
xxl: 'h-64 w-64',
root: {
size: {
xxl: 'h-64 w-64',
},
},
},
};
Expand All @@ -26,8 +28,10 @@ describe('Components / Avatar', () => {
it('should use custom colors', () => {
const theme: CustomFlowbiteTheme = {
avatar: {
color: {
rose: 'ring-rose-500 dark:ring-rose-400',
root: {
color: {
rose: 'ring-rose-500 dark:ring-rose-400',
},
},
},
};
Expand Down Expand Up @@ -77,8 +81,20 @@ describe('Components / Avatar', () => {
expect(img()).toHaveAttribute('referrerpolicy', 'no-referrer');
});
});
describe('Status', () => {
it('should have online status indicator', () => {
render(
<Flowbite>
<Avatar status="online" />
</Flowbite>,
);

expect(status()).toHaveClass('bg-green-400');
});
});
});

const status = () => screen.getByTestId('flowbite-avatar-status');
const img = () => screen.getByTestId('flowbite-avatar-img');
const initialsPlaceholder = () => screen.getByTestId('flowbite-avatar-initials-placeholder');
const initialsPlaceholderText = () => screen.getByTestId('flowbite-avatar-initials-placeholder-text');
Loading