@coopdigital/react 0.31.0 → 0.33.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 (37) hide show
  1. package/dist/components/Field/Field.d.ts +9 -0
  2. package/dist/components/Field/Field.js +12 -0
  3. package/dist/components/Field/index.d.ts +4 -0
  4. package/dist/components/FieldError/FieldError.d.ts +9 -0
  5. package/dist/components/FieldError/FieldError.js +12 -0
  6. package/dist/components/FieldError/index.d.ts +4 -0
  7. package/dist/components/FieldHint/FieldHint.d.ts +9 -0
  8. package/dist/components/FieldHint/FieldHint.js +12 -0
  9. package/dist/components/FieldHint/index.d.ts +4 -0
  10. package/dist/components/FieldLabel/FieldLabel.d.ts +13 -0
  11. package/dist/components/FieldLabel/FieldLabel.js +13 -0
  12. package/dist/components/FieldLabel/index.d.ts +4 -0
  13. package/dist/components/Input/Input.d.ts +31 -0
  14. package/dist/components/Input/Input.js +26 -0
  15. package/dist/components/Input/index.d.ts +4 -0
  16. package/dist/components/Input/index.js +5 -0
  17. package/dist/components/RootSVG/RootSVG.js +1 -1
  18. package/dist/components/SearchBox/SearchBox.d.ts +9 -9
  19. package/dist/components/SearchBox/SearchBox.js +9 -5
  20. package/dist/index.d.ts +5 -0
  21. package/dist/index.js +5 -0
  22. package/dist/types/index.d.ts +4 -0
  23. package/package.json +12 -12
  24. package/src/components/Field/Field.tsx +20 -0
  25. package/src/components/Field/index.ts +5 -0
  26. package/src/components/FieldError/FieldError.tsx +20 -0
  27. package/src/components/FieldError/index.ts +5 -0
  28. package/src/components/FieldHint/FieldHint.tsx +25 -0
  29. package/src/components/FieldHint/index.ts +5 -0
  30. package/src/components/FieldLabel/FieldLabel.tsx +31 -0
  31. package/src/components/FieldLabel/index.ts +5 -0
  32. package/src/components/Input/Input.tsx +87 -0
  33. package/src/components/Input/index.ts +5 -0
  34. package/src/components/RootSVG/RootSVG.tsx +1 -1
  35. package/src/components/SearchBox/SearchBox.tsx +25 -16
  36. package/src/index.ts +5 -0
  37. package/src/types/index.ts +2 -0
