libs/@guardian/source-development-kitchen/src/react-components/numeric-input/NumericInput.tsx (127 lines of code) (raw):
import type { SerializedStyles, Theme } from '@emotion/react';
import { descriptionId, generateSourceId } from '@guardian/source/foundations';
import type {
TextInputProps,
textInputThemeDefault,
} from '@guardian/source/react-components';
import {
InlineError,
InlineSuccess,
Label,
} from '@guardian/source/react-components';
import { InputExtension } from './InputExtension';
import {
hasExtensions,
inlineMessageMargin,
inputWrapper,
labelMargin,
supportingTextMargin,
textInput,
width10,
width30,
width4,
widthFluid,
} from './numericInputStyles';
import { errorInput, successInput } from './sharedStyles';
export interface InputTheme extends Theme {
textInput?: typeof textInputThemeDefault.textInput;
}
export type Width = 30 | 10 | 4;
const widths: Record<Width, SerializedStyles> = {
30: width30,
10: width10,
4: width4,
};
export type NumericInputProps = Omit<TextInputProps, 'inputmode'> & {
/**
* Appears after the input. May be used with or without a prefix. Should be kept short to avoid issues on small screens.
* Will not be read by screen readers, so do not rely on this alone to convey meaning.
*/
suffixText?: string;
/**
* Appears before the input. May be used with or without a suffix. Should be kept short to avoid issues on small screens.
* Will not be read by screen readers, so do not rely on this alone to convey meaning.
*/
prefixText?: string;
};
/**
* [Storybook](https://guardian.github.io/storybooks/?path=/story/source-development-kitchen_react-components-numericinput--default-default-theme) •
* [Design System](https://theguardian.design/2a1e5182b/p/097455-text-input-field/b/050445) •
* [GitHub](https://github.com/guardian/csnx/tree/main/libs/@guardian/source-development-kitchen/src/numeric-input/NumericInput.tsx) •
* [NPM](https://www.npmjs.com/package/@guardian/@guardian/source-development-kitchen)
*
* This is an iteration on the core TextInput component for taking numeric input, such as currency amounts.
* It can optionally display a prefix and/or suffix to add additonal visual context.
*
* **Note**: This component enforces inputmode="numeric" so a number keypad will appear on mobile devices
*/
export const NumericInput = ({
id,
label: labelText,
optional = false,
hideLabel = false,
supporting,
size = 'medium',
width,
error,
success,
prefixText,
suffixText,
cssOverrides,
...props
}: NumericInputProps) => {
const textInputId = id ?? generateSourceId();
return (
<>
<Label
text={labelText}
optional={!!optional}
hideLabel={hideLabel}
supporting={supporting}
size={size}
htmlFor={textInputId}
>
{error && (
<div css={inlineMessageMargin}>
<InlineError id={descriptionId(textInputId)} size={size}>
{error}
</InlineError>
</div>
)}
{!error && success && (
<div css={inlineMessageMargin}>
<InlineSuccess id={descriptionId(textInputId)} size={size}>
{success}
</InlineSuccess>
</div>
)}
</Label>
<div
css={[
inputWrapper,
width ? widths[width] : widthFluid,
supporting ? supportingTextMargin : labelMargin,
]}
>
{prefixText && (
<InputExtension
type="prefix"
size={size}
error={error}
success={success}
>
{prefixText}
</InputExtension>
)}
<input
css={(theme: InputTheme) => [
textInput(theme.textInput, size),
error ? errorInput(theme.textInput) : '',
!error && success ? successInput(theme.textInput) : '',
hasExtensions(prefixText, suffixText),
cssOverrides,
]}
type="text"
inputMode="numeric"
id={textInputId}
aria-required={!optional}
aria-invalid={!!error}
aria-describedby={error || success ? descriptionId(textInputId) : ''}
required={!optional}
{...props}
/>
{suffixText && (
<InputExtension
type="suffix"
size={size}
error={error}
success={success}
>
{suffixText}
</InputExtension>
)}
</div>
</>
);
};