@coopdigital/react 0.33.1 → 0.35.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 (39) hide show
  1. package/dist/components/Checkbox/Checkbox.d.ts +32 -0
  2. package/dist/components/{Input/Input.js → Checkbox/Checkbox.js} +6 -6
  3. package/dist/components/Checkbox/CheckboxGroup.d.ts +26 -0
  4. package/dist/components/Checkbox/CheckboxGroup.js +19 -0
  5. package/dist/components/Checkbox/index.d.ts +4 -0
  6. package/dist/components/Field/Field.js +1 -1
  7. package/dist/components/FieldHint/FieldHint.js +1 -1
  8. package/dist/components/SearchBox/SearchBox.d.ts +3 -3
  9. package/dist/components/SearchBox/SearchBox.js +2 -2
  10. package/dist/components/TextInput/TextInput.d.ts +38 -0
  11. package/dist/components/TextInput/TextInput.js +26 -0
  12. package/dist/components/TextInput/index.d.ts +4 -0
  13. package/dist/components/TextInput/index.js +5 -0
  14. package/dist/components/Textarea/Textarea.d.ts +47 -0
  15. package/dist/components/Textarea/Textarea.js +41 -0
  16. package/dist/components/Textarea/index.d.ts +4 -0
  17. package/dist/hooks/useDebounce.d.ts +1 -0
  18. package/dist/hooks/useDebounce.js +16 -0
  19. package/dist/index.d.ts +4 -1
  20. package/dist/index.js +4 -1
  21. package/dist/types/index.d.ts +1 -1
  22. package/package.json +10 -10
  23. package/src/components/Checkbox/Checkbox.tsx +87 -0
  24. package/src/components/Checkbox/CheckboxGroup.tsx +63 -0
  25. package/src/components/Checkbox/index.ts +5 -0
  26. package/src/components/Field/Field.tsx +1 -1
  27. package/src/components/FieldHint/FieldHint.tsx +1 -1
  28. package/src/components/SearchBox/SearchBox.tsx +7 -7
  29. package/src/components/{Input/Input.tsx → TextInput/TextInput.tsx} +28 -16
  30. package/src/components/TextInput/index.ts +5 -0
  31. package/src/components/Textarea/Textarea.tsx +144 -0
  32. package/src/components/Textarea/index.ts +5 -0
  33. package/src/hooks/useDebounce.ts +16 -0
  34. package/src/index.ts +4 -1
  35. package/src/types/index.ts +1 -1
  36. package/dist/components/Input/Input.d.ts +0 -31
  37. package/dist/components/Input/index.d.ts +0 -4
  38. package/dist/components/Input/index.js +0 -5
  39. package/src/components/Input/index.ts +0 -5
@@ -0,0 +1,32 @@
1
+ import type { InputHTMLAttributes, JSX } from "react";
2
+ import { FormFieldError, StandardSizes } from "../../../src/types";
3
+ export interface CheckboxProps 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 Checkbox error state.
7
+ *
8
+ * 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.
9
+ */
10
+ error?: FormFieldError;
11
+ /** **(Optional)** Specify the Checkbox hint.
12
+ *
13
+ * This text is rendered under the label to provide further guidance for users.
14
+ */
15
+ hint?: string;
16
+ /** **(Optional)** Specify the Checkbox id. Will be auto-generated if not set. */
17
+ id?: string;
18
+ /** **(Optional)** Specify the Checkbox label.
19
+ *
20
+ * This property is optional in case you need to render your own label, but all form elements *must* provide a label. */
21
+ label: string;
22
+ /** **(Optional)** Specify whether the label should be visible to humans or screen readers. */
23
+ labelVisible?: boolean;
24
+ /** Specify the Checkbox name. */
25
+ name: string;
26
+ /** **(Optional)** Specify the Checkbox placeholder text. Do not use in place of a form label. */
27
+ placeholder?: string;
28
+ /** **(Optional)** Specify the Checkbox size. */
29
+ size?: StandardSizes;
30
+ }
31
+ export declare const Checkbox: ({ "aria-placeholder": ariaPlaceholder, className, error, hint, id, label, labelVisible, name, placeholder, size, ...props }: CheckboxProps) => JSX.Element;
32
+ export default Checkbox;
@@ -1,26 +1,26 @@
1
- import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { clsx } from '../../node_modules/clsx/dist/clsx.js';
3
3
  import { useId } from 'react';
4
4
  import { FieldError } from '../FieldError/FieldError.js';
5
5
  import { FieldHint } from '../FieldHint/FieldHint.js';
6
6
  import { FieldLabel } from '../FieldLabel/FieldLabel.js';
7
7
 
8
- const Input = ({ "aria-placeholder": ariaPlaceholder, className, error = false, hint, id, label, labelVisible = true, name, placeholder, size = "md", type = "text", ...props }) => {
8
+ const Checkbox = ({ "aria-placeholder": ariaPlaceholder, className, error = false, hint, id, label, labelVisible = true, name, placeholder, size = "md", ...props }) => {
9
9
  var _a;
10
10
  const internalId = useId();
11
11
  id = id !== null && id !== void 0 ? id : internalId;
12
12
  const componentProps = {
13
13
  "aria-placeholder": (_a = placeholder !== null && placeholder !== void 0 ? placeholder : ariaPlaceholder) !== null && _a !== void 0 ? _a : undefined,
14
- className: clsx("coop-input", className),
14
+ className: clsx("coop-checkbox", className),
15
15
  "data-error": error ? "" : undefined,
16
16
  "data-size": size.length && size !== "md" ? size : undefined,
17
17
  id,
18
18
  name,
19
19
  placeholder,
20
- type,
20
+ type: "checkbox",
21
21
  ...props,
22
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 }) })] }));
23
+ return (jsxs("div", { className: "coop-form-item", children: [jsxs("div", { className: "coop-checkbox-wrapper", children: [jsx("input", { ...componentProps }), 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 })] }));
24
24
  };