@@ -0,0 +1,9 @@
1
+ import type { HTMLAttributes, JSX, ReactNode } from "react";
2
+ export interface FieldProps extends HTMLAttributes<HTMLDivElement> {
3
+ /** **(Optional)** Main content inside the component. It can be any valid JSX or string. */
4
+ children?: string | ReactNode;
5
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
6
+ className?: string;
7
+ }
8
+ export declare const Field: ({ children, className, ...props }: FieldProps) => JSX.Element;
9
+ export default Field;
@@ -0,0 +1,12 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { clsx } from '../../node_modules/clsx/dist/clsx.js';
3
+
4
+ const Field = ({ children, className, ...props }) => {
5
+ const componentProps = {
6
+ className: clsx("coop-field ", className),
7
+ ...props,
8
+ };
9
+ return jsx("div", { ...componentProps, children: children });
10
+ };
11
+
12
+ export { Field, Field as default };
@@ -0,0 +1,4 @@
1
+ import Field from "./Field";
2
+ export default Field;
3
+ export { Field };
4
+ export * from "./Field";
@@ -0,0 +1,9 @@
1
+ import type { HTMLAttributes, JSX, ReactNode } from "react";
2
+ export interface FieldErrorProps extends HTMLAttributes<HTMLSpanElement> {
3
+ /** **(Optional)** Main content inside the component. It can be any valid JSX or string. */
4
+ children?: string | ReactNode;
5
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
6
+ className?: string;
7
+ }
8
+ export declare const FieldError: ({ children, className, ...props }: FieldErrorProps) => JSX.Element;
9
+ export default FieldError;
@@ -0,0 +1,12 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { clsx } from '../../node_modules/clsx/dist/clsx.js';
3
+
4
+ const FieldError = ({ children, className, ...props }) => {
5
+ const componentProps = {
6
+ className: clsx("coop-field-error ", className),
7
+ ...props,
8
+ };
9
+ return jsx("span", { ...componentProps, children: children });
10
+ };
11
+
12
+ export { FieldError, FieldError as default };
@@ -0,0 +1,4 @@
1
+ import FieldError from "./FieldError";
2
+ export default FieldError;
3
+ export { FieldError };
4
+ export * from "./FieldError";
@@ -0,0 +1,9 @@
1
+ import type { HTMLAttributes, JSX, ReactNode } from "react";
2
+ export interface FieldHintProps extends HTMLAttributes<HTMLParagraphElement> {
3
+ /** Main content inside the component. It can be any valid JSX or string. */
4
+ children: string | ReactNode;
5
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
6
+ className?: string;
7
+ }
8
+ export declare const FieldHint: ({ children, className, ...props }: FieldHintProps) => JSX.Element | null;
9
+ export default FieldHint;
@@ -0,0 +1,12 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { clsx } from '../../node_modules/clsx/dist/clsx.js';
3
+
4
+ const FieldHint = ({ children, className, ...props }) => {
5
+ const componentProps = {
6
+ className: clsx("coop-field-hint ", className),
7
+ ...props,
8
+ };
9
+ return children ? jsx("p", { ...componentProps, children: children }) : null;
10
+ };
11
+
12
+ export { FieldHint, FieldHint as default };
@@ -0,0 +1,4 @@
1
+ import FieldHint from "./FieldHint";
2
+ export default FieldHint;
3
+ export { FieldHint };
4
+ export * from "./FieldHint";
@@ -0,0 +1,13 @@
1
+ import type { JSX, LabelHTMLAttributes, ReactNode } from "react";
2
+ export interface FieldLabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
3
+ /** **(Optional)** Main content inside the component. It can be any valid JSX or string. */
4
+ children?: string | ReactNode;
5
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
6
+ className?: string;
7
+ /** Specify the field ID to connect FieldLabel to the field itself. */
8
+ htmlFor: string;
9
+ /** **(Optional)** Specify whether the FieldLabel is visible for humans or only for screen readers. */
10
+ isVisible?: boolean;
11
+ }
12
+ export declare const FieldLabel: ({ children, className, htmlFor, isVisible, ...props }: FieldLabelProps) => JSX.Element | null;
13
+ export default FieldLabel;
@@ -0,0 +1,13 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { clsx } from '../../node_modules/clsx/dist/clsx.js';
3
+
4
+ const FieldLabel = ({ children, className, htmlFor, isVisible = true, ...props }) => {
5
+ const componentProps = {
6
+ className: clsx("coop-field-label ", isVisible ? "" : "sr-only", className),
7
+ htmlFor,
8
+ ...props,
9
+ };
10
+ return children && htmlFor ? jsx("label", { ...componentProps, children: children }) : null;
11
+ };
12
+
13
+ export { FieldLabel, FieldLabel as default };
@@ -0,0 +1,4 @@
1
+ import FieldLabel from "./FieldLabel";
2
+ export default FieldLabel;
3
+ export { FieldLabel };
4
+ export * from "./FieldLabel";
@@ -0,0 +1,31 @@
1
+ import type { InputHTMLAttributes, JSX } from "react";
2
+ import { FormFieldError, StandardSizes } from "../../../src/types";
3
+ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "type"> {
4
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
5
+ className?: string;
6
+ /** **(Optional)** Specify the Input error. */
7
+ error?: FormFieldError;
8
+ /** **(Optional)** Specify the Input hint.
9
+ *
10
+ * This text is rendered under the label to provide further guidance for users.
11
+ */
12
+ hint?: string;
13
+ /** **(Optional)** Specify the Input id. Will be auto-generated if not set. */
14
+ id?: string;
15
+ /** **(Optional)** Specify the Input label.
16
+ *
17
+ * This property is optional in case you need to render your own label, but all form elements *must* provide a label. */
18
+ label?: string;
19
+ /** **(Optional)** Specify whether the label should be visible to humans or screen readers. */
20
+ labelVisible?: boolean;
21
+ /** Specify the Input name. */
22
+ name: string;
23
+ /** **(Optional)** Specify the Input placeholder text. Do not use in place of a form label. */
24
+ placeholder?: string;
25
+ /** **(Optional)** Specify the Input size. */
26
+ size?: StandardSizes;
27
+ /** **(Optional)** Specify the Input type. */
28
+ type?: "text" | "email" | "number" | "password" | "search" | "tel" | "url";
29
+ }
30
+ export declare const Input: ({ "aria-placeholder": ariaPlaceholder, className, error, hint, id, label, labelVisible, name, placeholder, size, type, ...props }: InputProps) => JSX.Element;
31
+ export default Input;
@@ -0,0 +1,26 @@
1
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
+ import { clsx } from '../../node_modules/clsx/dist/clsx.js';
3
+ import { useId } from 'react';
4
+ import { FieldError } from '../FieldError/FieldError.js';
5
+ import { FieldHint } from '../FieldHint/FieldHint.js';
6
+ import { FieldLabel } from '../FieldLabel/FieldLabel.js';
7
+
8
+ const Input = ({ "aria-placeholder": ariaPlaceholder, className, error = false, hint, id, label, labelVisible = true, name, placeholder, size = "md", type = "text", ...props }) => {
9
+ var _a;
10
+ const internalId = useId();
11
+ id = id !== null && id !== void 0 ? id : internalId;
12
+ const componentProps = {
13
+ "aria-placeholder": (_a = placeholder !== null && placeholder !== void 0 ? placeholder : ariaPlaceholder) !== null && _a !== void 0 ? _a : undefined,
14
+ className: clsx("coop-input", className),
15
+ "data-error": error ? "" : undefined,
16
+ "data-size": size.length && size !== "md" ? size : undefined,
17
+ id,
18
+ name,
19
+ placeholder,
20
+ type,
21
+ ...props,
22
+ };
23
+ return (jsxs(Fragment, { 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("div", { className: "coop-field-control", children: jsx("input", { ...componentProps }) })] }));
24
+ };
25
+
26
+ export { Input, Input as default };
@@ -0,0 +1,4 @@
1
+ import Input from "./Input";
2
+ export default Input;
3
+ export { Input };
4
+ export * from "./Input";
@@ -0,0 +1,5 @@
1
+ import { Input } from './Input.js';
2
+
3
+
4
+
5
+ export { Input, Input as default };
@@ -2,7 +2,7 @@ import { jsx } from 'react/jsx-runtime';
2
2
 
