@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 +6 -0
- package/.eslintrc.js +4 -0
- package/.turbo/turbo-build.log +21 -0
- package/CHANGELOG.md +12 -0
- package/dist/esm/index.js +83 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/index.d.mts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +120 -0
- package/dist/index.js.map +1 -0
- package/jest.config.js +9 -0
- package/package.json +47 -0
- package/src/FormField.stories.tsx +100 -0
- package/src/FormField.tsx +79 -0
- package/src/FormFieldTypes.ts +34 -0
- package/src/__tests__/FormField.test.tsx +77 -0
- package/src/__tests__/FormField.typetest.tsx +84 -0
- package/src/index.ts +5 -0
- package/src/styled.d.ts +7 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +12 -0
package/.eslintignore
ADDED
package/.eslintrc.js
ADDED
|
@@ -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"]}
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
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
package/src/styled.d.ts
ADDED
package/tsconfig.json
ADDED
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
|
+
}));
|