25
25
 
26
- export { Input, Input as default };
26
+ export { Checkbox, Checkbox as default };
@@ -0,0 +1,26 @@
1
+ import type { FieldsetHTMLAttributes, JSX } from "react";
2
+ import { FormFieldError, StandardSizes } from "../../../src/types";
3
+ export interface CheckboxGroupProps extends FieldsetHTMLAttributes<HTMLFieldSetElement> {
4
+ /** **(Optional)** Main content inside the component. It can be any valid JSX or string. */
5
+ children?: React.ReactNode;
6
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
7
+ className?: string;
8
+ /** **(Optional)** Specify the CheckboxGroup error state.
9
+ *
10
+ * 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.
11
+ */
12
+ error?: FormFieldError;
13
+ /** **(Optional)** Specify the CheckboxGroup hint.
14
+ *
15
+ * This text is rendered under the label to provide further guidance for users.
16
+ */
17
+ hint?: string;
18
+ /** **(Optional)** Specify the label for the CheckboxGroup. This will be rendered as a fieldset legend. */
19
+ label?: string;
20
+ /** **(Optional)** Specify whether the label should be visible to humans or screen readers. */
21
+ labelVisible?: boolean;
22
+ /** **(Optional)** Specify the CheckboxGroup size. */
23
+ size?: StandardSizes;
24
+ }
25
+ export declare const CheckboxGroup: ({ children, className, error, hint, label, labelVisible, size, ...props }: CheckboxGroupProps) => JSX.Element;
26
+ export default CheckboxGroup;
@@ -0,0 +1,19 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { clsx } from '../../node_modules/clsx/dist/clsx.js';
3
+ import { FieldError } from '../FieldError/FieldError.js';
4
+ import { FieldHint } from '../FieldHint/FieldHint.js';
5
+
6
+ const CheckboxGroup = ({ children, className, error = false, hint, label, labelVisible = true, size = "md", ...props }) => {
7
+ const componentProps = {
8
+ className: clsx("coop-fieldset", "coop-checkbox-group", className),
9
+ "data-error": error ? "" : undefined,
10
+ "data-size": size.length && size !== "md" ? size : undefined,
11
+ ...props,
12
+ };
13
+ const legendProps = {
14
+ className: clsx("coop-field-label", !labelVisible && "sr-only"),
15
+ };
16
+ return (jsxs("fieldset", { ...componentProps, children: [label && jsx("legend", { ...legendProps, children: label }), hint && jsx(FieldHint, { children: hint }), typeof error === "object" && (error === null || error === void 0 ? void 0 : error.message) && jsx(FieldError, { children: error.message }), children] }));
17
+ };
18
+
19
+ export { CheckboxGroup, CheckboxGroup as default };
@@ -0,0 +1,4 @@
1
+ import Checkbox from "./Checkbox";
2
+ export default Checkbox;
3
+ export { Checkbox };
4
+ export * from "./Checkbox";
@@ -3,7 +3,7 @@ import { clsx } from '../../node_modules/clsx/dist/clsx.js';
3
3
 
4
4
  const Field = ({ children, className, ...props }) => {
5
5
  const componentProps = {
6
- className: clsx("coop-field ", className),
6
+ className: clsx("coop-form-item ", className),
7
7
  ...props,
8
8
  };
9
9
  return jsx("div", { ...componentProps, children: children });
@@ -6,7 +6,7 @@ const FieldHint = ({ children, className, ...props }) => {
6
6
  className: clsx("coop-field-hint ", className),
7
7
  ...props,
8
8
  };
9
- return children ? jsx("p", { ...componentProps, children: children }) : null;
9
+ return children ? jsx("div", { ...componentProps, children: children }) : null;
10
10
  };
11
11
 
12
12
  export { FieldHint, FieldHint as default };
@@ -11,17 +11,17 @@ export interface SearchBoxProps extends Omit<InputHTMLAttributes<HTMLInputElemen
11
11
  };
12
12
  /** **(Optional)** Specify additional CSS classes to be applied to the component. */
13
13
  className?: string;
