Skip to content

Commit

Permalink
refactor(inputs): adds theme support to TextInput and FileInput (#246)
Browse files Browse the repository at this point in the history
* refactor(inputs): adds theme support to TextInput and FileInput components

Adding theme support for TextInput and FileInput. It also rewrites the FileInput component to
decouple it from FileInput.

BREAKING CHANGE: Adding theme support to the component blocks the access to className property
directly

* fix(inputs): fix wrong default color name from `base` to `gray`
  • Loading branch information
rluders authored Jun 30, 2022
1 parent 3444a66 commit 366a119
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 83 deletions.
9 changes: 2 additions & 7 deletions src/docs/pages/ModalPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,13 @@ const ModalPage: FC = () => {
<div className="mb-2 block">
<Label htmlFor="email" value="Your email" />
</div>
<TextInput
id="email"
className="dark:border-gray-500 dark:bg-gray-600"
placeholder="name@company.com"
required
/>
<TextInput id="email" placeholder="name@company.com" required />
</div>
<div>
<div className="mb-2 block">
<Label htmlFor="password" value="Your password" />
</div>
<TextInput id="password" className="dark:border-gray-500 dark:bg-gray-600" type="password" required />
<TextInput id="password" type="password" required />
</div>
<div className="flex justify-between">
<div className="flex items-center gap-2">
Expand Down
32 changes: 31 additions & 1 deletion src/lib/components/Flowbite/FlowbiteTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
ButtonSizes,
} from '../Button';
import type { PositionInButtonGroup } from '../Button/ButtonGroup';
import type { HelperColors, LabelColors } from '../FormControls';
import type { HelperColors, LabelColors, TextInputColors, TextInputSizes } from '../FormControls';
import type { ModalPositions, ModalSizes } from '../Modal';
import type { ProgressColor, ProgressSizes } from '../Progress';
import type { StarSizes } from '../Rating';
Expand Down Expand Up @@ -173,6 +173,36 @@ export interface FlowbiteTheme {
checkbox: {
base: string;
};
textInput: {
base: string;
addon: string;
field: {
base: string;
icon: {
base: string;
svg: string;
};
input: {
base: string;
sizes: TextInputSizes;
colors: TextInputColors;
withIcon: FlowbiteBoolean;
withAddon: FlowbiteBoolean;
withShadow: FlowbiteBoolean;
};
};
};
fileInput: {
base: string;
field: {
base: string;
input: {
base: string;
sizes: TextInputSizes;
colors: TextInputColors;
};
};
};
toggleSwitch: {
base: string;
active: FlowbiteBoolean;
Expand Down
45 changes: 38 additions & 7 deletions src/lib/components/FormControls/FileInput.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,41 @@
import classNames from 'classnames';
import type { FC } from 'react';
import type { TextInputProps } from './TextInput';
import { TextInput } from './TextInput';
import type { ComponentProps, ReactNode } from 'react';
import { forwardRef } from 'react';
import { excludeClassName } from '../../helpers/exclude';
import { useTheme } from '../Flowbite/ThemeContext';
import HelperText from './HelperText';
import type { TextInputColors, TextInputSizes } from './TextInput';

export type FileInputProps = Omit<TextInputProps, 'type'>;
export interface FileInputProps extends Omit<ComponentProps<'input'>, 'type' | 'ref' | 'color' | 'className'> {
sizing?: keyof TextInputSizes;
helperText?: ReactNode;
color?: keyof TextInputColors;
}

export const FileInput: FC<FileInputProps> = ({ className, ...props }) => {
return <TextInput className={classNames('!p-0', className)} {...props} type="file" />;
};
export const FileInput = forwardRef<HTMLInputElement, FileInputProps>(
({ sizing = 'md', helperText, color = 'gray', ...props }, ref) => {
const theme = useTheme().theme.formControls.fileInput;
const theirProps = excludeClassName(props);
return (
<>
<div className={theme.base}>
<div className={theme.field.base}>
<input
className={classNames(
theme.field.input.base,
theme.field.input.colors[color],
theme.field.input.sizes[sizing],
)}
{...theirProps}
type="file"
ref={ref}
/>
</div>
</div>
{helperText && <HelperText color={color}>{helperText}</HelperText>}
</>
);
},
);

FileInput.displayName = 'FileInput';
4 changes: 2 additions & 2 deletions src/lib/components/FormControls/HelperText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export interface HelperColors extends Pick<FlowbiteColors, 'gray' | 'info' | 'fa
[key: string]: string;
}

export interface HelperTextProps extends PropsWithChildren<Omit<ComponentProps<'p'>, 'className'>> {
color?: string;
export interface HelperTextProps extends PropsWithChildren<Omit<ComponentProps<'p'>, 'color' | 'className'>> {
color?: keyof HelperColors;
value?: string;
}

Expand Down
103 changes: 44 additions & 59 deletions src/lib/components/FormControls/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -1,75 +1,60 @@
import classNames from 'classnames';
import type { ComponentProps, FC, ReactNode } from 'react';
import { forwardRef } from 'react';
import { excludeClassName } from '../../helpers/exclude';
import type { FlowbiteColors, FlowbiteSizes } from '../Flowbite/FlowbiteTheme';
import { useTheme } from '../Flowbite/ThemeContext';
import HelperText from './HelperText';

type Size = 'sm' | 'md' | 'lg';
type Color = 'base' | 'green' | 'red';
export interface TextInputColors extends Pick<FlowbiteColors, 'gray' | 'info' | 'failure' | 'warning' | 'success'> {
[key: string]: string;
}

export type TextInputProps = Omit<ComponentProps<'input'>, 'ref'> & {
sizing?: Size;
export interface TextInputSizes extends Pick<FlowbiteSizes, 'sm' | 'md' | 'lg'> {
[key: string]: string;
}

export interface TextInputProps extends Omit<ComponentProps<'input'>, 'ref' | 'color' | 'className'> {
sizing?: keyof TextInputSizes;
shadow?: boolean;
helperText?: ReactNode;
addon?: ReactNode;
icon?: FC<ComponentProps<'svg'>>;
color?: Color;
};

const colorClasses: Record<Color, { input: string; helperText: string }> = {
base: {
input:
'bg-gray-50 border-gray-300 text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500',
helperText: 'text-gray-500 dark:text-gray-400',
},
green: {
input:
'border-green-500 bg-green-50 text-green-900 placeholder-green-700 focus:border-green-500 focus:ring-green-500 dark:border-green-400 dark:bg-green-100 dark:focus:border-green-500 dark:focus:ring-green-500',
helperText: 'text-green-600 dark:text-green-500',
},
red: {
input:
'border-red-500 bg-red-50 text-red-900 placeholder-red-700 focus:border-red-500 focus:ring-red-500 dark:border-red-400 dark:bg-red-100 dark:focus:border-red-500 dark:focus:ring-red-500',
helperText: 'text-red-600 dark:text-red-500',
},
};
color?: keyof TextInputColors;
}

export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ className, sizing = 'md', shadow, helperText, addon, icon: Icon, color = 'base', ...props }, ref) => (
<>
<div className="flex">
{addon && (
<span className="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 bg-gray-200 px-3 text-sm text-gray-900 dark:border-gray-600 dark:bg-gray-600 dark:text-gray-400">
{addon}
</span>
)}
<div className="relative w-full">
{Icon && (
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<Icon className="h-5 w-5 text-gray-500 dark:text-gray-400" />
</div>
)}
<input
className={classNames(
'block w-full border disabled:cursor-not-allowed disabled:opacity-50',
colorClasses[color].input,
{
'pl-10': Icon,
'rounded-lg': !addon,
'rounded-r-lg': addon,
'shadow-sm dark:shadow-sm-light': shadow,
'p-2 sm:text-xs': sizing === 'sm',
'p-2.5 text-sm': sizing === 'md',
'sm:text-md p-4': sizing === 'lg',
},
className,
({ sizing = 'md', shadow, helperText, addon, icon: Icon, color = 'gray', ...props }, ref) => {
const theme = useTheme().theme.formControls.textInput;
const theirProps = excludeClassName(props);
return (
<>
<div className={theme.base}>
{addon && <span className={theme.addon}>{addon}</span>}
<div className={theme.field.base}>
{Icon && (
<div className={theme.field.icon.base}>
<Icon className={theme.field.icon.svg} />
</div>
)}
{...props}
ref={ref}
/>
<input
className={classNames(
theme.field.input.base,
theme.field.input.colors[color],
theme.field.input.withIcon[Icon ? 'on' : 'off'],
theme.field.input.withAddon[addon ? 'on' : 'off'],
theme.field.input.withShadow[shadow ? 'on' : 'off'],
theme.field.input.sizes[sizing],
)}
{...theirProps}
ref={ref}
/>
</div>
</div>
</div>
{helperText && <p className={classNames('mt-1 text-sm', colorClasses[color].helperText)}>{helperText}</p>}
</>
),
{helperText && <HelperText color={color}>{helperText}</HelperText>}
</>
);
},
);

TextInput.displayName = 'TextInput';
9 changes: 2 additions & 7 deletions src/lib/components/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,13 @@ FormElements.args = {
<div className="mb-2 block">
<Label htmlFor="email" value="Your email" />
</div>
<TextInput
id="email"
className="dark:border-gray-500 dark:bg-gray-600"
placeholder="name@company.com"
required
/>
<TextInput id="email" placeholder="name@company.com" required />
</div>
<div>
<div className="mb-2 block">
<Label htmlFor="password" value="Your password" />
</div>
<TextInput id="password" className="dark:border-gray-500 dark:bg-gray-600" type="password" required />
<TextInput id="password" type="password" required />
</div>
<div className="flex justify-between">
<div className="flex items-center gap-2">
Expand Down
66 changes: 66 additions & 0 deletions src/lib/theme/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,72 @@ export default {
checkbox: {
base: 'h-4 w-4 rounded border border-gray-300 bg-gray-100 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600',
},
textInput: {
base: 'flex',
addon:
'inline-flex items-center rounded-l-md border border-r-0 border-gray-300 bg-gray-200 px-3 text-sm text-gray-900 dark:border-gray-600 dark:bg-gray-600 dark:text-gray-400',
field: {
base: 'relative w-full',
icon: {
base: 'pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3',
svg: 'h-5 w-5 text-gray-500 dark:text-gray-400',
},
input: {
base: 'block w-full border disabled:cursor-not-allowed disabled:opacity-50',
sizes: {
sm: 'p-2 sm:text-xs',
md: 'p-2.5 text-sm',
lg: 'sm:text-md p-4',
},
colors: {
gray: 'bg-gray-50 border-gray-300 text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500',
info: 'border-blue-500 bg-blue-50 text-blue-900 placeholder-blue-700 focus:border-blue-500 focus:ring-blue-500 dark:border-blue-400 dark:bg-blue-100 dark:focus:border-blue-500 dark:focus:ring-blue-500',
failure:
'border-red-500 bg-red-50 text-red-900 placeholder-red-700 focus:border-red-500 focus:ring-red-500 dark:border-red-400 dark:bg-red-100 dark:focus:border-red-500 dark:focus:ring-red-500',
warning:
'border-yellow-500 bg-yellow-50 text-yellow-900 placeholder-yellow-700 focus:border-yellow-500 focus:ring-yellow-500 dark:border-yellow-400 dark:bg-yellow-100 dark:focus:border-yellow-500 dark:focus:ring-yellow-500',
success:
'border-green-500 bg-green-50 text-green-900 placeholder-green-700 focus:border-green-500 focus:ring-green-500 dark:border-green-400 dark:bg-green-100 dark:focus:border-green-500 dark:focus:ring-green-500',
},
withIcon: {
on: 'pl-10',
off: '',
},
withAddon: {
on: 'rounded-r-lg',
off: 'rounded-lg',
},
withShadow: {
on: 'shadow-sm dark:shadow-sm-light',
off: '',
},
},
},
},
fileInput: {
base: 'flex',
field: {
base: 'relative w-full',
input: {
base: 'rounded-lg block w-full border disabled:cursor-not-allowed disabled:opacity-50',
sizes: {
sm: 'sm:text-xs',
md: 'text-sm',
lg: 'sm:text-md',
},
colors: {
gray: 'bg-gray-50 border-gray-300 text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500',
info: 'border-blue-500 bg-blue-50 text-blue-900 placeholder-blue-700 focus:border-blue-500 focus:ring-blue-500 dark:border-blue-400 dark:bg-blue-100 dark:focus:border-blue-500 dark:focus:ring-blue-500',
failure:
'border-red-500 bg-red-50 text-red-900 placeholder-red-700 focus:border-red-500 focus:ring-red-500 dark:border-red-400 dark:bg-red-100 dark:focus:border-red-500 dark:focus:ring-red-500',
warning:
'border-yellow-500 bg-yellow-50 text-yellow-900 placeholder-yellow-700 focus:border-yellow-500 focus:ring-yellow-500 dark:border-yellow-400 dark:bg-yellow-100 dark:focus:border-yellow-500 dark:focus:ring-yellow-500',
success:
'border-green-500 bg-green-50 text-green-900 placeholder-green-700 focus:border-green-500 focus:ring-green-500 dark:border-green-400 dark:bg-green-100 dark:focus:border-green-500 dark:focus:ring-green-500',
},
},
},
},
toggleSwitch: {
base: 'group relative flex items-center rounded-lg focus:outline-none',
active: {
Expand Down

0 comments on commit 366a119

Please sign in to comment.