@coopdigital/react 0.43.0 → 0.45.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.
Files changed (66) hide show
  1. package/dist/components/Checkbox/Checkbox.d.ts +4 -24
  2. package/dist/components/Checkbox/Checkbox.js +7 -14
  3. package/dist/components/Checkbox/index.d.ts +1 -2
  4. package/dist/components/Field/Field.d.ts +44 -5
  5. package/dist/components/Field/Field.js +68 -5
  6. package/dist/components/FieldMarkers/Error.d.ts +9 -0
  7. package/dist/components/{FieldError/FieldError.js → FieldMarkers/Error.js} +2 -2
  8. package/dist/components/FieldMarkers/Hint.d.ts +9 -0
  9. package/dist/components/{FieldHint/FieldHint.js → FieldMarkers/Hint.js} +2 -2
  10. package/dist/components/FieldMarkers/Label.d.ts +11 -0
  11. package/dist/components/FieldMarkers/Label.js +8 -0
  12. package/dist/components/FieldMarkers/Legend.d.ts +11 -0
  13. package/dist/components/FieldMarkers/Legend.js +13 -0
  14. package/dist/components/Fieldset/Fieldset.d.ts +54 -0
  15. package/dist/components/Fieldset/Fieldset.js +44 -0
  16. package/dist/components/Fieldset/index.d.ts +4 -0
  17. package/dist/components/RadioButton/RadioButton.d.ts +4 -22
  18. package/dist/components/RadioButton/RadioButton.js +5 -9
  19. package/dist/components/RadioButton/index.d.ts +1 -2
  20. package/dist/components/SearchBox/SearchBox.js +3 -3
  21. package/dist/components/TextInput/TextInput.d.ts +4 -18
  22. package/dist/components/TextInput/TextInput.js +7 -11
  23. package/dist/components/Textarea/Textarea.d.ts +4 -18
  24. package/dist/components/Textarea/Textarea.js +8 -9
  25. package/dist/hooks/useId.d.ts +2 -0
  26. package/dist/hooks/useId.js +8 -0
  27. package/dist/index.d.ts +1 -3
  28. package/dist/index.js +2 -6
  29. package/dist/utils/slots.d.ts +3 -1
  30. package/dist/utils/slots.js +8 -2
  31. package/package.json +10 -10
  32. package/src/components/Checkbox/Checkbox.tsx +8 -64
  33. package/src/components/Checkbox/index.ts +1 -2
  34. package/src/components/Field/Field.tsx +144 -8
  35. package/src/components/{FieldError/FieldError.tsx → FieldMarkers/Error.tsx} +4 -4
  36. package/src/components/{FieldHint/FieldHint.tsx → FieldMarkers/Hint.tsx} +5 -9
  37. package/src/components/FieldMarkers/Label.tsx +21 -0
  38. package/src/components/FieldMarkers/Legend.tsx +28 -0
  39. package/src/components/Fieldset/Fieldset.tsx +98 -0
  40. package/src/components/Fieldset/index.ts +5 -0
  41. package/src/components/RadioButton/RadioButton.tsx +6 -55
  42. package/src/components/RadioButton/index.ts +1 -2
  43. package/src/components/SearchBox/SearchBox.tsx +4 -2
  44. package/src/components/TextInput/TextInput.tsx +12 -45
  45. package/src/components/Textarea/Textarea.tsx +12 -40
  46. package/src/hooks/useId.ts +9 -0
  47. package/src/index.ts +1 -3
  48. package/src/utils/slots.ts +10 -2
  49. package/dist/components/Checkbox/CheckboxGroup.d.ts +0 -32
  50. package/dist/components/Checkbox/CheckboxGroup.js +0 -21
  51. package/dist/components/FieldError/FieldError.d.ts +0 -9
  52. package/dist/components/FieldError/index.d.ts +0 -4
  53. package/dist/components/FieldHint/FieldHint.d.ts +0 -9
  54. package/dist/components/FieldHint/index.d.ts +0 -4
  55. package/dist/components/FieldLabel/FieldLabel.d.ts +0 -13
  56. package/dist/components/FieldLabel/FieldLabel.js +0 -13
  57. package/dist/components/FieldLabel/index.d.ts +0 -4
  58. package/dist/components/RadioButton/RadioButtonGroup.d.ts +0 -32
  59. package/dist/components/RadioButton/RadioButtonGroup.js +0 -21
  60. package/dist/components/Tag/index.js +0 -5
  61. package/src/components/Checkbox/CheckboxGroup.tsx +0 -73
  62. package/src/components/FieldError/index.ts +0 -5
  63. package/src/components/FieldHint/index.ts +0 -5
  64. package/src/components/FieldLabel/FieldLabel.tsx +0 -31
  65. package/src/components/FieldLabel/index.ts +0 -5
  66. package/src/components/RadioButton/RadioButtonGroup.tsx +0 -73
