@spark-web/field
Version:
--- title: Field isExperimentalPackage: false ---
199 lines (183 loc) • 5.93 kB
JavaScript
import { createContext, useContext, forwardRef, useMemo, Fragment } from 'react';
import { css } from '@emotion/css';
import { mergeIds, useId, composeId, VisuallyHidden } from '@spark-web/a11y';
import { Box } from '@spark-web/box';
import { ExclamationCircleIcon, CheckCircleIcon } from '@spark-web/icon';
import { Stack } from '@spark-web/stack';
import { Text } from '@spark-web/text';
import { useTheme } from '@spark-web/theme';
import { jsx, jsxs } from 'react/jsx-runtime';
var FieldContext = /*#__PURE__*/createContext(null);
var FieldContextProvider = FieldContext.Provider;
var FIELD_CONTEXT_ERROR_MESSAGE = 'Input components must be inside a `Field`.';
function useFieldContext() {
var ctx = useContext(FieldContext);
if (!ctx) {
throw new Error(FIELD_CONTEXT_ERROR_MESSAGE);
}
return ctx;
}
/**
* Using a [context](https://reactjs.org/docs/context.html), the field
* component connects the label, description, and message to the input element.
*/
var Field = /*#__PURE__*/forwardRef(function (_ref, forwardedRef) {
var children = _ref.children,
idProp = _ref.id,
data = _ref.data,
description = _ref.description,
_ref$disabled = _ref.disabled,
disabled = _ref$disabled === void 0 ? false : _ref$disabled,
label = _ref.label,
adornment = _ref.adornment,
_ref$labelVisibility = _ref.labelVisibility,
labelVisibility = _ref$labelVisibility === void 0 ? 'visible' : _ref$labelVisibility,
message = _ref.message,
secondaryLabel = _ref.secondaryLabel,
_ref$tone = _ref.tone,
tone = _ref$tone === void 0 ? 'neutral' : _ref$tone;
var _useFieldIds = useFieldIds(idProp),
descriptionId = _useFieldIds.descriptionId,
inputId = _useFieldIds.inputId,
messageId = _useFieldIds.messageId; // field context
var invalid = Boolean(message && tone === 'critical');
var fieldContext = useMemo(function () {
return [{
disabled: disabled,
invalid: invalid
}, {
'aria-describedby': mergeIds(message && messageId, description && descriptionId),
'aria-invalid': invalid || undefined,
id: inputId
}];
}, [description, descriptionId, disabled, inputId, invalid, message, messageId]); // label prep
var hiddenLabel = /*#__PURE__*/jsxs(VisuallyHidden, {
as: "label",
htmlFor: inputId,
children: [label, " ", secondaryLabel]
});
var labelElement = {
hidden: hiddenLabel,
visible: /*#__PURE__*/jsx(Box, {
as: "label",
htmlFor: inputId,
children: /*#__PURE__*/jsxs(Text, {
tone: disabled ? 'disabled' : 'neutral',
weight: "semibold",
children: [label, ' ', secondaryLabel && /*#__PURE__*/jsx(Text, {
inline: true,
tone: disabled ? 'disabled' : 'muted',
weight: "regular",
children: secondaryLabel
})]
})
}),
'reserve-space': /*#__PURE__*/jsxs(Fragment, {
children: [hiddenLabel, /*#__PURE__*/jsx(Text, {
"aria-hidden": true,
children: "\xA0"
})]
})
};
var LabelWrapper = labelVisibility === 'hidden' ? Fragment : FieldLabelWrapper;
return /*#__PURE__*/jsx(FieldContextProvider, {
value: fieldContext,
children: /*#__PURE__*/jsxs(Stack, {
ref: forwardedRef,
data: data,
gap: "medium",
children: [/*#__PURE__*/jsxs(LabelWrapper, {
children: [labelElement[labelVisibility], adornment]
}), description && /*#__PURE__*/jsx(Text, {
tone: "muted",
size: "small",
id: descriptionId,
children: description
}), children, message && /*#__PURE__*/jsx(FieldMessage, {
tone: tone,
id: messageId,
message: message
})]
})
});
});
Field.displayName = 'Field'; // Utils
// ------------------------------
function useFieldIds(id) {
var inputId = useId(id);
var descriptionId = composeId(inputId, 'description');
var messageId = composeId(inputId, 'message');
return {
descriptionId: descriptionId,
inputId: inputId,
messageId: messageId
};
} // Styled components
// ------------------------------
function FieldLabelWrapper(_ref2) {
var children = _ref2.children;
return /*#__PURE__*/jsx(Box, {
display: "flex",
alignItems: "center",
justifyContent: "spaceBetween",
gap: "large",
children: children
});
}
var messageToneMap = {
critical: 'critical',
neutral: 'muted',
positive: 'positive'
}; // NOTE: use icons in addition to color for folks with visions issues
var messageIconMap = {
critical: ExclamationCircleIcon,
neutral: null,
positive: CheckCircleIcon
};
var FieldMessage = function FieldMessage(_ref3) {
var message = _ref3.message,
id = _ref3.id,
tone = _ref3.tone;
var textTone = messageToneMap[tone];
var Icon = messageIconMap[tone];
return /*#__PURE__*/jsxs(Box, {
display: "flex",
gap: "xsmall",
children: [Icon ? /*#__PURE__*/jsx(IndicatorContainer, {
children: /*#__PURE__*/jsx(Icon, {
size: "xxsmall",
tone: tone
})
}) : null, /*#__PURE__*/jsx(Text, {
tone: textTone,
size: "small",
id: id,
children: message
})]
});
};
function IndicatorContainer(_ref4) {
var children = _ref4.children;
var theme = useTheme();
var _theme$typography$tex = theme.typography.text.small,
mobile = _theme$typography$tex.mobile,
tablet = _theme$typography$tex.tablet;
var responsiveStyles = theme.utils.responsiveStyles({
mobile: {
height: mobile.capHeight
},
tablet: {
height: tablet.capHeight
}
});
return /*#__PURE__*/jsx(Box, {
display: "flex",
alignItems: "center",
"aria-hidden": true,
cursor: "default",
flexShrink: 0,
className: css(responsiveStyles),
children: children
});
}
export { Field, FieldContextProvider, FieldMessage, useFieldContext, useFieldIds };