@shopify/polaris
Version:
Shopify’s product component library
339 lines (304 loc) • 10.8 kB
JavaScript
import { Key } from '../../types.js';
import { objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.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,
ariaExpanded,
ariaActiveDescendant,
ariaAutocomplete,
showCharacterCount,
align,
onClearButtonClick,
onChange,
onFocus,
onBlur
}) {
const i18n = useI18n();
const [height, setHeight] = useState(null);
const [focus, setFocus] = useState(Boolean(focused));
const isAfterInitial = useIsAfterInitialMount$1();
const id = useUniqueId('TextField', idProp);
const inputRef = useRef(null);
const prefixRef = useRef(null);
const suffixRef = useRef(null);
const buttonPressTimer = useRef();
useEffect(() => {
const input = inputRef.current;
if (!input || focused === undefined) return;
focused ? input.focus() : input.blur();
}, [focused]);
const {
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.
const normalizedValue = typeof value === 'string' ? value : '';
const normalizedStep = step != null ? step : 1;
const normalizedMax = max != null ? max : Infinity;
const normalizedMin = min != null ? min : -Infinity;
const 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);
const inputType = type === 'currency' ? 'text' : type;
const prefixMarkup = prefix ? /*#__PURE__*/React$1.createElement("div", {
className: styles.Prefix,
id: `${id}Prefix`,
ref: prefixRef
}, prefix) : null;
const suffixMarkup = suffix ? /*#__PURE__*/React$1.createElement("div", {
className: styles.Suffix,
id: `${id}Suffix`,
ref: suffixRef
}, suffix) : null;
let characterCountMarkup = null;
if (showCharacterCount) {
const characterCount = normalizedValue.length;
const characterCountLabel = maxLength ? i18n.translate('Polaris.TextField.characterCountWithMaxLength', {
count: characterCount,
limit: maxLength
}) : i18n.translate('Polaris.TextField.characterCount', {
count: characterCount
});
const characterCountClassName = classNames(styles.CharacterCount, multiline && styles.AlignFieldBottom);
const characterCountText = !maxLength ? characterCount : `${characterCount}/${maxLength}`;
characterCountMarkup = /*#__PURE__*/React$1.createElement("div", {
id: `${id}CharacterCounter`,
className: characterCountClassName,
"aria-label": characterCountLabel,
"aria-live": focus ? 'polite' : 'off',
"aria-atomic": "true"
}, characterCountText);
}
const clearButtonVisible = normalizedValue !== '';
const clearButtonClassName = classNames(styles.ClearButton, !clearButtonVisible && styles['ClearButton-hidden']);
const clearButtonMarkup = clearButton ? /*#__PURE__*/React$1.createElement("button", {
type: "button",
className: clearButtonClassName,
onClick: handleClearButtonPress,
disabled: disabled,
tabIndex: clearButtonVisible ? 0 : -1
}, /*#__PURE__*/React$1.createElement(VisuallyHidden$1, null, i18n.translate('Polaris.Common.clear')), _ref) : null;
const handleNumberChange = useCallback(steps => {
if (onChange == null) {
return;
} // Returns the length of decimal places in a number
const dpl = num => (num.toString().split('.')[1] || []).length;
const 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.
const decimalPlaces = Math.max(dpl(numericValue), dpl(normalizedStep));
const newValue = Math.min(Number(normalizedMax), Math.max(numericValue + steps * normalizedStep, Number(normalizedMin)));
onChange(String(newValue.toFixed(decimalPlaces)), id);
}, [id, normalizedMax, normalizedMin, onChange, normalizedStep, value]);
const handleButtonRelease = useCallback(() => {
clearTimeout(buttonPressTimer.current);
}, []);
const handleButtonPress = useCallback(onChange => {
const minInterval = 50;
const decrementBy = 10;
let interval = 200;
const onChangeInterval = () => {
if (interval > minInterval) interval -= decrementBy;
onChange(0);
buttonPressTimer.current = window.setTimeout(onChangeInterval, interval);
};
buttonPressTimer.current = window.setTimeout(onChangeInterval, interval);
document.addEventListener('mouseup', handleButtonRelease, {
once: true
});
}, [handleButtonRelease]);
const spinnerMarkup = type === 'number' && step !== 0 && !disabled && !readOnly ? /*#__PURE__*/React$1.createElement(Spinner$1, {
onChange: handleNumberChange,
onMouseDown: handleButtonPress,
onMouseUp: handleButtonRelease
}) : null;
const style = multiline && height ? {
height
} : null;
const handleExpandingResize = useCallback(height => {
setHeight(height);
}, []);
const resizer = multiline && isAfterInitial ? /*#__PURE__*/React$1.createElement(Resizer$1, {
contents: normalizedValue || placeholder,
currentHeight: height,
minimumLines: typeof multiline === 'number' ? multiline : 1,
onHeightChange: handleExpandingResize
}) : null;
const describedBy = [];
if (error) {
describedBy.push(`${id}Error`);
}
if (helpText) {
describedBy.push(helpTextID(id));
}
if (showCharacterCount) {
describedBy.push(`${id}CharacterCounter`);
}
const labelledBy = [];
if (prefix) {
labelledBy.push(`${id}Prefix`);
}
if (suffix) {
labelledBy.push(`${id}Suffix`);
}
labelledBy.unshift(labelID(id));
const inputClassName = classNames(styles.Input, align && styles[variationName('Input-align', align)], suffix && styles['Input-suffixed'], clearButton && styles['Input-hasClearButton']);
const input = /*#__PURE__*/createElement(multiline ? 'textarea' : 'input', _objectSpread2({
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-expanded': ariaExpanded
}, normalizeAriaMultiline(multiline)));
const 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) {
const {
key,
which
} = event;
const 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) {
if (!multiline) return undefined;
return Boolean(multiline) || multiline > 0 ? {
'aria-multiline': true
} : undefined;
}
export { TextField };