14
- /** **(Optional)** Specify the Input id. Will be auto-generated if not set. */
14
+ /** **(Optional)** Specify the TextInput id. Will be auto-generated if not set. */
15
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 Input name, also used as the URL search parameter. Defaults to `query`. */
20
+ /** **(Optional)** Specify the TextInput 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 the Input placeholder text Do not use in place of a form label. */
24
+ /** **(Optional)** Specify the TextInput 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;
@@ -6,7 +6,7 @@ import React, { useState, useId, useCallback } from 'react';
6
6
  import { Button } from '../Button/Button.js';
7
7
  import { FieldLabel } from '../FieldLabel/FieldLabel.js';
8
8
  import { SearchIcon } from '../Icon/SearchIcon.js';
9
- import { Input } from '../Input/Input.js';
9
+ import { TextInput } from '../TextInput/TextInput.js';
10
10
 
11
11
  const defaultButtonProps = {
12
12
  label: React.createElement(SearchIcon, { alt: "Search", stroke: "currentColor", strokeWidth: 2 }),
@@ -54,7 +54,7 @@ const SearchBox = ({ action, "aria-placeholder": ariaPlaceholder, autoCapitalize
54
54
  type: "search",
55
55
  ...props,
56
56
  };
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 })] })] }));
57
+ return (jsxs("form", { ...formProps, children: [label && (jsx(FieldLabel, { htmlFor: id, isVisible: labelVisible, children: label })), jsxs("div", { className: "coop-search-box--inner", children: [jsx(TextInput, { ...inputProps }), jsx(Button, { ...buttonProps, children: button.label })] })] }));
58
58
  };
59
59
 
60
60
  export { SearchBox, SearchBox as default };
@@ -0,0 +1,38 @@
1
+ import type { InputHTMLAttributes, JSX } from "react";
2
+ import { FormFieldError, StandardSizes } from "../../../src/types";
3
+ export interface TextInputProps 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 TextInput error state.
7
+ *
8
+ * 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.
9
+ */
10
+ error?: FormFieldError;
11
+ /** **(Optional)** Specify the TextInput hint.
12
+ *
13
+ * This text is rendered under the label to provide further guidance for users.
14
+ */
15
+ hint?: string;
16
+ /** **(Optional)** Specify the TextInput id. Will be auto-generated if not set. */
17
+ id?: string;
18
+ /** **(Optional)** Specify the TextInput label.
19
+ *
20
+ * This property is optional in case you need to render your own label, but all form elements *must* provide a label. */
21
+ label?: string;
22
+ /** **(Optional)** Specify whether the label should be visible to humans or screen readers. */
23
+ labelVisible?: boolean;
24
+ /** Specify the TextInput name. */
25
+ name: string;
26
+ /** **(Optional)** Specify the TextInput placeholder text. Do not use in place of a form label. */
27
+ placeholder?: string;
28
+ /** **(Optional)** Specify the prefix. */
29
+ prefix?: string;
30
+ /** **(Optional)** Specify the TextInput size. */
31
+ size?: StandardSizes;
32
+ /** **(Optional)** Specify the suffix. */
33
+ suffix?: string;
34
+ /** **(Optional)** Specify the TextInput type. */
35
+ type?: "text" | "email" | "number" | "password" | "search" | "tel" | "url";
36
+ }
37
+ export declare const TextInput: ({ "aria-placeholder": ariaPlaceholder, className, error, hint, id, label, labelVisible, name, placeholder, prefix, size, suffix, type, ...props }: TextInputProps) => JSX.Element;
38
+ export default TextInput;
@@ -0,0 +1,26 @@
1
+ import { jsxs, 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 TextInput = ({ "aria-placeholder": ariaPlaceholder, className, error = false, hint, id, label, labelVisible = true, name, placeholder, prefix, size = "md", suffix, 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-text-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("div", { className: "coop-form-item", 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 }), jsxs("div", { className: "coop-text-input-wrapper", children: [prefix && jsx("span", { className: "coop-text-input--prefix", children: prefix }), jsx("input", { ...componentProps }), suffix && jsx("span", { className: "coop-text-input--suffix", children: suffix })] })] }));
24
+ };
25
+
26
+ export { TextInput, TextInput as default };
@@ -0,0 +1,4 @@
1
+ import TextInput from "./TextInput";
2
+ export default TextInput;
3
+ export { TextInput };
4
+ export * from "./TextInput";
@@ -0,0 +1,5 @@
1
+ import { TextInput } from './TextInput.js';
2
+
3
+
4
+
5
+ export { TextInput, TextInput as default };
@@ -0,0 +1,47 @@
1
+ import type { JSX, TextareaHTMLAttributes } from "react";
2
+ import { FormFieldError, StandardSizes } from "../../types";
3
+ export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
4
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
5
+ className?: string;
6
+ /** Specify the number of columns (characters per row) in the Textarea. Defaults to `30`. */
7
+ cols?: number;
8
+ /** **(Optional)** Specify whether to show the character counter underneath the Textarea. */
9
+ counter?: boolean;
10
+ /** **(Optional)** Specify whether the Textarea should allow more characters than its `maxLength` value.
11
+ *
12
+ * Defaults to `false`, meaning users can enter more characters than the maximum but will be warned if they go over the limit. When set to `true`, users will be blocked from typing once they hit the character limit. This can be an accessiblity anti-pattern, so only use this option when absolutely necessary.
13
+ *
14
+ * Remember it is still your responsibility to handle validation on submission, this is simply a hint for the user.
15
+ */
16
+ cutoff?: boolean;
17
+ /** **(Optional)** Specify the Textarea error state.
18
+ *
19
+ * 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.
20
+ */
21
+ error?: FormFieldError;
22
+ /** **(Optional)** Specify the Textarea hint.
23
+ *
24
+ * This text is rendered under the label to provide further guidance for users.
25
+ */
26
+ hint?: string;
27
+ /** **(Optional)** Specify the Textarea id. Will be auto-generated if not set. */
28
+ id?: string;
29
+ /** **(Optional)** Specify the Textarea label.
30
+ *
31
+ * This property is optional in case you need to render your own label, but all form elements *must* provide a label. */
32
+ label?: string;
33
+ /** **(Optional)** Specify whether the label should be visible to humans or screen readers. */
34
+ labelVisible?: boolean;
35
+ /** **(Optional)** Specify the Textarea maxLength. This is the maximum number of characters users can enter. */
36
+ maxLength?: number;
37
+ /** Specify the Textarea name. */
38
+ name: string;
39
+ /** **(Optional)** Specify the Textarea placeholder text. Do not use in place of a form label. */
40
+ placeholder?: string;
41
+ /** Specify the number of rows (lines of text) in the Textarea. Defaults to `4`. */
42
+ rows?: number;
43
+ /** **(Optional)** Specify the Textarea size. */
44
+ size?: StandardSizes;
45
+ }
46
+ export declare const Textarea: ({ "aria-placeholder": ariaPlaceholder, className, cols, counter, cutoff, error, hint, id, label, labelVisible, maxLength, name, onChange: userOnChange, placeholder, rows, size, ...props }: TextareaProps) => JSX.Element;
47
+ export default Textarea;
@@ -0,0 +1,41 @@
1
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
+ import { clsx } from '../../node_modules/clsx/dist/clsx.js';
3
+ import { useId, useState } from 'react';
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';
8
+
9
+ const DEBOUNCE_DELAY = 750;
10
+ const charCountMessage = (remaining) => {
11
+ return `You have ${Math.abs(remaining).toLocaleString()} ${Math.abs(remaining) === 1 ? "character" : "characters"} ${remaining < 0 ? "too many" : "remaining"}`;
12
+ };
13
+ const Textarea = ({ "aria-placeholder": ariaPlaceholder, className, cols = 30, counter = false, cutoff = false, error = false, hint, id, label, labelVisible = true, maxLength, name, onChange: userOnChange = undefined, placeholder, rows = 4, size = "md", ...props }) => {
14
+ var _a;
15
+ const internalId = useId();
16
+ id = id !== null && id !== void 0 ? id : internalId;
17
+ const componentProps = {
18
+ "aria-placeholder": (_a = placeholder !== null && placeholder !== void 0 ? placeholder : ariaPlaceholder) !== null && _a !== void 0 ? _a : undefined,
19
+ className: clsx("coop-textarea", className),
20
+ cols,
21
+ "data-error": error ? "" : undefined,
22
+ "data-size": size.length && size !== "md" ? size : undefined,
23
+ id,
24
+ maxLength: cutoff ? maxLength : undefined,
25
+ name,
26
+ placeholder,
27
+ rows,
28
+ ...props,
29
+ };
30
+ const [remaining, setRemaining] = useState(maxLength);
31
+ const debouncedRemaining = useDebounce(remaining, DEBOUNCE_DELAY);
32
+ const handleChange = (e) => {
33
+ maxLength && e.target && setRemaining(maxLength - e.target.value.length);
34
+ };
35
+ return (jsxs("div", { className: "coop-form-item", 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) => {
36
+ userOnChange === null || userOnChange === void 0 ? void 0 : userOnChange(e);
37
+ handleChange(e);
38
+ } }), 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
+ };
40
+
41
+ export { Textarea, Textarea as default };
@@ -0,0 +1,4 @@
1
+ import Textarea from "./Textarea";
2
+ export default Textarea;
3
+ export { Textarea };
4
+ export * from "./Textarea";
@@ -0,0 +1 @@
1
+ export declare function useDebounce<T>(value: T, delay: number): T;
@@ -0,0 +1,16 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ function useDebounce(value, delay) {
4
+ const [debouncedValue, setDebouncedValue] = useState(value);
5
+ useEffect(() => {
6
+ const handler = setTimeout(() => {
7
+ setDebouncedValue(value);
8
+ }, delay);
9
+ return () => {
10
+ clearTimeout(handler);
11
+ };
12
+ }, [value, delay]);
13
+ return debouncedValue;
14
+ }
15
+
16
+ export { useDebounce };
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ export * from "./components/AlertBanner";
2
2
  export * from "./components/Author";
