@@ -51,6 +51,10 @@ export interface ComboBoxProps extends Omit<React.HTMLProps<ComboBoxDerivedEleme | |||
* Input mode of the component. | |||
*/ | |||
inputMode?: TextControl.InputMode, | |||
/** | |||
* Visual length of the input. | |||
*/ | |||
length?: number, | |||
} | |||
/** | |||
@@ -74,6 +78,7 @@ export const ComboBox = React.forwardRef<ComboBoxDerivedElement, ComboBoxProps>( | |||
inputMode = 'text' as const, | |||
id: idProp, | |||
style, | |||
length, | |||
...etcProps | |||
}: ComboBoxProps, | |||
forwardedRef, | |||
@@ -112,6 +117,7 @@ export const ComboBox = React.forwardRef<ComboBoxDerivedElement, ComboBoxProps>( | |||
> | |||
<input | |||
{...etcProps} | |||
size={length} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
@@ -245,4 +251,5 @@ ComboBox.defaultProps = { | |||
variant: 'default' as const, | |||
hiddenLabel: false as const, | |||
inputMode: 'text' as const, | |||
length: undefined, | |||
}; |
@@ -4,7 +4,7 @@ import clsx from 'clsx'; | |||
export type EmailInputDerivedElement = HTMLInputElement; | |||
export interface EmailInputProps extends Omit<React.HTMLProps<EmailInputDerivedElement>, 'size' | 'type' | 'label' | 'inputMode'> { | |||
export interface EmailInputProps extends Omit<React.HTMLProps<EmailInputDerivedElement>, 'size' | 'type' | 'label' | 'inputMode' | 'pattern'> { | |||
/** | |||
* Short textual description indicating the nature of the component's value. | |||
*/ | |||
@@ -37,6 +37,14 @@ export interface EmailInputProps extends Omit<React.HTMLProps<EmailInputDerivedE | |||
* Is the label hidden? | |||
*/ | |||
hiddenLabel?: boolean, | |||
/** | |||
* Allowed domains for emails. | |||
*/ | |||
domains?: string[], | |||
/** | |||
* Visual length of the input. | |||
*/ | |||
length?: number, | |||
} | |||
/** | |||
@@ -56,6 +64,8 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||
className, | |||
id: idProp, | |||
style, | |||
domains = [], | |||
length, | |||
...etcProps | |||
}: EmailInputProps, | |||
forwardedRef, | |||
@@ -64,6 +74,12 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
const pattern = ( | |||
Array.isArray(domains) && domains.length > 0 | |||
? `.+?@(${domains.join('|').replace(/\./g, '\\.')})$` | |||
: undefined | |||
); | |||
return ( | |||
<div | |||
className={clsx( | |||
@@ -79,11 +95,13 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||
> | |||
<input | |||
{...etcProps} | |||
size={length} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
type="email" | |||
data-testid="input" | |||
pattern={pattern} | |||
className={clsx( | |||
'bg-negative rounded-inherit w-full peer block font-inherit', | |||
'focus:outline-0', | |||
@@ -173,6 +191,7 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||
)} | |||
{indicator && ( | |||
<div | |||
data-testid="indicator" | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none', | |||
{ | |||
@@ -207,4 +226,6 @@ EmailInput.defaultProps = { | |||
block: false, | |||
variant: 'default', | |||
hiddenLabel: false, | |||
domains: [], | |||
length: undefined, | |||
}; |
@@ -15,20 +15,20 @@ import { | |||
} from 'vitest'; | |||
import matchers from '@testing-library/jest-dom/matchers'; | |||
import { | |||
TextInput, | |||
TextInputDerivedElement, | |||
PatternTextInput, | |||
PatternTextInputDerivedElement, | |||
} from '.'; | |||
expect.extend(matchers); | |||
describe('TextInput', () => { | |||
describe('PatternTextInput', () => { | |||
afterEach(() => { | |||
cleanup(); | |||
}); | |||
it('renders a textbox', () => { | |||
render( | |||
<TextInput />, | |||
<PatternTextInput />, | |||
); | |||
const textbox = screen.getByRole('textbox'); | |||
expect(textbox).toBeInTheDocument(); | |||
@@ -37,7 +37,7 @@ describe('TextInput', () => { | |||
it('renders a border', () => { | |||
render( | |||
<TextInput | |||
<PatternTextInput | |||
border | |||
/>, | |||
); | |||
@@ -47,7 +47,7 @@ describe('TextInput', () => { | |||
it('renders a label', () => { | |||
render( | |||
<TextInput | |||
<PatternTextInput | |||
label="foo" | |||
/>, | |||
); | |||
@@ -59,7 +59,7 @@ describe('TextInput', () => { | |||
it('renders a hidden label', () => { | |||
render( | |||
<TextInput | |||
<PatternTextInput | |||
label="foo" | |||
hiddenLabel | |||
/>, | |||
@@ -73,7 +73,7 @@ describe('TextInput', () => { | |||
it('renders a hint', () => { | |||
render( | |||
<TextInput | |||
<PatternTextInput | |||
hint="foo" | |||
/>, | |||
); | |||
@@ -83,7 +83,7 @@ describe('TextInput', () => { | |||
it('renders an indicator', () => { | |||
render( | |||
<TextInput | |||
<PatternTextInput | |||
indicator={ | |||
<div /> | |||
} | |||
@@ -111,7 +111,7 @@ describe('TextInput', () => { | |||
}) => { | |||
it('renders input styles', () => { | |||
render( | |||
<TextInput | |||
<PatternTextInput | |||
size={size} | |||
/>, | |||
); | |||
@@ -122,7 +122,7 @@ describe('TextInput', () => { | |||
it('renders label styles with indicator', () => { | |||
render( | |||
<TextInput | |||
<PatternTextInput | |||
size={size} | |||
label="foo" | |||
indicator={<div />} | |||
@@ -134,7 +134,7 @@ describe('TextInput', () => { | |||
it('renders hint styles when indicator is present', () => { | |||
render( | |||
<TextInput | |||
<PatternTextInput | |||
size={size} | |||
hint="hint" | |||
indicator={<div />} | |||
@@ -147,7 +147,7 @@ describe('TextInput', () => { | |||
it('renders indicator styles', () => { | |||
render( | |||
<TextInput | |||
<PatternTextInput | |||
size={size} | |||
indicator={ | |||
<div /> | |||
@@ -162,7 +162,7 @@ describe('TextInput', () => { | |||
it('renders a block textbox', () => { | |||
render( | |||
<TextInput | |||
<PatternTextInput | |||
block | |||
/>, | |||
); | |||
@@ -173,7 +173,7 @@ describe('TextInput', () => { | |||
it.each(TextControl.AVAILABLE_INPUT_TYPES)('renders a textbox with type %s', (inputType) => { | |||
render( | |||
<TextInput | |||
<PatternTextInput | |||
type={inputType} | |||
/>, | |||
); | |||
@@ -183,7 +183,7 @@ describe('TextInput', () => { | |||
it('falls back to text input mode when it clashes with the input type', () => { | |||
render( | |||
<TextInput | |||
<PatternTextInput | |||
type="text" | |||
inputMode="search" | |||
/>, | |||
@@ -207,7 +207,7 @@ describe('TextInput', () => { | |||
}) => { | |||
it('renders input styles', () => { | |||
render( | |||
<TextInput | |||
<PatternTextInput | |||
variant={variant} | |||
/>, | |||
); | |||
@@ -218,7 +218,7 @@ describe('TextInput', () => { | |||
it('renders hint styles', () => { | |||
render( | |||
<TextInput | |||
<PatternTextInput | |||
variant={variant} | |||
hint="hint" | |||
/>, | |||
@@ -231,12 +231,12 @@ describe('TextInput', () => { | |||
it('handles change events', async () => { | |||
const onChange = vi.fn().mockImplementationOnce( | |||
(e: React.ChangeEvent<TextInputDerivedElement>) => { | |||
(e: React.ChangeEvent<PatternTextInputDerivedElement>) => { | |||
e.preventDefault(); | |||
}, | |||
); | |||
render( | |||
<TextInput | |||
<PatternTextInput | |||
onChange={onChange} | |||
/>, | |||
); | |||
@@ -247,12 +247,12 @@ describe('TextInput', () => { | |||
it('handles input events', async () => { | |||
const onInput = vi.fn().mockImplementationOnce( | |||
(e: React.SyntheticEvent<TextInputDerivedElement>) => { | |||
(e: React.SyntheticEvent<PatternTextInputDerivedElement>) => { | |||
e.preventDefault(); | |||
}, | |||
); | |||
render( | |||
<TextInput | |||
<PatternTextInput | |||
onInput={onInput} | |||
/>, | |||
); |
@@ -45,6 +45,10 @@ export interface PatternTextInputProps extends Omit<React.HTMLProps<PatternTextI | |||
* Input mode of the component. | |||
*/ | |||
inputMode?: TextControl.InputMode, | |||
/** | |||
* Visual length of the input. | |||
*/ | |||
length?: number, | |||
} | |||
/** | |||
@@ -52,7 +56,10 @@ export interface PatternTextInputProps extends Omit<React.HTMLProps<PatternTextI | |||
* | |||
* This component supports multiline input and adjusts its layout accordingly. | |||
*/ | |||
export const PatternTextInput = React.forwardRef<PatternTextInputDerivedElement, PatternTextInputProps>(( | |||
export const PatternTextInput = React.forwardRef< | |||
PatternTextInputDerivedElement, | |||
PatternTextInputProps | |||
>(( | |||
{ | |||
label, | |||
hint, | |||
@@ -67,6 +74,7 @@ export const PatternTextInput = React.forwardRef<PatternTextInputDerivedElement, | |||
id: idProp, | |||
style, | |||
inputMode = type, | |||
length, | |||
...etcProps | |||
}, | |||
forwardedRef, | |||
@@ -98,6 +106,7 @@ export const PatternTextInput = React.forwardRef<PatternTextInputDerivedElement, | |||
> | |||
<input | |||
{...etcProps} | |||
size={length} | |||
ref={forwardedRef} | |||
aria-labelledby={labelId} | |||
id={id} | |||
@@ -229,4 +238,5 @@ PatternTextInput.defaultProps = { | |||
variant: 'default', | |||
hiddenLabel: false, | |||
inputMode: 'text', | |||
length: undefined, | |||
}; |
@@ -47,6 +47,10 @@ export interface PhoneNumberInputProps extends Omit<React.HTMLProps<PhoneNumberI | |||
* Country where the phone number should be formatted for. | |||
*/ | |||
country?: Country, | |||
/** | |||
* Visual length of the input. | |||
*/ | |||
length?: number, | |||
} | |||
/** | |||
@@ -73,6 +77,7 @@ export const PhoneNumberInput = React.forwardRef< | |||
value, | |||
onChange, | |||
name, | |||
length, | |||
...etcProps | |||
}: PhoneNumberInputProps, | |||
forwardedRef, | |||
@@ -140,9 +145,11 @@ export const PhoneNumberInput = React.forwardRef< | |||
className, | |||
)} | |||
style={style} | |||
data-testid="base" | |||
> | |||
<input | |||
{...etcProps} | |||
size={length} | |||
value={value} | |||
onChange={onChange} | |||
ref={ref} | |||
@@ -222,6 +229,7 @@ export const PhoneNumberInput = React.forwardRef< | |||
)} | |||
{indicator && ( | |||
<div | |||
data-testid="indicator" | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none', | |||
{ | |||
@@ -37,6 +37,10 @@ export interface UrlInputProps extends Omit<React.HTMLProps<UrlInputDerivedEleme | |||
* Is the label hidden? | |||
*/ | |||
hiddenLabel?: boolean, | |||
/** | |||
* Visual length of the input. | |||
*/ | |||
length?: number, | |||
} | |||
/** | |||
@@ -54,6 +58,7 @@ export const UrlInput = React.forwardRef<UrlInputDerivedElement, UrlInputProps>( | |||
hiddenLabel = false, | |||
className, | |||
style, | |||
length, | |||
id: idProp, | |||
...etcProps | |||
}: UrlInputProps, | |||
@@ -75,9 +80,11 @@ export const UrlInput = React.forwardRef<UrlInputDerivedElement, UrlInputProps>( | |||
className, | |||
)} | |||
style={style} | |||
data-testid="base" | |||
> | |||
<input | |||
{...etcProps} | |||
size={length} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
@@ -172,6 +179,7 @@ export const UrlInput = React.forwardRef<UrlInputDerivedElement, UrlInputProps>( | |||
)} | |||
{indicator && ( | |||
<div | |||
data-testid="indicator" | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none', | |||
{ | |||
@@ -205,4 +213,5 @@ UrlInput.defaultProps = { | |||
block: false, | |||
variant: 'default', | |||
hiddenLabel: false, | |||
length: undefined, | |||
}; |
@@ -37,6 +37,10 @@ export interface MaskedTextInputProps extends Omit<React.HTMLProps<MaskedTextInp | |||
* Is the label hidden? | |||
*/ | |||
hiddenLabel?: boolean, | |||
/** | |||
* Visual length of the input. | |||
*/ | |||
length?: number, | |||
} | |||
/** | |||
@@ -58,6 +62,7 @@ export const MaskedTextInput = React.forwardRef< | |||
className, | |||
id: idProp, | |||
style, | |||
length, | |||
...etcProps | |||
}: MaskedTextInputProps, | |||
forwardedRef, | |||
@@ -82,6 +87,7 @@ export const MaskedTextInput = React.forwardRef< | |||
> | |||
<input | |||
{...etcProps} | |||
size={length} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
@@ -210,4 +216,5 @@ MaskedTextInput.defaultProps = { | |||
block: false, | |||
variant: 'default', | |||
hiddenLabel: false, | |||
length: undefined, | |||
}; |
@@ -45,6 +45,10 @@ export interface TextInputProps extends Omit<React.HTMLProps<TextInputDerivedEle | |||
* Input mode of the component. | |||
*/ | |||
inputMode?: TextControl.InputMode, | |||
/** | |||
* Visual length of the input. | |||
*/ | |||
length?: number, | |||
} | |||
/** | |||
@@ -67,6 +71,7 @@ export const TextInput = React.forwardRef<TextInputDerivedElement, TextInputProp | |||
id: idProp, | |||
style, | |||
inputMode = type, | |||
length, | |||
...etcProps | |||
}, | |||
forwardedRef, | |||
@@ -98,6 +103,7 @@ export const TextInput = React.forwardRef<TextInputDerivedElement, TextInputProp | |||
> | |||
<input | |||
{...etcProps} | |||
size={length} | |||
ref={forwardedRef} | |||
aria-labelledby={labelId} | |||
id={id} | |||
@@ -229,4 +235,5 @@ TextInput.defaultProps = { | |||
variant: 'default', | |||
hiddenLabel: false, | |||
inputMode: 'text', | |||
length: undefined, | |||
}; |
@@ -55,6 +55,9 @@ export interface TagInputProps extends Omit<React.HTMLProps<TagInputDerivedEleme | |||
* Separators for splitting the input value into multiple tags. | |||
*/ | |||
separator?: TagInputSeparator[], | |||
/** | |||
* Should the last tag be editable when removed? | |||
*/ | |||
editOnRemove?: boolean, | |||
} | |||
@@ -221,6 +224,7 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>( | |||
variant === 'alternate' && 'tag-input-alternate', | |||
className, | |||
)} | |||
data-testid="base" | |||
onFocusCapture={handleFocusCapture} | |||
> | |||
<textarea | |||
@@ -37,6 +37,10 @@ export interface NumberSpinnerProps extends Omit<React.HTMLProps<NumberSpinnerDe | |||
* Is the label hidden? | |||
*/ | |||
hiddenLabel?: boolean, | |||
/** | |||
* Visual length of the input. | |||
*/ | |||
length?: number, | |||
} | |||
/** | |||
@@ -55,6 +59,7 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe | |||
className, | |||
id: idProp, | |||
style, | |||
length, | |||
...etcProps | |||
}: NumberSpinnerProps, | |||
forwardedRef, | |||
@@ -79,6 +84,7 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe | |||
> | |||
<input | |||
{...etcProps} | |||
size={length} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
@@ -208,4 +214,5 @@ NumberSpinner.defaultProps = { | |||
hiddenLabel: false, | |||
size: 'medium', | |||
variant: 'default', | |||
length: undefined, | |||
}; |
@@ -37,6 +37,10 @@ export interface DateDropdownProps extends Omit<React.HTMLProps<DateDropdownDeri | |||
* Is the label hidden? | |||
*/ | |||
hiddenLabel?: boolean, | |||
/** | |||
* Visual length of the input. | |||
*/ | |||
length?: number, | |||
} | |||
/** | |||
@@ -58,6 +62,7 @@ export const DateDropdown = React.forwardRef< | |||
className, | |||
id: idProp, | |||
style, | |||
length, | |||
...etcProps | |||
}: DateDropdownProps, | |||
forwardedRef, | |||
@@ -82,6 +87,7 @@ export const DateDropdown = React.forwardRef< | |||
> | |||
<input | |||
{...etcProps} | |||
size={length} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
@@ -211,4 +217,5 @@ DateDropdown.defaultProps = { | |||
block: false, | |||
variant: 'default', | |||
hiddenLabel: false, | |||
length: undefined, | |||
}; |
@@ -19,6 +19,26 @@ const TemporalPage: NextPage = () => { | |||
/> | |||
</Subsection> | |||
</Section> | |||
<Section title="EmailInput"> | |||
<Subsection title="Default"> | |||
<Formatted.EmailInput | |||
label="Email" | |||
name="email" | |||
border | |||
/> | |||
</Subsection> | |||
<Subsection title="With domains"> | |||
<Formatted.EmailInput | |||
label="Email" | |||
name="email" | |||
border | |||
variant="alternate" | |||
domains={['gmail.com', 'yahoo.com']} | |||
hint="Only GMail or Yahoo Mail allowed" | |||
length={50} | |||
/> | |||
</Subsection> | |||
</Section> | |||
</DefaultLayout> | |||
) | |||
} | |||