Feat/Input prepend and append elements (#402)
* add custom element support to inputs * use classnames instead of template literals
This commit is contained in:
parent
348badca2a
commit
a352702bc5
@ -7,6 +7,18 @@ export default {
|
|||||||
|
|
||||||
const Template: Story = (args) => <Input {...args} value="I type words" />;
|
const Template: Story = (args) => <Input {...args} value="I type words" />;
|
||||||
|
|
||||||
|
const customElementPlaceholder = (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
backgroundColor: 'grey',
|
||||||
|
padding: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ω
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
export const Default = Template.bind({});
|
export const Default = Template.bind({});
|
||||||
Default.args = {};
|
Default.args = {};
|
||||||
|
|
||||||
@ -37,3 +49,11 @@ export const IconPrepend: Story = () => (
|
|||||||
export const IconAppend: Story = () => (
|
export const IconAppend: Story = () => (
|
||||||
<Input value="I type words and even more words" appendIconName="search" />
|
<Input value="I type words and even more words" appendIconName="search" />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ElementPrepend: Story = () => (
|
||||||
|
<Input value="<- custom element" prependElement={customElementPlaceholder} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ElementAppend: Story = () => (
|
||||||
|
<Input value="custom element ->" appendElement={customElementPlaceholder} />
|
||||||
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { InputHTMLAttributes } from 'react';
|
import type { InputHTMLAttributes, ReactNode } from 'react';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { IconName } from '../icon';
|
import type { IconName } from '../icon';
|
||||||
@ -8,13 +8,58 @@ import {
|
|||||||
includesRightPadding,
|
includesRightPadding,
|
||||||
} from '../../utils/class-names';
|
} from '../../utils/class-names';
|
||||||
|
|
||||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
type InputRootProps = InputHTMLAttributes<HTMLInputElement> & {
|
||||||
hasError?: boolean;
|
hasError?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
prependIconName?: IconName;
|
};
|
||||||
appendIconName?: IconName;
|
|
||||||
}
|
type NoPrepend = {
|
||||||
|
prependIconName?: never;
|
||||||
|
prependIconDescription?: string;
|
||||||
|
prependElement?: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NoAppend = {
|
||||||
|
appendIconName?: never;
|
||||||
|
appendIconDescription?: string;
|
||||||
|
appendElement?: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
type InputPrepend = NoAppend &
|
||||||
|
(
|
||||||
|
| NoPrepend
|
||||||
|
| {
|
||||||
|
prependIconName: IconName;
|
||||||
|
prependIconDescription?: string;
|
||||||
|
prependElement?: never;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
prependIconName?: never;
|
||||||
|
prependIconDescription?: never;
|
||||||
|
prependElement: ReactNode;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type InputAppend = NoPrepend &
|
||||||
|
(
|
||||||
|
| NoAppend
|
||||||
|
| {
|
||||||
|
appendIconName: IconName;
|
||||||
|
appendIconDescription?: string;
|
||||||
|
appendElement?: never;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
appendIconName?: never;
|
||||||
|
appendIconDescription?: never;
|
||||||
|
appendElement: ReactNode;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type AffixProps = InputPrepend | InputAppend;
|
||||||
|
|
||||||
|
type InputProps = InputRootProps & AffixProps;
|
||||||
|
|
||||||
export const inputClassNames = ({
|
export const inputClassNames = ({
|
||||||
hasError,
|
hasError,
|
||||||
className,
|
className,
|
||||||
@ -61,41 +106,98 @@ export const inputStyle = ({
|
|||||||
}
|
}
|
||||||
: style;
|
: style;
|
||||||
|
|
||||||
|
const getAffixElement = ({
|
||||||
|
prependElement,
|
||||||
|
prependIconName,
|
||||||
|
prependIconDescription,
|
||||||
|
appendElement,
|
||||||
|
appendIconName,
|
||||||
|
appendIconDescription,
|
||||||
|
}: Pick<InputProps, keyof AffixProps>) => {
|
||||||
|
const position = prependIconName || prependElement ? 'pre' : 'post';
|
||||||
|
|
||||||
|
const className = classNames(
|
||||||
|
['fill-black-60 dark:fill-white-60', 'absolute', 'z-10'],
|
||||||
|
{
|
||||||
|
'left-8': position === 'pre',
|
||||||
|
'right-8': position === 'post',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const element = prependElement || appendElement;
|
||||||
|
const iconName = prependIconName || appendIconName;
|
||||||
|
const iconDescription = prependIconDescription || appendIconDescription;
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
return <div className={className}>{element}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iconName) {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
name={iconName}
|
||||||
|
className={className}
|
||||||
|
size={16}
|
||||||
|
aria-label={iconDescription}
|
||||||
|
aria-hidden={!iconDescription}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ prependIconName, appendIconName, className, hasError, ...props }, ref) => {
|
(
|
||||||
className = `h-28 ${className}`;
|
{
|
||||||
if (prependIconName) {
|
prependIconName,
|
||||||
className += ' pl-28';
|
prependIconDescription,
|
||||||
}
|
appendIconName,
|
||||||
if (appendIconName) {
|
appendIconDescription,
|
||||||
className += ' pr-28';
|
prependElement,
|
||||||
}
|
appendElement,
|
||||||
|
className,
|
||||||
|
hasError,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const hasPrepended = !!(prependIconName || prependElement);
|
||||||
|
const hasAppended = !!(appendIconName || appendElement);
|
||||||
|
|
||||||
|
const inputClassName = classNames('h-28', className, {
|
||||||
|
'pl-28': hasPrepended ?? hasAppended,
|
||||||
|
});
|
||||||
|
|
||||||
const input = (
|
const input = (
|
||||||
<input
|
<input
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames(inputClassNames({ className, hasError }))}
|
className={classNames(
|
||||||
|
inputClassNames({ className: inputClassName, hasError })
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const iconName = prependIconName || appendIconName;
|
|
||||||
if (iconName !== undefined) {
|
const element = getAffixElement({
|
||||||
const iconClassName = classNames(
|
prependIconName,
|
||||||
['fill-black-60 dark:fill-white-60', 'absolute', 'z-10'],
|
prependIconDescription,
|
||||||
{
|
appendIconName,
|
||||||
'left-8': prependIconName,
|
appendIconDescription,
|
||||||
'right-8': appendIconName,
|
prependElement,
|
||||||
}
|
appendElement,
|
||||||
);
|
});
|
||||||
const icon = <Icon name={iconName} className={iconClassName} size={16} />;
|
|
||||||
|
if (element) {
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center relative">
|
<div className="inline-flex items-center relative">
|
||||||
{prependIconName && icon}
|
{hasPrepended && element}
|
||||||
{input}
|
{input}
|
||||||
{appendIconName && icon}
|
{hasAppended && element}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user