/* * Copyright 2020 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ import {AriaTextFieldProps} from '@react-types/textfield'; import { ChangeEvent, HTMLAttributes, type JSX, LabelHTMLAttributes, RefObject, useEffect } from 'react'; import {DOMAttributes, ValidationResult} from '@react-types/shared'; import {filterDOMProps, getOwnerWindow, mergeProps, useFormReset} from '@react-aria/utils'; import {useControlledState} from '@react-stately/utils'; import {useField} from '@react-aria/label'; import {useFocusable} from '@react-aria/focus'; import {useFormValidation} from '@react-aria/form'; import {useFormValidationState} from '@react-stately/form'; /** * A map of HTML element names and their interface types. * For example `'a'` -> `HTMLAnchorElement`. */ type IntrinsicHTMLElements = { [K in keyof IntrinsicHTMLAttributes]: IntrinsicHTMLAttributes[K] extends HTMLAttributes ? T : never }; /** * A map of HTML element names and their attribute interface types. * For example `'a'` -> `AnchorHTMLAttributes`. */ type IntrinsicHTMLAttributes = JSX.IntrinsicElements; type DefaultElementType = 'input'; /** * The intrinsic HTML element names that `useTextField` supports; e.g. `input`, * `textarea`. */ type TextFieldIntrinsicElements = keyof Pick; /** * The HTML element interfaces that `useTextField` supports based on what is * defined for `TextFieldIntrinsicElements`; e.g. `HTMLInputElement`, * `HTMLTextAreaElement`. */ type TextFieldHTMLElementType = Pick; /** * The HTML attributes interfaces that `useTextField` supports based on what * is defined for `TextFieldIntrinsicElements`; e.g. `InputHTMLAttributes`, * `TextareaHTMLAttributes`. */ type TextFieldHTMLAttributesType = Pick; /** * The type of `inputProps` returned by `useTextField`; e.g. `InputHTMLAttributes`, * `TextareaHTMLAttributes`. */ type TextFieldInputProps = TextFieldHTMLAttributesType[T]; export interface AriaTextFieldOptions extends AriaTextFieldProps { /** * The HTML element used to render the input, e.g. 'input', or 'textarea'. * It determines whether certain HTML attributes will be included in `inputProps`. * For example, [`type`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-type). * @default 'input' */ inputElementType?: T, /** * Controls whether inputted text is automatically capitalized and, if so, in what manner. * See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autocapitalize). */ autoCapitalize?: 'off' | 'none' | 'on' | 'sentences' | 'words' | 'characters' } /** * The type of `ref` object that can be passed to `useTextField` based on the given * intrinsic HTML element name; e.g.`RefObject`, * `RefObject`. */ type TextFieldRefObject = RefObject; export interface TextFieldAria extends ValidationResult { /** Props for the input element. */ inputProps: TextFieldInputProps, /** Props for the text field's visible label element, if any. */ labelProps: DOMAttributes | LabelHTMLAttributes, /** Props for the text field's description element, if any. */ descriptionProps: DOMAttributes, /** Props for the text field's error message element, if any. */ errorMessageProps: DOMAttributes } /** * Provides the behavior and accessibility implementation for a text field. * @param props - Props for the text field. * @param ref - Ref to the HTML input or textarea element. */ export function useTextField( props: AriaTextFieldOptions, ref: TextFieldRefObject ): TextFieldAria { let { inputElementType = 'input', isDisabled = false, isRequired = false, isReadOnly = false, type = 'text', validationBehavior = 'aria' } = props; let [value, setValue] = useControlledState(props.value, props.defaultValue || '', props.onChange); let {focusableProps} = useFocusable(props, ref); let validationState = useFormValidationState({ ...props, value }); let {isInvalid, validationErrors, validationDetails} = validationState.displayValidation; let {labelProps, fieldProps, descriptionProps, errorMessageProps} = useField({ ...props, isInvalid, errorMessage: props.errorMessage || validationErrors }); let domProps = filterDOMProps(props, {labelable: true}); const inputOnlyProps = { type, pattern: props.pattern }; useFormReset(ref, value, setValue); useFormValidation(props, validationState, ref); useEffect(() => { // This works around a React/Chrome bug that prevents textarea elements from validating when controlled. // We prevent React from updating defaultValue (i.e. children) of textarea when `value` changes, // which causes Chrome to skip validation. Only updating `value` is ok in our case since our // textareas are always controlled. React is planning on removing this synchronization in a // future major version. // https://github.com/facebook/react/issues/19474 // https://github.com/facebook/react/issues/11896 if (ref.current instanceof getOwnerWindow(ref.current).HTMLTextAreaElement) { let input = ref.current; Object.defineProperty(input, 'defaultValue', { get: () => input.value, set: () => {}, configurable: true }); } }, [ref]); return { labelProps, inputProps: mergeProps( domProps, inputElementType === 'input' ? inputOnlyProps : undefined, { disabled: isDisabled, readOnly: isReadOnly, required: isRequired && validationBehavior === 'native', 'aria-required': (isRequired && validationBehavior === 'aria') || undefined, 'aria-invalid': isInvalid || undefined, 'aria-errormessage': props['aria-errormessage'], 'aria-activedescendant': props['aria-activedescendant'], 'aria-autocomplete': props['aria-autocomplete'], 'aria-haspopup': props['aria-haspopup'], value, onChange: (e: ChangeEvent) => setValue(e.target.value), autoComplete: props.autoComplete, autoCapitalize: props.autoCapitalize, maxLength: props.maxLength, minLength: props.minLength, name: props.name, placeholder: props.placeholder, inputMode: props.inputMode, // Clipboard events onCopy: props.onCopy, onCut: props.onCut, onPaste: props.onPaste, // Composition events onCompositionEnd: props.onCompositionEnd, onCompositionStart: props.onCompositionStart, onCompositionUpdate: props.onCompositionUpdate, // Selection events onSelect: props.onSelect, // Input events onBeforeInput: props.onBeforeInput, onInput: props.onInput, ...focusableProps, ...fieldProps } ), descriptionProps, errorMessageProps, isInvalid, validationErrors, validationDetails }; }