UNPKG

@shopify/polaris

Version:

Shopify’s product component library

340 lines (303 loc) • 10.5 kB
import { Key } from '../../types.js'; import React$1, { useState, useRef, useEffect, useCallback, createElement } from 'react'; import { useFeatures } from '../../utilities/features/hooks.js'; import { useUniqueId } from '../../utilities/unique-id/hooks.js'; import { useI18n } from '../../utilities/i18n/hooks.js'; import { classNames, variationName } from '../../utilities/css.js'; import { useIsAfterInitialMount as useIsAfterInitialMount$1 } from '../../utilities/use-is-after-initial-mount.js'; import { CircleCancelMinor } from '@shopify/polaris-icons'; import { Icon as Icon$1 } from '../Icon/Icon.js'; import { VisuallyHidden as VisuallyHidden$1 } from '../VisuallyHidden/VisuallyHidden.js'; import { labelID } from '../Label/Label.js'; import { Labelled as Labelled$1, helpTextID } from '../Labelled/Labelled.js'; import { Connected as Connected$1 } from '../Connected/Connected.js'; import styles from './TextField.scss.js'; import { Resizer as Resizer$1 } from './components/Resizer/Resizer.js'; import { Spinner as Spinner$1 } from './components/Spinner/Spinner.js'; var _ref = /*#__PURE__*/React$1.createElement(Icon$1, { source: CircleCancelMinor, color: "inkLightest" }); function TextField({ prefix, suffix, placeholder, value, helpText, label, labelAction, labelHidden, disabled, clearButton, readOnly, autoFocus, focused, multiline, error, connectedRight, connectedLeft, type, name, id: idProp, role, step, autoComplete, max, maxLength, min, minLength, pattern, inputMode, spellCheck, ariaOwns, ariaControls, ariaActiveDescendant, ariaAutocomplete, showCharacterCount, align, onClearButtonClick, onChange, onFocus, onBlur }) { var i18n = useI18n(); var [height, setHeight] = useState(null); var [focus, setFocus] = useState(Boolean(focused)); var isAfterInitial = useIsAfterInitialMount$1(); var id = useUniqueId('TextField', idProp); var inputRef = useRef(null); var prefixRef = useRef(null); var suffixRef = useRef(null); var buttonPressTimer = useRef(); useEffect(() => { var input = inputRef.current; if (!input || focused === undefined) return; focused ? input.focus() : input.blur(); }, [focused]); var { newDesignLanguage } = useFeatures(); // Use a typeof check here as Typescript mostly protects us from non-stringy // values but overzealous usage of `any` in consuming apps means people have // been known to pass a number in, so make it clear that doesn't work. var normalizedValue = typeof value === 'string' ? value : ''; var normalizedStep = step != null ? step : 1; var normalizedMax = max != null ? max : Infinity; var normalizedMin = min != null ? min : -Infinity; var className = classNames(styles.TextField, Boolean(normalizedValue) && styles.hasValue, disabled && styles.disabled, readOnly && styles.readOnly, error && styles.error, multiline && styles.multiline, focus && styles.focus, newDesignLanguage && styles.newDesignLanguage); var inputType = type === 'currency' ? 'text' : type; var prefixMarkup = prefix ? /*#__PURE__*/React$1.createElement("div", { className: styles.Prefix, id: "".concat(id, "Prefix"), ref: prefixRef }, prefix) : null; var suffixMarkup = suffix ? /*#__PURE__*/React$1.createElement("div", { className: styles.Suffix, id: "".concat(id, "Suffix"), ref: suffixRef }, suffix) : null; var characterCountMarkup = null; if (showCharacterCount) { var characterCount = normalizedValue.length; var characterCountLabel = maxLength ? i18n.translate('Polaris.TextField.characterCountWithMaxLength', { count: characterCount, limit: maxLength }) : i18n.translate('Polaris.TextField.characterCount', { count: characterCount }); var characterCountClassName = classNames(styles.CharacterCount, multiline && styles.AlignFieldBottom); var characterCountText = !maxLength ? characterCount : "".concat(characterCount, "/").concat(maxLength); characterCountMarkup = /*#__PURE__*/React$1.createElement("div", { id: "".concat(id, "CharacterCounter"), className: characterCountClassName, "aria-label": characterCountLabel, "aria-live": focus ? 'polite' : 'off', "aria-atomic": "true" }, characterCountText); } var clearButtonMarkup = clearButton && normalizedValue !== '' ? /*#__PURE__*/React$1.createElement("button", { type: "button", className: styles.ClearButton, onClick: handleClearButtonPress, disabled: disabled }, /*#__PURE__*/React$1.createElement(VisuallyHidden$1, null, i18n.translate('Polaris.Common.clear')), _ref) : null; var handleNumberChange = useCallback(steps => { if (onChange == null) { return; } // Returns the length of decimal places in a number var dpl = num => (num.toString().split('.')[1] || []).length; var numericValue = value ? parseFloat(value) : 0; if (isNaN(numericValue)) { return; } // Making sure the new value has the same length of decimal places as the // step / value has. var decimalPlaces = Math.max(dpl(numericValue), dpl(normalizedStep)); var newValue = Math.min(Number(normalizedMax), Math.max(numericValue + steps * normalizedStep, Number(normalizedMin))); onChange(String(newValue.toFixed(decimalPlaces)), id); }, [id, normalizedMax, normalizedMin, onChange, normalizedStep, value]); var handleButtonRelease = useCallback(() => { clearTimeout(buttonPressTimer.current); }, []); var handleButtonPress = useCallback(onChange => { var minInterval = 50; var decrementBy = 10; var interval = 200; var onChangeInterval = () => { if (interval > minInterval) interval -= decrementBy; onChange(); buttonPressTimer.current = window.setTimeout(onChangeInterval, interval); }; buttonPressTimer.current = window.setTimeout(onChangeInterval, interval); document.addEventListener('mouseup', handleButtonRelease, { once: true }); }, [handleButtonRelease]); var spinnerMarkup = type === 'number' && !disabled && !readOnly ? /*#__PURE__*/React$1.createElement(Spinner$1, { onChange: handleNumberChange, onMouseDown: handleButtonPress, onMouseUp: handleButtonRelease }) : null; var style = multiline && height ? { height } : null; var handleExpandingResize = useCallback(height => { setHeight(height); }, []); var resizer = multiline && isAfterInitial ? /*#__PURE__*/React$1.createElement(Resizer$1, { contents: normalizedValue || placeholder, currentHeight: height, minimumLines: typeof multiline === 'number' ? multiline : 1, onHeightChange: handleExpandingResize }) : null; var describedBy = []; if (error) { describedBy.push("".concat(id, "Error")); } if (helpText) { describedBy.push(helpTextID(id)); } if (showCharacterCount) { describedBy.push("".concat(id, "CharacterCounter")); } var labelledBy = []; if (prefix) { labelledBy.push("".concat(id, "Prefix")); } if (suffix) { labelledBy.push("".concat(id, "Suffix")); } labelledBy.unshift(labelID(id)); var inputClassName = classNames(styles.Input, align && styles[variationName('Input-align', align)], suffix && styles['Input-suffixed'], clearButton && styles['Input-hasClearButton']); var input = /*#__PURE__*/createElement(multiline ? 'textarea' : 'input', { name, id, disabled, readOnly, role, autoFocus, value: normalizedValue, placeholder, onFocus, onBlur, onKeyPress: handleKeyPress, style, autoComplete: normalizeAutoComplete(autoComplete), className: inputClassName, onChange: handleChange, ref: inputRef, min, max, step, minLength, maxLength, spellCheck, pattern, inputMode, type: inputType, 'aria-describedby': describedBy.length ? describedBy.join(' ') : undefined, 'aria-labelledby': labelledBy.join(' '), 'aria-invalid': Boolean(error), 'aria-owns': ariaOwns, 'aria-activedescendant': ariaActiveDescendant, 'aria-autocomplete': ariaAutocomplete, 'aria-controls': ariaControls, 'aria-multiline': normalizeAriaMultiline(multiline) }); var backdropClassName = classNames(styles.Backdrop, newDesignLanguage && connectedLeft && styles['Backdrop-connectedLeft'], newDesignLanguage && connectedRight && styles['Backdrop-connectedRight']); return /*#__PURE__*/React$1.createElement(Labelled$1, { label: label, id: id, error: error, action: labelAction, labelHidden: labelHidden, helpText: helpText }, /*#__PURE__*/React$1.createElement(Connected$1, { left: connectedLeft, right: connectedRight }, /*#__PURE__*/React$1.createElement("div", { className: className, onFocus: handleFocus, onBlur: handleBlur, onClick: handleClick }, prefixMarkup, input, suffixMarkup, characterCountMarkup, clearButtonMarkup, spinnerMarkup, /*#__PURE__*/React$1.createElement("div", { className: backdropClassName }), resizer))); function handleClearButtonPress() { onClearButtonClick && onClearButtonClick(id); } function handleKeyPress(event) { var { key, which } = event; var numbersSpec = /[\d.eE+-]$/; if (type !== 'number' || which === Key.Enter || numbersSpec.test(key)) { return; } event.preventDefault(); } function containsAffix(target) { return target instanceof HTMLElement && (prefixRef.current && prefixRef.current.contains(target) || suffixRef.current && suffixRef.current.contains(target)); } function handleChange(event) { onChange && onChange(event.currentTarget.value, id); } function handleFocus({ target }) { if (containsAffix(target)) { return; } setFocus(true); } function handleBlur() { setFocus(false); } function handleClick({ target }) { if (containsAffix(target)) { return; } inputRef.current && inputRef.current.focus(); } } function normalizeAutoComplete(autoComplete) { if (autoComplete === true) { return 'on'; } else if (autoComplete === false) { return 'off'; } else { return autoComplete; } } function normalizeAriaMultiline(multiline) { switch (typeof multiline) { case 'undefined': return false; case 'boolean': return multiline; case 'number': return Boolean(multiline > 0); } } export { TextField };