UNPKG

grommet

Version:

focus on the essential experience

480 lines (470 loc) 24.3 kB
"use strict"; exports.__esModule = true; exports.FormField = void 0; var _react = _interopRequireWildcard(require("react")); var _styledComponents = _interopRequireDefault(require("styled-components")); var _utils = require("../../utils"); var _useDebounce = require("../../utils/use-debounce"); var _styles = require("../../utils/styles"); var _mixins = require("../../utils/mixins"); var _refs = require("../../utils/refs"); var _Box = require("../Box"); var _CheckBox = require("../CheckBox"); var _CheckBoxGroup = require("../CheckBoxGroup"); var _RadioButtonGroup = require("../RadioButtonGroup"); var _Text = require("../Text"); var _TextInput = require("../TextInput"); var _FormContext = require("../Form/FormContext"); var _propTypes = require("./propTypes"); var _useThemeValue3 = require("../../utils/useThemeValue"); var _AnnounceContext = require("../../contexts/AnnounceContext"); var _excluded = ["error", "info", "message", "type"], _excluded2 = ["component", "disabled", "invalid", "name", "onChange"], _excluded3 = ["children", "className", "component", "contentProps", "disabled", "error", "help", "htmlFor", "info", "label", "margin", "name", "onBlur", "onChange", "onFocus", "pad", "required", "style", "validate", "validateOn"]; 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 grommetInputFocusNames = ['CheckBox', 'CheckBoxGroup', 'RadioButton', 'RadioButtonGroup', 'RangeInput', 'RangeSelector', 'StarRating', 'ThumbsRating']; var grommetInputNames = ['CheckBox', 'CheckBoxGroup', 'TextInput', 'Select', 'MaskedInput', 'SelectMultiple', 'TextArea', 'DateInput', 'FileInput', 'RadioButton', 'RadioButtonGroup', 'RangeInput', 'RangeSelector', 'StarRating', 'ThumbsRating']; var grommetInputPadNames = ['CheckBox', 'CheckBoxGroup', 'RadioButton', 'RadioButtonGroup', 'RangeInput', 'RangeSelector']; var isGrommetInput = function isGrommetInput(comp) { return comp && (grommetInputNames.indexOf(comp.displayName) !== -1 || grommetInputPadNames.indexOf(comp.displayName) !== -1); }; var getFocusStyle = function getFocusStyle(props) { var _props$theme$formFiel; if (props.focus && props.containerFocus === false && ((_props$theme$formFiel = props.theme.formField) == null || (_props$theme$formFiel = _props$theme$formFiel.focus) == null ? void 0 : _props$theme$formFiel.containerFocus) === false) { return null; } return props.focus ? (0, _styles.focusStyle)({ justBorder: true }) : undefined; }; var FormFieldBox = (0, _styledComponents["default"])(_Box.Box).withConfig({ displayName: "FormField__FormFieldBox", componentId: "sc-m9hood-0" })(["", " ", ""], function (props) { return getFocusStyle(props); }, function (props) { var _props$theme$formFiel2; return (_props$theme$formFiel2 = props.theme.formField) == null ? void 0 : _props$theme$formFiel2.extend; }); var FormFieldContentBox = (0, _styledComponents["default"])(_Box.Box).withConfig({ displayName: "FormField__FormFieldContentBox", componentId: "sc-m9hood-1" })(["", " ", ""], function (props) { return getFocusStyle(props); }, function (props) { var _props$theme$formFiel3; return props.theme.formField && ((_props$theme$formFiel3 = props.theme.formField[props == null ? void 0 : props.componentName]) == null || (_props$theme$formFiel3 = _props$theme$formFiel3.container) == null ? void 0 : _props$theme$formFiel3.extend); }); var StyledContentsBox = (0, _styledComponents["default"])(_Box.Box).withConfig({ displayName: "FormField__StyledContentsBox", componentId: "sc-m9hood-2" })(["", ""], function (props) { var _props$theme$formFiel4; return props.theme.formField && ((_props$theme$formFiel4 = props.theme.formField[props == null ? void 0 : props.componentName]) == null || (_props$theme$formFiel4 = _props$theme$formFiel4.container) == null ? void 0 : _props$theme$formFiel4.extend); }); var StyledMessageContainer = (0, _styledComponents["default"])(_Box.Box).withConfig({ displayName: "FormField__StyledMessageContainer", componentId: "sc-m9hood-3" })(["", ""], function (props) { return props.messageType && props.theme.formField[props.messageType].container && props.theme.formField[props.messageType].container.extend; }); var RequiredText = (0, _styledComponents["default"])(_Text.Text).withConfig({ displayName: "FormField__RequiredText", componentId: "sc-m9hood-4" })(["color:inherit;font-weight:inherit;line-height:inherit;"]); var ScreenReaderOnly = (0, _styledComponents["default"])(_Text.Text).withConfig({ displayName: "FormField__ScreenReaderOnly", componentId: "sc-m9hood-5" })(["position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0;"]); var Message = function Message(_ref) { var error = _ref.error, info = _ref.info, message = _ref.message, type = _ref.type, rest = _objectWithoutPropertiesLoose(_ref, _excluded); var _useThemeValue = (0, _useThemeValue3.useThemeValue)(), theme = _useThemeValue.theme, passThemeFlag = _useThemeValue.passThemeFlag; if (message) { var icon; var containerProps; if (type) { icon = theme.formField[type] && theme.formField[type].icon; containerProps = theme.formField[type] && theme.formField[type].container; } var messageContent; if (typeof message === 'string') messageContent = /*#__PURE__*/_react["default"].createElement(_Text.Text, rest, message);else messageContent = /*#__PURE__*/_react["default"].createElement(_Box.Box, rest, message); return icon || containerProps ? /*#__PURE__*/_react["default"].createElement(StyledMessageContainer, _extends({ direction: "row", messageType: type }, containerProps, passThemeFlag), icon && /*#__PURE__*/_react["default"].createElement(_Box.Box, { flex: false }, icon), messageContent) : messageContent; } return null; }; var Input = function Input(_ref2) { var component = _ref2.component, disabled = _ref2.disabled, invalid = _ref2.invalid, name = _ref2.name, _onChange = _ref2.onChange, rest = _objectWithoutPropertiesLoose(_ref2, _excluded2); var formContext = (0, _react.useContext)(_FormContext.FormContext); var _formContext$useFormI = formContext.useFormInput({ name: name, value: rest.value }), value = _formContext$useFormI[0], setValue = _formContext$useFormI[1]; var InputComponent = component || _TextInput.TextInput; // Grommet input components already check for FormContext // and, using their `name`, end up calling the useFormInput.setValue() // already. For custom components, we expect they will call // this onChange() and we'll call setValue() here, primarily // for backwards compatibility. var extraProps = isGrommetInput(InputComponent) ? { focusIndicator: false, onChange: _onChange, plain: true } : { value: value, onChange: function onChange(event) { setValue(event.value !== undefined ? event.value : event.target.value); if (_onChange) _onChange(event); } }; return /*#__PURE__*/_react["default"].createElement(InputComponent, _extends({ name: name, disabled: disabled, "aria-invalid": invalid || undefined }, rest, extraProps)); }; var FormField = exports.FormField = /*#__PURE__*/(0, _react.forwardRef)(function (_ref3, ref) { var _theme$formField2, _theme$global$input, _formFieldTheme$disab, _formFieldTheme$disab2; var children = _ref3.children, className = _ref3.className, component = _ref3.component, contentProps = _ref3.contentProps, disabled = _ref3.disabled, errorProp = _ref3.error, help = _ref3.help, htmlFor = _ref3.htmlFor, infoProp = _ref3.info, label = _ref3.label, margin = _ref3.margin, name = _ref3.name, _onBlur = _ref3.onBlur, onChange = _ref3.onChange, _onFocus = _ref3.onFocus, pad = _ref3.pad, required = _ref3.required, style = _ref3.style, validate = _ref3.validate, validateOn = _ref3.validateOn, rest = _objectWithoutPropertiesLoose(_ref3, _excluded3); var _useThemeValue2 = (0, _useThemeValue3.useThemeValue)(), theme = _useThemeValue2.theme, passThemeFlag = _useThemeValue2.passThemeFlag; var formContext = (0, _react.useContext)(_FormContext.FormContext); var _formContext$useFormF = formContext.useFormField({ disabled: disabled, error: errorProp, info: infoProp, name: name, required: required, validate: validate, validateOn: validateOn }), error = _formContext$useFormF.error, info = _formContext$useFormF.info, inForm = _formContext$useFormF.inForm, contextOnBlur = _formContext$useFormF.onBlur, contextOnChange = _formContext$useFormF.onChange; var formKind = formContext.kind; var _useState = (0, _react.useState)(), focus = _useState[0], setFocus = _useState[1]; var formFieldRef = (0, _refs.useForwardedRef)(ref); var formFieldTheme = theme.formField; var themeBorder = formFieldTheme.border; var debounce = (0, _useDebounce.useDebounce)(); var portalContext = (0, _react.useContext)(_utils.PortalContext); var announce = (0, _react.useContext)(_AnnounceContext.AnnounceContext); (0, _react.useEffect)(function () { if (error && validate != null && validate.max) { announce(error, 'polite', 5000); } }, [error, announce, validate == null ? void 0 : validate.max]); var readOnlyField = (0, _react.useMemo)(function () { var readOnly = false; if (children) { _react.Children.map(children, function (child) { var _child$props, _child$props2; if (((child == null || (_child$props = child.props) == null ? void 0 : _child$props.readOnly) === true || (child == null || (_child$props2 = child.props) == null ? void 0 : _child$props2.readOnlyCopy) === true) && child.type && ('TextInput'.indexOf(child.type.displayName) !== -1 || 'DateInput'.indexOf(child.type.displayName) !== -1)) { readOnly = true; } }); } return readOnly; }, [children]); var containerFocus = (0, _react.useMemo)(function () { var focusIndicatorFlag = true; _react.Children.forEach(children, function (child) { var _theme$formField; if (child && child.type && grommetInputFocusNames.includes(child.type.displayName) && ((_theme$formField = theme.formField) == null || (_theme$formField = _theme$formField.focus) == null ? void 0 : _theme$formField.containerFocus) !== true) { focusIndicatorFlag = false; } }); return focusIndicatorFlag; }, [children, (_theme$formField2 = theme.formField) == null || (_theme$formField2 = _theme$formField2.focus) == null ? void 0 : _theme$formField2.containerFocus]); // This is here for backwards compatibility. In case the child is a grommet // input component, set plain and focusIndicator props, if they aren't // already set. var wantContentPad = component && (component === _CheckBox.CheckBox || component === _CheckBoxGroup.CheckBoxGroup || component === _RadioButtonGroup.RadioButtonGroup); var contents = themeBorder && children && _react.Children.map(children, function (child) { if (child && child.type && grommetInputPadNames.indexOf(child.type.displayName) !== -1) { wantContentPad = true; } var isInputComponent = child && child.type && grommetInputNames.indexOf(child.type.displayName) !== -1; if (isInputComponent && child.props.plain === undefined && child.props.focusIndicator === undefined) { var _formFieldTheme$check; return /*#__PURE__*/(0, _react.cloneElement)(child, { plain: true, focusIndicator: !containerFocus, pad: 'CheckBox'.indexOf(child.type.displayName) !== -1 ? formFieldTheme == null || (_formFieldTheme$check = formFieldTheme.checkBox) == null ? void 0 : _formFieldTheme$check.pad : undefined }); } return child; }) || children; // put rest on container, unless we use internal Input var containerRest = rest; if (inForm) { if (!contents) containerRest = {}; contents = contents || /*#__PURE__*/_react["default"].createElement(Input, _extends({ component: component, disabled: disabled, invalid: !!error, name: name, label: component === _CheckBox.CheckBox ? label : undefined }, rest)); } var themeContentProps = _extends({}, formFieldTheme.content); if (!pad && !wantContentPad) { themeContentProps.pad = undefined; } if (themeBorder && themeBorder.position === 'inner') { if (readOnlyField) { var _theme$global$input$r; themeContentProps.background = (_theme$global$input$r = theme.global.input.readOnly) == null ? void 0 : _theme$global$input$r.background; } else if (error && formFieldTheme.error) { themeContentProps.background = formFieldTheme.error.background; } else if (disabled && formFieldTheme.disabled) { themeContentProps.background = formFieldTheme.disabled.background; } } // fileinput handle // use fileinput plain use formfield to drive the border var isFileInputComponent; if (children && _react.Children.forEach(children, function (child) { if (child && child.type && 'FileInput'.indexOf(child.type.displayName) !== -1) isFileInputComponent = true; })) ; if (component && component.displayName === 'FileInput' && !isFileInputComponent) { isFileInputComponent = true; } var childName; _react.Children.forEach(children, function (child) { if (child && child.type) { var _childName; childName = child.type.displayName; // camelCase component name to match theme object key if (((_childName = childName) == null ? void 0 : _childName.length) > 0) childName = childName.charAt(0).toLowerCase() + childName.slice(1); } }); if (!themeBorder) { contents = /*#__PURE__*/_react["default"].createElement(StyledContentsBox, _extends({ disabledProp: disabled, error: error, componentName: childName }, themeContentProps, contentProps), contents); } var borderColor; if (disabled && formFieldTheme.disabled.border && formFieldTheme.disabled.border.color) { borderColor = formFieldTheme.disabled.border.color; } else if (readOnlyField && (_theme$global$input = theme.global.input) != null && (_theme$global$input = _theme$global$input.readOnly) != null && (_theme$global$input = _theme$global$input.border) != null && _theme$global$input.color) { var _theme$global$input2; borderColor = (_theme$global$input2 = theme.global.input) == null || (_theme$global$input2 = _theme$global$input2.readOnly) == null || (_theme$global$input2 = _theme$global$input2.border) == null ? void 0 : _theme$global$input2.color; } else if ( // backward compatibility check error && themeBorder && themeBorder.error.color || error && formFieldTheme.error && formFieldTheme.error.border) { if (themeBorder.error.color && formFieldTheme.error.border === undefined) { borderColor = themeBorder.error.color || 'status-critical'; } else if (formFieldTheme.error.border && formFieldTheme.error.border.color) { borderColor = formFieldTheme.error.border.color || 'status-critical'; } } else if (focus && formFieldTheme.focus && formFieldTheme.focus.border && formFieldTheme.focus.border.color) { borderColor = formFieldTheme.focus.border.color; } else { borderColor = themeBorder && themeBorder.color || 'border'; } var labelStyle; if (formKind) { labelStyle = _extends({}, formFieldTheme[formKind].label); } else labelStyle = _extends({}, formFieldTheme.label); if (disabled) { labelStyle.color = formFieldTheme.disabled && formFieldTheme.disabled.label ? formFieldTheme.disabled.label.color : labelStyle.color; } var themeHelpProps = _extends({}, formFieldTheme.help, disabled && { color: formFieldTheme == null || (_formFieldTheme$disab = formFieldTheme.disabled) == null || (_formFieldTheme$disab = _formFieldTheme$disab.help) == null ? void 0 : _formFieldTheme$disab.color }); var themeInfoProps = _extends({}, formFieldTheme.info, disabled && { color: formFieldTheme == null || (_formFieldTheme$disab2 = formFieldTheme.disabled) == null || (_formFieldTheme$disab2 = _formFieldTheme$disab2.info) == null ? void 0 : _formFieldTheme$disab2.color }); var abut; var abutMargin; var outerStyle = style; // If fileinput is wrapped in a formfield we want to use // the border style from the fileInput.theme. We also do not // want the foocus around the formfield since the the focus // is on the anchor/button inside fileinput if (themeBorder) { var innerProps = themeBorder.position === 'inner' ? { border: _extends({}, themeBorder, { size: isFileInputComponent ? theme.fileInput.border.size : undefined, style: isFileInputComponent ? theme.fileInput.border.style : undefined, side: isFileInputComponent ? theme.fileInput.border.side : themeBorder.side || 'bottom', color: borderColor }), round: formFieldTheme.round, focus: isFileInputComponent ? undefined : focus } : {}; contents = /*#__PURE__*/_react["default"].createElement(FormFieldContentBox, _extends({ disabledProp: disabled, error: error, componentName: childName }, themeContentProps, innerProps, contentProps, { containerFocus: containerFocus // internal prop }, passThemeFlag), contents); var mergedMargin = margin || formFieldTheme.margin; abut = themeBorder.position === 'outer' && (themeBorder.side === 'all' || themeBorder.side === 'horizontal' || !themeBorder.side) && !(mergedMargin && (typeof mergedMargin === 'string' && mergedMargin !== 'none' || mergedMargin.bottom && mergedMargin.bottom !== 'none' || mergedMargin.horizontal && mergedMargin.horizontal !== 'none')); if (abut) { // marginBottom is set to overlap adjacent fields abutMargin = { bottom: '-1px' }; if (margin) { abutMargin = margin; } else if (themeBorder.size) { // if the user defines a margin, // then the default margin below will be overridden abutMargin = { bottom: "-" + (0, _mixins.parseMetricToNum)(theme.global.borderSize[themeBorder.size] || themeBorder.size) + "px" }; } outerStyle = _extends({ position: focus ? 'relative' : undefined, zIndex: focus ? 10 : undefined }, style); } } var outerBackground; if (themeBorder && themeBorder.position === 'outer') { if (error && formFieldTheme.error && formFieldTheme.error.background) { outerBackground = formFieldTheme.error.background; } else if (focus && formFieldTheme.focus && formFieldTheme.focus.background && formFieldTheme.focus.background.color) { outerBackground = formFieldTheme.focus.background.color; } else if (disabled && formFieldTheme.disabled && formFieldTheme.disabled.background) { outerBackground = formFieldTheme.disabled.background; } } var outerProps = themeBorder && themeBorder.position === 'outer' ? { border: _extends({}, themeBorder, { color: borderColor }), round: formFieldTheme.round, focus: focus } : {}; var requiredIndicator = theme.formField.label.requiredIndicator; if (requiredIndicator === true) // accessibility resource: https://www.deque.com/blog/anatomy-of-accessible-forms-required-form-fields/ // this approach allows the required indicator to be hidden visually, // but present for assistive tech. // using aria-hidden so screen does not read out "star" and // just reads out "required" requiredIndicator = /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement(RequiredText, { "aria-hidden": "true" }, "*"), /*#__PURE__*/_react["default"].createElement(ScreenReaderOnly, null, "required")); var showRequiredIndicator = required && requiredIndicator; if (typeof required === 'object' && required.indicator === false) showRequiredIndicator = false; // Check if child is Select or SelectMultiple and modify htmlFor if needed var adjustedHtmlFor = htmlFor; if (htmlFor) { var isSelectComponent = false; // Check if children contain Select or SelectMultiple if (children) { _react.Children.forEach(children, function (child) { if (child && child.type && (child.type.displayName === 'Select' || child.type.displayName === 'SelectMultiple') && child.props.id === htmlFor) { isSelectComponent = true; } }); } // If it's a Select component and htmlFor doesn't end with __input, add it if (isSelectComponent && !htmlFor.endsWith('__input')) { adjustedHtmlFor = htmlFor + "__input"; } } return /*#__PURE__*/_react["default"].createElement(FormFieldBox, _extends({ ref: formFieldRef, className: className, background: outerBackground, margin: abut ? abutMargin : margin || _extends({}, formFieldTheme.margin) }, outerProps, { style: outerStyle, containerFocus: containerFocus // internal prop , onFocus: function onFocus(event) { var _formFieldRef$current; var root = (_formFieldRef$current = formFieldRef.current) == null ? void 0 : _formFieldRef$current.getRootNode(); if (root) { setFocus((0, _utils.containsFocus)(formFieldRef.current) && (0, _utils.shouldKeepFocus)(root)); } if (_onFocus) _onFocus(event); }, onBlur: function onBlur(event) { setFocus(false); // if input has a drop and focus is within drop // prevent onBlur validation from running until // focus is no longer within the drop or input if (contextOnBlur && !formFieldRef.current.contains(event.relatedTarget) && !(0, _utils.withinDropPortal)(event.relatedTarget, portalContext)) { contextOnBlur(event); } if (_onBlur) _onBlur(event); }, onChange: contextOnChange || onChange ? function (event) { event.persist(); if (onChange) onChange(event); if (contextOnChange) debounce(function () { return function () { return contextOnChange(event); }; }); } : undefined }, containerRest, passThemeFlag), label && component !== _CheckBox.CheckBox || help ? /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, label && component !== _CheckBox.CheckBox && /*#__PURE__*/_react["default"].createElement(_Text.Text, _extends({ as: "label", id: htmlFor ? "grommet-" + adjustedHtmlFor + "__label" : undefined, htmlFor: adjustedHtmlFor }, labelStyle), label, showRequiredIndicator ? requiredIndicator : undefined), /*#__PURE__*/_react["default"].createElement(Message, _extends({ message: help }, themeHelpProps))) : undefined, contents, /*#__PURE__*/_react["default"].createElement(Message, _extends({ type: "error", message: error }, formFieldTheme.error)), /*#__PURE__*/_react["default"].createElement(Message, _extends({ type: "info", message: info }, themeInfoProps))); }); FormField.displayName = 'FormField'; FormField.propTypes = _propTypes.FormFieldPropTypes;