@sproutsocial/seeds-react-form-field 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintignore ADDED
@@ -0,0 +1,6 @@
1
+ # Node modules
2
+ node_modules/
3
+
4
+ # Build output
5
+ dist/
6
+ coverage/
package/.eslintrc.js ADDED
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ root: true,
3
+ extends: ["eslint-config-seeds/racine"],
4
+ };
@@ -0,0 +1,21 @@
1
+ yarn run v1.22.22
2
+ $ tsup --dts
3
+ CLI Building entry: src/index.ts
4
+ CLI Using tsconfig: tsconfig.json
5
+ CLI tsup v8.0.2
6
+ CLI Using tsup config: /home/runner/work/seeds/seeds/seeds-react/seeds-react-form-field/tsup.config.ts
7
+ CLI Target: es2022
8
+ CLI Cleaning output folder
9
+ CJS Build start
10
+ ESM Build start
11
+ CJS dist/index.js 4.55 KB
12
+ CJS dist/index.js.map 13.89 KB
13
+ CJS ⚡️ Build success in 157ms
14
+ ESM dist/esm/index.js 2.55 KB
15
+ ESM dist/esm/index.js.map 13.85 KB
16
+ ESM ⚡️ Build success in 155ms
17
+ DTS Build start
18
+ DTS ⚡️ Build success in 10033ms
19
+ DTS dist/index.d.ts 1.35 KB
20
+ DTS dist/index.d.mts 1.35 KB
21
+ Done in 12.21s.
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # @sproutsocial/seeds-react-form-field
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 8ca97dd: Migrated FormField, Fieldset, and Label to their own seeds-react component
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [8ca97dd]
12
+ - @sproutsocial/seeds-react-label@1.0.0
@@ -0,0 +1,83 @@
1
+ // src/FormField.tsx
2
+ import { useState as useState2 } from "react";
3
+
4
+ // ../seeds-react-hooks/dist/index.mjs
5
+ import { useState, useLayoutEffect, useCallback, useReducer, useRef, useEffect, useMemo } from "react";
6
+ import { useTheme } from "styled-components";
7
+ var v = Object.freeze({ x: 0, y: 0, width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0 });
8
+ function X(r) {
9
+ let [t, e] = useState(r), s = useCallback((n) => {
10
+ n && n.textContent !== null && e(n.textContent);
11
+ }, []);
12
+ return s.current = t, s;
13
+ }
14
+
15
+ // src/FormField.tsx
16
+ import Box from "@sproutsocial/seeds-react-box";
17
+ import Label from "@sproutsocial/seeds-react-label";
18
+ import Text from "@sproutsocial/seeds-react-text";
19
+ import { VisuallyHidden } from "@sproutsocial/seeds-react-visually-hidden";
20
+ import { jsx, jsxs } from "react/jsx-runtime";
21
+ var idCounter = 0;
22
+ var FormField = ({
23
+ children,
24
+ error,
25
+ helperText,
26
+ id: identifier,
27
+ isInvalid = false,
28
+ label,
29
+ mb = 400,
30
+ qa,
31
+ isLabelHidden = false,
32
+ required,
33
+ ...rest
34
+ }) => {
35
+ const [id] = useState2(identifier || `FormField-${idCounter++}`);
36
+ const errorId = `Error-${id}`;
37
+ const containerText = X("");
38
+ const errorContainerText = X("");
39
+ return /* @__PURE__ */ jsxs(
40
+ Box,
41
+ {
42
+ ...rest,
43
+ ...qa,
44
+ mb,
45
+ "data-qa-formfield": qa && qa["data-qa-formfield"] || id || containerText.current,
46
+ "data-qa-formfield-isinvalid": isInvalid === true,
47
+ children: [
48
+ isLabelHidden ? /* @__PURE__ */ jsx(VisuallyHidden, { "data-testid": "visually-hidden", children: /* @__PURE__ */ jsx(Label, { htmlFor: id, required, children: label }) }) : /* @__PURE__ */ jsx(Label, { mb: helperText ? 100 : 300, htmlFor: id, required, children: label }),
49
+ helperText && /* @__PURE__ */ jsx(Text, { as: "p", fontSize: 200, mb: 300, color: "text.subtext", children: helperText }),
50
+ children({
51
+ id,
52
+ isInvalid,
53
+ ariaDescribedby: errorId,
54
+ ...required !== void 0 && { required }
55
+ }),
56
+ isInvalid && error && /* @__PURE__ */ jsx(
57
+ Text,
58
+ {
59
+ as: "div",
60
+ fontSize: 200,
61
+ color: "text.error",
62
+ mt: 300,
63
+ id: errorId,
64
+ "data-qa-formfield-error": qa && qa["data-qa-formfield-error"] || errorContainerText.current,
65
+ children: error
66
+ }
67
+ )
68
+ ]
69
+ }
70
+ );
71
+ };
72
+ var FormField_default = FormField;
73
+
74
+ // src/FormFieldTypes.ts
75
+ import "react";
76
+
77
+ // src/index.ts
78
+ var src_default = FormField_default;
79
+ export {
80
+ FormField_default as FormField,
81
+ src_default as default
82
+ };
83
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/FormField.tsx","../../../seeds-react-hooks/src/useMeasure/useMeasure.ts","../../../seeds-react-hooks/src/useSelect/useSelect.ts","../../../seeds-react-hooks/src/useMultiselect/useMultiselect.ts","../../../seeds-react-hooks/src/useMutationObserver/useMutationObserver.ts","../../../seeds-react-hooks/src/useTextContent/useTextContent.ts","../../../seeds-react-hooks/src/useWhyDidYouUpdate/useWhyDidYouUpdate.ts","../../../seeds-react-hooks/src/useInteractiveColor/useInteractiveColor.ts","../../src/FormFieldTypes.ts","../../src/index.ts"],"sourcesContent":["import React, { useState } from \"react\";\nimport { useTextContent } from \"@sproutsocial/seeds-react-hooks\";\nimport Box from \"@sproutsocial/seeds-react-box\";\nimport Label from \"@sproutsocial/seeds-react-label\";\nimport Text from \"@sproutsocial/seeds-react-text\";\nimport { VisuallyHidden } from \"@sproutsocial/seeds-react-visually-hidden\";\nimport type { TypeFormFieldProps } from \"./FormFieldTypes\";\n\nlet idCounter = 0;\n\nconst FormField = ({\n children,\n error,\n helperText,\n id: identifier,\n isInvalid = false,\n label,\n mb = 400,\n qa,\n isLabelHidden = false,\n required,\n ...rest\n}: TypeFormFieldProps) => {\n const [id] = useState(identifier || `FormField-${idCounter++}`);\n const errorId = `Error-${id}`;\n const containerText = useTextContent(\"\");\n const errorContainerText = useTextContent(\"\");\n\n return (\n <Box\n {...rest}\n {...qa}\n mb={mb}\n data-qa-formfield={\n (qa && qa[\"data-qa-formfield\"]) || id || containerText.current\n }\n data-qa-formfield-isinvalid={isInvalid === true}\n >\n {isLabelHidden ? (\n <VisuallyHidden data-testid=\"visually-hidden\">\n <Label htmlFor={id} required={required}>\n {label}\n </Label>\n </VisuallyHidden>\n ) : (\n <Label mb={helperText ? 100 : 300} htmlFor={id} required={required}>\n {label}\n </Label>\n )}\n {helperText && (\n <Text as=\"p\" fontSize={200} mb={300} color=\"text.subtext\">\n {helperText}\n </Text>\n )}\n {children({\n id,\n isInvalid,\n ariaDescribedby: errorId,\n ...(required !== undefined && { required }),\n })}\n {isInvalid && error && (\n <Text\n as=\"div\"\n fontSize={200}\n color=\"text.error\"\n mt={300}\n id={errorId}\n data-qa-formfield-error={\n (qa && qa[\"data-qa-formfield-error\"]) || errorContainerText.current\n }\n >\n {error}\n </Text>\n )}\n </Box>\n );\n};\n\nexport default FormField;\n","import { useState, useLayoutEffect, type RefObject } from \"react\";\n\ninterface DOMRectObject {\n x: number;\n y: number;\n width: number;\n height: number;\n top: number;\n right: number;\n bottom: number;\n left: number;\n}\nconst initialBounds = Object.freeze({\n x: 0,\n y: 0,\n width: 0,\n height: 0,\n top: 0,\n right: 0,\n bottom: 0,\n left: 0,\n});\n\nexport function useMeasure<TElement extends Element>(ref: RefObject<TElement>) {\n const [bounds, setContentRect] =\n useState<Readonly<DOMRectObject>>(initialBounds);\n\n useLayoutEffect(() => {\n const element = ref.current;\n\n if (\n !element ||\n // in non-browser environments (e.g. Jest tests) ResizeObserver is not defined\n !(\"ResizeObserver\" in window)\n ) {\n return;\n }\n\n const resizeObserver = new ResizeObserver(([entry]) => {\n if (!entry) return;\n const { x, y, width, height, top, right, bottom, left } =\n entry.contentRect;\n setContentRect({\n x,\n y,\n width,\n height,\n top,\n right,\n bottom,\n left,\n });\n });\n resizeObserver.observe(ref.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, [ref]);\n\n return bounds;\n}\n","import { useState, useCallback } from \"react\";\n\ntype TypeSingleSelectProps<T extends string> = {\n initialValue?: T | \"\";\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onChange?: (value: string | T) => any;\n};\n\nexport const useSelect = <T extends string>(\n {\n initialValue = \"\",\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n onChange: userOnChange = () => {},\n }: TypeSingleSelectProps<T> = {\n initialValue: \"\",\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n onChange: () => {},\n }\n) => {\n const [value, setValue] = useState<string | T>(initialValue);\n\n const onChange = useCallback(\n (newValue: string) => {\n if (newValue !== value) {\n setValue(newValue);\n userOnChange(newValue);\n }\n },\n [userOnChange, value]\n );\n\n return { value, onChange };\n};\n","import { useCallback, useEffect, useReducer, useRef } from \"react\";\n\ntype TypeMultiSelectProps<T extends string> = {\n initialValue?: T[];\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onChange?: (value: Array<string | T>) => any;\n};\n\nconst valueReducer = (\n state: Set<string>,\n action: { type: string; value?: string }\n): Set<string> => {\n const newState = new Set(state);\n switch (action.type) {\n case \"reset\": {\n return new Set();\n }\n case \"toggle_item\":\n default: {\n if (action.value) {\n if (newState.has(action.value)) {\n newState.delete(action.value);\n } else {\n newState.add(action.value);\n }\n }\n return newState;\n }\n }\n};\n\nexport const useMultiselect = <T extends string>(\n {\n initialValue = [],\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n onChange: userOnChange = () => {},\n }: TypeMultiSelectProps<T> = {\n initialValue: [],\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n onChange: () => {},\n }\n) => {\n const [value, dispatch] = useReducer(valueReducer, new Set(initialValue));\n\n const getArrayValue = (value: Set<string | T>) =>\n Array.from<string | T>(value);\n\n const onChange = useCallback(\n (newValue: string) => {\n dispatch({ type: \"toggle_item\", value: newValue });\n },\n [dispatch]\n );\n\n const isFirstRun = useRef(true);\n\n useEffect(() => {\n if (isFirstRun.current) {\n isFirstRun.current = false;\n return;\n }\n userOnChange(getArrayValue(value));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [userOnChange, value]);\n\n const onClear = useCallback(() => {\n dispatch({ type: \"reset\" });\n }, [dispatch]);\n\n return { value: getArrayValue(value), onChange, onClear };\n};\n","import { canUseDOM } from \"@sproutsocial/seeds-react-utilities\";\nimport { useEffect, useMemo, useState } from \"react\";\n\ntype TypeMutationObserverInitRequired =\n | {\n childList: true;\n }\n | {\n attributes: true;\n }\n | {\n characterData: true;\n };\n\ntype TypeMutationObserverInit = {\n subtree?: boolean;\n attributeOldValue?: boolean;\n characterDataOldValue?: boolean;\n attributeFilter?: Array<string>;\n} & TypeMutationObserverInitRequired;\n\ntype TypeMutationObserverCallback = (\n mutationList?: MutationRecord[],\n observer?: MutationObserver\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n) => any;\n\nconst defaultCallback: TypeMutationObserverCallback = (mutationList) =>\n mutationList;\n\nexport function useMutationObserver(\n targetNode: Node | null,\n config: TypeMutationObserverInit,\n callback: TypeMutationObserverCallback = defaultCallback\n) {\n if (!canUseDOM()) {\n return;\n }\n /* eslint-disable-next-line */\n const [value, setValue] = useState(undefined);\n /* eslint-disable-next-line */\n const observer = useMemo(\n () =>\n new MutationObserver((mutationList, observer) => {\n const result = callback(mutationList, observer);\n setValue(result);\n }),\n [callback]\n );\n /* eslint-disable-next-line */\n useEffect(() => {\n if (targetNode) {\n observer.observe(targetNode, config);\n return () => {\n observer.disconnect();\n };\n }\n }, [targetNode, config, observer]);\n\n return value;\n}\n\nexport function useMutationObserverOnce(\n targetNode: Node | null,\n config: TypeMutationObserverInit,\n callback: TypeMutationObserverCallback\n) {\n const [isObserving, setObserving] = useState(true);\n const node = isObserving ? targetNode : null;\n const value = useMutationObserver(node, config, callback);\n if (value !== undefined && isObserving) {\n setObserving(false);\n }\n return value;\n}\n","import { useCallback, useState } from \"react\";\n\nexport type textContentRef = ((node: Node) => void) & { current?: string };\nexport function useTextContent(initial: string) {\n const [textContent, setTextContent] = useState(initial);\n\n const ref: textContentRef = useCallback((node: Node) => {\n if (node && node.textContent !== null) {\n setTextContent(node.textContent);\n }\n }, []);\n\n ref.current = textContent;\n return ref;\n}\n","import { useRef, useEffect } from \"react\";\n\nexport function useWhyDidYouUpdate(\n name: string,\n props: { [key: string]: any }\n) {\n // Get a mutable ref object where we can store props ...\n // ... for comparison next time this hook runs.\n const previousProps = useRef<typeof props>({});\n\n useEffect(() => {\n if (previousProps.current) {\n // Get all keys from previous and current props\n const allKeys = Object.keys({ ...previousProps.current, ...props });\n // Use this object to keep track of changed props\n const changesObj: typeof props = {};\n // Iterate through keys\n allKeys.forEach((key) => {\n // If previous is different from current\n\n if (previousProps.current[key] !== props[key]) {\n // Add to changesObj\n\n changesObj[key] = {\n from: previousProps.current[key],\n\n to: props[key],\n };\n }\n });\n\n // If changesObj not empty then output to console\n if (Object.keys(changesObj).length) {\n // eslint-disable-next-line no-console\n console.log(\"[why-did-you-update]\", name, changesObj);\n }\n }\n\n // Finally update previousProps with current props for next hook call\n previousProps.current = props;\n });\n}\n","import { darken, lighten } from \"polished\";\nimport { useTheme } from \"styled-components\";\nimport type { TypeTheme } from \"@sproutsocial/seeds-react-theme\";\n\n/**\n * The useInteractiveColor hook has context of theme mode (light or dark)\n * and can be used to lighten or darken a color dynamically\n *\n * note: colors are limited to our theme colors\n */\nconst useInteractiveColor = (themeColor: string): string => {\n // Throw error if used outside of a ThemeProvider (styled-components)\n if (!useTheme()) {\n throw new Error(\n \"useInteractiveColor() must be used within a Styled Components ThemeProvider\"\n );\n }\n\n // Get the current theme mode ie. 'light' or 'dark'\n const theme: TypeTheme = useTheme() as TypeTheme;\n const themeMode = theme.mode;\n\n // If the theme mode is dark, return a lightened version of the themeValue\n if (themeMode === \"dark\") {\n return lighten(0.2, themeColor);\n } else {\n // If the theme mode is light, return a darkened version of the themeValue\n return darken(0.2, themeColor);\n }\n};\n\nexport { useInteractiveColor };\n","import * as React from \"react\";\nimport type { TypeBoxProps } from \"@sproutsocial/seeds-react-box\";\n\nexport interface TypeFormFieldProps extends Omit<TypeBoxProps, \"children\"> {\n /** A function that receives props that need to be spread onto the child element */\n children: (options: {\n id: string;\n isInvalid: boolean;\n ariaDescribedby: string;\n required?: boolean;\n }) => React.ReactNode;\n\n /** Text describing any error with the field's content */\n error?: React.ReactNode;\n\n /** Text acting as a description blurb below the main label **/\n helperText?: React.ReactNode;\n\n /** ID of the form element (will be auto-generated if not provided) */\n id?: string;\n\n /** Whether the current contents of the field are invalid */\n isInvalid?: boolean;\n\n /** Label text to display above the form field */\n label: React.ReactNode;\n qa?: Record<string, any>;\n\n /** Whether the label text should be visually hidden */\n isLabelHidden?: boolean;\n\n /** Whether the form element is required */\n required?: boolean;\n}\n","import FormField from \"./FormField\";\n\nexport default FormField;\nexport { FormField };\nexport * from \"./FormFieldTypes\";\n"],"mappings":";AAAA,SAAgB,YAAAA,iBAAgB;;;ACAhC,SAAS,UAAAC,iBAAU,aAAuC,YAYpC,QAAO,WAE3B,eAEA;;;;;;;;;;;ADdF,OAAO,SAAS;AAChB,OAAO,WAAW;AAClB,OAAO,UAAU;AACjB,SAAS,sBAAsB;AAwB3B,SAWM,KAXN;AArBJ,IAAI,YAAY;AAEhB,IAAM,YAAY,CAAC;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA,IAAI;AAAA,EACJ,YAAY;AAAA,EACZ;AAAA,EACA,KAAK;AAAA,EACL;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA,GAAG;AACL,MAA0B;AACxB,QAAM,CAAC,EAAE,IAAIC,UAAS,cAAc,aAAa,WAAW,EAAE;AAC9D,QAAM,UAAU,SAAS,EAAE;AAC3B,QAAM,gBAAgB,EAAe,EAAE;AACvC,QAAM,qBAAqB,EAAe,EAAE;AAE5C,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACH,GAAG;AAAA,MACJ;AAAA,MACA,qBACG,MAAM,GAAG,mBAAmB,KAAM,MAAM,cAAc;AAAA,MAEzD,+BAA6B,cAAc;AAAA,MAE1C;AAAA,wBACC,oBAAC,kBAAe,eAAY,mBAC1B,8BAAC,SAAM,SAAS,IAAI,UACjB,iBACH,GACF,IAEA,oBAAC,SAAM,IAAI,aAAa,MAAM,KAAK,SAAS,IAAI,UAC7C,iBACH;AAAA,QAED,cACC,oBAAC,QAAK,IAAG,KAAI,UAAU,KAAK,IAAI,KAAK,OAAM,gBACxC,sBACH;AAAA,QAED,SAAS;AAAA,UACR;AAAA,UACA;AAAA,UACA,iBAAiB;AAAA,UACjB,GAAI,aAAa,UAAa,EAAE,SAAS;AAAA,QAC3C,CAAC;AAAA,QACA,aAAa,SACZ;AAAA,UAAC;AAAA;AAAA,YACC,IAAG;AAAA,YACH,UAAU;AAAA,YACV,OAAM;AAAA,YACN,IAAI;AAAA,YACJ,IAAI;AAAA,YACJ,2BACG,MAAM,GAAG,yBAAyB,KAAM,mBAAmB;AAAA,YAG7D;AAAA;AAAA,QACH;AAAA;AAAA;AAAA,EAEJ;AAEJ;AAEA,IAAO,oBAAQ;;;AQ9Ef,OAAuB;;;ACEvB,IAAO,cAAQ;","names":["useState","useState","useState"]}
@@ -0,0 +1,32 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as React from 'react';
3
+ import { TypeBoxProps } from '@sproutsocial/seeds-react-box';
4
+
5
+ interface TypeFormFieldProps extends Omit<TypeBoxProps, "children"> {
6
+ /** A function that receives props that need to be spread onto the child element */
7
+ children: (options: {
8
+ id: string;
9
+ isInvalid: boolean;
10
+ ariaDescribedby: string;
11
+ required?: boolean;
12
+ }) => React.ReactNode;
13
+ /** Text describing any error with the field's content */
14
+ error?: React.ReactNode;
15
+ /** Text acting as a description blurb below the main label **/
16
+ helperText?: React.ReactNode;
17
+ /** ID of the form element (will be auto-generated if not provided) */
18
+ id?: string;
19
+ /** Whether the current contents of the field are invalid */
20
+ isInvalid?: boolean;
21
+ /** Label text to display above the form field */
22
+ label: React.ReactNode;
23
+ qa?: Record<string, any>;
24
+ /** Whether the label text should be visually hidden */
25
+ isLabelHidden?: boolean;
26
+ /** Whether the form element is required */
27
+ required?: boolean;
28
+ }
29
+
30
+ declare const FormField: ({ children, error, helperText, id: identifier, isInvalid, label, mb, qa, isLabelHidden, required, ...rest }: TypeFormFieldProps) => react_jsx_runtime.JSX.Element;
31
+
32
+ export { FormField, type TypeFormFieldProps, FormField as default };
@@ -0,0 +1,32 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as React from 'react';
3
+ import { TypeBoxProps } from '@sproutsocial/seeds-react-box';
4
+
5
+ interface TypeFormFieldProps extends Omit<TypeBoxProps, "children"> {
6
+ /** A function that receives props that need to be spread onto the child element */
7
+ children: (options: {
8
+ id: string;
9
+ isInvalid: boolean;
10
+ ariaDescribedby: string;
11
+ required?: boolean;
12
+ }) => React.ReactNode;
13
+ /** Text describing any error with the field's content */
14
+ error?: React.ReactNode;
15
+ /** Text acting as a description blurb below the main label **/
16
+ helperText?: React.ReactNode;
17
+ /** ID of the form element (will be auto-generated if not provided) */
18
+ id?: string;
19
+ /** Whether the current contents of the field are invalid */
20
+ isInvalid?: boolean;
21
+ /** Label text to display above the form field */
22
+ label: React.ReactNode;
23
+ qa?: Record<string, any>;
24
+ /** Whether the label text should be visually hidden */
25
+ isLabelHidden?: boolean;
26
+ /** Whether the form element is required */
27
+ required?: boolean;
28
+ }
29
+
30
+ declare const FormField: ({ children, error, helperText, id: identifier, isInvalid, label, mb, qa, isLabelHidden, required, ...rest }: TypeFormFieldProps) => react_jsx_runtime.JSX.Element;
31
+
32
+ export { FormField, type TypeFormFieldProps, FormField as default };
package/dist/index.js ADDED
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ FormField: () => FormField_default,
34
+ default: () => src_default
35
+ });
36
+ module.exports = __toCommonJS(src_exports);
37
+
38
+ // src/FormField.tsx
39
+ var import_react2 = require("react");
40
+
41
+ // ../seeds-react-hooks/dist/index.mjs
42
+ var import_react = require("react");
43
+ var import_styled_components = require("styled-components");
44
+ var v = Object.freeze({ x: 0, y: 0, width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0 });
45
+ function X(r) {
46
+ let [t, e] = (0, import_react.useState)(r), s = (0, import_react.useCallback)((n) => {
47
+ n && n.textContent !== null && e(n.textContent);
48
+ }, []);
49
+ return s.current = t, s;
50
+ }
51
+
52
+ // src/FormField.tsx
53
+ var import_seeds_react_box = __toESM(require("@sproutsocial/seeds-react-box"));
54
+ var import_seeds_react_label = __toESM(require("@sproutsocial/seeds-react-label"));
55
+ var import_seeds_react_text = __toESM(require("@sproutsocial/seeds-react-text"));
56
+ var import_seeds_react_visually_hidden = require("@sproutsocial/seeds-react-visually-hidden");
57
+ var import_jsx_runtime = require("react/jsx-runtime");
58
+ var idCounter = 0;
59
+ var FormField = ({
60
+ children,
61
+ error,
62
+ helperText,
63
+ id: identifier,
64
+ isInvalid = false,
65
+ label,
66
+ mb = 400,
67
+ qa,
68
+ isLabelHidden = false,
69
+ required,
70
+ ...rest
71
+ }) => {
72
+ const [id] = (0, import_react2.useState)(identifier || `FormField-${idCounter++}`);
73
+ const errorId = `Error-${id}`;
74
+ const containerText = X("");
75
+ const errorContainerText = X("");
76
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
77
+ import_seeds_react_box.default,
78
+ {
79
+ ...rest,
80
+ ...qa,
81
+ mb,
82
+ "data-qa-formfield": qa && qa["data-qa-formfield"] || id || containerText.current,
83
+ "data-qa-formfield-isinvalid": isInvalid === true,
84
+ children: [
85
+ isLabelHidden ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_seeds_react_visually_hidden.VisuallyHidden, { "data-testid": "visually-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_seeds_react_label.default, { htmlFor: id, required, children: label }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_seeds_react_label.default, { mb: helperText ? 100 : 300, htmlFor: id, required, children: label }),
86
+ helperText && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_seeds_react_text.default, { as: "p", fontSize: 200, mb: 300, color: "text.subtext", children: helperText }),
87
+ children({
88
+ id,
89
+ isInvalid,
90
+ ariaDescribedby: errorId,
91
+ ...required !== void 0 && { required }
92
+ }),
93
+ isInvalid && error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
94
+ import_seeds_react_text.default,
95
+ {
96
+ as: "div",
97
+ fontSize: 200,
98
+ color: "text.error",
99
+ mt: 300,
100
+ id: errorId,
101
+ "data-qa-formfield-error": qa && qa["data-qa-formfield-error"] || errorContainerText.current,
102
+ children: error
103
+ }
104
+ )
105
+ ]
106
+ }
107
+ );
108
+ };
109
+ var FormField_default = FormField;
110
+
111
+ // src/FormFieldTypes.ts
112
+ var React2 = require("react");
113
+
114
+ // src/index.ts
115
+ var src_default = FormField_default;
116
+ // Annotate the CommonJS export names for ESM import in node:
117
+ 0 && (module.exports = {
118
+ FormField
119
+ });
120
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/FormField.tsx","../../seeds-react-hooks/src/useMeasure/useMeasure.ts","../../seeds-react-hooks/src/useSelect/useSelect.ts","../../seeds-react-hooks/src/useMultiselect/useMultiselect.ts","../../seeds-react-hooks/src/useMutationObserver/useMutationObserver.ts","../../seeds-react-hooks/src/useTextContent/useTextContent.ts","../../seeds-react-hooks/src/useWhyDidYouUpdate/useWhyDidYouUpdate.ts","../../seeds-react-hooks/src/useInteractiveColor/useInteractiveColor.ts","../src/FormFieldTypes.ts"],"sourcesContent":["import FormField from \"./FormField\";\n\nexport default FormField;\nexport { FormField };\nexport * from \"./FormFieldTypes\";\n","import React, { useState } from \"react\";\nimport { useTextContent } from \"@sproutsocial/seeds-react-hooks\";\nimport Box from \"@sproutsocial/seeds-react-box\";\nimport Label from \"@sproutsocial/seeds-react-label\";\nimport Text from \"@sproutsocial/seeds-react-text\";\nimport { VisuallyHidden } from \"@sproutsocial/seeds-react-visually-hidden\";\nimport type { TypeFormFieldProps } from \"./FormFieldTypes\";\n\nlet idCounter = 0;\n\nconst FormField = ({\n children,\n error,\n helperText,\n id: identifier,\n isInvalid = false,\n label,\n mb = 400,\n qa,\n isLabelHidden = false,\n required,\n ...rest\n}: TypeFormFieldProps) => {\n const [id] = useState(identifier || `FormField-${idCounter++}`);\n const errorId = `Error-${id}`;\n const containerText = useTextContent(\"\");\n const errorContainerText = useTextContent(\"\");\n\n return (\n <Box\n {...rest}\n {...qa}\n mb={mb}\n data-qa-formfield={\n (qa && qa[\"data-qa-formfield\"]) || id || containerText.current\n }\n data-qa-formfield-isinvalid={isInvalid === true}\n >\n {isLabelHidden ? (\n <VisuallyHidden data-testid=\"visually-hidden\">\n <Label htmlFor={id} required={required}>\n {label}\n </Label>\n </VisuallyHidden>\n ) : (\n <Label mb={helperText ? 100 : 300} htmlFor={id} required={required}>\n {label}\n </Label>\n )}\n {helperText && (\n <Text as=\"p\" fontSize={200} mb={300} color=\"text.subtext\">\n {helperText}\n </Text>\n )}\n {children({\n id,\n isInvalid,\n ariaDescribedby: errorId,\n ...(required !== undefined && { required }),\n })}\n {isInvalid && error && (\n <Text\n as=\"div\"\n fontSize={200}\n color=\"text.error\"\n mt={300}\n id={errorId}\n data-qa-formfield-error={\n (qa && qa[\"data-qa-formfield-error\"]) || errorContainerText.current\n }\n >\n {error}\n </Text>\n )}\n </Box>\n );\n};\n\nexport default FormField;\n","import { useState, useLayoutEffect, type RefObject } from \"react\";\n\ninterface DOMRectObject {\n x: number;\n y: number;\n width: number;\n height: number;\n top: number;\n right: number;\n bottom: number;\n left: number;\n}\nconst initialBounds = Object.freeze({\n x: 0,\n y: 0,\n width: 0,\n height: 0,\n top: 0,\n right: 0,\n bottom: 0,\n left: 0,\n});\n\nexport function useMeasure<TElement extends Element>(ref: RefObject<TElement>) {\n const [bounds, setContentRect] =\n useState<Readonly<DOMRectObject>>(initialBounds);\n\n useLayoutEffect(() => {\n const element = ref.current;\n\n if (\n !element ||\n // in non-browser environments (e.g. Jest tests) ResizeObserver is not defined\n !(\"ResizeObserver\" in window)\n ) {\n return;\n }\n\n const resizeObserver = new ResizeObserver(([entry]) => {\n if (!entry) return;\n const { x, y, width, height, top, right, bottom, left } =\n entry.contentRect;\n setContentRect({\n x,\n y,\n width,\n height,\n top,\n right,\n bottom,\n left,\n });\n });\n resizeObserver.observe(ref.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, [ref]);\n\n return bounds;\n}\n","import { useState, useCallback } from \"react\";\n\ntype TypeSingleSelectProps<T extends string> = {\n initialValue?: T | \"\";\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onChange?: (value: string | T) => any;\n};\n\nexport const useSelect = <T extends string>(\n {\n initialValue = \"\",\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n onChange: userOnChange = () => {},\n }: TypeSingleSelectProps<T> = {\n initialValue: \"\",\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n onChange: () => {},\n }\n) => {\n const [value, setValue] = useState<string | T>(initialValue);\n\n const onChange = useCallback(\n (newValue: string) => {\n if (newValue !== value) {\n setValue(newValue);\n userOnChange(newValue);\n }\n },\n [userOnChange, value]\n );\n\n return { value, onChange };\n};\n","import { useCallback, useEffect, useReducer, useRef } from \"react\";\n\ntype TypeMultiSelectProps<T extends string> = {\n initialValue?: T[];\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onChange?: (value: Array<string | T>) => any;\n};\n\nconst valueReducer = (\n state: Set<string>,\n action: { type: string; value?: string }\n): Set<string> => {\n const newState = new Set(state);\n switch (action.type) {\n case \"reset\": {\n return new Set();\n }\n case \"toggle_item\":\n default: {\n if (action.value) {\n if (newState.has(action.value)) {\n newState.delete(action.value);\n } else {\n newState.add(action.value);\n }\n }\n return newState;\n }\n }\n};\n\nexport const useMultiselect = <T extends string>(\n {\n initialValue = [],\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n onChange: userOnChange = () => {},\n }: TypeMultiSelectProps<T> = {\n initialValue: [],\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n onChange: () => {},\n }\n) => {\n const [value, dispatch] = useReducer(valueReducer, new Set(initialValue));\n\n const getArrayValue = (value: Set<string | T>) =>\n Array.from<string | T>(value);\n\n const onChange = useCallback(\n (newValue: string) => {\n dispatch({ type: \"toggle_item\", value: newValue });\n },\n [dispatch]\n );\n\n const isFirstRun = useRef(true);\n\n useEffect(() => {\n if (isFirstRun.current) {\n isFirstRun.current = false;\n return;\n }\n userOnChange(getArrayValue(value));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [userOnChange, value]);\n\n const onClear = useCallback(() => {\n dispatch({ type: \"reset\" });\n }, [dispatch]);\n\n return { value: getArrayValue(value), onChange, onClear };\n};\n","import { canUseDOM } from \"@sproutsocial/seeds-react-utilities\";\nimport { useEffect, useMemo, useState } from \"react\";\n\ntype TypeMutationObserverInitRequired =\n | {\n childList: true;\n }\n | {\n attributes: true;\n }\n | {\n characterData: true;\n };\n\ntype TypeMutationObserverInit = {\n subtree?: boolean;\n attributeOldValue?: boolean;\n characterDataOldValue?: boolean;\n attributeFilter?: Array<string>;\n} & TypeMutationObserverInitRequired;\n\ntype TypeMutationObserverCallback = (\n mutationList?: MutationRecord[],\n observer?: MutationObserver\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n) => any;\n\nconst defaultCallback: TypeMutationObserverCallback = (mutationList) =>\n mutationList;\n\nexport function useMutationObserver(\n targetNode: Node | null,\n config: TypeMutationObserverInit,\n callback: TypeMutationObserverCallback = defaultCallback\n) {\n if (!canUseDOM()) {\n return;\n }\n /* eslint-disable-next-line */\n const [value, setValue] = useState(undefined);\n /* eslint-disable-next-line */\n const observer = useMemo(\n () =>\n new MutationObserver((mutationList, observer) => {\n const result = callback(mutationList, observer);\n setValue(result);\n }),\n [callback]\n );\n /* eslint-disable-next-line */\n useEffect(() => {\n if (targetNode) {\n observer.observe(targetNode, config);\n return () => {\n observer.disconnect();\n };\n }\n }, [targetNode, config, observer]);\n\n return value;\n}\n\nexport function useMutationObserverOnce(\n targetNode: Node | null,\n config: TypeMutationObserverInit,\n callback: TypeMutationObserverCallback\n) {\n const [isObserving, setObserving] = useState(true);\n const node = isObserving ? targetNode : null;\n const value = useMutationObserver(node, config, callback);\n if (value !== undefined && isObserving) {\n setObserving(false);\n }\n return value;\n}\n","import { useCallback, useState } from \"react\";\n\nexport type textContentRef = ((node: Node) => void) & { current?: string };\nexport function useTextContent(initial: string) {\n const [textContent, setTextContent] = useState(initial);\n\n const ref: textContentRef = useCallback((node: Node) => {\n if (node && node.textContent !== null) {\n setTextContent(node.textContent);\n }\n }, []);\n\n ref.current = textContent;\n return ref;\n}\n","import { useRef, useEffect } from \"react\";\n\nexport function useWhyDidYouUpdate(\n name: string,\n props: { [key: string]: any }\n) {\n // Get a mutable ref object where we can store props ...\n // ... for comparison next time this hook runs.\n const previousProps = useRef<typeof props>({});\n\n useEffect(() => {\n if (previousProps.current) {\n // Get all keys from previous and current props\n const allKeys = Object.keys({ ...previousProps.current, ...props });\n // Use this object to keep track of changed props\n const changesObj: typeof props = {};\n // Iterate through keys\n allKeys.forEach((key) => {\n // If previous is different from current\n\n if (previousProps.current[key] !== props[key]) {\n // Add to changesObj\n\n changesObj[key] = {\n from: previousProps.current[key],\n\n to: props[key],\n };\n }\n });\n\n // If changesObj not empty then output to console\n if (Object.keys(changesObj).length) {\n // eslint-disable-next-line no-console\n console.log(\"[why-did-you-update]\", name, changesObj);\n }\n }\n\n // Finally update previousProps with current props for next hook call\n previousProps.current = props;\n });\n}\n","import { darken, lighten } from \"polished\";\nimport { useTheme } from \"styled-components\";\nimport type { TypeTheme } from \"@sproutsocial/seeds-react-theme\";\n\n/**\n * The useInteractiveColor hook has context of theme mode (light or dark)\n * and can be used to lighten or darken a color dynamically\n *\n * note: colors are limited to our theme colors\n */\nconst useInteractiveColor = (themeColor: string): string => {\n // Throw error if used outside of a ThemeProvider (styled-components)\n if (!useTheme()) {\n throw new Error(\n \"useInteractiveColor() must be used within a Styled Components ThemeProvider\"\n );\n }\n\n // Get the current theme mode ie. 'light' or 'dark'\n const theme: TypeTheme = useTheme() as TypeTheme;\n const themeMode = theme.mode;\n\n // If the theme mode is dark, return a lightened version of the themeValue\n if (themeMode === \"dark\") {\n return lighten(0.2, themeColor);\n } else {\n // If the theme mode is light, return a darkened version of the themeValue\n return darken(0.2, themeColor);\n }\n};\n\nexport { useInteractiveColor };\n","import * as React from \"react\";\nimport type { TypeBoxProps } from \"@sproutsocial/seeds-react-box\";\n\nexport interface TypeFormFieldProps extends Omit<TypeBoxProps, \"children\"> {\n /** A function that receives props that need to be spread onto the child element */\n children: (options: {\n id: string;\n isInvalid: boolean;\n ariaDescribedby: string;\n required?: boolean;\n }) => React.ReactNode;\n\n /** Text describing any error with the field's content */\n error?: React.ReactNode;\n\n /** Text acting as a description blurb below the main label **/\n helperText?: React.ReactNode;\n\n /** ID of the form element (will be auto-generated if not provided) */\n id?: string;\n\n /** Whether the current contents of the field are invalid */\n isInvalid?: boolean;\n\n /** Label text to display above the form field */\n label: React.ReactNode;\n qa?: Record<string, any>;\n\n /** Whether the label text should be visually hidden */\n isLabelHidden?: boolean;\n\n /** Whether the form element is required */\n required?: boolean;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,IAAAA,gBAAgC;;;ACAhC,mBAgBE;;;;;;;;;;;ADdF,6BAAgB;AAChB,+BAAkB;AAClB,8BAAiB;AACjB,yCAA+B;AAwB3B;AArBJ,IAAI,YAAY;AAEhB,IAAM,YAAY,CAAC;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA,IAAI;AAAA,EACJ,YAAY;AAAA,EACZ;AAAA,EACA,KAAK;AAAA,EACL;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA,GAAG;AACL,MAA0B;AACxB,QAAM,CAAC,EAAE,QAAI,wBAAS,cAAc,aAAa,WAAW,EAAE;AAC9D,QAAM,UAAU,SAAS,EAAE;AAC3B,QAAM,gBAAgB,EAAe,EAAE;AACvC,QAAM,qBAAqB,EAAe,EAAE;AAE5C,SACE;AAAA,IAAC,uBAAAC;AAAA,IAAA;AAAA,MACE,GAAG;AAAA,MACH,GAAG;AAAA,MACJ;AAAA,MACA,qBACG,MAAM,GAAG,mBAAmB,KAAM,MAAM,cAAc;AAAA,MAEzD,+BAA6B,cAAc;AAAA,MAE1C;AAAA,wBACC,4CAAC,qDAAe,eAAY,mBAC1B,sDAAC,yBAAAC,SAAA,EAAM,SAAS,IAAI,UACjB,iBACH,GACF,IAEA,4CAAC,yBAAAA,SAAA,EAAM,IAAI,aAAa,MAAM,KAAK,SAAS,IAAI,UAC7C,iBACH;AAAA,QAED,cACC,4CAAC,wBAAAC,SAAA,EAAK,IAAG,KAAI,UAAU,KAAK,IAAI,KAAK,OAAM,gBACxC,sBACH;AAAA,QAED,SAAS;AAAA,UACR;AAAA,UACA;AAAA,UACA,iBAAiB;AAAA,UACjB,GAAI,aAAa,UAAa,EAAE,SAAS;AAAA,QAC3C,CAAC;AAAA,QACA,aAAa,SACZ;AAAA,UAAC,wBAAAA;AAAA,UAAA;AAAA,YACC,IAAG;AAAA,YACH,UAAU;AAAA,YACV,OAAM;AAAA,YACN,IAAI;AAAA,YACJ,IAAI;AAAA,YACJ,2BACG,MAAM,GAAG,yBAAyB,KAAM,mBAAmB;AAAA,YAG7D;AAAA;AAAA,QACH;AAAA;AAAA;AAAA,EAEJ;AAEJ;AAEA,IAAO,oBAAQ;;;AQ9Ef,IAAAC,SAAuB;;;ATEvB,IAAO,cAAQ;","names":["import_react","Box","Label","Text","React"]}
package/jest.config.js ADDED
@@ -0,0 +1,9 @@
1
+ const baseConfig = require("@sproutsocial/seeds-testing");
2
+
3
+ /** * @type {import('jest').Config} */
4
+ const config = {
5
+ ...baseConfig,
6
+ displayName: "seeds-react-form-field",
7
+ };
8
+
9
+ module.exports = config;
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@sproutsocial/seeds-react-form-field",
3
+ "version": "1.0.0",
4
+ "description": "Seeds React FormField",
5
+ "author": "Sprout Social, Inc.",
6
+ "license": "MIT",
7
+ "main": "dist/index.js",
8
+ "module": "dist/esm/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "scripts": {
11
+ "build": "tsup --dts",
12
+ "build:debug": "tsup --dts --metafile",
13
+ "dev": "tsup --watch --dts",
14
+ "clean": "rm -rf .turbo dist",
15
+ "clean:modules": "rm -rf node_modules",
16
+ "typecheck": "tsc --noEmit",
17
+ "test": "jest",
18
+ "test:watch": "jest --watch --coverage=false"
19
+ },
20
+ "dependencies": {
21
+ "@sproutsocial/seeds-react-theme": "^*",
22
+ "@sproutsocial/seeds-react-system-props": "^*",
23
+ "@sproutsocial/seeds-react-box": "*",
24
+ "@sproutsocial/seeds-react-text": "*",
25
+ "@sproutsocial/seeds-react-visually-hidden": "*",
26
+ "@sproutsocial/seeds-react-label": "*"
27
+ },
28
+ "devDependencies": {
29
+ "@types/react": "^18.0.0",
30
+ "@types/styled-components": "^5.1.26",
31
+ "@sproutsocial/eslint-config-seeds": "*",
32
+ "react": "^18.0.0",
33
+ "styled-components": "^5.2.3",
34
+ "tsup": "^8.0.2",
35
+ "typescript": "^5.6.2",
36
+ "@sproutsocial/seeds-tsconfig": "*",
37
+ "@sproutsocial/seeds-testing": "*",
38
+ "@sproutsocial/seeds-react-testing-library": "*",
39
+ "@sproutsocial/seeds-react-input": "*"
40
+ },
41
+ "peerDependencies": {
42
+ "styled-components": "^5.2.3"
43
+ },
44
+ "engines": {
45
+ "node": ">=18"
46
+ }
47
+ }
@@ -0,0 +1,100 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { Box } from "@sproutsocial/seeds-react-box";
4
+ import { Input } from "@sproutsocial/seeds-react-input";
5
+ // import { Select } from "@sproutsocial/seeds-react-select";
6
+ // import { Textarea } from "@sproutsocial/seeds-react-textarea";
7
+ import FormField from "./FormField";
8
+
9
+ interface FormFieldStoryArgs extends React.ComponentProps<typeof FormField> {
10
+ value: string;
11
+ }
12
+
13
+ type Story = StoryObj<FormFieldStoryArgs>;
14
+
15
+ const meta: Meta<FormFieldStoryArgs> = {
16
+ title: "Components/Form Elements/FormField",
17
+ component: FormField,
18
+ args: {
19
+ value: "",
20
+ mb: 100,
21
+ isInvalid: true,
22
+ required: false,
23
+ },
24
+ };
25
+ export default meta;
26
+
27
+ export const Default: Story = {
28
+ render: (args) => (
29
+ <Box width="443px" margin="0 auto">
30
+ <FormField
31
+ label="Title"
32
+ helperText="This is a line of helper text"
33
+ required={args.required}
34
+ >
35
+ {(props) => <Input {...props} name="title" value={args.value} />}
36
+ </FormField>
37
+ {/* TODO: COME BACK AND UNCOMMENT WHEN SELECT AND TEXTAREA ARE MIGRATED OVER */}
38
+ {/* <FormField
39
+ id="descriptionIdThatIWantedToPassIn"
40
+ label="Description"
41
+ mb={args.mb}
42
+ required={args.required}
43
+ >
44
+ {(props) => (
45
+ <Textarea {...props} name="description" value={args.value} />
46
+ )}
47
+ </FormField> */}
48
+ {/* <FormField label="Reason" required={args.required}>
49
+ {(props) => (
50
+ <Select {...props} name="reason">
51
+ <option value="">Select a reason</option>
52
+ <option value="something">Something goes here</option>
53
+ </Select>
54
+ )}
55
+ </FormField> */}
56
+ </Box>
57
+ ),
58
+ };
59
+
60
+ export const Error: Story = {
61
+ render: (args) => (
62
+ <Box width="443px" margin="0 auto">
63
+ <FormField
64
+ label="Title"
65
+ helperText="This is a line of helper text"
66
+ error="There is an error associated with this input"
67
+ isInvalid={args.isInvalid}
68
+ required={args.required}
69
+ >
70
+ {(props) => <Input {...props} name="title" value={args.value} />}
71
+ </FormField>
72
+ {/* TODO: COME BACK AND UNCOMMENT WHEN SELECT AND TEXTAREA ARE MIGRATED OVER */}
73
+ {/* <FormField
74
+ id="descriptionIdThatIWantedToPassIn"
75
+ label="Description"
76
+ isInvalid={args.isInvalid}
77
+ error="There is an error associated with this textarea"
78
+ mb={args.mb}
79
+ required={args.required}
80
+ >
81
+ {(props) => (
82
+ <Textarea {...props} name="description" value={args.value} />
83
+ )}
84
+ </FormField> */}
85
+ {/* <FormField
86
+ label="Reason"
87
+ error="There is an error associated with this select"
88
+ isInvalid={args.isInvalid}
89
+ required={args.required}
90
+ >
91
+ {(props) => (
92
+ <Select {...props} name="reason">
93
+ <option value="">Select a reason</option>
94
+ <option value="something">Something goes here</option>
95
+ </Select>
96
+ )}
97
+ </FormField> */}
98
+ </Box>
99
+ ),
100
+ };
@@ -0,0 +1,79 @@
1
+ import React, { useState } from "react";
2
+ import { useTextContent } from "@sproutsocial/seeds-react-hooks";
3
+ import Box from "@sproutsocial/seeds-react-box";
4
+ import Label from "@sproutsocial/seeds-react-label";
5
+ import Text from "@sproutsocial/seeds-react-text";
6
+ import { VisuallyHidden } from "@sproutsocial/seeds-react-visually-hidden";
7
+ import type { TypeFormFieldProps } from "./FormFieldTypes";
8
+
9
+ let idCounter = 0;
10
+
11
+ const FormField = ({
12
+ children,
13
+ error,
14
+ helperText,
15
+ id: identifier,
16
+ isInvalid = false,
17
+ label,
18
+ mb = 400,
19
+ qa,
20
+ isLabelHidden = false,
21
+ required,
22
+ ...rest
23
+ }: TypeFormFieldProps) => {
24
+ const [id] = useState(identifier || `FormField-${idCounter++}`);
25
+ const errorId = `Error-${id}`;
26
+ const containerText = useTextContent("");
27
+ const errorContainerText = useTextContent("");
28
+
29
+ return (
30
+ <Box
31
+ {...rest}
32
+ {...qa}
33
+ mb={mb}
34
+ data-qa-formfield={
35
+ (qa && qa["data-qa-formfield"]) || id || containerText.current
36
+ }
37
+ data-qa-formfield-isinvalid={isInvalid === true}
38
+ >
39
+ {isLabelHidden ? (
40
+ <VisuallyHidden data-testid="visually-hidden">
41
+ <Label htmlFor={id} required={required}>
42
+ {label}
43
+ </Label>
44
+ </VisuallyHidden>
45
+ ) : (
46
+ <Label mb={helperText ? 100 : 300} htmlFor={id} required={required}>
47
+ {label}
48
+ </Label>
49
+ )}
50
+ {helperText && (
51
+ <Text as="p" fontSize={200} mb={300} color="text.subtext">
52
+ {helperText}
53
+ </Text>
54
+ )}
55
+ {children({
56
+ id,
57
+ isInvalid,
58
+ ariaDescribedby: errorId,
59
+ ...(required !== undefined && { required }),
60
+ })}
61
+ {isInvalid && error && (
62
+ <Text
63
+ as="div"
64
+ fontSize={200}
65
+ color="text.error"
66
+ mt={300}
67
+ id={errorId}
68
+ data-qa-formfield-error={
69
+ (qa && qa["data-qa-formfield-error"]) || errorContainerText.current
70
+ }
71
+ >
72
+ {error}
73
+ </Text>
74
+ )}
75
+ </Box>
76
+ );
77
+ };
78
+
79
+ export default FormField;
@@ -0,0 +1,34 @@
1
+ import * as React from "react";
2
+ import type { TypeBoxProps } from "@sproutsocial/seeds-react-box";
3
+
4
+ export interface TypeFormFieldProps extends Omit<TypeBoxProps, "children"> {
5
+ /** A function that receives props that need to be spread onto the child element */
6
+ children: (options: {
7
+ id: string;
8
+ isInvalid: boolean;
9
+ ariaDescribedby: string;
10
+ required?: boolean;
11
+ }) => React.ReactNode;
12
+
13
+ /** Text describing any error with the field's content */
14
+ error?: React.ReactNode;
15
+
16
+ /** Text acting as a description blurb below the main label **/
17
+ helperText?: React.ReactNode;
18
+
19
+ /** ID of the form element (will be auto-generated if not provided) */
20
+ id?: string;
21
+
22
+ /** Whether the current contents of the field are invalid */
23
+ isInvalid?: boolean;
24
+
25
+ /** Label text to display above the form field */
26
+ label: React.ReactNode;
27
+ qa?: Record<string, any>;
28
+
29
+ /** Whether the label text should be visually hidden */
30
+ isLabelHidden?: boolean;
31
+
32
+ /** Whether the form element is required */
33
+ required?: boolean;
34
+ }
@@ -0,0 +1,77 @@
1
+ import React from "react";
2
+ import { render, screen } from "@sproutsocial/seeds-react-testing-library";
3
+ import { Input } from "@sproutsocial/seeds-react-input";
4
+ import FormField from "../FormField";
5
+
6
+ describe("FormField", () => {
7
+ const label = "Label";
8
+ const inputVal = "value";
9
+ it("passes the autogenerated id down", () => {
10
+ render(
11
+ <FormField
12
+ label={label}
13
+ error="There is an error associated with this input"
14
+ isInvalid={true}
15
+ >
16
+ {(props) => (
17
+ <Input
18
+ {...props}
19
+ placeholder="Enter a title"
20
+ name="title"
21
+ value={inputVal}
22
+ />
23
+ )}
24
+ </FormField>
25
+ );
26
+ expect(screen.getByLabelText(label)).toHaveValue(inputVal);
27
+ });
28
+ it("passes the id passed in down to the input field", () => {
29
+ const manualId = "manualId";
30
+ render(
31
+ <FormField
32
+ id={manualId}
33
+ label={label}
34
+ error="There is an error associated with this input"
35
+ isInvalid={true}
36
+ >
37
+ {(props) => (
38
+ <Input {...props} placeholder="Enter a title" name="title" value="" />
39
+ )}
40
+ </FormField>
41
+ );
42
+ expect(screen.getByLabelText(label)).toHaveAttribute("id", manualId);
43
+ });
44
+ it("is accessible", async () => {
45
+ const { runA11yCheck } = render(
46
+ <FormField
47
+ label="Title"
48
+ error="There is an error associated with this input"
49
+ isInvalid={true}
50
+ >
51
+ {(props) => (
52
+ <Input {...props} placeholder="Enter a title" name="title" value="" />
53
+ )}
54
+ </FormField>
55
+ );
56
+ await runA11yCheck();
57
+ });
58
+ it("should visually hide label", () => {
59
+ const manualId = "manualId";
60
+ render(
61
+ <FormField id={manualId} label={label} isLabelHidden={true}>
62
+ {(props) => (
63
+ <Input {...props} placeholder="Enter a title" name="title" value="" />
64
+ )}
65
+ </FormField>
66
+ );
67
+ expect(screen.getByTestId("visually-hidden")).toBeInTheDocument();
68
+ });
69
+ it("should be required when required is true", () => {
70
+ render(
71
+ <FormField label={label} required>
72
+ {(props) => <Input {...props} name="title" />}
73
+ </FormField>
74
+ );
75
+ expect(screen.getByLabelText(`${label}*`)).toBeRequired();
76
+ });
77
+ });
@@ -0,0 +1,84 @@
1
+ import * as React from "react";
2
+ import { Input } from "@sproutsocial/seeds-react-input";
3
+ // import Select from "@sproutsocial/seeds-react-select";
4
+ // import Textarea from "@sproutsocial/seeds-react-textarea";
5
+ import FormField from "../FormField";
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
8
+ function FormFieldTypes() {
9
+ return (
10
+ <>
11
+ <FormField label="Title" helperText="This is a line of helper text">
12
+ {(props) => (
13
+ <Input {...props} name="title" placeholder="Enter a title" />
14
+ )}
15
+ </FormField>
16
+ {/* <FormField id="descriptionIdThatIWantedToPassIn" label="Description">
17
+ {(props) => (
18
+ <Textarea
19
+ {...props}
20
+ name="description"
21
+ placeholder="Enter a description"
22
+ />
23
+ )}
24
+ </FormField>
25
+ <FormField label="Reason">
26
+ {(props) => (
27
+ <Select {...props} name="reason">
28
+ <option value="">Select a reason</option>
29
+ <option value="something">Something goes here</option>
30
+ </Select>
31
+ )}
32
+ </FormField> */}
33
+ <FormField
34
+ label="Title"
35
+ helperText="This is a line of helper text"
36
+ error="There is an error associated with this input"
37
+ >
38
+ {(props) => (
39
+ <Input {...props} name="title" placeholder="Enter a title" />
40
+ )}
41
+ </FormField>
42
+ {/* <FormField
43
+ id="descriptionIdThatIWantedToPassIn"
44
+ label="Description"
45
+ error="There is an error associated with this textarea"
46
+ >
47
+ {(props) => (
48
+ <Textarea
49
+ {...props}
50
+ name="description"
51
+ placeholder="Enter a description"
52
+ />
53
+ )}
54
+ </FormField>
55
+ <FormField
56
+ label="Reason"
57
+ error="There is an error associated with this select"
58
+ >
59
+ {(props) => (
60
+ <Select {...props} name="reason">
61
+ <option value="">Select a reason</option>
62
+ <option value="something">Something goes here</option>
63
+ </Select>
64
+ )}
65
+ </FormField> */}
66
+ <FormField label="Title" required>
67
+ {(props) => <Input {...props} name="title" />}
68
+ </FormField>
69
+ {/* <FormField label="Title" required>
70
+ {(props) => <Textarea {...props} name="title" />}
71
+ </FormField>
72
+ <FormField label="Title" required>
73
+ {(props) => (
74
+ <Select {...props} name="reason">
75
+ <option value="">Select a reason</option>
76
+ <option value="something">Something goes here</option>
77
+ </Select>
78
+ )}
79
+ </FormField> */}
80
+ {/* @ts-expect-error - test that missing required props is rejected */}
81
+ <FormField />
82
+ </>
83
+ );
84
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import FormField from "./FormField";
2
+
3
+ export default FormField;
4
+ export { FormField };
5
+ export * from "./FormFieldTypes";
@@ -0,0 +1,7 @@
1
+ import "styled-components";
2
+ import { TypeTheme } from "@sproutsocial/seeds-react-theme";
3
+
4
+ declare module "styled-components" {
5
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
6
+ export interface DefaultTheme extends TypeTheme {}
7
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@sproutsocial/seeds-tsconfig/bundler/dom/library-monorepo",
3
+ "compilerOptions": {
4
+ "jsx": "react-jsx",
5
+ "module": "esnext"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist", "coverage"]
9
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig((options) => ({
4
+ entry: ["src/index.ts"],
5
+ format: ["cjs", "esm"],
6
+ clean: true,
7
+ legacyOutput: true,
8
+ dts: options.dts,
9
+ external: ["react"],
10
+ sourcemap: true,
11
+ metafile: options.metafile,
12
+ }));