@mantine/core
Version:
React components library focused on usability, accessibility and developer experience
1 lines • 18.5 kB
Source Map (JSON)
{"version":3,"file":"PinInput.cjs","names":["createVarsResolver","getSize","factory","useProps","useStyles","useResolvedStylesApi","createPinArray","Group","Input","classes","InputBase"],"sources":["../../../src/components/PinInput/PinInput.tsx"],"sourcesContent":["import { useRef, useState } from 'react';\nimport { assignRef, useId, useUncontrolled } from '@mantine/hooks';\nimport {\n BoxProps,\n createVarsResolver,\n DataAttributes,\n ElementProps,\n Factory,\n factory,\n getSize,\n MantineRadius,\n MantineSize,\n MantineSpacing,\n StylesApiProps,\n useProps,\n useResolvedStylesApi,\n useStyles,\n} from '../../core';\nimport { Group } from '../Group';\nimport { Input, InputProps } from '../Input';\nimport { InputBase } from '../InputBase';\nimport { createPinArray } from './create-pin-array/create-pin-array';\nimport classes from './PinInput.module.css';\n\nconst regex = {\n number: /^[0-9]+$/,\n alphanumeric: /^[a-zA-Z0-9]+$/i,\n};\n\nexport type PinInputStylesNames = 'root' | 'pinInput' | 'input';\n\nexport type PinInputCssVariables = {\n root: '--pin-input-size';\n};\n\nexport interface PinInputProps\n extends BoxProps, StylesApiProps<PinInputFactory>, ElementProps<'div', 'onChange' | 'ref'> {\n ref?: React.Ref<HTMLInputElement>;\n\n /** Hidden input `name` attribute */\n name?: string;\n\n /** Hidden input `form` attribute */\n form?: string;\n\n /** Key of `theme.spacing` or any valid CSS value to set `gap` between inputs, numbers are converted to rem @default 'md' */\n gap?: MantineSpacing;\n\n /** Key of `theme.radius` or any valid CSS value to set `border-radius`, numbers are converted to rem @default theme.defaultRadius */\n radius?: MantineRadius;\n\n /** Controls inputs `width` and `height` @default 'sm' */\n size?: MantineSize;\n\n /** If set, the first input is focused when component is mounted @default false */\n autoFocus?: boolean;\n\n /** Controlled component value */\n value?: string;\n\n /** Uncontrolled component default value */\n defaultValue?: string;\n\n /** Called when value changes */\n onChange?: (value: string) => void;\n\n /** Called when all inputs have value */\n onComplete?: (value: string) => void;\n\n /** Inputs placeholder @default '○' */\n placeholder?: string;\n\n /** Determines whether focus should be moved automatically to the next input once filled @default true */\n manageFocus?: boolean;\n\n /** Determines whether `autocomplete=\"one-time-code\"` attribute should be set on all inputs @default true */\n oneTimeCode?: boolean;\n\n /** Base id used to generate unique ids for inputs */\n id?: string;\n\n /** Adds disabled attribute to all inputs */\n disabled?: boolean;\n\n /** Sets `aria-invalid` attribute and applies error styles to all inputs */\n error?: boolean;\n\n /** Determines which values can be entered @default 'alphanumeric' */\n type?: 'alphanumeric' | 'number' | RegExp;\n\n /** Changes input type to `\"password\"` @default false */\n mask?: boolean;\n\n /** Number of inputs @default 4 */\n length?: number;\n\n /** If set, the user cannot edit the value */\n readOnly?: boolean;\n\n /** Inputs `type` attribute, inferred from the `type` prop if not specified */\n inputType?: React.HTMLInputTypeAttribute;\n\n /** `inputmode` attribute, inferred from the `type` prop if not specified */\n inputMode?:\n | 'none'\n | 'text'\n | 'tel'\n | 'url'\n | 'email'\n | 'numeric'\n | 'decimal'\n | 'search'\n | undefined;\n\n /** `aria-label` attribute */\n ariaLabel?: string;\n\n /** Props passed down to the hidden input */\n hiddenInputProps?: React.ComponentProps<'input'>;\n\n /** Assigns ref of the root element */\n rootRef?: React.Ref<HTMLDivElement>;\n\n /** Props added to the input element depending on its index */\n getInputProps?: (index: number) => InputProps & ElementProps<'input', 'size'> & DataAttributes;\n}\n\nexport type PinInputFactory = Factory<{\n props: PinInputProps;\n ref: HTMLDivElement;\n stylesNames: PinInputStylesNames;\n vars: PinInputCssVariables;\n}>;\n\nconst defaultProps = {\n gap: 'sm',\n length: 4,\n manageFocus: true,\n oneTimeCode: true,\n placeholder: '○',\n type: 'alphanumeric',\n ariaLabel: 'PinInput',\n size: 'sm',\n} satisfies Partial<PinInputProps>;\n\nconst varsResolver = createVarsResolver<PinInputFactory>((_, { size }) => ({\n root: {\n '--pin-input-size': getSize(size ?? 'sm', 'pin-input-size'),\n },\n}));\n\nexport const PinInput = factory<PinInputFactory>((props) => {\n const {\n name,\n form,\n className,\n value,\n defaultValue,\n variant,\n gap,\n style,\n size,\n classNames,\n styles,\n unstyled,\n length,\n onChange,\n onComplete,\n manageFocus,\n autoFocus,\n error,\n radius,\n disabled,\n oneTimeCode,\n placeholder,\n type,\n mask,\n readOnly,\n inputType,\n inputMode,\n ariaLabel,\n vars,\n id,\n hiddenInputProps,\n rootRef,\n getInputProps,\n attributes,\n ref,\n ...others\n } = useProps(['Input', 'PinInput'], defaultProps, props);\n\n const uuid = useId(id);\n\n const getStyles = useStyles<PinInputFactory>({\n name: 'PinInput',\n classes,\n props,\n className,\n style,\n classNames,\n styles,\n unstyled,\n attributes,\n vars,\n varsResolver,\n });\n\n const { resolvedClassNames, resolvedStyles } = useResolvedStylesApi<PinInputFactory>({\n classNames,\n styles,\n props,\n });\n\n const [focusedIndex, setFocusedIndex] = useState(-1);\n const inputsRef = useRef<Array<HTMLInputElement>>([]);\n const currentLength = length ?? 4;\n\n const completedRef = useRef(false);\n\n const [_value, setValues] = useUncontrolled<string[]>({\n value: value !== undefined ? createPinArray(currentLength, value) : undefined,\n defaultValue: defaultValue?.split('').slice(0, currentLength),\n finalValue: createPinArray(currentLength, ''),\n onChange: (val) => {\n const stringValue = val.join('').trim();\n onChange?.(stringValue);\n if (stringValue.length === currentLength && !completedRef.current) {\n completedRef.current = true;\n onComplete?.(stringValue);\n } else if (stringValue.length < currentLength) {\n completedRef.current = false;\n }\n },\n });\n\n const currentValue =\n _value.length !== currentLength ? createPinArray(currentLength, _value.join('')) : _value;\n\n const _valueToString = currentValue.join('').trim();\n\n const validate = (code: string) => {\n const re = type instanceof RegExp ? type : type && type in regex ? regex[type] : null;\n return re?.test(code);\n };\n\n const focusInputField = (dir: 'next' | 'prev', index: number) => {\n if (!manageFocus) {\n return;\n }\n\n if (dir === 'next') {\n const nextIndex = index + 1;\n if (nextIndex < currentLength) {\n inputsRef.current[nextIndex]?.focus();\n }\n } else if (dir === 'prev') {\n const prevIndex = index - 1;\n if (prevIndex >= 0) {\n inputsRef.current[prevIndex]?.focus();\n }\n }\n };\n\n const setFieldValue = (val: string, index: number) => {\n const values = [...currentValue];\n values[index] = val;\n setValues(values);\n return values;\n };\n\n const handleChange = (event: React.ChangeEvent<HTMLInputElement>, index: number) => {\n const inputValue = event.target.value;\n\n if (inputValue.length > 1) {\n const isPaste = inputValue.length > 2;\n if (isPaste) {\n const isValid = validate(inputValue);\n if (isValid) {\n setValues(createPinArray(currentLength, inputValue));\n const filledCount = Math.min(inputValue.length, currentLength);\n if (filledCount < currentLength) {\n focusInputField('next', filledCount - 1);\n }\n }\n return;\n }\n\n const newChar = inputValue.split('')[inputValue.length - 1];\n if (validate(newChar)) {\n setFieldValue(newChar, index);\n focusInputField('next', index);\n }\n return;\n }\n\n if (inputValue.length === 1) {\n if (validate(inputValue)) {\n setFieldValue(inputValue, index);\n focusInputField('next', index);\n } else {\n setFieldValue('', index);\n }\n } else if (inputValue.length === 0) {\n setFieldValue('', index);\n }\n };\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>, index: number) => {\n const { ctrlKey, metaKey, key, shiftKey, target } = event;\n const inputValue = (target as HTMLInputElement).value;\n\n if (inputMode === 'numeric') {\n const allowedKeys = ['Backspace', 'Tab', 'Control', 'Delete', 'ArrowLeft', 'ArrowRight'];\n const isModifierShortcut = ctrlKey || metaKey;\n const isAllowedKey =\n allowedKeys.includes(key) || isModifierShortcut || !Number.isNaN(Number(key));\n\n if (!isAllowedKey) {\n event.preventDefault();\n return;\n }\n }\n\n switch (key) {\n case 'ArrowLeft':\n event.preventDefault();\n focusInputField('prev', index);\n break;\n\n case 'ArrowRight':\n event.preventDefault();\n focusInputField('next', index);\n break;\n\n case 'Tab':\n if (shiftKey) {\n if (index > 0 && manageFocus) {\n event.preventDefault();\n focusInputField('prev', index);\n }\n }\n break;\n\n case ' ':\n event.preventDefault();\n focusInputField('next', index);\n break;\n\n case 'Delete':\n event.preventDefault();\n setFieldValue('', index);\n break;\n\n case 'Backspace':\n if (inputValue === '') {\n event.preventDefault();\n focusInputField('prev', index);\n } else {\n setFieldValue('', index);\n if (index < currentLength - 1) {\n event.preventDefault();\n focusInputField('prev', index);\n }\n }\n break;\n\n default:\n if (inputValue.length > 0 && key === currentValue[index]) {\n event.preventDefault();\n focusInputField('next', index);\n }\n }\n };\n\n const handleFocus = (event: React.FocusEvent<HTMLInputElement>, index: number) => {\n event.target.select();\n setFocusedIndex(index);\n };\n\n const handleBlur = () => {\n setFocusedIndex(-1);\n };\n\n const handlePaste = (event: React.ClipboardEvent<HTMLInputElement>) => {\n event.preventDefault();\n const pasteData = event.clipboardData.getData('text/plain').replace(/[\\n\\r\\s]+/g, '');\n const isValid = validate(pasteData.trim());\n\n if (isValid) {\n const pasteArray = createPinArray(currentLength, pasteData);\n setValues(pasteArray);\n const filledCount = pasteArray.filter((val) => val !== '').length;\n if (filledCount >= currentLength) {\n inputsRef.current[currentLength - 1]?.focus();\n } else {\n inputsRef.current[filledCount]?.focus();\n }\n }\n };\n\n return (\n <>\n <Group\n {...others}\n {...getStyles('root')}\n ref={rootRef}\n role=\"group\"\n id={uuid}\n gap={gap}\n unstyled={unstyled}\n wrap=\"nowrap\"\n variant={variant}\n __size={size}\n dir=\"ltr\"\n >\n {currentValue.map((char: string, index: number) => (\n <Input\n component=\"input\"\n {...getStyles('pinInput', {\n style: {\n '--input-padding': '0',\n '--input-text-align': 'center',\n } as React.CSSProperties,\n })}\n classNames={resolvedClassNames}\n styles={resolvedStyles}\n size={size}\n __staticSelector=\"PinInput\"\n id={`${uuid}-${index + 1}`}\n key={`${uuid}-${index}`}\n inputMode={inputMode || (type === 'number' ? 'numeric' : 'text')}\n onChange={(event) => handleChange(event, index)}\n onKeyDown={(event) => handleKeyDown(event, index)}\n onFocus={(event) => handleFocus(event, index)}\n onBlur={handleBlur}\n onPaste={handlePaste}\n type={inputType || (mask ? 'password' : type === 'number' ? 'tel' : 'text')}\n radius={radius}\n error={error}\n variant={variant}\n disabled={disabled}\n ref={(node) => {\n if (node) {\n index === 0 && assignRef(ref, node);\n inputsRef.current[index] = node;\n }\n }}\n autoComplete={oneTimeCode ? 'one-time-code' : 'off'}\n placeholder={focusedIndex === index ? '' : placeholder}\n value={char}\n autoFocus={autoFocus && index === 0}\n unstyled={unstyled}\n aria-label={ariaLabel}\n readOnly={readOnly}\n {...getInputProps?.(index)}\n />\n ))}\n </Group>\n\n <input type=\"hidden\" name={name} form={form} value={_valueToString} {...hiddenInputProps} />\n </>\n );\n});\n\nPinInput.classes = { ...classes, ...InputBase.classes };\nPinInput.varsResolver = varsResolver;\nPinInput.displayName = '@mantine/core/PinInput';\n\nexport namespace PinInput {\n export type Props = PinInputProps;\n export type StylesNames = PinInputStylesNames;\n export type Factory = PinInputFactory;\n}\n"],"mappings":"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"}