3
3
  export * from "./components/Button";
4
4
  export * from "./components/Card";
5
+ export * from "./components/Checkbox";
6
+ export * from "./components/Checkbox/CheckboxGroup";
5
7
  export * from "./components/Expandable";
6
8
  export * from "./components/Field";
7
9
  export * from "./components/FieldError";
@@ -9,7 +11,6 @@ export * from "./components/FieldHint";
9
11
  export * from "./components/FieldLabel";
10
12
  export * from "./components/Flourish";
11
13
  export * from "./components/Image";
12
- export * from "./components/Input";
13
14
  export * from "./components/Pill";
14
15
  export * from "./components/RootSVG";
15
16
  export * from "./components/SearchBox";
@@ -17,3 +18,5 @@ export * from "./components/Signpost";
17
18
  export * from "./components/SkipNav";
18
19
  export * from "./components/Squircle";
19
20
  export * from "./components/Tag";
21
+ export * from "./components/Textarea";
22
+ export * from "./components/TextInput";
package/dist/index.js CHANGED
@@ -2,6 +2,8 @@ export { AlertBanner } from './components/AlertBanner/AlertBanner.js';
2
2
  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
+ export { Checkbox } from './components/Checkbox/Checkbox.js';
6
+ export { CheckboxGroup } from './components/Checkbox/CheckboxGroup.js';
5
7
  export { Expandable } from './components/Expandable/Expandable.js';
6
8
  export { Field } from './components/Field/Field.js';
7
9
  export { FieldError } from './components/FieldError/FieldError.js';
@@ -9,7 +11,6 @@ export { FieldHint } from './components/FieldHint/FieldHint.js';
9
11
  export { FieldLabel } from './components/FieldLabel/FieldLabel.js';
10
12
  export { Flourish } from './components/Flourish/Flourish.js';
11
13
  export { Image } from './components/Image/Image.js';
12
- export { Input } from './components/Input/Input.js';
13
14
  export { Pill } from './components/Pill/Pill.js';
14
15
  export { RootSVG } from './components/RootSVG/RootSVG.js';
15
16
  export { SearchBox } from './components/SearchBox/SearchBox.js';
@@ -17,3 +18,5 @@ export { Signpost } from './components/Signpost/Signpost.js';
17
18
  export { SkipNav } from './components/SkipNav/SkipNav.js';
18
19
  export { Squircle } from './components/Squircle/Squircle.js';
19
20
  export { Tag } from './components/Tag/Tag.js';
21
+ export { Textarea } from './components/Textarea/Textarea.js';
22
+ export { TextInput } from './components/TextInput/TextInput.js';
@@ -1,5 +1,5 @@
1
1
  export type StandardSizes = "sm" | "md" | "lg";
