@coopdigital/react 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Input/Input.d.ts +4 -1
- package/dist/components/Textarea/Textarea.d.ts +47 -0
- package/dist/components/Textarea/Textarea.js +41 -0
- package/dist/components/Textarea/index.d.ts +4 -0
- package/dist/hooks/useDebounce.d.ts +1 -0
- package/dist/hooks/useDebounce.js +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +3 -3
- package/src/components/Input/Input.tsx +4 -1
- package/src/components/Textarea/Textarea.tsx +145 -0
- package/src/components/Textarea/index.ts +5 -0
- package/src/hooks/useDebounce.ts +16 -0
- package/src/index.ts +1 -0
|
@@ -3,7 +3,10 @@ import { FormFieldError, StandardSizes } from "../../../src/types";
|
|
|
3
3
|
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "type"> {
|
|
4
4
|
/** **(Optional)** Specify additional CSS classes to be applied to the component. */
|
|
5
5
|
className?: string;
|
|
6
|
-
/** **(Optional)** Specify the Input error.
|
|
6
|
+
/** **(Optional)** Specify the Input 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
|
+
*/
|
|
7
10
|
error?: FormFieldError;
|
|
8
11
|
/** **(Optional)** Specify the Input hint.
|
|
9
12
|
*
|
|
@@ -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 the Textarea should allow more characters than its `maxLength` value.
|
|
9
|
+
*
|
|
10
|
+
* 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.
|
|
11
|
+
*
|
|
12
|
+
* Remember it is still your responsibility to handle validation on submission, this is simply a hint for the user.
|
|
13
|
+
*/
|
|
14
|
+
cutoff?: boolean;
|
|
15
|
+
/** **(Optional)** Specify the Textarea 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 Textarea hint.
|
|
21
|
+
*
|
|
22
|
+
* This text is rendered under the label to provide further guidance for users.
|
|
23
|
+
*/
|
|
24
|
+
hint?: string;
|
|
25
|
+
/** **(Optional)** Specify the Textarea id. Will be auto-generated if not set. */
|
|
26
|
+
id?: string;
|
|
27
|
+
/** **(Optional)** Specify the Textarea 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
|
+
/** **(Optional)** Specify the Textarea maxLength. This is the maximum number of characters users can enter. */
|
|
34
|
+
maxLength?: number;
|
|
35
|
+
/** Specify the Textarea name. */
|
|
36
|
+
name: string;
|
|
37
|
+
/** **(Optional)** Specify the Textarea placeholder text. Do not use in place of a form label. */
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
/** Specify the number of rows (lines of text) in the Textarea. Defaults to `4`. */
|
|
40
|
+
rows?: number;
|
|
41
|
+
/** **(Optional)** Specify whether to show the remaining character count underneath the Textarea. */
|
|
42
|
+
showRemaining?: boolean;
|
|
43
|
+
/** **(Optional)** Specify the Textarea size. */
|
|
44
|
+
size?: StandardSizes;
|
|
45
|
+
}
|
|
46
|
+
export declare const Textarea: ({ "aria-placeholder": ariaPlaceholder, className, cols, cutoff, error, hint, id, label, labelVisible, maxLength, name, onChange: userOnChange, placeholder, rows, showRemaining, size, ...props }: TextareaProps) => JSX.Element;
|
|
47
|
+
export default Textarea;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsxs, Fragment, jsx } 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)} ${Math.abs(remaining) === 1 ? "character" : "characters"} ${remaining < 0 ? "too many" : "remaining"}`;
|
|
12
|
+
};
|
|
13
|
+
const Textarea = ({ "aria-placeholder": ariaPlaceholder, className, cols = 30, cutoff = false, error = false, hint, id, label, labelVisible = true, maxLength, name, onChange: userOnChange = undefined, placeholder, rows = 4, showRemaining = false, 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(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("textarea", { ...componentProps, onChange: (e) => {
|
|
36
|
+
userOnChange === null || userOnChange === void 0 ? void 0 : userOnChange(e);
|
|
37
|
+
handleChange(e);
|
|
38
|
+
} }) }), showRemaining && 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 @@
|
|
|
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
package/dist/index.js
CHANGED
|
@@ -17,3 +17,4 @@ export { Signpost } from './components/Signpost/Signpost.js';
|
|
|
17
17
|
export { SkipNav } from './components/SkipNav/SkipNav.js';
|
|
18
18
|
export { Squircle } from './components/Squircle/Squircle.js';
|
|
19
19
|
export { Tag } from './components/Tag/Tag.js';
|
|
20
|
+
export { Textarea } from './components/Textarea/Textarea.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coopdigital/react",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.34.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"storybook": "$storybook"
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
|
-
"@coopdigital/styles": "^0.
|
|
83
|
+
"@coopdigital/styles": "^0.30.0"
|
|
84
84
|
},
|
|
85
|
-
"gitHead": "
|
|
85
|
+
"gitHead": "858102db210474fc31e01a2f4dc97551af2621d3"
|
|
86
86
|
}
|
|
@@ -11,7 +11,10 @@ import { FieldLabel } from "../FieldLabel"
|
|
|
11
11
|
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "type"> {
|
|
12
12
|
/** **(Optional)** Specify additional CSS classes to be applied to the component. */
|
|
13
13
|
className?: string
|
|
14
|
-
/** **(Optional)** Specify the Input error.
|
|
14
|
+
/** **(Optional)** Specify the Input 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
|
+
*/
|
|
15
18
|
error?: FormFieldError
|
|
16
19
|
/** **(Optional)** Specify the Input hint.
|
|
17
20
|
*
|
|
@@ -0,0 +1,145 @@
|
|
|
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)} ${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 the Textarea should allow more characters than its `maxLength` value.
|
|
24
|
+
*
|
|
25
|
+
* 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.
|
|
26
|
+
*
|
|
27
|
+
* Remember it is still your responsibility to handle validation on submission, this is simply a hint for the user.
|
|
28
|
+
*/
|
|
29
|
+
cutoff?: boolean
|
|
30
|
+
/** **(Optional)** Specify the Textarea error state.
|
|
31
|
+
*
|
|
32
|
+
* 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.
|
|
33
|
+
*/
|
|
34
|
+
error?: FormFieldError
|
|
35
|
+
/** **(Optional)** Specify the Textarea hint.
|
|
36
|
+
*
|
|
37
|
+
* This text is rendered under the label to provide further guidance for users.
|
|
38
|
+
*/
|
|
39
|
+
hint?: string
|
|
40
|
+
/** **(Optional)** Specify the Textarea id. Will be auto-generated if not set. */
|
|
41
|
+
id?: string
|
|
42
|
+
/** **(Optional)** Specify the Textarea label.
|
|
43
|
+
*
|
|
44
|
+
* This property is optional in case you need to render your own label, but all form elements *must* provide a label. */
|
|
45
|
+
label?: string
|
|
46
|
+
/** **(Optional)** Specify whether the label should be visible to humans or screen readers. */
|
|
47
|
+
labelVisible?: boolean
|
|
48
|
+
/** **(Optional)** Specify the Textarea maxLength. This is the maximum number of characters users can enter. */
|
|
49
|
+
maxLength?: number
|
|
50
|
+
/** Specify the Textarea name. */
|
|
51
|
+
name: string
|
|
52
|
+
/** **(Optional)** Specify the Textarea placeholder text. Do not use in place of a form label. */
|
|
53
|
+
placeholder?: string
|
|
54
|
+
/** Specify the number of rows (lines of text) in the Textarea. Defaults to `4`. */
|
|
55
|
+
rows?: number
|
|
56
|
+
/** **(Optional)** Specify whether to show the remaining character count underneath the Textarea. */
|
|
57
|
+
showRemaining?: boolean
|
|
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
|
+
cutoff = false,
|
|
67
|
+
error = false,
|
|
68
|
+
hint,
|
|
69
|
+
id,
|
|
70
|
+
label,
|
|
71
|
+
labelVisible = true,
|
|
72
|
+
maxLength,
|
|
73
|
+
name,
|
|
74
|
+
onChange: userOnChange = undefined,
|
|
75
|
+
placeholder,
|
|
76
|
+
rows = 4,
|
|
77
|
+
showRemaining = false,
|
|
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
|
+
<>
|
|
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
|
+
<div className="coop-field-control">
|
|
118
|
+
<textarea
|
|
119
|
+
{...componentProps}
|
|
120
|
+
onChange={(e) => {
|
|
121
|
+
userOnChange?.(e)
|
|
122
|
+
handleChange(e)
|
|
123
|
+
}}
|
|
124
|
+
></textarea>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{showRemaining && maxLength && remaining != null && debouncedRemaining != null && (
|
|
128
|
+
<>
|
|
129
|
+
<small
|
|
130
|
+
aria-hidden="true"
|
|
131
|
+
className="coop-textarea-counter"
|
|
132
|
+
{...(remaining < 0 && { "data-error": "" })}
|
|
133
|
+
>
|
|
134
|
+
{charCountMessage(remaining)}
|
|
135
|
+
</small>
|
|
136
|
+
<span aria-live="polite" className="sr-only">
|
|
137
|
+
{charCountMessage(debouncedRemaining)}
|
|
138
|
+
</span>
|
|
139
|
+
</>
|
|
140
|
+
)}
|
|
141
|
+
</>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default 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