import React from 'react'; import styled from 'styled-components'; import { getFocusRing, getRadius } from '@lendi-ui/commons/borders'; import { pxToRem } from '@lendi-ui/commons/utils'; import type { GlobalProps } from '@lendi-ui/commons/utils'; // TODO: refactor text field into a cleaner package (e.g. `@lendi-ui/input`) and promote the InputBase to include all common functionality between numeric, phone, text and unit fields, possibly Select? export interface InputFieldBaseProps extends GlobalProps { /** * The id to target the input element. This is different than the `id` global prop, which applies to the outer wrapper (which includes the labels, etc) */ inputId?: string; /** * The name of the input element, used when submitting an HTML form. */ name?: string; isRequired?: boolean; isDisabled?: boolean; placeholder?: string; /** * Any descriptor text that will be rendered below the input. */ helpText?: string; /** * When validation state is invalid, this text will display below the field or help text, if present. */ errorText?: string; validationState?: 'valid' | 'invalid'; /** * The width of the whole field including the labels and help text * @default '100%' */ width?: string; minWidth?: string; maxWidth?: string; maxCharCount?: number; minCharCount?: number; onChange?: (e: React.ChangeEvent) => void; onFocus?: (e: React.FocusEvent) => void; onBlur?: (e: React.FocusEvent) => void; /** * An action button rendered to the right of the field. 'Clear' renders an X that removes previous data when clicked. */ inlineAction?: 'clear'; } interface ControlledProps { /** * The current value (controlled). */ value?: V extends number ? number : V extends Date ? Date : string; onChange?: (e: React.ChangeEvent) => void; defaultValue?: never; } interface UncontrolledProps { /** * The default value (uncontrolled). */ defaultValue?: V extends number ? number : V extends Date ? Date : string; value?: V extends number ? number : V extends Date ? Date : string; onChange?: never; } interface WithLabelProps { label: string; labelSuffix?: '(optional)' | '(required)'; ariaLabelledBy?: never; ariaLabel?: never; } interface NoLabelProps { ariaLabel: string; ariaLabelledBy?: never; label?: never; labelSuffix?: never; } interface LabelledByProps { /** * Use ariaLabelledBy to refer to the id of the element being used as the label in the rare cases that another element is meant to serve as the label (like a heading) */ ariaLabelledBy: string; ariaLabel?: never; label?: never; labelSuffix?: never; } type LabelProps = LabelledByProps | NoLabelProps | WithLabelProps; type ControlProps = ControlledProps | UncontrolledProps; export type InputProps = T & LabelProps & ControlProps; export const InputBaseComponent = styled.input` font-family: ${({ theme }) => theme.asset.font.body.family}; font-size: ${pxToRem(16)}; color: ${({ theme }) => theme.color.text.main}; background-color: ${({ theme }) => theme.color.background.container}; border: ${({ validationState, theme }) => validationState === 'invalid' ? `2px solid ${theme.color.interaction.error}` : `1px solid ${theme.color.border.interaction.default}`}; border-radius: ${() => getRadius()}; padding: ${pxToRem(16)}; height: ${pxToRem(48)}; width: 100%; &:hover { ${({ validationState, theme }) => validationState !== 'invalid' && `border: 1px solid ${theme.color.border.interaction.hover}`}; } &::placeholder { color: ${({ theme }) => theme.color.text.main}; } &:focus-visible { outline: 0; box-shadow: ${getFocusRing()}; } &:disabled { background-color: ${({ theme }) => theme.color.fill.disabled}; border: 1px solid ${({ theme }) => theme.color.interaction.disabled}; &::placeholder { color: ${({ theme }) => theme.color.interaction.disabled}; } } `;