2
2
  export type FormFieldError = {
3
- message: string;
3
+ message?: string;
4
4
  type?: string;
5
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.33.1",
4
+ "version": "0.35.0",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -57,19 +57,19 @@
57
57
  "devDependencies": {
58
58
  "@axe-core/playwright": "^4.10.2",
59
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",
60
+ "@storybook/addon-a11y": "^9.1.5",
61
+ "@storybook/addon-docs": "^9.1.5",
62
+ "@storybook/addon-onboarding": "^9.1.5",
63
+ "@storybook/react-vite": "^9.1.5",
64
64
  "@testing-library/jest-dom": "^6.8.0",
65
65
  "@testing-library/react": "^16.3.0",
66
- "@types/react": "^19.1.12",
66
+ "@types/react": "^19.1.13",
67
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
- "serve": "^14.2.4",
72
- "storybook": "^9.1.3"
71
+ "serve": "^14.2.5",
72
+ "storybook": "^9.1.5"
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.29.0"
83
+ "@coopdigital/styles": "^0.31.0"
84
84
  },
85
- "gitHead": "765010c67a092c8501c3ea6327ffaef67df7113f"
85
+ "gitHead": "eedf8c0062e77a007636c54ec5776aa6cc670cee"
86
86
  }
@@ -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 CheckboxProps
12
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "type"> {
13
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
14
+ className?: string
15
+ /** **(Optional)** Specify the Checkbox error state.
16
+ *
17
+ * 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.
18
+ */
19
+ error?: FormFieldError
20
+ /** **(Optional)** Specify the Checkbox hint.
21
+ *
22
+ * This text is rendered under the label to provide further guidance for users.
23
+ */
24
+ hint?: string
25
+ /** **(Optional)** Specify the Checkbox id. Will be auto-generated if not set. */
26
+ id?: string
27
+ /** **(Optional)** Specify the Checkbox label.
28
+ *
29
+ * This property is optional in case you need to render your own label, but all form elements *must* provide a label. */
30
+ label: string
31
+ /** **(Optional)** Specify whether the label should be visible to humans or screen readers. */
32
+ labelVisible?: boolean
33
+ /** Specify the Checkbox name. */
34
+ name: string
35
+ /** **(Optional)** Specify the Checkbox placeholder text. Do not use in place of a form label. */
36
+ placeholder?: string
37
+ /** **(Optional)** Specify the Checkbox size. */
38
+ size?: StandardSizes
39
+ }
40
+
41
+ export const Checkbox = ({
42
+ "aria-placeholder": ariaPlaceholder,
43
+ className,
44
+ error = false,
45
+ hint,
46
+ id,
47
+ label,
48
+ labelVisible = true,
49
+ name,
50
+ placeholder,
51
+ size = "md",
52
+ ...props
53
+ }: CheckboxProps): JSX.Element => {
54
+ const internalId = useId()
55
+
56
+ id = id ?? internalId
57
+
58
+ const componentProps = {
59
+ "aria-placeholder": placeholder ?? ariaPlaceholder ?? undefined,
60
+ className: clsx("coop-checkbox", className),
61
+ "data-error": error ? "" : undefined,
62
+ "data-size": size.length && size !== "md" ? size : undefined,
63
+ id,
64
+ name,
65
+ placeholder,
66
+ type: "checkbox",
67
+ ...props,
68
+ }
69
+
70
+ return (
71
+ <div className="coop-form-item">
72
+ <div className="coop-checkbox-wrapper">
73
+ <input {...componentProps} />
74
+
75
+ {label && (
76
+ <FieldLabel htmlFor={id} isVisible={labelVisible}>
77
+ {label}
78
+ </FieldLabel>
79
+ )}
80
+ {hint && <FieldHint>{hint}</FieldHint>}
81
+ </div>
82
+ {typeof error === "object" && error?.message && <FieldError>{error.message}</FieldError>}
83
+ </div>
84
+ )
85
+ }
86
+
87
+ export default Checkbox
@@ -0,0 +1,63 @@
1
+ import type { FieldsetHTMLAttributes, JSX } from "react"
2
+
3
+ import clsx from "clsx"
4
+
5
+ import { FormFieldError, StandardSizes } from "../../../src/types"
6
+ import { FieldError } from "../FieldError"
7
+ import { FieldHint } from "../FieldHint"
8
+
9
+ export interface CheckboxGroupProps extends FieldsetHTMLAttributes<HTMLFieldSetElement> {
10
+ /** **(Optional)** Main content inside the component. It can be any valid JSX or string. */
11
+ children?: React.ReactNode
12
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
13
+ className?: string
14
+ /** **(Optional)** Specify the CheckboxGroup error state.
15
+ *
16
+ * 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.
17
+ */
18
+ error?: FormFieldError
19
+ /** **(Optional)** Specify the CheckboxGroup hint.
20
+ *
21
+ * This text is rendered under the label to provide further guidance for users.
22
+ */
23
+ hint?: string
24
+ /** **(Optional)** Specify the label for the CheckboxGroup. This will be rendered as a fieldset legend. */
25
+ label?: string
26
+ /** **(Optional)** Specify whether the label should be visible to humans or screen readers. */
27
+ labelVisible?: boolean
28
+ /** **(Optional)** Specify the CheckboxGroup size. */
29
+ size?: StandardSizes
30
+ }
31
+
32
+ export const CheckboxGroup = ({
33
+ children,
34
+ className,
35
+ error = false,
36
+ hint,
37
+ label,
38
+ labelVisible = true,
39
+ size = "md",
40
+ ...props
41
+ }: CheckboxGroupProps): JSX.Element => {
42
+ const componentProps = {
43
+ className: clsx("coop-fieldset", "coop-checkbox-group", className),
44
+ "data-error": error ? "" : undefined,
45
+ "data-size": size.length && size !== "md" ? size : undefined,
46
+ ...props,
47
+ }
48
+
49
+ const legendProps = {
50
+ className: clsx("coop-field-label", !labelVisible && "sr-only"),
51
+ }
52
+
53
+ return (
54
+ <fieldset {...componentProps}>
55
+ {label && <legend {...legendProps}>{label}</legend>}
56
+ {hint && <FieldHint>{hint}</FieldHint>}
57
+ {typeof error === "object" && error?.message && <FieldError>{error.message}</FieldError>}
58
+ {children}
59
+ </fieldset>
60
+ )
61
+ }
62
+
63
+ export default CheckboxGroup
@@ -0,0 +1,5 @@
1
+ import Checkbox from "./Checkbox"
2
+
3
+ export default Checkbox
4
+ export { Checkbox }
5
+ export * from "./Checkbox"
@@ -11,7 +11,7 @@ export interface FieldProps extends HTMLAttributes<HTMLDivElement> {
11
11
 
12
12
  export const Field = ({ children, className, ...props }: FieldProps): JSX.Element => {
13
13
  const componentProps = {
14
- className: clsx("coop-field ", className),
14
+ className: clsx("coop-form-item ", className),
15
15
  ...props,
16
16
  }
17
17
  return <div {...componentProps}>{children}</div>
@@ -19,7 +19,7 @@ export const FieldHint = ({
19
19
  ...props,
20
20
  }
21
21
 
22
- return children ? <p {...componentProps}>{children}</p> : null
22
+ return children ? <div {...componentProps}>{children}</div> : null
23
23
  }
24
24
 
25
25
  export default FieldHint
@@ -9,7 +9,7 @@ import { StandardSizes } from "../../../src/types"
9
9
  import { Button, type ButtonProps } from "../Button"
10
10
  import { FieldLabel } from "../FieldLabel"
11
11
  import { SearchIcon } from "../Icon"
12
- import Input, { InputProps } from "../Input"
12
+ import TextInput, { TextInputProps } from "../TextInput"
13
13
 
14
14
  export interface SearchBoxProps
15
15
  extends Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "type"> {
@@ -19,17 +19,17 @@ export interface SearchBoxProps
19
19
  button?: Pick<ButtonProps, "className" | "loadingText"> & { label?: React.ReactNode }
20
20
  /** **(Optional)** Specify additional CSS classes to be applied to the component. */
21
21
  className?: string
22
- /** **(Optional)** Specify the Input id. Will be auto-generated if not set. */
22
+ /** **(Optional)** Specify the TextInput id. Will be auto-generated if not set. */
23
23
  id?: string
24
24
  /** Specify the label displayed above the search field. Hidden by default, but visible to screen readers. */
25
25
  label: string
26
26
  /** **(Optional)** Specify whether the label should be visible to humans or screenreaders. */
27
27
  labelVisible?: boolean
28
- /** **(Optional)** Specify the Input name, also used as the URL search parameter. Defaults to `query`. */
28
+ /** **(Optional)** Specify the TextInput name, also used as the URL search parameter. Defaults to `query`. */
29
29
  name?: string
30
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. */
31
31
  onSubmit?: React.FormEventHandler<HTMLElement> | undefined
32
- /** **(Optional)** Specify the Input placeholder text Do not use in place of a form label. */
32
+ /** **(Optional)** Specify the TextInput placeholder text Do not use in place of a form label. */
33
33
  placeholder?: string
34
34
  /** **(Optional)** Specify the SearchBox size. */
35
35
  size?: StandardSizes
@@ -112,8 +112,8 @@ export const SearchBox = ({
112
112
  id,
113
113
  name,
114
114
  placeholder,
115
- size: size as keyof InputProps["size"],
116
- type: "search" as keyof InputProps["type"],
115
+ size: size as keyof TextInputProps["size"],
116
+ type: "search" as keyof TextInputProps["type"],
117
117
  ...props,
118
118
  }
119
119
 
@@ -125,7 +125,7 @@ export const SearchBox = ({
125
125
  </FieldLabel>
126
126
  )}
127
127
  <div className="coop-search-box--inner">
128
- <Input {...inputProps} />
128
+ <TextInput {...inputProps} />
129
129
  <Button {...buttonProps}>{button.label}</Button>
130
130
  </div>
131
131
  </form>
@@ -8,35 +8,43 @@ import { FieldError } from "../FieldError"
8
8
  import { FieldHint } from "../FieldHint"
9
9
  import { FieldLabel } from "../FieldLabel"
10
10
 
11
- export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "type"> {
11
+ export interface TextInputProps
12
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "type"> {
12
13
  /** **(Optional)** Specify additional CSS classes to be applied to the component. */
13
14
  className?: string
14
- /** **(Optional)** Specify the Input error. */
15
+ /** **(Optional)** Specify the TextInput error state.
16
+ *
17
+ * 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.
18
+ */
15
19
  error?: FormFieldError
16
- /** **(Optional)** Specify the Input hint.
20
+ /** **(Optional)** Specify the TextInput hint.
17
21
  *
18
22
  * This text is rendered under the label to provide further guidance for users.
19
23
  */
20
24
  hint?: string
21
- /** **(Optional)** Specify the Input id. Will be auto-generated if not set. */
25
+ /** **(Optional)** Specify the TextInput id. Will be auto-generated if not set. */
22
26
  id?: string
23
- /** **(Optional)** Specify the Input label.
27
+ /** **(Optional)** Specify the TextInput label.
24
28
  *
25
29
  * This property is optional in case you need to render your own label, but all form elements *must* provide a label. */
26
30
  label?: string
27
31
  /** **(Optional)** Specify whether the label should be visible to humans or screen readers. */
28
32
  labelVisible?: boolean
29
- /** Specify the Input name. */
33
+ /** Specify the TextInput name. */
30
34
  name: string
31
- /** **(Optional)** Specify the Input placeholder text. Do not use in place of a form label. */
35
+ /** **(Optional)** Specify the TextInput placeholder text. Do not use in place of a form label. */
32
36
  placeholder?: string
33
- /** **(Optional)** Specify the Input size. */
37
+ /** **(Optional)** Specify the prefix. */
38
+ prefix?: string
39
+ /** **(Optional)** Specify the TextInput size. */
34
40
  size?: StandardSizes
35
- /** **(Optional)** Specify the Input type. */
41
+ /** **(Optional)** Specify the suffix. */
42
+ suffix?: string
43
+ /** **(Optional)** Specify the TextInput type. */
36
44
  type?: "text" | "email" | "number" | "password" | "search" | "tel" | "url"
37
45
  }
38
46
 
39
- export const Input = ({
47
+ export const TextInput = ({
40
48
  "aria-placeholder": ariaPlaceholder,
41
49
  className,
42
50
  error = false,
@@ -46,17 +54,19 @@ export const Input = ({
46
54
  labelVisible = true,
47
55
  name,
48
56
  placeholder,
57
+ prefix,
49
58
  size = "md",
59
+ suffix,
50
60
  type = "text",
51
61
  ...props
52
- }: InputProps): JSX.Element => {
62
+ }: TextInputProps): JSX.Element => {
53
63
  const internalId = useId()
54
64
 
55
65
  id = id ?? internalId
56
66
 
57
67
  const componentProps = {
58
68
  "aria-placeholder": placeholder ?? ariaPlaceholder ?? undefined,
59
- className: clsx("coop-input", className),
69
+ className: clsx("coop-text-input", className),
60
70
  "data-error": error ? "" : undefined,
61
71
  "data-size": size.length && size !== "md" ? size : undefined,
62
72
  id,
@@ -67,7 +77,7 @@ export const Input = ({
67
77
  }
68
78
 
69
79
  return (
70
- <>
80
+ <div className="coop-form-item">
71
81
  {label && (
72
82
  <FieldLabel htmlFor={id} isVisible={labelVisible}>
73
83
  {label}
@@ -77,11 +87,13 @@ export const Input = ({
77
87
  {hint && <FieldHint>{hint}</FieldHint>}
78
88
 
79
89
  {typeof error === "object" && error?.message && <FieldError>{error.message}</FieldError>}
80
- <div className="coop-field-control">
90
+ <div className="coop-text-input-wrapper">
91
+ {prefix && <span className="coop-text-input--prefix">{prefix}</span>}
81
92
  <input {...componentProps} />
93
+ {suffix && <span className="coop-text-input--suffix">{suffix}</span>}
82
94
  </div>
83
- </>
95
+ </div>
84
96
  )
85
97
  }
86
98
 
87
- export default Input
99
+ export default TextInput
@@ -0,0 +1,5 @@
1
+ import TextInput from "./TextInput"
2
+
3
+ export default TextInput
4
+ export { TextInput }
5
+ export * from "./TextInput"
@@ -0,0 +1,144 @@
1
+ import type { ChangeEvent, JSX, TextareaHTMLAttributes } from "react"
2
+
3
+ import clsx from "clsx"
4
+ import { useId, useState } from "react"
5
+
6
+ import { useDebounce } from "../../hooks/useDebounce"
7
+ import { FormFieldError, StandardSizes } from "../../types"
8
+ import { FieldError } from "../FieldError"
9
+ import { FieldHint } from "../FieldHint"
10
+ import { FieldLabel } from "../FieldLabel"
11
+
12
+ const DEBOUNCE_DELAY = 750
13
+
14
+ const charCountMessage = (remaining: number) => {
15
+ return `You have ${Math.abs(remaining).toLocaleString()} ${Math.abs(remaining) === 1 ? "character" : "characters"} ${remaining < 0 ? "too many" : "remaining"}`
16
+ }
17
+
18
+ export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
19
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
20
+ className?: string
21
+ /** Specify the number of columns (characters per row) in the Textarea. Defaults to `30`. */
22
+ cols?: number
23
+ /** **(Optional)** Specify whether to show the character counter underneath the Textarea. */
24
+ counter?: boolean
25
+ /** **(Optional)** Specify whether the Textarea should allow more characters than its `maxLength` value.
26
+ *
27
+ * Defaults to `false`, meaning users can enter more characters than the maximum but will be warned if they go over the limit. When set to `true`, users will be blocked from typing once they hit the character limit. This can be an accessiblity anti-pattern, so only use this option when absolutely necessary.
28
+ *
29
+ * Remember it is still your responsibility to handle validation on submission, this is simply a hint for the user.
30
+ */
31
+ cutoff?: boolean
32
+ /** **(Optional)** Specify the Textarea error state.
33
+ *
34
+ * 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.
35
+ */
36
+ error?: FormFieldError
37
+ /** **(Optional)** Specify the Textarea hint.
38
+ *
39
+ * This text is rendered under the label to provide further guidance for users.
40
+ */
41
+ hint?: string
42
+ /** **(Optional)** Specify the Textarea id. Will be auto-generated if not set. */
43
+ id?: string
44
+ /** **(Optional)** Specify the Textarea label.
45
+ *
46
+ * This property is optional in case you need to render your own label, but all form elements *must* provide a label. */
47
+ label?: string
48
+ /** **(Optional)** Specify whether the label should be visible to humans or screen readers. */
49
+ labelVisible?: boolean
50
+ /** **(Optional)** Specify the Textarea maxLength. This is the maximum number of characters users can enter. */
51
+ maxLength?: number
52
+ /** Specify the Textarea name. */
53
+ name: string
54
+ /** **(Optional)** Specify the Textarea placeholder text. Do not use in place of a form label. */
55
+ placeholder?: string
56
+ /** Specify the number of rows (lines of text) in the Textarea. Defaults to `4`. */
57
+ rows?: number
58
+ /** **(Optional)** Specify the Textarea size. */
59
+ size?: StandardSizes
60
+ }
61
+
62
+ export const Textarea = ({
63
+ "aria-placeholder": ariaPlaceholder,
64
+ className,
65
+ cols = 30,
66
+ counter = false,
67
+ cutoff = false,
68
+ error = false,
69
+ hint,
70
+ id,
71
+ label,
72
+ labelVisible = true,
73
+ maxLength,
74
+ name,
75
+ onChange: userOnChange = undefined,
76
+ placeholder,
77
+ rows = 4,
78
+ size = "md",
79
+ ...props
80
+ }: TextareaProps): JSX.Element => {
81
+ const internalId = useId()
82
+
83
+ id = id ?? internalId
84
+
85
+ const componentProps = {
86
+ "aria-placeholder": placeholder ?? ariaPlaceholder ?? undefined,
87
+ className: clsx("coop-textarea", className),
88
+ cols,
89
+ "data-error": error ? "" : undefined,
90
+ "data-size": size.length && size !== "md" ? size : undefined,
91
+ id,
92
+ maxLength: cutoff ? maxLength : undefined,
93
+ name,
94
+ placeholder,
95
+ rows,
96
+ ...props,
97
+ }
98
+
99
+ const [remaining, setRemaining] = useState(maxLength)
100
+ const debouncedRemaining = useDebounce(remaining, DEBOUNCE_DELAY)
101
+
102
+ const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
103
+ maxLength && e.target && setRemaining(maxLength - e.target.value.length)
104
+ }
105
+
106
+ return (
107
+ <div className="coop-form-item">
108
+ {label && (
109
+ <FieldLabel htmlFor={id} isVisible={labelVisible}>
110
+ {label}
111
+ </FieldLabel>
112
+ )}
113
+
114
+ {hint && <FieldHint>{hint}</FieldHint>}
115
+
116
+ {typeof error === "object" && error?.message && <FieldError>{error.message}</FieldError>}
117
+
118
+ <textarea
119
+ {...componentProps}
120
+ onChange={(e) => {
121
+ userOnChange?.(e)
122
+ handleChange(e)
123
+ }}
124
+ ></textarea>
125
+
126
+ {counter && maxLength && remaining != null && debouncedRemaining != null && (
127
+ <>
128
+ <small
129
+ aria-hidden="true"
130
+ className="coop-textarea-counter"
131
+ {...(remaining < 0 && { "data-error": "" })}
132
+ >
133
+ {charCountMessage(remaining)}
134
+ </small>
135
+ <span aria-live="polite" className="sr-only">
136
+ {charCountMessage(debouncedRemaining)}
137
+ </span>
138
+ </>
139
+ )}
140
+ </div>
141
+ )
142
+ }
143
+
144
+ export default Textarea
@@ -0,0 +1,5 @@
1
+ import Textarea from "./Textarea"
2
+
3
+ export default Textarea
4
+ export { Textarea }
5
+ export * from "./Textarea"
@@ -0,0 +1,16 @@
1
+ import { useEffect, useState } from "react"
2
+
3
+ export function useDebounce<T>(value: T, delay: number) {
4
+ const [debouncedValue, setDebouncedValue] = useState(value)
5
+ useEffect(() => {
6
+ const handler = setTimeout(() => {
7
+ setDebouncedValue(value)
8
+ }, delay)
9
+
10
+ return () => {
11
+ clearTimeout(handler)
12
+ }
13
+ }, [value, delay])
14
+
15
+ return debouncedValue
16
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,8 @@ export * from "./components/AlertBanner"
2
2
  export * from "./components/Author"
3
3
  export * from "./components/Button"
4
4
  export * from "./components/Card"
5
+ export * from "./components/Checkbox"
6
+ export * from "./components/Checkbox/CheckboxGroup"
5
7
  export * from "./components/Expandable"
6
8
  export * from "./components/Field"
7
9
  export * from "./components/FieldError"
@@ -9,7 +11,6 @@ export * from "./components/FieldHint"
9
11
  export * from "./components/FieldLabel"
10
12
  export * from "./components/Flourish"
11
13
  export * from "./components/Image"
12
- export * from "./components/Input"
13
14
  export * from "./components/Pill"
14
15
  export * from "./components/RootSVG"
15
16
  export * from "./components/SearchBox"
@@ -17,3 +18,5 @@ export * from "./components/Signpost"
17
18
  export * from "./components/SkipNav"
18
19
  export * from "./components/Squircle"
19
20
  export * from "./components/Tag"
21
+ export * from "./components/Textarea"
22
+ export * from "./components/TextInput"
@@ -1,3 +1,3 @@
1
1
  export type StandardSizes = "sm" | "md" | "lg"
2
2
 
3
- export type FormFieldError = { message: string; type?: string } | boolean | null | undefined
3
+ export type FormFieldError = { message?: string; type?: string } | boolean | null | undefined
@@ -1,31 +0,0 @@
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;
@@ -1,4 +0,0 @@
1
- import Input from "./Input";
2
- export default Input;
3
- export { Input };
4
- export * from "./Input";
@@ -1,5 +0,0 @@
1
- import { Input } from './Input.js';
2
-
3
-
4
-
5
- export { Input, Input as default };
@@ -1,5 +0,0 @@
1
- import Input from "./Input"
2
-
3
- export default Input
4
- export { Input }
5
- export * from "./Input"