UNPKG

grommet

Version:

focus on the essential experience

477 lines (463 loc) 21.1 kB
"use strict"; exports.__esModule = true; exports.TextInput = 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 _InfiniteScroll = require("../InfiniteScroll"); var _Keyboard = require("../Keyboard"); var _FormContext = require("../Form/FormContext"); var _contexts = require("../../contexts"); var _utils = require("../../utils"); var _StyledTextInput = require("./StyledTextInput"); var _MessageContext = require("../../contexts/MessageContext"); var _propTypes = require("./propTypes"); var _CopyButton = require("./CopyButton"); var _useThemeValue2 = require("../../utils/useThemeValue"); var _excluded = ["a11yTitle", "defaultSuggestion", "defaultValue", "disabled", "dropAlign", "dropHeight", "dropTarget", "dropProps", "focusIndicator", "icon", "id", "messages", "name", "onBlur", "onChange", "onFocus", "onKeyDown", "onSelect", "onSuggestionSelect", "onSuggestionsClose", "onSuggestionsOpen", "placeholder", "plain", "readOnly", "readOnlyCopy", "reverse", "suggestions", "textAlign", "value", "width"]; 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 renderLabel = function renderLabel(suggestion) { if (suggestion && typeof suggestion === 'object') { return suggestion.label || suggestion.value; } return suggestion; }; var stringLabel = function stringLabel(suggestion) { if (suggestion && typeof suggestion === 'object') { if (suggestion.label && typeof suggestion.label === 'string') { return suggestion.label; } return suggestion.value; } return suggestion; }; var ContainerBox = (0, _styledComponents["default"])(_Box.Box).withConfig({ displayName: "TextInput__ContainerBox", componentId: "sc-1ai0c08-0" })(["", ";@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){width:100%;}"], function (props) { return props.dropHeight ? (0, _utils.sizeStyle)('max-height', props.dropHeight, props.theme) : 'max-height: inherit;'; }); var defaultDropAlign = { top: 'bottom', left: 'left' }; var TextInput = exports.TextInput = /*#__PURE__*/(0, _react.forwardRef)(function (_ref, ref) { var _inputRef$current, _inputRef$current2; var a11yTitle = _ref.a11yTitle, defaultSuggestion = _ref.defaultSuggestion, defaultValue = _ref.defaultValue, disabled = _ref.disabled, _ref$dropAlign = _ref.dropAlign, dropAlign = _ref$dropAlign === void 0 ? defaultDropAlign : _ref$dropAlign, dropHeight = _ref.dropHeight, dropTarget = _ref.dropTarget, dropProps = _ref.dropProps, _ref$focusIndicator = _ref.focusIndicator, focusIndicator = _ref$focusIndicator === void 0 ? true : _ref$focusIndicator, icon = _ref.icon, id = _ref.id, messages = _ref.messages, name = _ref.name, _onBlur = _ref.onBlur, onChange = _ref.onChange, _onFocus = _ref.onFocus, onKeyDown = _ref.onKeyDown, onSelect = _ref.onSelect, onSuggestionSelect = _ref.onSuggestionSelect, onSuggestionsClose = _ref.onSuggestionsClose, onSuggestionsOpen = _ref.onSuggestionsOpen, placeholder = _ref.placeholder, plain = _ref.plain, readOnlyProp = _ref.readOnly, readOnlyCopy = _ref.readOnlyCopy, reverse = _ref.reverse, suggestions = _ref.suggestions, textAlign = _ref.textAlign, valueProp = _ref.value, widthProp = _ref.width, rest = _objectWithoutPropertiesLoose(_ref, _excluded); var _useThemeValue = (0, _useThemeValue2.useThemeValue)(), theme = _useThemeValue.theme, passThemeFlag = _useThemeValue.passThemeFlag; var _useContext = (0, _react.useContext)(_MessageContext.MessageContext), format = _useContext.format; var announce = (0, _react.useContext)(_contexts.AnnounceContext); var formContext = (0, _react.useContext)(_FormContext.FormContext); var inputRef = (0, _utils.useForwardedRef)(ref); var dropRef = (0, _react.useRef)(); var suggestionsRef = (0, _react.useRef)(); var readOnly = readOnlyProp || readOnlyCopy; // if this is a readOnly property, don't set a name with the form context // this allows Select to control the form context for the name. var _formContext$useFormI = formContext.useFormInput({ name: readOnly ? undefined : name, value: valueProp }), value = _formContext$useFormI[0], setValue = _formContext$useFormI[1]; var _useState = (0, _react.useState)(), focus = _useState[0], setFocus = _useState[1]; var _useState2 = (0, _react.useState)(false), showDrop = _useState2[0], setShowDrop = _useState2[1]; var handleSuggestionSelect = (0, _react.useMemo)(function () { return onSelect && !onSuggestionSelect ? onSelect : onSuggestionSelect; }, [onSelect, onSuggestionSelect]); var handleTextSelect = (0, _react.useMemo)(function () { return onSelect && onSuggestionSelect ? onSelect : undefined; }, [onSelect, onSuggestionSelect]); var _useState3 = (0, _react.useState)(), suggestionsAtClose = _useState3[0], setSuggestionsAtClose = _useState3[1]; var readOnlyCopyValidation = format({ id: 'input.readOnlyCopy.validation', messages: messages }); var readOnlyCopyPrompt = format({ id: 'input.readOnlyCopy.prompt', messages: messages }); var _useState4 = (0, _react.useState)(readOnlyCopyPrompt), tip = _useState4[0], setTip = _useState4[1]; var onClickCopy = function onClickCopy() { global.navigator.clipboard.writeText(value); announce(readOnlyCopyValidation, 'assertive'); setTip(readOnlyCopyValidation); }; var onBlurCopy = function onBlurCopy() { if (tip === readOnlyCopyValidation) setTip(readOnlyCopyPrompt); }; var openDrop = (0, _react.useCallback)(function () { setShowDrop(true); announce(format({ id: 'textInput.suggestionIsOpen', messages: messages })); announce(suggestions.length + " " + format({ id: 'textInput.suggestionsCount', messages: messages })); if (onSuggestionsOpen) onSuggestionsOpen(); }, [announce, messages, format, onSuggestionsOpen, suggestions]); var closeDrop = (0, _react.useCallback)(function () { setSuggestionsAtClose(suggestions); // must be before closing drop setShowDrop(false); if (onSuggestionsClose) onSuggestionsClose(); }, [onSuggestionsClose, suggestions]); var clickOutside = (0, _react.useCallback)(function (event) { if (event.target !== inputRef.current) closeDrop(); }, [inputRef, closeDrop]); // Handle scenarios where we have focus, the drop isn't showing, // and the suggestions change. We don't want to open the drop if // the drop has been closed by onEsc and the suggestions haven't // changed. So, we remember the suggestions we are showing when // the drop was closed and only re-open it when the suggestions // subsequently change. (0, _react.useEffect)(function () { if (focus && !showDrop && suggestions && suggestions.length && (!suggestionsAtClose || suggestionsAtClose.length !== suggestions.length)) { openDrop(); } }, [focus, openDrop, showDrop, suggestions, suggestionsAtClose]); // if we have no suggestions, close drop if it's open (0, _react.useEffect)(function () { if (showDrop && (!suggestions || !suggestions.length)) { closeDrop(); } }, [closeDrop, showDrop, suggestions]); var valueSuggestionIndex = (0, _react.useMemo)(function () { return suggestions ? suggestions.map(function (suggestion) { return typeof suggestion === 'object' ? suggestion.value : suggestion; }).indexOf(value) : -1; }, [suggestions, value]); // choose the best suggestion, either the explicit default or the one // that matches the current value var resetSuggestionIndex = (0, _react.useMemo)(function () { if (valueSuggestionIndex === -1 && typeof defaultSuggestion === 'number') { return defaultSuggestion; } return valueSuggestionIndex; }, [defaultSuggestion, valueSuggestionIndex]); // activeSuggestionIndex unifies mouse and keyboard interaction of // the suggestions var _useState5 = (0, _react.useState)(resetSuggestionIndex), activeSuggestionIndex = _useState5[0], setActiveSuggestionIndex = _useState5[1]; // Only update active suggestion index when the mouse actually moves, // not when suggestions are moving under the mouse. var _useState6 = (0, _react.useState)(), mouseMovedSinceLastKey = _useState6[0], setMouseMovedSinceLastKey = _useState6[1]; // set activeSuggestionIndex when value changes (0, _react.useEffect)(function () { return setActiveSuggestionIndex(valueSuggestionIndex); }, [valueSuggestionIndex]); // reset activeSuggestionIndex when the drop is closed (0, _react.useEffect)(function () { if (!showDrop) setActiveSuggestionIndex(resetSuggestionIndex); }, [resetSuggestionIndex, showDrop]); // announce active suggestion (0, _react.useEffect)(function () { if (activeSuggestionIndex >= 0) { var label = stringLabel(suggestions[activeSuggestionIndex]); announce(label + " " + format({ id: 'textInput.enterSelect', messages: messages })); } }, [activeSuggestionIndex, announce, messages, format, suggestions]); // make sure activeSuggestion is visible in scroll (0, _react.useEffect)(function () { var timer = setTimeout(function () { var list = suggestionsRef.current; if (showDrop && activeSuggestionIndex !== -1 && list) { var container = list.parentNode; var item = list.children[activeSuggestionIndex]; if (container.scrollTo) { if ((0, _utils.isNodeAfterScroll)(item, container)) container.scrollTo(0, item.offsetTop - (container.getBoundingClientRect().height - item.getBoundingClientRect().height));else if ((0, _utils.isNodeBeforeScroll)(item, container)) container.scrollTo(0, item.offsetTop); } } }, 50); // delay to allow Drop to animate in return function () { return clearTimeout(timer); }; }, [activeSuggestionIndex, showDrop]); (0, _react.useEffect)(function () { if (readOnly && inputRef != null && inputRef.current && inputRef.current.scrollLeft > 0) { inputRef.current.scrollLeft = 0; } }, [readOnly, inputRef, inputRef == null || (_inputRef$current = inputRef.current) == null ? void 0 : _inputRef$current.scrollLeft]); var setValueFromSuggestion = function setValueFromSuggestion(event, suggestion) { // if we stole the focus in the drop, perhaps by interacting with // a suggestion button or the scrollbar, give it back inputRef.current.focus(); inputRef.current.value = suggestion; // needed for uncontrolled cases closeDrop(); if (handleSuggestionSelect) { if (event.persist) event.persist(); var adjustedEvent = event; adjustedEvent.suggestion = suggestion; handleSuggestionSelect(adjustedEvent); } setValue(suggestion); }; var onNextSuggestion = (0, _react.useCallback)(function (event) { event.preventDefault(); var nextActiveIndex = Math.min(activeSuggestionIndex + 1, suggestions.length - 1); setActiveSuggestionIndex(nextActiveIndex); setMouseMovedSinceLastKey(false); }, [activeSuggestionIndex, suggestions]); var onPreviousSuggestion = (0, _react.useCallback)(function (event) { event.preventDefault(); var nextActiveIndex = Math.max(activeSuggestionIndex - 1, 0); setActiveSuggestionIndex(nextActiveIndex); setMouseMovedSinceLastKey(false); }, [activeSuggestionIndex]); // account for input value in both controlled and uncontrolled scenarios var hasValue = value || ((_inputRef$current2 = inputRef.current) == null ? void 0 : _inputRef$current2.value); var showStyledPlaceholder = (0, _react.useMemo)(function () { return placeholder && typeof placeholder !== 'string' && !hasValue; }, [hasValue, placeholder]); var drop; var extraProps = { onSelect: handleTextSelect }; if (showDrop) { drop = /*#__PURE__*/_react["default"].createElement(_Drop.Drop, _extends({ ref: dropRef, id: id ? "text-input-drop__" + id : undefined, align: dropAlign, responsive: false, target: dropTarget || inputRef.current, onClickOutside: clickOutside, onEsc: closeDrop // TextInput manages its own keyboard behavior via keyboardProps , trapFocus: false }, dropProps), /*#__PURE__*/_react["default"].createElement(ContainerBox, _extends({ id: id ? "listbox__" + id : undefined, role: "listbox", overflow: "auto", dropHeight: dropHeight, onMouseMove: function onMouseMove() { return setMouseMovedSinceLastKey(true); } }, passThemeFlag), /*#__PURE__*/_react["default"].createElement(_StyledTextInput.StyledSuggestions, _extends({ ref: suggestionsRef }, passThemeFlag), /*#__PURE__*/_react["default"].createElement(_InfiniteScroll.InfiniteScroll, { items: suggestions, step: theme.select.step, show: activeSuggestionIndex !== -1 ? activeSuggestionIndex : undefined }, function (suggestion, index, itemRef) { var active = activeSuggestionIndex === index; var selected = suggestion === value; // Determine whether the label is done as a child or // as an option Button kind property. var renderedLabel = renderLabel(suggestion); var child; if (typeof renderedLabel !== 'string') // must be an element rendered by suggestions.label child = renderedLabel;else if (!theme.button.option) // don't have theme support, need to layout here /* 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. */ child = /*#__PURE__*/_react["default"].createElement(_Box.Box, { align: "start", pad: "small" }, renderedLabel); // if we have a child, turn on plain return /*#__PURE__*/_react["default"].createElement("li", { key: stringLabel(suggestion) + "-" + index, ref: itemRef }, /*#__PURE__*/_react["default"].createElement(_Button.Button, { id: id ? "listbox-option-" + index + "__" + id : undefined, role: "option", "aria-selected": selected ? 'true' : 'false', active: active, fill: "horizontal", plain: !child ? undefined : true, align: "start", kind: !child ? 'option' : undefined, label: !child ? renderedLabel : undefined, onClick: function onClick(event) { return setValueFromSuggestion(event, suggestion); }, onMouseMove: mouseMovedSinceLastKey && activeSuggestionIndex !== index ? function () { return setActiveSuggestionIndex(index); } : undefined, keyboard: !mouseMovedSinceLastKey }, child)); })))); } var keyboardProps = { onKeyDown: onKeyDown }; if (showDrop) { keyboardProps.onEnter = function (event) { // prevent submitting forms via Enter when the drop is open event.preventDefault(); if (activeSuggestionIndex >= 0) setValueFromSuggestion(event, suggestions[activeSuggestionIndex]);else closeDrop(); }; if (activeSuggestionIndex > 0) keyboardProps.onUp = onPreviousSuggestion; if (activeSuggestionIndex < suggestions.length - 1) keyboardProps.onDown = onNextSuggestion; keyboardProps.onTab = closeDrop; } else if (suggestions && suggestions.length > 0) { keyboardProps.onDown = openDrop; } /* If the text input has a list of suggestions, add the WAI-ARIA 1.2 combobox role and states. */ var comboboxProps = {}; var activeOptionID; if (id && (suggestions == null ? void 0 : suggestions.length) > -1) { if (showDrop && activeSuggestionIndex > -1) { activeOptionID = "listbox-option-" + activeSuggestionIndex + "__" + id; } comboboxProps = { 'aria-activedescendant': activeOptionID, 'aria-autocomplete': 'list', 'aria-expanded': showDrop ? 'true' : 'false', 'aria-controls': showDrop ? "listbox__" + id : undefined, role: 'combobox' }; } // For the Keyboard target below, if we have focus, // either on the input element or within the drop, // then we set the target to the document, // otherwise we only listen to onDown on the input element itself, // primarily for tests. var textInputIcon = (0, _utils.useSizedIcon)(icon, rest.size, theme); var ReadOnlyCopyButton = /*#__PURE__*/_react["default"].createElement(_CopyButton.CopyButton, { disabled: disabled, onBlurCopy: onBlurCopy, onClickCopy: onClickCopy, readOnlyCopyPrompt: readOnlyCopyPrompt, tip: tip, value: value }); return /*#__PURE__*/_react["default"].createElement(_StyledTextInput.StyledTextInputContainer, _extends({ readOnlyProp: readOnly // readOnlyProp to avoid passing to DOM , readOnlyCopy: readOnlyCopy, plain: plain, border: !plain, onMouseMove: function onMouseMove() { return setMouseMovedSinceLastKey(true); } }, passThemeFlag), reverse && readOnlyCopy && ReadOnlyCopyButton, showStyledPlaceholder && /*#__PURE__*/_react["default"].createElement(_StyledTextInput.StyledPlaceholder, passThemeFlag, placeholder), textInputIcon && !readOnly && /*#__PURE__*/_react["default"].createElement(_StyledTextInput.StyledIcon, { reverse: reverse, theme: theme }, textInputIcon), /*#__PURE__*/_react["default"].createElement(_Keyboard.Keyboard, _extends({ target: focus ? 'document' : undefined }, keyboardProps), /*#__PURE__*/_react["default"].createElement(_StyledTextInput.StyledTextInput, _extends({ "aria-label": a11yTitle, ref: inputRef, id: id, name: name, autoComplete: "off", disabled: disabled, plain: plain, placeholder: typeof placeholder === 'string' ? placeholder : undefined, icon: !readOnly && icon, reverse: reverse, focus: focus, focusIndicator: focusIndicator, textAlign: textAlign, widthProp: widthProp }, passThemeFlag, rest, extraProps, comboboxProps, { defaultValue: renderLabel(defaultValue), value: renderLabel(value), readOnly: readOnly, readOnlyCopy: readOnlyCopy, onFocus: function onFocus(event) { // Don't do anything if we are acting like we already have // focus. This can happen when this input loses focus temporarily // to our drop, see onBlur() handler below. if (!focus) { setFocus(true); if (suggestions && suggestions.length > 0) { announce(format({ id: 'textInput.suggestionsExist', messages: messages })); openDrop(); } if (_onFocus) _onFocus(event); } }, onBlur: function onBlur(event) { // Only treat it as a blur if the element receiving focus // isn't in our drop. The relatedTarget will be our drop // when the user clicks on a suggestion or interacts with the // scrollbar in the drop. if (!event.relatedTarget || event.relatedTarget !== dropRef.current) { setFocus(false); if (_onBlur) _onBlur(event); } }, onChange: readOnly ? undefined : function (event) { // when TextInput is not contained in a Form, no re-render // will come from this onChange and remove the placeholder // so we need to update state to ensure the styled // placeholder only appears when there is no value if (suggestions && focus && !showDrop) { openDrop(); } setValue(event.target.value); setActiveSuggestionIndex(resetSuggestionIndex); if (onChange) onChange(event); } }))), !reverse && readOnlyCopy && ReadOnlyCopyButton, !readOnly && drop); }); TextInput.displayName = 'TextInput'; TextInput.propTypes = _propTypes.TextInputPropTypes;