@shopify/polaris
Version:
Shopify’s product component library
340 lines (303 loc) • 10.5 kB
JavaScript
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 };