3
3
  const RootSVG = () => {
4
4
  return (jsx("svg", { height: "0", style: {
5
- clip: "rect(0,0,0,0)}",
5
+ clip: "rect(0,0,0,0)",
6
6
  clipPath: "inset(50%)",
7
7
  overflow: "hidden",
8
8
  position: "absolute",
@@ -1,32 +1,32 @@
1
- import React, { HTMLAttributes } from "react";
2
- import { type JSX } from "react";
3
- import { StandardSizes } from "src/types";
1
+ import type { InputHTMLAttributes, JSX } from "react";
2
+ import React from "react";
3
+ import { StandardSizes } from "../../../src/types";
4
4
  import { type ButtonProps } from "../Button";
5
- export interface SearchBoxProps extends HTMLAttributes<HTMLInputElement> {
5
+ export interface SearchBoxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "type"> {
6
6
  /** **(Optional)** Specify a server endpoint to submit the form. Will be ignored if onSubmit is also set. */
7
7
  action?: string;
8
- /** **(Optional)** Specify whether or not to enable autocomplete on the input field. Default: `off`. */
9
- autoComplete?: "off" | "on";
10
8
  /** **(Optional)** Specify props to forward to the Button element. Use `label` to set Button text. */
11
9
  button?: Pick<ButtonProps, "className" | "loadingText"> & {
12
10
  label?: React.ReactNode;
13
11
  };
14
12
  /** **(Optional)** Specify additional CSS classes to be applied to the component. */
15
13
  className?: string;
14
+ /** **(Optional)** Specify the Input id. Will be auto-generated if not set. */
15
+ id?: string;
16
16
  /** Specify the label displayed above the search field. Hidden by default, but visible to screen readers. */
17
17
  label: string;
18
18
  /** **(Optional)** Specify whether the label should be visible to humans or screenreaders. */
19
19
  labelVisible?: boolean;
20
- /** **(Optional)** Specify the name of the input element, also used as the URL search parameter. Defaults to `query`. */
20
+ /** **(Optional)** Specify the Input name, also used as the URL search parameter. Defaults to `query`. */
21
21
  name?: string;
22
22
  /** **(Optional)** Callback to run when the form is submitted. If this is an async function, it will be awaited and the SearchBox will be in a pending state until the promise is resolved. */
23
23
  onSubmit?: React.FormEventHandler<HTMLElement> | undefined;
24
- /** **(Optional)** Specify placeholder text for the search field. Do not in place of a form label. */
24
+ /** **(Optional)** Specify the Input placeholder text Do not use in place of a form label. */
25
25
  placeholder?: string;
26
26
  /** **(Optional)** Specify the SearchBox size. */
27
27
  size?: StandardSizes;
28
28
  /** **(Optional)** Specify the SearchBox variant. */
29
29
  variant?: "green" | "blue" | "white" | "grey" | "green-ghost" | "blue-ghost" | "white-ghost" | "grey-ghost";
30
30
  }
31
- export declare const SearchBox: ({ action, "aria-placeholder": ariaPlaceholder, autoCapitalize, autoComplete, button, className, label, labelVisible, name, onSubmit, placeholder, size, variant, ...props }: SearchBoxProps) => JSX.Element;
31
+ export declare const SearchBox: ({ action, "aria-placeholder": ariaPlaceholder, autoCapitalize, autoComplete, button, className, id, label, labelVisible, name, onSubmit, placeholder, size, variant, ...props }: SearchBoxProps) => JSX.Element;
32
32
  export default SearchBox;
@@ -2,17 +2,21 @@
2
2
  "use client";
3
3
  import { jsxs, jsx } from 'react/jsx-runtime';
4
4
  import { clsx } from '../../node_modules/clsx/dist/clsx.js';
5
- import React, { useState, useCallback, useId } from 'react';
5
+ import React, { useState, useId, useCallback } from 'react';
6
6
  import { Button } from '../Button/Button.js';
7
+ import { FieldLabel } from '../FieldLabel/FieldLabel.js';
7
8
  import { SearchIcon } from '../Icon/SearchIcon.js';
9
+ import { Input } from '../Input/Input.js';
8
10
 
9
11
  const defaultButtonProps = {
10
12
  label: React.createElement(SearchIcon, { alt: "Search", stroke: "currentColor", strokeWidth: 2 }),
11
13
  loadingText: "",
12
14
  };
13
- const SearchBox = ({ action, "aria-placeholder": ariaPlaceholder, autoCapitalize = "off", autoComplete = "off", button = defaultButtonProps, className, label, labelVisible = false, name = "query", onSubmit, placeholder, size = "md", variant = "green", ...props }) => {
15
+ const SearchBox = ({ action, "aria-placeholder": ariaPlaceholder, autoCapitalize = "off", autoComplete = "off", button = defaultButtonProps, className, id, label, labelVisible = false, name = "query", onSubmit, placeholder, size = "md", variant = "green", ...props }) => {
14
16
  var _a, _b;
15
17
  const [isPending, setIsPending] = useState(false);
18
+ const internalId = useId();
19
+ id = id !== null && id !== void 0 ? id : internalId;
16
20
  const handleSubmit = useCallback(async (event) => {
17
21
  event.preventDefault();
18
22
  if (isPending || !onSubmit)
@@ -25,11 +29,10 @@ const SearchBox = ({ action, "aria-placeholder": ariaPlaceholder, autoCapitalize
25
29
  setIsPending(false);
26
30
  }
27
31
  }, [onSubmit, isPending]);
28
- const id = useId();
29
32
  const formProps = {
30
33
  action: action !== null && action !== void 0 ? action : undefined,
31
34
  className: clsx("coop-search-box", className),
32
- "data-size": size.length && size !== "md" ? size : undefined,
35
+ //"data-size": size && size !== "md" ? size : undefined,
33
36
  "data-variant": variant.length && variant !== "green" ? variant : undefined,
34
37
  onSubmit: onSubmit ? handleSubmit : undefined,
35
38
  };
@@ -47,10 +50,11 @@ const SearchBox = ({ action, "aria-placeholder": ariaPlaceholder, autoCapitalize
47
50
  id,
48
51
  name,
49
52
  placeholder,
53
+ size: size,
50
54
  type: "search",
51
55
  ...props,
52
56
  };
53
- return (jsxs("form", { ...formProps, children: [jsx("label", { className: labelVisible ? "" : "sr-only", htmlFor: id, children: label }), jsxs("div", { className: "coop-search-box--inner", children: [jsx("input", { ...inputProps }), jsx(Button, { ...buttonProps, children: button.label })] })] }));
57
+ return (jsxs("form", { ...formProps, children: [label && (jsx(FieldLabel, { htmlFor: id, isVisible: labelVisible, children: label })), jsxs("div", { className: "coop-search-box--inner", children: [jsx(Input, { ...inputProps }), jsx(Button, { ...buttonProps, children: button.label })] })] }));
54
58
  };
55
59
 
56
60
  export { SearchBox, SearchBox as default };
package/dist/index.d.ts CHANGED
@@ -3,8 +3,13 @@ export * from "./components/Author";
3
3
  export * from "./components/Button";
4
4
  export * from "./components/Card";
5
5
  export * from "./components/Expandable";
6
+ export * from "./components/Field";
7
+ export * from "./components/FieldError";
8
+ export * from "./components/FieldHint";
9
+ export * from "./components/FieldLabel";
6
10
  export * from "./components/Flourish";
7
11
  export * from "./components/Image";
12
+ export * from "./components/Input";
8
13
  export * from "./components/Pill";
9
14
  export * from "./components/RootSVG";
10
15
  export * from "./components/SearchBox";
package/dist/index.js CHANGED
@@ -3,8 +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 { Expandable } from './components/Expandable/Expandable.js';
6
+ export { Field } from './components/Field/Field.js';
7
+ export { FieldError } from './components/FieldError/FieldError.js';
8
+ export { FieldHint } from './components/FieldHint/FieldHint.js';
9
+ export { FieldLabel } from './components/FieldLabel/FieldLabel.js';
6
10
  export { Flourish } from './components/Flourish/Flourish.js';
7
11
  export { Image } from './components/Image/Image.js';
12
+ export { Input } from './components/Input/Input.js';
8
13
  export { Pill } from './components/Pill/Pill.js';
9
14
  export { RootSVG } from './components/RootSVG/RootSVG.js';
10
15
  export { SearchBox } from './components/SearchBox/SearchBox.js';
@@ -1 +1,5 @@
1
1
  export type StandardSizes = "sm" | "md" | "lg";
2
+ export type FormFieldError = {
3
+ message: string;
4
+ type?: string;
5
+ } | boolean | null | undefined;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coopdigital/react",
3
3
  "type": "module",
4
- "version": "0.31.0",
4
+ "version": "0.33.0",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -56,20 +56,20 @@
56
56
  "description": "",
57
57
  "devDependencies": {
58
58
  "@axe-core/playwright": "^4.10.2",
59
- "@playwright/test": "^1.54.2",
60
- "@storybook/addon-a11y": "^9.1.2",
61
- "@storybook/addon-docs": "^9.1.2",
62
- "@storybook/addon-onboarding": "^9.1.2",
63
- "@storybook/react-vite": "^9.1.2",
64
- "@testing-library/jest-dom": "^6.7.0",
59
+ "@playwright/test": "^1.55.0",
60
+ "@storybook/addon-a11y": "^9.1.3",
61
+ "@storybook/addon-docs": "^9.1.3",
62
+ "@storybook/addon-onboarding": "^9.1.3",
63
+ "@storybook/react-vite": "^9.1.3",
64
+ "@testing-library/jest-dom": "^6.8.0",
65
65
  "@testing-library/react": "^16.3.0",
66
- "@types/react": "^19.1.10",
67
- "@types/react-dom": "^19.1.7",
66
+ "@types/react": "^19.1.12",
67
+ "@types/react-dom": "^19.1.9",
68
68
  "clsx": "^2.1.1",
69
69
  "react": "^19.1.1",
70
70
  "react-dom": "^19.1.1",
71
71
  "serve": "^14.2.4",
72
- "storybook": "^9.1.2"
72
+ "storybook": "^9.1.3"
73
73
  },
74
74
  "peerDependencies": {
75
75
  "clsx": "^2.1.1",
@@ -80,7 +80,7 @@
80
80
  "storybook": "$storybook"
81
81
  },
82
82
  "dependencies": {
83
- "@coopdigital/styles": "^0.27.0"
83
+ "@coopdigital/styles": "^0.29.0"
84
84
  },
85
- "gitHead": "fce3f35446116384dabdd8553404d9e933261264"
85
+ "gitHead": "b7f3df204e1f4e38b3c978af0506243b30365b4a"
86
86
  }
@@ -0,0 +1,20 @@
1
+ import type { HTMLAttributes, JSX, ReactNode } from "react"
2
+
3
+ import clsx from "clsx"
4
+
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
8
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
9
+ className?: string
10
+ }
11
+
12
+ export const Field = ({ children, className, ...props }: FieldProps): JSX.Element => {
13
+ const componentProps = {
14
+ className: clsx("coop-field ", className),
15
+ ...props,
16
+ }
17
+ return <div {...componentProps}>{children}</div>
18
+ }
19
+
20
+ export default Field
@@ -0,0 +1,5 @@
1
+ import Field from "./Field"
2
+
3
+ export default Field
4
+ export { Field }
5
+ export * from "./Field"
@@ -0,0 +1,20 @@
1
+ import type { HTMLAttributes, JSX, ReactNode } from "react"
2
+
3
+ import clsx from "clsx"
4
+
5
+ export interface FieldErrorProps extends HTMLAttributes<HTMLSpanElement> {
6
+ /** **(Optional)** Main content inside the component. It can be any valid JSX or string. */
7
+ children?: string | ReactNode
8
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
9
+ className?: string
10
+ }
11
+
12
+ export const FieldError = ({ children, className, ...props }: FieldErrorProps): JSX.Element => {
13
+ const componentProps = {
14
+ className: clsx("coop-field-error ", className),
15
+ ...props,
16
+ }
17
+ return <span {...componentProps}>{children}</span>
18
+ }
19
+
20
+ export default FieldError
@@ -0,0 +1,5 @@
1
+ import FieldError from "./FieldError"
2
+
3
+ export default FieldError
4
+ export { FieldError }
5
+ export * from "./FieldError"
@@ -0,0 +1,25 @@
1
+ import type { HTMLAttributes, JSX, ReactNode } from "react"
2
+
3
+ import clsx from "clsx"
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
8
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
9
+ className?: string
10
+ }
11
+
12
+ export const FieldHint = ({
13
+ children,
14
+ className,
15
+ ...props
16
+ }: FieldHintProps): JSX.Element | null => {
17
+ const componentProps = {
18
+ className: clsx("coop-field-hint ", className),
19
+ ...props,
20
+ }
21
+
22
+ return children ? <p {...componentProps}>{children}</p> : null
23
+ }
24
+
25
+ export default FieldHint
@@ -0,0 +1,5 @@
1
+ import FieldHint from "./FieldHint"
2
+
3
+ export default FieldHint
4
+ export { FieldHint }
5
+ export * from "./FieldHint"
@@ -0,0 +1,31 @@
1
+ import type { JSX, LabelHTMLAttributes, ReactNode } from "react"
2
+
3
+ import clsx from "clsx"
4
+
5
+ export interface FieldLabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
6
+ /** **(Optional)** Main content inside the component. It can be any valid JSX or string. */
7
+ children?: string | ReactNode
8
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
9
+ className?: string
10
+ /** Specify the field ID to connect FieldLabel to the field itself. */
11
+ htmlFor: string
12
+ /** **(Optional)** Specify whether the FieldLabel is visible for humans or only for screen readers. */
13
+ isVisible?: boolean
14
+ }
15
+
16
+ export const FieldLabel = ({
17
+ children,
18
+ className,
19
+ htmlFor,
20
+ isVisible = true,
21
+ ...props
22
+ }: FieldLabelProps): JSX.Element | null => {
23
+ const componentProps = {
24
+ className: clsx("coop-field-label ", isVisible ? "" : "sr-only", className),
25
+ htmlFor,
26
+ ...props,
27
+ }
28
+ return children && htmlFor ? <label {...componentProps}>{children}</label> : null
29
+ }
30
+
31
+ export default FieldLabel
@@ -0,0 +1,5 @@
1
+ import FieldLabel from "./FieldLabel"
2
+
3
+ export default FieldLabel
4
+ export { FieldLabel }
5
+ export * from "./FieldLabel"
@@ -0,0 +1,87 @@
1
+ import type { InputHTMLAttributes, JSX } from "react"
2
+
3
+ import clsx from "clsx"
4
+ import { useId } from "react"
5
+
6
+ import { FormFieldError, StandardSizes } from "../../../src/types"
7
+ import { FieldError } from "../FieldError"
8
+ import { FieldHint } from "../FieldHint"
9
+ import { FieldLabel } from "../FieldLabel"
10
+
11
+ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "type"> {
12
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
13
+ className?: string
14
+ /** **(Optional)** Specify the Input error. */
15
+ error?: FormFieldError
16
+ /** **(Optional)** Specify the Input hint.
17
+ *
18
+ * This text is rendered under the label to provide further guidance for users.
19
+ */
20
+ hint?: string
21
+ /** **(Optional)** Specify the Input id. Will be auto-generated if not set. */
22
+ id?: string
23
+ /** **(Optional)** Specify the Input label.
24
+ *
25
+ * This property is optional in case you need to render your own label, but all form elements *must* provide a label. */
26
+ label?: string
27
+ /** **(Optional)** Specify whether the label should be visible to humans or screen readers. */
28
+ labelVisible?: boolean
29
+ /** Specify the Input name. */
30
+ name: string
31
+ /** **(Optional)** Specify the Input placeholder text. Do not use in place of a form label. */
32
+ placeholder?: string
33
+ /** **(Optional)** Specify the Input size. */
34
+ size?: StandardSizes
35
+ /** **(Optional)** Specify the Input type. */
36
+ type?: "text" | "email" | "number" | "password" | "search" | "tel" | "url"
37
+ }
38
+
39
+ export const Input = ({
40
+ "aria-placeholder": ariaPlaceholder,
41
+ className,
42
+ error = false,
43
+ hint,
44
+ id,
45
+ label,
46
+ labelVisible = true,
47
+ name,
48
+ placeholder,
49
+ size = "md",
50
+ type = "text",
51
+ ...props
52
+ }: InputProps): JSX.Element => {
53
+ const internalId = useId()
54
+
55
+ id = id ?? internalId
56
+
57
+ const componentProps = {
58
+ "aria-placeholder": placeholder ?? ariaPlaceholder ?? undefined,
59
+ className: clsx("coop-input", className),
60
+ "data-error": error ? "" : undefined,
61
+ "data-size": size.length && size !== "md" ? size : undefined,
62
+ id,
63
+ name,
64
+ placeholder,
65
+ type,
66
+ ...props,
67
+ }
68
+
69
+ return (
70
+ <>
71
+ {label && (
72
+ <FieldLabel htmlFor={id} isVisible={labelVisible}>
73
+ {label}
74
+ </FieldLabel>
75
+ )}
76
+
77
+ {hint && <FieldHint>{hint}</FieldHint>}
78
+
79
+ {typeof error === "object" && error?.message && <FieldError>{error.message}</FieldError>}
80
+ <div className="coop-field-control">
81
+ <input {...componentProps} />
82
+ </div>
83
+ </>
84
+ )
85
+ }
86
+
87
+ export default Input
@@ -0,0 +1,5 @@
1
+ import Input from "./Input"
2
+
3
+ export default Input
4
+ export { Input }
5
+ export * from "./Input"
@@ -3,7 +3,7 @@ export const RootSVG = () => {
3
3
  <svg
4
4
  height="0"
5
5
  style={{
6
- clip: "rect(0,0,0,0)}",
6
+ clip: "rect(0,0,0,0)",
7
7
  clipPath: "inset(50%)",
8
8
  overflow: "hidden",
9
9
  position: "absolute",
@@ -1,31 +1,35 @@
1
1
  "use client"
2
2
 
3
+ import type { InputHTMLAttributes, JSX } from "react"
4
+
3
5
  import clsx from "clsx"
4
- import React, { HTMLAttributes, useCallback, useId, useState } from "react"
5
- import { type JSX } from "react"
6
- import { StandardSizes } from "src/types"
6
+ import React, { useCallback, useId, useState } from "react"
7
7
 
8
+ import { StandardSizes } from "../../../src/types"
8
9
  import { Button, type ButtonProps } from "../Button"
10
+ import { FieldLabel } from "../FieldLabel"
9
11
  import { SearchIcon } from "../Icon"
12
+ import Input, { InputProps } from "../Input"
10
13
 
11
- export interface SearchBoxProps extends HTMLAttributes<HTMLInputElement> {
14
+ export interface SearchBoxProps
15
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "type"> {
12
16
  /** **(Optional)** Specify a server endpoint to submit the form. Will be ignored if onSubmit is also set. */
13
17
  action?: string
14
- /** **(Optional)** Specify whether or not to enable autocomplete on the input field. Default: `off`. */
15
- autoComplete?: "off" | "on"
16
18
  /** **(Optional)** Specify props to forward to the Button element. Use `label` to set Button text. */
17
19
  button?: Pick<ButtonProps, "className" | "loadingText"> & { label?: React.ReactNode }
18
20
  /** **(Optional)** Specify additional CSS classes to be applied to the component. */
19
21
  className?: string
22
+ /** **(Optional)** Specify the Input id. Will be auto-generated if not set. */
23
+ id?: string
20
24
  /** Specify the label displayed above the search field. Hidden by default, but visible to screen readers. */
21
25
  label: string
22
26
  /** **(Optional)** Specify whether the label should be visible to humans or screenreaders. */
23
27
  labelVisible?: boolean
24
- /** **(Optional)** Specify the name of the input element, also used as the URL search parameter. Defaults to `query`. */
28
+ /** **(Optional)** Specify the Input name, also used as the URL search parameter. Defaults to `query`. */
25
29
  name?: string
26
30
  /** **(Optional)** Callback to run when the form is submitted. If this is an async function, it will be awaited and the SearchBox will be in a pending state until the promise is resolved. */
27
31
  onSubmit?: React.FormEventHandler<HTMLElement> | undefined
28
- /** **(Optional)** Specify placeholder text for the search field. Do not in place of a form label. */
32
+ /** **(Optional)** Specify the Input placeholder text Do not use in place of a form label. */
29
33
  placeholder?: string
30
34
  /** **(Optional)** Specify the SearchBox size. */
31
35
  size?: StandardSizes
@@ -53,6 +57,7 @@ export const SearchBox = ({
53
57
  autoComplete = "off",
54
58
  button = defaultButtonProps,
55
59
  className,
60
+ id,
56
61
  label,
57
62
  labelVisible = false,
58
63
  name = "query",
@@ -63,6 +68,9 @@ export const SearchBox = ({
63
68
  ...props
64
69
  }: SearchBoxProps): JSX.Element => {
65
70
  const [isPending, setIsPending] = useState(false)
71
+ const internalId = useId()
72
+
73
+ id = id ?? internalId
66
74
 
67
75
  const handleSubmit = useCallback(
68
76
  async (event: React.FormEvent<HTMLFormElement>) => {
@@ -81,12 +89,10 @@ export const SearchBox = ({
81
89
  [onSubmit, isPending]
82
90
  )
83
91
 
84
- const id = useId()
85
-
86
92
  const formProps = {
87
93
  action: action ?? undefined,
88
94
  className: clsx("coop-search-box", className),
89
- "data-size": size.length && size !== "md" ? size : undefined,
95
+ //"data-size": size && size !== "md" ? size : undefined,
90
96
  "data-variant": variant.length && variant !== "green" ? variant : undefined,
91
97
  onSubmit: onSubmit ? handleSubmit : undefined,
92
98
  }
@@ -106,17 +112,20 @@ export const SearchBox = ({
106
112
  id,
107
113
  name,
108
114
  placeholder,
109
- type: "search",
115
+ size: size as keyof InputProps["size"],
116
+ type: "search" as keyof InputProps["type"],
110
117
  ...props,
111
118
  }
112
119
 
113
120
  return (
114
121
  <form {...formProps}>
115
- <label className={labelVisible ? "" : "sr-only"} htmlFor={id}>
116
- {label}
117
- </label>
122
+ {label && (
123
+ <FieldLabel htmlFor={id} isVisible={labelVisible}>
124
+ {label}
125
+ </FieldLabel>
126
+ )}
118
127
  <div className="coop-search-box--inner">
119
- <input {...inputProps}></input>
128
+ <Input {...inputProps} />
120
129
  <Button {...buttonProps}>{button.label}</Button>
121
130
  </div>
122
131
  </form>
package/src/index.ts CHANGED
@@ -3,8 +3,13 @@ export * from "./components/Author"
3
3
  export * from "./components/Button"
4
4
  export * from "./components/Card"
5
5
  export * from "./components/Expandable"
6
+ export * from "./components/Field"
7
+ export * from "./components/FieldError"
8
+ export * from "./components/FieldHint"
9
+ export * from "./components/FieldLabel"
6
10
  export * from "./components/Flourish"
7
11
  export * from "./components/Image"
12
+ export * from "./components/Input"
8
13
  export * from "./components/Pill"
9
14
  export * from "./components/RootSVG"
10
15
  export * from "./components/SearchBox"
@@ -1 +1,3 @@
1
1
  export type StandardSizes = "sm" | "md" | "lg"
2
+
3
+ export type FormFieldError = { message: string; type?: string } | boolean | null | undefined