@@ -1,5 +1,5 @@
1
1
  import type { JSX, TextareaHTMLAttributes } from "react";
2
- import { FormFieldError, StandardSizes } from "../../types";
2
+ import { StandardSizes } from "../../types";
3
3
  export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
4
4
  /** **(Optional)** Specify additional CSS classes to be applied to the component. */
5
5
  className?: string;
@@ -16,24 +16,10 @@ export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElemen
16
16
  cutoff?: boolean;
17
17
  /** **(Optional)** Specify whether the Textarea should be disabled. Refer to Experience Library guidance on disabled form controls and accessibility. */
18
18
  disabled?: boolean;
19
- /** **(Optional)** Specify the Textarea error state.
20
- *
21
- * This is an instance of `FormFieldError`. You can provide either an object with a `message` key, or a boolean value if you need to render the message independently.
22
- */
23
- error?: FormFieldError;
24
- /** **(Optional)** Specify the Textarea hint.
25
- *
26
- * This text is rendered under the label to provide further guidance for users.
27
- */
28
- hint?: string;
19
+ /** **(Optional)** Specify the Textarea error state */
20
+ error?: boolean;
29
21
  /** **(Optional)** Specify the Textarea id. Will be auto-generated if not set. */
30
22
  id?: string;
31
- /** **(Optional)** Specify the Textarea label.
32
- *
33
- * This property is optional in case you need to render your own label, but all form elements *must* provide a label. */
34
- label?: string;
35
- /** **(Optional)** Specify whether the label should be visible to humans or screen readers. */
36
- labelVisible?: boolean;
37
23
  /** **(Optional)** Specify the Textarea maxLength. This is the maximum number of characters users can enter. */
38
24
  maxLength?: number;
39
25
  /** Specify the Textarea name. */
@@ -45,5 +31,5 @@ export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElemen
45
31
  /** **(Optional)** Specify the Textarea size. */
46
32
  size?: StandardSizes;
47
33
  }
48
- export declare const Textarea: ({ "aria-placeholder": ariaPlaceholder, className, cols, counter, cutoff, disabled, error, hint, id, label, labelVisible, maxLength, name, onChange: userOnChange, placeholder, rows, size, ...props }: TextareaProps) => JSX.Element;
34
+ export declare const Textarea: ({ "aria-placeholder": ariaPlaceholder, className, cols, counter, cutoff, disabled, error, id, maxLength, name, onChange: userOnChange, placeholder, rows, size, ...props }: TextareaProps) => JSX.Element;
49
35
  export default Textarea;
@@ -1,16 +1,14 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import clsx from 'clsx';
3
- import { useId, useState } from 'react';
3
+ import { useState } from 'react';
4
4
  import { useDebounce } from '../../hooks/useDebounce.js';
5
- import { FieldError } from '../FieldError/FieldError.js';
6
- import { FieldHint } from '../FieldHint/FieldHint.js';
7
- import { FieldLabel } from '../FieldLabel/FieldLabel.js';
5
+ import { useId } from '../../hooks/useId.js';
8
6
 
9
7
  const DEBOUNCE_DELAY = 750;
10
8
  const charCountMessage = (remaining) => {
11
9
  return `You have ${Math.abs(remaining).toLocaleString()} ${Math.abs(remaining) === 1 ? "character" : "characters"} ${remaining < 0 ? "too many" : "remaining"}`;
12
10
  };
