@sproutsocial/seeds-react-form-field 1.0.6 → 1.1.1

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.
@@ -8,14 +8,14 @@ $ tsup --dts
8
8
  CLI Cleaning output folder
9
9
  CJS Build start
10
10
  ESM Build start
11
- ESM dist/esm/index.js 2.55 KB
12
- ESM dist/esm/index.js.map 14.21 KB
13
- ESM ⚡️ Build success in 299ms
14
- CJS dist/index.js 4.56 KB
15
- CJS dist/index.js.map 14.29 KB
16
- CJS ⚡️ Build success in 305ms
11
+ CJS dist/index.js 5.00 KB
12
+ CJS dist/index.js.map 14.96 KB
13
+ CJS ⚡️ Build success in 194ms
14
+ ESM dist/esm/index.js 2.99 KB
15
+ ESM dist/esm/index.js.map 14.90 KB
16
+ ESM ⚡️ Build success in 192ms
17
17
  DTS Build start
18
- DTS ⚡️ Build success in 16696ms
18
+ DTS ⚡️ Build success in 17283ms
19
19
  DTS dist/index.d.ts 1.35 KB
20
20
  DTS dist/index.d.mts 1.35 KB
21
- Done in 22.47s.
21
+ Done in 22.99s.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @sproutsocial/seeds-react-form-field
2
2
 
3
+ ## 1.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - @sproutsocial/seeds-react-theme@3.3.1
8
+ - @sproutsocial/seeds-react-box@1.1.9
9
+ - @sproutsocial/seeds-react-label@1.0.8
10
+ - @sproutsocial/seeds-react-visually-hidden@1.0.9
11
+
12
+ ## 1.1.0
13
+
14
+ ### Minor Changes
15
+
16
+ - 81584cb: Add helper text to input element's `aria-describedby`
17
+ Bug fix: only add `aria-describedby` if error or helper text exists
18
+
19
+ ## 1.0.7
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies [750d1ea]
24
+ - @sproutsocial/seeds-react-theme@3.3.0
25
+ - @sproutsocial/seeds-react-visually-hidden@1.0.8
26
+ - @sproutsocial/seeds-react-box@1.1.8
27
+ - @sproutsocial/seeds-react-label@1.0.7
28
+
3
29
  ## 1.0.6
4
30
 
5
31
  ### Patch Changes
package/dist/esm/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // src/FormField.tsx
2
- import { useState as useState2 } from "react";
2
+ import { useMemo as useMemo2, useState as useState2 } from "react";
3
3
 
4
4
  // ../seeds-react-hooks/dist/index.mjs
5
5
  import { useState, useLayoutEffect, useCallback, useReducer, useRef, useEffect, useMemo } from "react";
6
6
  import { useTheme } from "styled-components";
