@shopify/polaris
Version:
Shopify’s admin product component library
337 lines (299 loc) • 11.2 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var React = require('react');
var polarisIcons = require('@shopify/polaris-icons');
var css = require('../../utilities/css.js');
var useIsAfterInitialMount = require('../../utilities/use-is-after-initial-mount.js');
var types = require('../../types.js');
var TextField$1 = require('./TextField.scss.js');
var Labelled = require('../Labelled/Labelled.js');
var Connected = require('../Connected/Connected.js');
var Spinner = require('./components/Spinner/Spinner.js');
var Resizer = require('./components/Resizer/Resizer.js');
var Label = require('../Label/Label.js');
var hooks = require('../../utilities/i18n/hooks.js');
var hooks$1 = require('../../utilities/unique-id/hooks.js');
var VisuallyHidden = require('../VisuallyHidden/VisuallyHidden.js');
var Icon = require('../Icon/Icon.js');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
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,
maxHeight,
min,
minLength,
pattern,
inputMode,
spellCheck,
ariaOwns,
ariaControls,
ariaExpanded,
ariaActiveDescendant,
ariaAutocomplete,
showCharacterCount,
align,
onClearButtonClick,
onChange,
onFocus,
onBlur,
requiredIndicator,
monospaced
}) {
const i18n = hooks.useI18n();
const [height, setHeight] = React.useState(null);
const [focus, setFocus] = React.useState(Boolean(focused));
const isAfterInitial = useIsAfterInitialMount.useIsAfterInitialMount();
const id = hooks$1.useUniqueId('TextField', idProp);
const inputRef = React.useRef(null);
const prefixRef = React.useRef(null);
const suffixRef = React.useRef(null);
const buttonPressTimer = React.useRef();
React.useEffect(() => {
const input = inputRef.current;
if (!input || focused === undefined) return;
focused ? input.focus() : input.blur();
}, [focused]); // 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 = css.classNames(TextField$1['default'].TextField, Boolean(normalizedValue) && TextField$1['default'].hasValue, disabled && TextField$1['default'].disabled, readOnly && TextField$1['default'].readOnly, error && TextField$1['default'].error, multiline && TextField$1['default'].multiline, focus && TextField$1['default'].focus);
const inputType = type === 'currency' ? 'text' : type;
const prefixMarkup = prefix ? /*#__PURE__*/React__default['default'].createElement("div", {
className: TextField$1['default'].Prefix,
id: `${id}Prefix`,
ref: prefixRef
}, prefix) : null;
const suffixMarkup = suffix ? /*#__PURE__*/React__default['default'].createElement("div", {
className: TextField$1['default'].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 = css.classNames(TextField$1['default'].CharacterCount, multiline && TextField$1['default'].AlignFieldBottom);
const characterCountText = !maxLength ? characterCount : `${characterCount}/${maxLength}`;
characterCountMarkup = /*#__PURE__*/React__default['default'].createElement("div", {
id: `${id}CharacterCounter`,
className: characterCountClassName,
"aria-label": characterCountLabel,
"aria-live": focus ? 'polite' : 'off',
"aria-atomic": "true"
}, characterCountText);
}
const clearButtonVisible = normalizedValue !== '';
const clearButtonMarkup = clearButtonVisible && clearButton ? /*#__PURE__*/React__default['default'].createElement("button", {
type: "button",
className: TextField$1['default'].ClearButton,
onClick: handleClearButtonPress,
disabled: disabled
}, /*#__PURE__*/React__default['default'].createElement(VisuallyHidden.VisuallyHidden, null, i18n.translate('Polaris.Common.clear')), /*#__PURE__*/React__default['default'].createElement(Icon.Icon, {
source: polarisIcons.CircleCancelMinor,
color: "base"
})) : null;
const handleNumberChange = React.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 = React.useCallback(() => {
clearTimeout(buttonPressTimer.current);
}, []);
const handleButtonPress = React.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__default['default'].createElement(Spinner.Spinner, {
onChange: handleNumberChange,
onMouseDown: handleButtonPress,
onMouseUp: handleButtonRelease
}) : null;
const style = multiline && height ? {
height,
maxHeight
} : null;
const handleExpandingResize = React.useCallback(height => {
setHeight(height);
}, []);
const resizer = multiline && isAfterInitial ? /*#__PURE__*/React__default['default'].createElement(Resizer.Resizer, {
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(Labelled.helpTextID(id));
}
if (showCharacterCount) {
describedBy.push(`${id}CharacterCounter`);
}
const labelledBy = [];
if (prefix) {
labelledBy.push(`${id}Prefix`);
}
if (suffix) {
labelledBy.push(`${id}Suffix`);
}
labelledBy.unshift(Label.labelID(id));
const inputClassName = css.classNames(TextField$1['default'].Input, align && TextField$1['default'][css.variationName('Input-align', align)], suffix && TextField$1['default']['Input-suffixed'], clearButton && TextField$1['default']['Input-hasClearButton'], monospaced && TextField$1['default'].monospaced);
const input = /*#__PURE__*/React.createElement(multiline ? 'textarea' : 'input', {
name,
id,
disabled,
readOnly,
role,
autoFocus,
value: normalizedValue,
placeholder,
onFocus,
onBlur,
onKeyPress: handleKeyPress,
style,
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,
'aria-required': requiredIndicator,
...normalizeAriaMultiline(multiline)
});
const backdropClassName = css.classNames(TextField$1['default'].Backdrop, connectedLeft && TextField$1['default']['Backdrop-connectedLeft'], connectedRight && TextField$1['default']['Backdrop-connectedRight']);
return /*#__PURE__*/React__default['default'].createElement(Labelled.Labelled, {
label: label,
id: id,
error: error,
action: labelAction,
labelHidden: labelHidden,
helpText: helpText,
requiredIndicator: requiredIndicator
}, /*#__PURE__*/React__default['default'].createElement(Connected.Connected, {
left: connectedLeft,
right: connectedRight
}, /*#__PURE__*/React__default['default'].createElement("div", {
className: className,
onFocus: handleFocus,
onBlur: handleBlur,
onClick: handleClick
}, prefixMarkup, input, suffixMarkup, characterCountMarkup, clearButtonMarkup, spinnerMarkup, /*#__PURE__*/React__default['default'].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 === types.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
}) {
var _inputRef$current;
if (containsAffix(target) || focus) {
return;
}
(_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 ? void 0 : _inputRef$current.focus();
}
}
function normalizeAriaMultiline(multiline) {
if (!multiline) return undefined;
return Boolean(multiline) || multiline > 0 ? {
'aria-multiline': true
} : undefined;
}
exports.TextField = TextField;