13
- const Textarea = ({ "aria-placeholder": ariaPlaceholder, className, cols = 30, counter = false, cutoff = false, disabled = false, error = false, hint, id, label, labelVisible = true, maxLength, name, onChange: userOnChange = undefined, placeholder, rows = 4, size = "md", ...props }) => {
11
+ const Textarea = ({ "aria-placeholder": ariaPlaceholder, className, cols = 30, counter = false, cutoff = false, disabled = false, error = false, id, maxLength, name, onChange: userOnChange = undefined, placeholder, rows = 4, size = "md", ...props }) => {
14
12
  var _a;
15
13
  const internalId = useId();
16
14
  id = id !== null && id !== void 0 ? id : internalId;
@@ -18,7 +16,7 @@ const Textarea = ({ "aria-placeholder": ariaPlaceholder, className, cols = 30, c
18
16
  "aria-placeholder": (_a = placeholder !== null && placeholder !== void 0 ? placeholder : ariaPlaceholder) !== null && _a !== void 0 ? _a : undefined,
19
17
  className: clsx("coop-textarea", className),
20
18
  cols,
21
- "data-error": error ? "" : undefined,
19
+ "data-error": error !== null && error !== void 0 ? error : undefined,
22
20
  "data-size": size.length && size !== "md" ? size : undefined,
23
21
  disabled,
24
22
  id,
@@ -30,14 +28,15 @@ const Textarea = ({ "aria-placeholder": ariaPlaceholder, className, cols = 30, c
30
28
  };
31
29
  const [remaining, setRemaining] = useState(maxLength);
32
30
  const debouncedRemaining = useDebounce(remaining, DEBOUNCE_DELAY);
31
+ const showCounter = !disabled && counter && maxLength && remaining != null && debouncedRemaining != null;
33
32
  const handleChange = (e) => {
34
33
  maxLength && e.target && setRemaining(maxLength - e.target.value.length);
35
34
  };
36
- const formItemProps = { "aria-disabled": disabled ? true : undefined };
37
- return (jsxs("div", { className: "coop-form-item", ...formItemProps, children: [label && (jsx(FieldLabel, { htmlFor: id, isVisible: labelVisible, children: label })), hint && jsx(FieldHint, { children: hint }), typeof error === "object" && (error === null || error === void 0 ? void 0 : error.message) && jsx(FieldError, { children: error.message }), jsx("textarea", { ...componentProps, onChange: (e) => {
35
+ // const formItemProps = { "aria-disabled": disabled ? true : undefined }
36
+ return (jsxs("div", { className: "coop-textarea-wrapper", children: [jsx("textarea", { ...componentProps, onChange: (e) => {
38
37
  userOnChange === null || userOnChange === void 0 ? void 0 : userOnChange(e);
39
38
  handleChange(e);
40
- } }), !disabled && counter && maxLength && remaining != null && debouncedRemaining != null && (jsxs(Fragment, { children: [jsx("small", { "aria-hidden": "true", className: "coop-textarea-counter", ...(remaining < 0 && { "data-error": "" }), children: charCountMessage(remaining) }), jsx("span", { "aria-live": "polite", className: "sr-only", children: charCountMessage(debouncedRemaining) })] }))] }));
39
+ } }), showCounter && (jsxs(Fragment, { children: [jsx("small", { "aria-hidden": "true", className: "coop-textarea-counter", ...(remaining < 0 && { "data-error": true }), children: charCountMessage(remaining) }), jsx("span", { "aria-live": "polite", className: "sr-only", children: charCountMessage(debouncedRemaining) })] }))] }));
41
40
  };
42
41
 
43
42
  export { Textarea, Textarea as default };
@@ -0,0 +1,2 @@
1
+ export type UseId = (id?: string) => string;
2
+ export declare const useId: UseId;
@@ -0,0 +1,8 @@
1
+ import { useId as useId$1 } from 'react';
2
+
3
+ const useId = (id) => {
4
+ const uniqueId = useId$1();
5
+ return id !== null && id !== void 0 ? id : uniqueId;
6
+ };
7
+
8
+ export { useId };
package/dist/index.d.ts CHANGED
@@ -5,9 +5,7 @@ export * from "./components/Card";
5
5
  export * from "./components/Checkbox";
6
6
  export * from "./components/Expandable";
7
7
  export * from "./components/Field";
8
- export * from "./components/FieldError";
9
- export * from "./components/FieldHint";
10
- export * from "./components/FieldLabel";
8
+ export * from "./components/Fieldset";
11
9
  export * from "./components/Flourish";
12
10
  export * from "./components/Image";
13
11
  export * from "./components/Pill";
package/dist/index.js CHANGED
@@ -3,17 +3,13 @@ export { Author } from './components/Author/Author.js';
3
3
  export { Button } from './components/Button/Button.js';
4
4
  export { Card } from './components/Card/Card.js';
5
5
  export { Checkbox } from './components/Checkbox/Checkbox.js';
6
- export { CheckboxGroup } from './components/Checkbox/CheckboxGroup.js';
7
6
  export { Expandable } from './components/Expandable/Expandable.js';
8
- export { Field } from './components/Field/Field.js';
9
- export { FieldError } from './components/FieldError/FieldError.js';
10
- export { FieldHint } from './components/FieldHint/FieldHint.js';
11
- export { FieldLabel } from './components/FieldLabel/FieldLabel.js';
7
+ export { Field, FieldControl } from './components/Field/Field.js';
8
+ export { Fieldset, FieldsetFields } from './components/Fieldset/Fieldset.js';
12
9
  export { Flourish } from './components/Flourish/Flourish.js';
13
10
  export { Image } from './components/Image/Image.js';
14
11
  export { Pill } from './components/Pill/Pill.js';
15
12
  export { RadioButton } from './components/RadioButton/RadioButton.js';
16
- export { RadioButtonGroup } from './components/RadioButton/RadioButtonGroup.js';
17
13
  export { RootSVG } from './components/RootSVG/RootSVG.js';
18
14
  export { SearchBox } from './components/SearchBox/SearchBox.js';
19
15
  export { Signpost } from './components/Signpost/Signpost.js';
@@ -2,5 +2,7 @@ import React from "react";
2
2
  type Slots<T> = Record<keyof T, React.ReactNode>;
3
3
  export declare function isKey<T extends object>(x: T, k: PropertyKey): k is keyof T;
4
4
  export declare function getSlotName(node: React.ReactNode): string | false;
5
- export declare function getSlots<T>(componentSlots: Slots<T>, children: React.ReactNode): Slots<T>;
5
+ export declare function getSlots<T>(componentSlots: Slots<T>, children: React.ReactNode, options?: {
6
+ collect?: string[];
7
+ }): Slots<T>;
6
8
  export {};
@@ -8,11 +8,17 @@ function getSlotName(node) {
8
8
  ? node.type.name
9
9
  : false;
10
10
  }
11
- function getSlots(componentSlots, children) {
11
+ function getSlots(componentSlots, children, options) {
12
12
  return React.Children.toArray(children).reduce((slots, child) => {
13
+ var _a;
13
14
  const slotName = getSlotName(child);
14
15
  if (child && slotName && isKey(componentSlots, slotName)) {
15
- slots[slotName] = child;
16
+ if ((_a = void 0 ) === null || _a === void 0 ? void 0 : _a.includes(slotName)) {
17
+ slots[slotName] = slots[slotName] ? [...[slots[slotName]].flat(), child] : [child];
18
+ }
19
+ else {
20
+ slots[slotName] = child;
21
+ }
16
22
  }
17
23
  else if ("Children" in slots) {
18
24
  slots.Children = slots.Children ? [...[slots.Children].flat(), child] : [child];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coopdigital/react",
3
3
  "type": "module",
4
- "version": "0.43.0",
4
+ "version": "0.45.0",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -57,18 +57,18 @@
57
57
  "devDependencies": {
58
58
  "@axe-core/playwright": "^4.11.0",
59
59
  "@playwright/test": "^1.56.1",
60
- "@storybook/addon-a11y": "^10.0.3",
61
- "@storybook/addon-docs": "^10.0.3",
62
- "@storybook/addon-onboarding": "^10.0.3",
63
- "@storybook/react-vite": "^10.0.3",
60
+ "@storybook/addon-a11y": "^10.0.7",
61
+ "@storybook/addon-docs": "^10.0.7",
62
+ "@storybook/addon-onboarding": "^10.0.7",
63
+ "@storybook/react-vite": "^10.0.7",
64
64
  "@testing-library/jest-dom": "^6.9.1",
65
65
  "@testing-library/react": "^16.3.0",
66
- "@types/react": "^19.2.2",
67
- "@types/react-dom": "^19.2.2",
66
+ "@types/react": "^19.2.4",
67
+ "@types/react-dom": "^19.2.3",
68
68
  "react": "^19.2.0",
69
69
  "react-dom": "^19.2.0",
70
70
  "serve": "^14.2.5",
71
- "storybook": "^10.0.3"
71
+ "storybook": "^10.0.7"
72
72
  },
73
73
  "peerDependencies": {
74
74
  "react": "^19.1.0",
@@ -78,8 +78,8 @@
78
78
  "storybook": "$storybook"
79
79
  },
80
80
  "dependencies": {
81
- "@coopdigital/styles": "^0.38.0",
81
+ "@coopdigital/styles": "^0.39.0",
82
82
  "clsx": "^2.1.1"
83
83
  },
84
- "gitHead": "c3dc3d710bed7e0bc415887e1d484ce5f048f98c"
84
+ "gitHead": "113b5fc44feee8b4e899c75c303f1ed26f9f2629"
85
85
  }
@@ -1,13 +1,9 @@
1
1
  import type { InputHTMLAttributes, JSX } from "react"
2
2
 
3
3
  import clsx from "clsx"
4
- import { useId } from "react"
5
4
 
6
- import { FormFieldError, StandardSizes } from "../../../src/types"
7
- import { FieldError } from "../FieldError"
8
- import { FieldHint } from "../FieldHint"
9
- import { FieldLabel } from "../FieldLabel"
10
- import Tag from "../Tag"
5
+ import { StandardSizes } from "../../../src/types"
6
+ import { useId } from "../../hooks/useId"
11
7
 
12
8
  export interface CheckboxProps
13
9
  extends Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "type"> {
@@ -15,89 +11,37 @@ export interface CheckboxProps
15
11
  className?: string
16
12
  /** **(Optional)** Specify whether the Checkbox should be disabled. Refer to Experience Library guidance on disabled form controls and accessibility. */
17
13
  disabled?: boolean
18
- /** **(Optional)** Specify the Checkbox error state.
19
- *
20
- * This is an instance of `FormFieldError`. You can provide either an object with a `message` key, or a boolean value if you need to render the message independently.
21
- */
22
- error?: FormFieldError
23
- /** **(Optional)** Specify the Checkbox hint.
24
- *
25
- * This text is rendered under the label to provide further guidance for users.
26
- */
27
- hint?: string
14
+ /** **(Optional)** Specify the Checkbox error state. */
15
+ error?: boolean
28
16
  /** **(Optional)** Specify the Checkbox id. Will be auto-generated if not set. */
29
17
  id?: string
30
- /** **(Optional)** Specify the Checkbox label.
31
- *
32
- * This property is optional in case you need to render your own label, but all form elements *must* provide a label. */
33
- label: string
34
- /** **(Optional)** Specify whether the label should be visible to humans or screen readers. */
35
- labelVisible?: boolean
36
18
  /** Specify the Checkbox name. */
37
19
  name: string
38
20
  /** **(Optional)** Specify the Checkbox size. */
39
21
  size?: StandardSizes
40
- /** **(Optional)** Specify the Checkbox tag text. */
41
- tag?: string
42
- /** **(Optional)** Specify the Checkbox tag className. */
43
- tagClassName?: string
44
- /** **(Optional)** Specify the Checkbox variant. */
45
- variant?: "default" | "boxed"
46
22
  }
47
23
 
48
24
  export const Checkbox = ({
49
25
  className,
50
26
  disabled,
51
27
  error = false,
52
- hint,
53
28
  id,
54
- label,
55
- labelVisible = true,
56
29
  name,
57
30
  size = "md",
58
- tag,
59
- tagClassName,
60
- variant = "default",
61
31
  ...props
62
32
  }: CheckboxProps): JSX.Element => {
63
- const internalId = useId()
64
-
65
- id = id ?? internalId
66
-
33
+ const uid = useId(id)
67
34
  const componentProps = {
68
35
  className: clsx("coop-checkbox", className),
69
- "data-error": error ? "" : undefined,
36
+ "data-error": error || undefined,
70
37
  "data-size": size.length && size !== "md" ? size : undefined,
71
- "data-variant": variant !== "default" ? variant : undefined,
72
38
  disabled,
73
- id,
39
+ id: uid,
74
40
  name,
75
41
  type: "checkbox",
76
42
  ...props,
77
43
  }
78
- const formItemProps = { "aria-disabled": disabled ? true : undefined }
79
- return (
80
- <div className="coop-form-item" {...formItemProps}>
81
- <div className="coop-checkbox-wrapper">
82
- <div className="coop-checkbox-input-wrapper">
83
- <input {...componentProps} />
84
-
85
- {label && (
86
- <FieldLabel htmlFor={id} isVisible={labelVisible}>
87
- <span>{label}</span>{" "}
88
- {tag && (
89
- <Tag className={tagClassName} size="sm">
90
- {tag}
91
- </Tag>
92
- )}
93
- </FieldLabel>
94
- )}
95
- </div>
96
- {hint && <FieldHint>{hint}</FieldHint>}
97
- </div>
98
- {typeof error === "object" && error?.message && <FieldError>{error.message}</FieldError>}
99
- </div>
100
- )
44
+ return <input {...componentProps} />
101
45
  }
102
46
 
103
47
  export default Checkbox
@@ -1,6 +1,5 @@
1
1
  import Checkbox from "./Checkbox"
2
- import CheckboxGroup from "./CheckboxGroup"
3
2
 
4
3
  export default Checkbox
5
- export { Checkbox, CheckboxGroup }
4
+ export { Checkbox }
6
5
  export * from "./Checkbox"
@@ -1,20 +1,156 @@
1
- import type { HTMLAttributes, JSX, ReactNode } from "react"
1
+ import type { HTMLAttributes, HTMLProps, JSX } from "react"
2
2
 
3
3
  import clsx from "clsx"
4
+ import React from "react"
4
5
 
5
- export interface FieldProps extends HTMLAttributes<HTMLDivElement> {
6
- /** **(Optional)** Main content inside the component. It can be any valid JSX or string. */
7
- children?: string | ReactNode
6
+ import { useId } from "../../hooks/useId"
7
+ import { getSlots } from "../../utils/slots"
8
+ import { Error as BaseError, type ErrorProps } from "../FieldMarkers/Error"
9
+ import { Hint as BaseHint, type HintProps } from "../FieldMarkers/Hint"
10
+ import { Label as BaseLabel, type LabelProps } from "../FieldMarkers/Label"
11
+
12
+ const componentSlots = {
13
+ Checkbox: null,
14
+ Children: null,
15
+ Control: null,
16
+ FieldError: null,
17
+ FieldHint: null,
18
+ FieldLabel: null,
19
+ RadioButton: null,
20
+ Textarea: null,
21
+ TextInput: null,
22
+ }
23
+
24
+ const standardFields = ["Children", "FieldError", "FieldHint", "FieldLabel"]
25
+ const inlineFields = ["Checkbox", "RadioButton"]
26
+
27
+ interface FieldProps extends HTMLAttributes<HTMLDivElement> {
28
+ /** **(Optional)** Specify whether the Field should render inside a box. */
29
+ boxed?: boolean
30
+ /** Form elements inside the Field.
31
+ *
32
+ * Usually this will be a combination of `Field.Label`, `Field.Hint`, `Field.Error` and an associated control such as `TextInput` or `Checkbox`. */
33
+ children: React.ReactNode
8
34
  /** **(Optional)** Specify additional CSS classes to be applied to the component. */
9
- className?: string
35
+ classname?: string
36
+ /** **(Optional)** Specify the Field error state. Use this to apply visual styling to the field control. */
37
+ error?: boolean
38
+ /** **(Optional)** Specify whether the Field should show the left-hand error bar if it is in an error state. */
39
+ hideErrorBar?: boolean
10
40
  }
11
41
 
12
- export const Field = ({ children, className, ...props }: FieldProps): JSX.Element => {
42
+ const Root = ({
43
+ boxed = false,
44
+ children,
45
+ className,
46
+ error = false,
47
+ hideErrorBar = false,
48
+ ...props
49
+ }: FieldProps) => {
50
+ const slots = getSlots(componentSlots, children)
51
+ const slotsArray = Object.entries(slots)
52
+
53
+ const uid = useId()
54
+
55
+ const isInlineControl = slotsArray.some((s) => s[1] !== null && inlineFields.includes(s[0]))
56
+ const hasFieldMarkers = !!(slots.FieldLabel ?? slots.FieldError ?? slots.FieldHint)
57
+ const [, control] =
58
+ slotsArray.find((s) => (!standardFields.includes(s[0]) && s[1] !== null ? s : null)) ?? []
59
+
60
+ let controlId = uid
61
+ let controlDisabled = false
62
+
63
+ if (control) {
64
+ if (typeof control === "object" && "props" in control) {
65
+ const controlProps = control.props as HTMLProps<HTMLElement>
66
+ controlId = controlProps.id ?? controlId
67
+ controlDisabled = controlProps.disabled ?? false
68
+ }
69
+
70
+ slots.Control = React.cloneElement(control as React.ReactElement<HTMLInputElement>, {
71
+ id: controlId,
72
+ })
73
+ }
74
+
75
+ if (slots.FieldLabel && React.isValidElement(slots.FieldLabel)) {
76
+ slots.FieldLabel = React.cloneElement(slots.FieldLabel as React.ReactElement<LabelProps>, {
77
+ htmlFor: (slots.FieldLabel.props as LabelProps).htmlFor ?? controlId,
78
+ })
79
+ }
80
+
13
81
  const componentProps = {
14
- className: clsx("coop-form-item ", className),
15
82
  ...props,
83
+ "aria-disabled": controlDisabled || undefined,
84
+ className: clsx("coop-field", isInlineControl && "coop-field-inline", className),
85
+ "data-boxed": boxed || undefined,
86
+ "data-error": error || undefined,
87
+ "data-hide-error": hideErrorBar || undefined,
16
88
  }
17
- return <div {...componentProps}>{children}</div>
89
+
90
+ return (
91
+ <div {...componentProps}>
92
+ {!isInlineControl && hasFieldMarkers && (
93
+ <div className="coop-field-markers">
94
+ {slots.FieldLabel}
95
+ {slots.FieldHint}
96
+ {slots.FieldError}
97
+ </div>
98
+ )}
99
+ {slots.Control && (
100
+ <div className="coop-field-control">
101
+ {slots.Control}
102
+ {isInlineControl && hasFieldMarkers && (
103
+ <div className="coop-field-markers">
104
+ {slots.FieldLabel}
105
+ {slots.FieldHint}
106
+ {slots.FieldError}
107
+ </div>
108
+ )}
109
+ </div>
110
+ )}
111
+ {slots.Children}
112
+ </div>
113
+ )
18
114
  }
19
115
 
116
+ interface ControlProps extends HTMLAttributes<HTMLDivElement> {
117
+ /** **(Optional)** Elements inside the form control. */
118
+ children?: string | React.ReactNode
119
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
120
+ className?: string
121
+ }
122
+
123
+ export const FieldControl = ({ children, className, ...props }: ControlProps): JSX.Element => {
124
+ return (
125
+ <div className={clsx("coop-field-control ", className)} {...props}>
126
+ {children}
127
+ </div>
128
+ )
129
+ }
130
+
131
+ FieldControl.displayName = "Field.Control"
132
+
133
+ const FieldLabel = (props: LabelProps) => <BaseLabel {...props} />
134
+
135
+ FieldLabel.displayName = "Field.Label"
136
+
137
+ const FieldHint = (props: HintProps) => <BaseHint {...props} />
138
+
139
+ FieldHint.displayName = "Field.Hint"
140
+
141
+ const FieldError = (props: ErrorProps) => <BaseError {...props} />
142
+
143
+ FieldError.displayName = "Field.Error"
144
+
145
+ export const Field = Object.assign(Root, {
146
+ Control: FieldControl,
147
+ Error: FieldError,
148
+ Hint: FieldHint,
149
+ Label: FieldLabel,
150
+ })
151
+
152
+ // Field.Label = FieldLabel
153
+ // Field.Hint = FieldHint
154
+ // Field.Error = FieldError
155
+
20
156
  export default Field
@@ -2,14 +2,14 @@ import type { HTMLAttributes, JSX, ReactNode } from "react"
2
2
 
3
3
  import clsx from "clsx"
4
4
 
5
- export interface FieldErrorProps extends HTMLAttributes<HTMLSpanElement> {
6
- /** **(Optional)** Main content inside the component. It can be any valid JSX or string. */
5
+ export interface ErrorProps extends HTMLAttributes<HTMLSpanElement> {
6
+ /** **(Optional)** Error text for the form element. Accepts any valid JSX or string. */
7
7
  children?: string | ReactNode
8
8
  /** **(Optional)** Specify additional CSS classes to be applied to the component. */
9
9
  className?: string
10
10
  }
11
11
 
12
- export const FieldError = ({ children, className, ...props }: FieldErrorProps): JSX.Element => {
12
+ export const Error = ({ children, className, ...props }: ErrorProps): JSX.Element => {
13
13
  const componentProps = {
14
14
  className: clsx("coop-field-error ", className),
15
15
  ...props,
@@ -17,4 +17,4 @@ export const FieldError = ({ children, className, ...props }: FieldErrorProps):
17
17
  return <span {...componentProps}>{children}</span>
18
18
  }
19
19
 
20
- export default FieldError
20
+ export default Error
@@ -2,18 +2,14 @@ import type { HTMLAttributes, JSX, ReactNode } from "react"
2
2
 
3
3
  import clsx from "clsx"
4
4
 
5
- export interface FieldHintProps extends HTMLAttributes<HTMLParagraphElement> {
6
- /** Main content inside the component. It can be any valid JSX or string. */
7
- children: string | ReactNode
5
+ export interface HintProps extends HTMLAttributes<HTMLParagraphElement> {
6
+ /** **(Optional)** Hint text for the form element. Accepts any valid JSX or string. */
7
+ children?: string | ReactNode
8
8
  /** **(Optional)** Specify additional CSS classes to be applied to the component. */
9
9
  className?: string
10
10
  }
11
11
 
12
- export const FieldHint = ({
13
- children,
14
- className,
15
- ...props
16
- }: FieldHintProps): JSX.Element | null => {
12
+ export const Hint = ({ children, className, ...props }: HintProps): JSX.Element | null => {
17
13
  const componentProps = {
18
14
  className: clsx("coop-field-hint ", className),
19
15
  ...props,
@@ -22,4 +18,4 @@ export const FieldHint = ({
22
18
  return children ? <div {...componentProps}>{children}</div> : null
23
19
  }
24
20
 
25
- export default FieldHint
21
+ export default Hint
@@ -0,0 +1,21 @@
1
+ import clsx from "clsx"
2
+ import { LabelHTMLAttributes } from "react"
3
+
4
+ export interface LabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
5
+ /** **(Optional)** Main label for the form element. Accepts any valid JSX or string. */
6
+ children?: string | React.ReactNode
7
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
8
+ className?: string
9
+ /** **(Optional)** Specify whether the label is visible for humans or only for screen readers. */
10
+ isVisible?: boolean
11
+ }
12
+
13
+ export const Label = ({ children, className, isVisible = true, ...props }: LabelProps) => {
14
+ return (
15
+ <label {...props} className={clsx("coop-field-label", !isVisible && "sr-only", className)}>
16
+ <span>{children}</span>
17
+ </label>
18
+ )
19
+ }
20
+
21
+ export default Label
@@ -0,0 +1,28 @@
1
+ import clsx from "clsx"
2
+ import { HTMLAttributes } from "react"
3
+
4
+ export interface LegendProps extends HTMLAttributes<HTMLLegendElement> {
5
+ /** **(Optional)** Main legend for the fieldset. Accepts any valid JSX or string. */
6
+ children?: string | React.ReactNode
7
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
8
+ className?: string
9
+ /** **(Optional)** Specify whether the legend should be visible to humans or screen readers. */
10
+ isVisible?: boolean
11
+ }
12
+
13
+ export const Legend = ({ children, className, isVisible = true, ...props }: LegendProps) => {
14
+ const componentProps = {
15
+ "aria-hidden": true,
16
+ className: clsx("coop-field-label", !isVisible && "sr-only", className),
17
+ ...props,
18
+ }
19
+
20
+ return (
21
+ <>
22
+ <legend className="sr-only">{children}</legend>
23
+ {isVisible && <div {...componentProps}>{children}</div>}
24
+ </>
25
+ )
26
+ }
27
+
28
+ export default Legend