7
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) {
8
+ function H(r) {
9
9
  let [t, e] = useState(r), s = useCallback((n) => {
10
10
  n && n.textContent !== null && e(n.textContent);
11
11
  }, []);
@@ -34,8 +34,16 @@ var FormField = ({
34
34
  }) => {
35
35
  const [id] = useState2(identifier || `FormField-${idCounter++}`);
36
36
  const errorId = `Error-${id}`;
37
- const containerText = X("");
38
- const errorContainerText = X("");
37
+ const helperTextId = `HelperText-${id}`;
38
+ const containerText = H("");
39
+ const errorContainerText = H("");
40
+ const ariaDescribedby = useMemo2(() => {
41
+ const ids = [
42
+ helperText && helperTextId,
43
+ isInvalid && error && errorId
44
+ ].filter(Boolean);
45
+ return ids.length > 0 ? ids.join(" ") : void 0;
46
+ }, [helperText, helperTextId, isInvalid, error, errorId]);
39
47
  return /* @__PURE__ */ jsxs(
40
48
  Box,
41
49
  {
@@ -46,11 +54,21 @@ var FormField = ({
46
54
  "data-qa-formfield-isinvalid": isInvalid === true,
47
55
  children: [
48
56
  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 }),
57
+ helperText && /* @__PURE__ */ jsx(
58
+ Text,
59
+ {
60
+ as: "p",
61
+ fontSize: 200,
62
+ mb: 300,
63
+ color: "text.subtext",
64
+ id: helperTextId,
65
+ children: helperText
66
+ }
67
+ ),
50
68
  children({
51
69
  id,
52
70
  isInvalid,
53
- ariaDescribedby: errorId,
71
+ ariaDescribedby,
54
72
  ...required !== void 0 && { required }
55
73
  }),
56
74
  isInvalid && error && /* @__PURE__ */ jsx(
@@ -1 +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;A;;;;ACYhC,IAAMC,IAAgB,OAAO,OAAO,EAClC,GAAG,GACH,GAAG,GACH,OAAO,GACP,QAAQ,GACR,KAAK,GACL,OAAO,GACP,QAAQ,GACR,MAAM,EACR,CAAC;AIlBM,SAASC,EAAeC,GAAiB;AAC9C,MAAM,CAACC,GAAaC,CAAc,IAAIC,SAASH,CAAO,GAEhDI,IAAsBC,YAAaC,OAAe;AAClDA,SAAQA,EAAK,gBAAgB,QAC/BJ,EAAeI,EAAK,WAAW;EAEnC,GAAG,CAAA,CAAE;AAEL,SAAAF,EAAI,UAAUH,GACPG;AACT;;;ALZA,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,IAAIG,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,gBAAQ;","names":["useState","initialBounds","useTextContent","initial","textContent","setTextContent","useState","ref","useCallback","node","useState"]}
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, { useMemo, 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 helperTextId = `HelperText-${id}`;\n const containerText = useTextContent(\"\");\n const errorContainerText = useTextContent(\"\");\n\n const ariaDescribedby = useMemo(() => {\n const ids = [\n helperText && helperTextId,\n isInvalid && error && errorId,\n ].filter(Boolean);\n return ids.length > 0 ? ids.join(\" \") : undefined;\n }, [helperText, helperTextId, isInvalid, error, errorId]);\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\n as=\"p\"\n fontSize={200}\n mb={300}\n color=\"text.subtext\"\n id={helperTextId}\n >\n {helperText}\n </Text>\n )}\n {children({\n id,\n isInvalid,\n ariaDescribedby,\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 { 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 `color-mix(in srgb, ${themeColor}, white 20%)`;\n } else {\n // If the theme mode is light, return a darkened version of the themeValue\n return `color-mix(in srgb, ${themeColor}, black 20%)`;\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,WAAAA,UAAS,YAAAC,iBAAgB;A;;;;ACYzC,IAAMC,IAAgB,OAAO,OAAO,EAClC,GAAG,GACH,GAAG,GACH,OAAO,GACP,QAAQ,GACR,KAAK,GACL,OAAO,GACP,QAAQ,GACR,MAAM,EACR,CAAC;AIlBM,SAASC,EAAeC,GAAiB;AAC9C,MAAM,CAACC,GAAaC,CAAc,IAAIC,SAASH,CAAO,GAEhDI,IAAsBC,YAAaC,OAAe;AAClDA,SAAQA,EAAK,gBAAgB,QAC/BJ,EAAeI,EAAK,WAAW;EAEnC,GAAG,CAAA,CAAE;AAEL,SAAAF,EAAI,UAAUH,GACPG;AACT;;;ALZA,OAAO,SAAS;AAChB,OAAO,WAAW;AAClB,OAAO,UAAU;AACjB,SAAS,sBAAsB;AAiC3B,SAWM,KAXN;AA9BJ,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,IAAIG,UAAS,cAAc,aAAa,WAAW,EAAE;AAC9D,QAAM,UAAU,SAAS,EAAE;AAC3B,QAAM,eAAe,cAAc,EAAE;AACrC,QAAM,gBAAgB,EAAe,EAAE;AACvC,QAAM,qBAAqB,EAAe,EAAE;AAE5C,QAAM,kBAAkBC,SAAQ,MAAM;AACpC,UAAM,MAAM;AAAA,MACV,cAAc;AAAA,MACd,aAAa,SAAS;AAAA,IACxB,EAAE,OAAO,OAAO;AAChB,WAAO,IAAI,SAAS,IAAI,IAAI,KAAK,GAAG,IAAI;AAAA,EAC1C,GAAG,CAAC,YAAY,cAAc,WAAW,OAAO,OAAO,CAAC;AAExD,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;AAAA,UAAC;AAAA;AAAA,YACC,IAAG;AAAA,YACH,UAAU;AAAA,YACV,IAAI;AAAA,YACJ,OAAM;AAAA,YACN,IAAI;AAAA,YAEH;AAAA;AAAA,QACH;AAAA,QAED,SAAS;AAAA,UACR;AAAA,UACA;AAAA,UACA;AAAA,UACA,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;;;AQ7Ff,OAAuB;;;ACEvB,IAAO,gBAAQ;","names":["useMemo","useState","initialBounds","useTextContent","initial","textContent","setTextContent","useState","ref","useCallback","node","useState","useMemo"]}
package/dist/index.d.mts CHANGED
@@ -7,7 +7,7 @@ interface TypeFormFieldProps extends Omit<TypeBoxProps, "children"> {
7
7
  children: (options: {
8
8
  id: string;
9
9
  isInvalid: boolean;
10
- ariaDescribedby: string;
10
+ ariaDescribedby?: string;
11
11
  required?: boolean;
12
12
  }) => React.ReactNode;
13
13
  /** Text describing any error with the field's content */
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ interface TypeFormFieldProps extends Omit<TypeBoxProps, "children"> {
7
7
  children: (options: {
8
8
  id: string;
9
9
  isInvalid: boolean;
10
- ariaDescribedby: string;
10
+ ariaDescribedby?: string;
11
11
  required?: boolean;
12
12
  }) => React.ReactNode;
13
13
  /** Text describing any error with the field's content */
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ var import_react2 = require("react");
42
42
  var import_react = require("react");
43
43
  var import_styled_components = require("styled-components");
44
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) {
45
+ function H(r) {
46
46
  let [t, e] = (0, import_react.useState)(r), s = (0, import_react.useCallback)((n) => {
47
47
  n && n.textContent !== null && e(n.textContent);
48
48
  }, []);
@@ -71,8 +71,16 @@ var FormField = ({
71
71
  }) => {
72
72
  const [id] = (0, import_react2.useState)(identifier || `FormField-${idCounter++}`);
73
73
  const errorId = `Error-${id}`;
74
- const containerText = X("");
75
- const errorContainerText = X("");
74
+ const helperTextId = `HelperText-${id}`;
75
+ const containerText = H("");
76
+ const errorContainerText = H("");
77
+ const ariaDescribedby = (0, import_react2.useMemo)(() => {
78
+ const ids = [
79
+ helperText && helperTextId,
80
+ isInvalid && error && errorId
81
+ ].filter(Boolean);
82
+ return ids.length > 0 ? ids.join(" ") : void 0;
83
+ }, [helperText, helperTextId, isInvalid, error, errorId]);
76
84
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
77
85
  import_seeds_react_box.default,
78
86
  {
@@ -83,11 +91,21 @@ var FormField = ({
83
91
  "data-qa-formfield-isinvalid": isInvalid === true,
84
92
  children: [
85
93
  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 }),
94
+ helperText && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
95
+ import_seeds_react_text.default,
96
+ {
97
+ as: "p",
98
+ fontSize: 200,
99
+ mb: 300,
100
+ color: "text.subtext",
101
+ id: helperTextId,
102
+ children: helperText
103
+ }
104
+ ),
87
105
  children({
88
106
  id,
89
107
  isInvalid,
90
- ariaDescribedby: errorId,
108
+ ariaDescribedby,
91
109
  ...required !== void 0 && { required }
92
110
  }),
93
111
  isInvalid && error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
package/dist/index.js.map CHANGED
@@ -1 +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;A;;;;ACYhC,IAAMC,IAAgB,OAAO,OAAO,EAClC,GAAG,GACH,GAAG,GACH,OAAO,GACP,QAAQ,GACR,KAAK,GACL,OAAO,GACP,QAAQ,GACR,MAAM,EACR,CAAC;AIlBM,SAASC,EAAeC,GAAiB;AAC9C,MAAM,CAACC,GAAaC,CAAc,QAAIC,uBAASH,CAAO,GAEhDI,QAAsBC,0BAAaC,OAAe;AAClDA,SAAQA,EAAK,gBAAgB,QAC/BJ,EAAeI,EAAK,WAAW;EAEnC,GAAG,CAAA,CAAE;AAEL,SAAAF,EAAI,UAAUH,GACPG;AACT;;;ALZA,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,uBAAAG;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,gBAAQ;","names":["import_react","initialBounds","useTextContent","initial","textContent","setTextContent","useState","ref","useCallback","node","Box","Label","Text","React"]}
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, { useMemo, 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 helperTextId = `HelperText-${id}`;\n const containerText = useTextContent(\"\");\n const errorContainerText = useTextContent(\"\");\n\n const ariaDescribedby = useMemo(() => {\n const ids = [\n helperText && helperTextId,\n isInvalid && error && errorId,\n ].filter(Boolean);\n return ids.length > 0 ? ids.join(\" \") : undefined;\n }, [helperText, helperTextId, isInvalid, error, errorId]);\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\n as=\"p\"\n fontSize={200}\n mb={300}\n color=\"text.subtext\"\n id={helperTextId}\n >\n {helperText}\n </Text>\n )}\n {children({\n id,\n isInvalid,\n ariaDescribedby,\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 { 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 `color-mix(in srgb, ${themeColor}, white 20%)`;\n } else {\n // If the theme mode is light, return a darkened version of the themeValue\n return `color-mix(in srgb, ${themeColor}, black 20%)`;\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,gBAAyC;A;;;;ACYzC,IAAMC,IAAgB,OAAO,OAAO,EAClC,GAAG,GACH,GAAG,GACH,OAAO,GACP,QAAQ,GACR,KAAK,GACL,OAAO,GACP,QAAQ,GACR,MAAM,EACR,CAAC;AIlBM,SAASC,EAAeC,GAAiB;AAC9C,MAAM,CAACC,GAAaC,CAAc,QAAIC,uBAASH,CAAO,GAEhDI,QAAsBC,0BAAaC,OAAe;AAClDA,SAAQA,EAAK,gBAAgB,QAC/BJ,EAAeI,EAAK,WAAW;EAEnC,GAAG,CAAA,CAAE;AAEL,SAAAF,EAAI,UAAUH,GACPG;AACT;;;ALZA,6BAAgB;AAChB,+BAAkB;AAClB,8BAAiB;AACjB,yCAA+B;AAiC3B;AA9BJ,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,eAAe,cAAc,EAAE;AACrC,QAAM,gBAAgB,EAAe,EAAE;AACvC,QAAM,qBAAqB,EAAe,EAAE;AAE5C,QAAM,sBAAkB,uBAAQ,MAAM;AACpC,UAAM,MAAM;AAAA,MACV,cAAc;AAAA,MACd,aAAa,SAAS;AAAA,IACxB,EAAE,OAAO,OAAO;AAChB,WAAO,IAAI,SAAS,IAAI,IAAI,KAAK,GAAG,IAAI;AAAA,EAC1C,GAAG,CAAC,YAAY,cAAc,WAAW,OAAO,OAAO,CAAC;AAExD,SACE;AAAA,IAAC,uBAAAG;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;AAAA,UAAC,wBAAAC;AAAA,UAAA;AAAA,YACC,IAAG;AAAA,YACH,UAAU;AAAA,YACV,IAAI;AAAA,YACJ,OAAM;AAAA,YACN,IAAI;AAAA,YAEH;AAAA;AAAA,QACH;AAAA,QAED,SAAS;AAAA,UACR;AAAA,UACA;AAAA,UACA;AAAA,UACA,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;;;AQ7Ff,IAAAC,SAAuB;;;ATEvB,IAAO,gBAAQ;","names":["import_react","initialBounds","useTextContent","initial","textContent","setTextContent","useState","ref","useCallback","node","Box","Label","Text","React"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutsocial/seeds-react-form-field",
3
- "version": "1.0.6",
3
+ "version": "1.1.1",
4
4
  "description": "Seeds React FormField",
5
5
  "author": "Sprout Social, Inc.",
6
6
  "license": "MIT",
@@ -18,12 +18,12 @@
18
18
  "test:watch": "jest --watch --coverage=false"
19
19
  },
20
20
  "dependencies": {
21
- "@sproutsocial/seeds-react-theme": "^3.2.1",
21
+ "@sproutsocial/seeds-react-theme": "^3.3.1",
22
22
  "@sproutsocial/seeds-react-system-props": "^3.0.1",
23
- "@sproutsocial/seeds-react-box": "^1.1.7",
23
+ "@sproutsocial/seeds-react-box": "^1.1.9",
24
24
  "@sproutsocial/seeds-react-text": "^1.3.2",
25
- "@sproutsocial/seeds-react-visually-hidden": "^1.0.7",
26
- "@sproutsocial/seeds-react-label": "^1.0.6"
25
+ "@sproutsocial/seeds-react-visually-hidden": "^1.0.9",
26
+ "@sproutsocial/seeds-react-label": "^1.0.8"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/react": "^18.0.0",
@@ -36,7 +36,7 @@
36
36
  "@sproutsocial/seeds-tsconfig": "*",
37
37
  "@sproutsocial/seeds-testing": "*",
38
38
  "@sproutsocial/seeds-react-testing-library": "*",
39
- "@sproutsocial/seeds-react-input": "^1.4.12"
39
+ "@sproutsocial/seeds-react-input": "^1.4.16"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "styled-components": "^5.2.3"
@@ -18,8 +18,28 @@ const meta: Meta<FormFieldStoryArgs> = {
18
18
  args: {
19
19
  value: "",
20
20
  mb: 100,
21
- isInvalid: true,
21
+ isInvalid: false,
22
22
  required: false,
23
+ helperText: "This is a line of helper text",
24
+ error: "",
25
+ },
26
+ argTypes: {
27
+ helperText: {
28
+ control: "text",
29
+ description: "Text acting as a description blurb below the main label",
30
+ },
31
+ error: {
32
+ control: "text",
33
+ description: "Text describing any error with the field's content",
34
+ },
35
+ isInvalid: {
36
+ control: "boolean",
37
+ description: "Whether the current contents of the field are invalid",
38
+ },
39
+ value: {
40
+ control: "text",
41
+ description: "Input value",
42
+ },
23
43
  },
24
44
  };
25
45
  export default meta;
@@ -28,9 +48,11 @@ export const Default: Story = {
28
48
  render: (args) => (
29
49
  <Box width="443px" margin="0 auto">
30
50
  <FormField
31
- label="Title"
32
- helperText="This is a line of helper text"
51
+ label="Form label"
52
+ helperText={args.helperText}
33
53
  required={args.required}
54
+ error={args.error}
55
+ isInvalid={args.isInvalid}
34
56
  >
35
57
  {(props) => <Input {...props} name="title" value={args.value} />}
36
58
  </FormField>
@@ -58,12 +80,16 @@ export const Default: Story = {
58
80
  };
59
81
 
60
82
  export const Error: Story = {
83
+ args: {
84
+ isInvalid: true,
85
+ error: "There is an error associated with this input",
86
+ },
61
87
  render: (args) => (
62
88
  <Box width="443px" margin="0 auto">
63
89
  <FormField
64
90
  label="Title"
65
91
  helperText="This is a line of helper text"
66
- error="There is an error associated with this input"
92
+ error={args.error}
67
93
  isInvalid={args.isInvalid}
68
94
  required={args.required}
69
95
  >
package/src/FormField.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState } from "react";
1
+ import React, { useMemo, useState } from "react";
2
2
  import { useTextContent } from "@sproutsocial/seeds-react-hooks";
3
3
  import Box from "@sproutsocial/seeds-react-box";
4
4
  import Label from "@sproutsocial/seeds-react-label";
@@ -23,9 +23,18 @@ const FormField = ({
23
23
  }: TypeFormFieldProps) => {
24
24
  const [id] = useState(identifier || `FormField-${idCounter++}`);
25
25
  const errorId = `Error-${id}`;
26
+ const helperTextId = `HelperText-${id}`;
26
27
  const containerText = useTextContent("");
27
28
  const errorContainerText = useTextContent("");
28
29
 
30
+ const ariaDescribedby = useMemo(() => {
31
+ const ids = [
32
+ helperText && helperTextId,
33
+ isInvalid && error && errorId,
34
+ ].filter(Boolean);
35
+ return ids.length > 0 ? ids.join(" ") : undefined;
36
+ }, [helperText, helperTextId, isInvalid, error, errorId]);
37
+
29
38
  return (
30
39
  <Box
31
40
  {...rest}
@@ -48,14 +57,20 @@ const FormField = ({
48
57
  </Label>
49
58
  )}
50
59
  {helperText && (
51
- <Text as="p" fontSize={200} mb={300} color="text.subtext">
60
+ <Text
61
+ as="p"
62
+ fontSize={200}
63
+ mb={300}
64
+ color="text.subtext"
65
+ id={helperTextId}
66
+ >
52
67
  {helperText}
53
68
  </Text>
54
69
  )}
55
70
  {children({
56
71
  id,
57
72
  isInvalid,
58
- ariaDescribedby: errorId,
73
+ ariaDescribedby,
59
74
  ...(required !== undefined && { required }),
60
75
  })}
61
76
  {isInvalid && error && (
@@ -6,7 +6,7 @@ export interface TypeFormFieldProps extends Omit<TypeBoxProps, "children"> {
6
6
  children: (options: {
7
7
  id: string;
8
8
  isInvalid: boolean;
9
- ariaDescribedby: string;
9
+ ariaDescribedby?: string;
10
10
  required?: boolean;
11
11
  }) => React.ReactNode;
12
12
 
@@ -7,12 +7,9 @@ describe("FormField", () => {
7
7
  const label = "Label";
8
8
  const inputVal = "value";
9
9
  it("passes the autogenerated id down", () => {
10
+ const errorText = "There is an error associated with this input";
10
11
  render(
11
- <FormField
12
- label={label}
13
- error="There is an error associated with this input"
14
- isInvalid={true}
15
- >
12
+ <FormField label={label} error={errorText} isInvalid={true}>
16
13
  {(props) => (
17
14
  <Input
18
15
  {...props}
@@ -23,7 +20,12 @@ describe("FormField", () => {
23
20
  )}
24
21
  </FormField>
25
22
  );
26
- expect(screen.getByLabelText(label)).toHaveValue(inputVal);
23
+ const input = screen.getByLabelText(label);
24
+ expect(input).toHaveValue(inputVal);
25
+ // Verify aria-describedby is set to error ID
26
+ const describedBy = input.getAttribute("aria-describedby");
27
+ expect(describedBy).toContain("Error-");
28
+ expect(screen.getByText(errorText)).toHaveAttribute("id", describedBy);
27
29
  });
28
30
  it("passes the id passed in down to the input field", () => {
29
31
  const manualId = "manualId";
@@ -65,6 +67,10 @@ describe("FormField", () => {
65
67
  </FormField>
66
68
  );
67
69
  expect(screen.getByTestId("visually-hidden")).toBeInTheDocument();
70
+ // Verify aria-describedby is not set when no helper text or error
71
+ expect(screen.getByLabelText(label)).not.toHaveAttribute(
72
+ "aria-describedby"
73
+ );
68
74
  });
69
75
  it("should be required when required is true", () => {
70
76
  render(
@@ -74,4 +80,36 @@ describe("FormField", () => {
74
80
  );
75
81
  expect(screen.getByLabelText(`${label}*`)).toBeRequired();
76
82
  });
83
+ it("should set aria-describedby to helper text ID when helper text is provided", () => {
84
+ const helperText = "This is helper text";
85
+ render(
86
+ <FormField label={label} helperText={helperText}>
87
+ {(props) => <Input {...props} name="title" />}
88
+ </FormField>
89
+ );
90
+ const input = screen.getByLabelText(label);
91
+ const describedBy = input.getAttribute("aria-describedby");
92
+ expect(describedBy).toContain("HelperText-");
93
+ expect(screen.getByText(helperText)).toHaveAttribute("id", describedBy);
94
+ });
95
+ it("should set aria-describedby to both helper text and error IDs when both are provided", () => {
96
+ const helperText = "This is helper text";
97
+ const errorText = "This is an error";
98
+ render(
99
+ <FormField
100
+ label={label}
101
+ helperText={helperText}
102
+ error={errorText}
103
+ isInvalid={true}
104
+ >
105
+ {(props) => <Input {...props} name="title" />}
106
+ </FormField>
107
+ );
108
+ const input = screen.getByLabelText(label);
109
+ const describedBy = input.getAttribute("aria-describedby");
110
+ const ids = describedBy!.split(" ");
111
+ expect(ids).toHaveLength(2);
112
+ expect(ids[0]).toContain("HelperText-");
113
+ expect(ids[1]).toContain("Error-");
114
+ });
77
115
  });