grommet
Version:
focus on the essential experience
477 lines (471 loc) • 19 kB
JavaScript
;
exports.__esModule = true;
exports.MaskedInput = void 0;
var _react = _interopRequireWildcard(require("react"));
var _styledComponents = _interopRequireDefault(require("styled-components"));
var _Box = require("../Box");
var _Button = require("../Button");
var _Drop = require("../Drop");
var _FormContext = require("../Form/FormContext");
var _Keyboard = require("../Keyboard");
var _utils = require("../../utils");
var _StyledMaskedInput = require("./StyledMaskedInput");
var _propTypes = require("./propTypes");
var _useThemeValue2 = require("../../utils/useThemeValue");
var _excluded = ["a11yTitle", "dropHeight", "dropProps", "focus", "focusIndicator", "icon", "id", "mask", "name", "onBlur", "onChange", "onFocus", "onKeyDown", "placeholder", "plain", "reverse", "textAlign", "value"];
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, "default": e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
var parseValue = function parseValue(mask, value) {
// break the value up into mask parts
var valueParts = []; // { part, beginIndex, endIndex }
var valueIndex = 0;
var maskIndex = 0;
while (value !== undefined && valueIndex < value.length && maskIndex < mask.length) {
var item = mask[maskIndex];
var found = void 0;
if (item.fixed) {
var length = item.fixed.length;
// grab however much of value (starting at valueIndex) matches
// item.fixed. If none matches it and there is more in value
// add in the fixed item.
var matching = 0;
while (matching < length && value[valueIndex + matching] === item.fixed[matching]) {
matching += 1;
}
if (matching > 0) {
var part = value.slice(valueIndex, valueIndex + matching);
if (valueIndex + matching < value.length) {
// matched part of the fixed portion but there's more stuff
// after it. Go ahead and fill in the entire fixed chunk
part = item.fixed;
}
valueParts.push({
part: part,
beginIndex: valueIndex,
endIndex: valueIndex + matching - 1
});
valueIndex += matching;
} else {
valueParts.push({
part: item.fixed,
beginIndex: valueIndex,
endIndex: valueIndex + length - 1
});
}
maskIndex += 1;
found = true;
} else if (item.options && item.restrictToOptions !== false) {
// reverse assuming larger is later
found = item.options.slice(0).reverse()
// eslint-disable-next-line no-loop-func
.some(function (option) {
var length = option.length;
var part = value.slice(valueIndex, valueIndex + length);
if (part === option) {
valueParts.push({
part: part,
beginIndex: valueIndex,
endIndex: valueIndex + length - 1
});
valueIndex += length;
maskIndex += 1;
return true;
}
return false;
});
}
if (!found) {
if (item.regexp) {
var minLength = Array.isArray(item.length) && item.length[0] || item.length || 1;
var maxLength = Array.isArray(item.length) && item.length[1] || item.length || value.length - valueIndex;
var _length = maxLength;
while (!found && _length >= minLength) {
var _part = value.slice(valueIndex, valueIndex + _length);
if (item.regexp.test(_part)) {
valueParts.push({
part: _part,
beginIndex: valueIndex,
endIndex: valueIndex + _length - 1
});
valueIndex += _length;
maskIndex += 1;
found = true;
}
_length -= 1;
}
if (!found) {
valueIndex = value.length;
}
} else {
var _length2 = Array.isArray(item.length) ? item.length[1] : item.length || value.length - valueIndex;
var _part2 = value.slice(valueIndex, valueIndex + _length2);
valueParts.push({
part: _part2,
beginIndex: valueIndex,
endIndex: valueIndex + _length2 - 1
});
valueIndex += _length2;
maskIndex += 1;
}
}
}
return valueParts;
};
var defaultMask = [{
regexp: /[^]*/
}];
var ContainerBox = (0, _styledComponents["default"])(_Box.Box).withConfig({
displayName: "MaskedInput__ContainerBox",
componentId: "sc-af8hzu-0"
})(["", ";"], function (props) {
return props.dropHeight ? (0, _utils.sizeStyle)('max-height', props.dropHeight, props.theme) : 'max-height: inherit;';
});
var dropAlign = {
top: 'bottom',
left: 'left'
};
var MaskedInput = exports.MaskedInput = /*#__PURE__*/(0, _react.forwardRef)(function (_ref, ref) {
var a11yTitle = _ref.a11yTitle,
dropHeight = _ref.dropHeight,
dropProps = _ref.dropProps,
focusProp = _ref.focus,
_ref$focusIndicator = _ref.focusIndicator,
focusIndicator = _ref$focusIndicator === void 0 ? true : _ref$focusIndicator,
icon = _ref.icon,
id = _ref.id,
_ref$mask = _ref.mask,
mask = _ref$mask === void 0 ? defaultMask : _ref$mask,
name = _ref.name,
_onBlur = _ref.onBlur,
onChange = _ref.onChange,
_onFocus = _ref.onFocus,
onKeyDown = _ref.onKeyDown,
placeholder = _ref.placeholder,
plain = _ref.plain,
reverse = _ref.reverse,
textAlign = _ref.textAlign,
valueProp = _ref.value,
rest = _objectWithoutPropertiesLoose(_ref, _excluded);
var _useThemeValue = (0, _useThemeValue2.useThemeValue)(),
theme = _useThemeValue.theme,
passThemeFlag = _useThemeValue.passThemeFlag;
var formContext = (0, _react.useContext)(_FormContext.FormContext);
var _formContext$useFormI = formContext.useFormInput({
name: name,
value: valueProp,
initialValue: ''
}),
value = _formContext$useFormI[0],
setValue = _formContext$useFormI[1];
var valueParts = (0, _react.useMemo)(function () {
return parseValue(mask, value);
}, [mask, value]);
var inputRef = (0, _utils.useForwardedRef)(ref);
var dropRef = (0, _react.useRef)();
// Caller's ref, if provided
var _useState = (0, _react.useState)(),
dropPropsTarget = _useState[0],
setDropPropsTarget = _useState[1];
(0, _react.useEffect)(function () {
var nextDropPropsTarget;
// If caller provided a ref, set to 'pending' until ref.current is defined
if (dropProps && 'target' in dropProps) {
nextDropPropsTarget = dropProps.target || 'pending';
setDropPropsTarget(nextDropPropsTarget);
}
}, [dropProps]);
var _useState2 = (0, _react.useState)(focusProp),
focus = _useState2[0],
setFocus = _useState2[1];
var _useState3 = (0, _react.useState)(),
activeMaskIndex = _useState3[0],
setActiveMaskIndex = _useState3[1];
var _useState4 = (0, _react.useState)(),
activeOptionIndex = _useState4[0],
setActiveOptionIndex = _useState4[1];
var _useState5 = (0, _react.useState)(),
showDrop = _useState5[0],
setShowDrop = _useState5[1];
(0, _react.useEffect)(function () {
if (focus) {
var timer = setTimeout(function () {
// determine which mask element the caret is at
var caretIndex = inputRef.current.selectionStart;
var maskIndex;
valueParts.some(function (part, index) {
if (part.beginIndex <= caretIndex && part.endIndex >= caretIndex) {
maskIndex = index;
return true;
}
return false;
});
if (maskIndex === undefined && valueParts.length < mask.length) {
maskIndex = valueParts.length; // first unused one
}
if (maskIndex && mask[maskIndex].fixed) {
maskIndex -= 1; // fixed mask parts are never "active"
}
if (maskIndex !== activeMaskIndex) {
setActiveMaskIndex(maskIndex);
setActiveOptionIndex(-1);
setShowDrop(maskIndex >= 0 && mask[maskIndex].options && true);
}
}, 10); // 10ms empirically chosen
return function () {
return clearTimeout(timer);
};
}
return undefined;
}, [activeMaskIndex, focus, inputRef, mask, valueParts]);
var setInputValue = (0, _react.useCallback)(function (nextValue) {
// Calling set value function directly on input because React library
// overrides setter `event.target.value =` and loses original event
// target fidelity.
// https://stackoverflow.com/a/46012210 &&
// https://github.com/grommet/grommet/pull/3171#discussion_r296415239
var nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeInputValueSetter.call(inputRef.current, nextValue);
var event = new Event('input', {
bubbles: true
});
inputRef.current.dispatchEvent(event);
}, [inputRef]);
var _useState6 = (0, _react.useState)(),
mouseMovedSinceLastKey = _useState6[0],
setMouseMovedSinceLastKey = _useState6[1];
// This could be due to a paste or as the user is typing.
var onChangeInput = (0, _react.useCallback)(function (event) {
var eventValue = event.target.value;
// Align with the mask.
var nextValueParts = parseValue(mask, eventValue);
var nextValue = nextValueParts.map(function (part) {
return part.part;
}).join('');
if (nextValue !== eventValue) {
// The mask adjusted the next value. If something was added,
// the value must be valid. Change the actual input value
// to correspond.
// This will re-trigger this callback with the next value.
if (nextValue.length > eventValue.length) setInputValue(nextValue);
// If the nextValue is shorter, something must be invalid.
else if (value && eventValue.length < value.length) {
// If the user is removing characters, preserve what the
// user is working on.
setValue(eventValue);
if (onChange) onChange(event);
} else {
// If the user is adding invalid characters, don't allow it.
// Revert to the prior value.
setInputValue(value);
}
} else if (value !== nextValue) {
setValue(nextValue);
if (onChange) onChange(event);
}
}, [mask, onChange, setInputValue, setValue, value]);
var onOption = (0, _react.useCallback)(function (option) {
return function () {
var nextValueParts = [].concat(valueParts);
nextValueParts[activeMaskIndex] = {
part: option
};
// add any fixed parts that follow
var index = activeMaskIndex + 1;
while (index < mask.length && !nextValueParts[index] && mask[index].fixed) {
nextValueParts[index] = {
part: mask[index].fixed
};
index += 1;
}
var nextValue = nextValueParts.map(function (part) {
return part.part;
}).join('');
setInputValue(nextValue);
// restore focus to input
inputRef.current.focus();
};
}, [activeMaskIndex, inputRef, mask, setInputValue, valueParts]);
var onNextOption = (0, _react.useCallback)(function (event) {
var item = mask[activeMaskIndex];
if (item && item.options) {
event.preventDefault();
var index = Math.min(activeOptionIndex + 1, item.options.length - 1);
setMouseMovedSinceLastKey(false);
setActiveOptionIndex(index);
}
}, [activeMaskIndex, activeOptionIndex, mask]);
var onPreviousOption = (0, _react.useCallback)(function (event) {
if (activeMaskIndex >= 0 && mask[activeMaskIndex].options) {
event.preventDefault();
var index = Math.max(activeOptionIndex - 1, 0);
setMouseMovedSinceLastKey(false);
setActiveOptionIndex(index);
}
}, [activeMaskIndex, activeOptionIndex, mask]);
var onSelectOption = (0, _react.useCallback)(function (event) {
if (activeMaskIndex >= 0 && activeOptionIndex >= 0) {
event.preventDefault();
var option = mask[activeMaskIndex].options[activeOptionIndex];
onOption(option)();
}
}, [activeMaskIndex, activeOptionIndex, mask, onOption]);
var onEsc = (0, _react.useCallback)(function (event) {
if (showDrop) {
// we have to stop both synthetic events and native events
// drop and layer should not close by pressing esc on this input
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
setShowDrop(false);
}
}, [showDrop]);
var onHideDrop = (0, _react.useCallback)(function () {
return setShowDrop(false);
}, []);
var renderPlaceholder = function renderPlaceholder() {
return mask.map(function (item) {
return item.placeholder || item.fixed;
}).join('');
};
var maskedInputIcon = (0, _utils.useSizedIcon)(icon, rest.size, theme);
/*
If the masked input has a list of options, add the WAI-ARIA 1.2
combobox role and states.
*/
var comboboxProps = {};
var activeOptionID;
var options = (0, _react.useMemo)(function () {
var _mask$find, _mask$activeMaskIndex;
var res;
if (!activeMaskIndex)
// ensures that comboboxProps gets set on input initially
res = (_mask$find = mask.find(function (item) {
var _item$options;
return (item == null || (_item$options = item.options) == null ? void 0 : _item$options.length) > 0;
})) == null ? void 0 : _mask$find.options;else res = (_mask$activeMaskIndex = mask[activeMaskIndex]) == null ? void 0 : _mask$activeMaskIndex.options;
return res;
}, [mask, activeMaskIndex]);
if (id && (options == null ? void 0 : options.length) > 0) {
if (showDrop && options) {
activeOptionID = "listbox-option-" + activeOptionIndex + "__" + id;
}
comboboxProps = {
'aria-activedescendant': activeOptionID,
'aria-autocomplete': 'list',
'aria-expanded': showDrop ? 'true' : 'false',
'aria-controls': showDrop ? "listbox__" + id : undefined,
role: 'combobox'
};
}
return /*#__PURE__*/_react["default"].createElement(_StyledMaskedInput.StyledMaskedInputContainer, _extends({
plain: plain,
onMouseMove: function onMouseMove() {
return setMouseMovedSinceLastKey(true);
}
}, passThemeFlag), maskedInputIcon && /*#__PURE__*/_react["default"].createElement(_StyledMaskedInput.StyledIcon, {
reverse: reverse,
theme: theme
}, maskedInputIcon), /*#__PURE__*/_react["default"].createElement(_Keyboard.Keyboard, {
onEsc: onEsc,
onTab: showDrop ? function () {
return setShowDrop(false);
} : undefined,
onLeft: undefined,
onRight: undefined,
onUp: onPreviousOption,
onDown: showDrop ? onNextOption : function () {
return setShowDrop(true);
},
onEnter: onSelectOption,
onKeyDown: onKeyDown
}, /*#__PURE__*/_react["default"].createElement(_StyledMaskedInput.StyledMaskedInput, _extends({
ref: inputRef,
"aria-label": a11yTitle,
id: id,
name: name,
autoComplete: "off",
focusIndicator: focusIndicator,
plain: plain,
placeholder: placeholder || renderPlaceholder(),
icon: icon,
reverse: reverse,
focus: focus,
textAlign: textAlign
}, comboboxProps, rest, {
value: value,
theme: theme,
onFocus: function onFocus(event) {
setFocus(true);
setShowDrop(true);
if (_onFocus) _onFocus(event);
},
onBlur: function onBlur(event) {
setFocus(false);
// This will be called when the user clicks on a suggestion,
// check for that and don't remove the drop in that case.
// Drop will already have removed itself if the user has focused
// outside of the Drop.
if (!dropRef.current) setShowDrop(false);
if (_onBlur) _onBlur(event);
},
onChange: onChangeInput
}))), showDrop && mask[activeMaskIndex] && mask[activeMaskIndex].options &&
// If caller has specified dropProps.target, ensure target is defined
dropPropsTarget !== 'pending' && /*#__PURE__*/_react["default"].createElement(_Drop.Drop, _extends({
id: id ? "masked-input-drop__" + id : undefined,
align: dropAlign,
responsive: false,
target: inputRef.current,
onClickOutside: onHideDrop,
onEsc: onHideDrop
// MaskedInput manages its own keyboard behavior via Keyboard
,
trapFocus: false
}, dropProps), /*#__PURE__*/_react["default"].createElement(ContainerBox, _extends({
ref: dropRef,
overflow: "auto",
id: id ? "listbox__" + id : undefined,
role: "listbox",
dropHeight: dropHeight,
onMouseOver: function onMouseOver() {
return setMouseMovedSinceLastKey(true);
}
}, passThemeFlag), mask[activeMaskIndex].options.map(function (option, index) {
// Determine whether the label is done as a child or
// as an option Button kind property.
var child = !theme.button.option ?
/*#__PURE__*/
// Not adding a theme object now because this code path
// is not used in the HPE theme, but we may add theme
// support here in the future.
_react["default"].createElement(_Box.Box, {
pad: {
horizontal: 'small',
vertical: 'xsmall'
}
}, option) : undefined;
// if we have a child, turn on plain, and hoverIndicator
return /*#__PURE__*/_react["default"].createElement(_Box.Box, {
key: option,
flex: false
}, /*#__PURE__*/_react["default"].createElement(_Button.Button, {
id: id ? "listbox-option-" + index + "__" + id : undefined,
tabIndex: "-1",
onClick: onOption(option),
onMouseOver: function onMouseOver() {
return setActiveOptionIndex(index);
},
onFocus: function onFocus() {},
active: index === activeOptionIndex,
plain: !child ? undefined : true,
align: "start",
kind: !child ? 'option' : undefined,
hoverIndicator: !child ? undefined : 'background',
label: !child ? option : undefined,
keyboard: !mouseMovedSinceLastKey
}, child));
}))));
});
MaskedInput.displayName = 'MaskedInput';
MaskedInput.propTypes = _propTypes.MaskedInputPropTypes;