@uktrade/react-component-library 0.22.11 → 0.22.13

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ApiCreateHooks.d.ts","sourceRoot":"","sources":["../../../src/components/ApiQuery/ApiCreateHooks.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAKpD,wBAAgB,cAAc,CAAC,KAAK,SAAS,EAAE,EAC7C,MAAM,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;UAQzC,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,QACrC,CAAC,UACC,GAAG;;;;;;;;;;WAYN,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,QACvC,CAAC,UACC,GAAG;;;;;;;;;;mBAaE,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC;;;;;;;;;;kBAQlD,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC;;;;;;;;;;oBAQ9C,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC;;;;;;;;;;qBAQjD,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC;;;;;;;;;;EAQvE"}
1
+ {"version":3,"file":"ApiCreateHooks.d.ts","sourceRoot":"","sources":["../../../src/components/ApiQuery/ApiCreateHooks.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAqBpD,wBAAgB,cAAc,CAAC,KAAK,SAAS,EAAE,EAC7C,MAAM,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;UAQzC,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,QACrC,CAAC,UACC,GAAG;;;;;;;;;;WAQN,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,QACvC,CAAC,UACC,GAAG;;;;;;;;;;mBAUE,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC;;;;;;;;;;kBAKlD,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC;;;;;;;;;;oBAK9C,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC;;;;;;;;;;qBAKjD,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC;;;;;;;;;;EAKvE"}
@@ -1,6 +1,15 @@
1
1
  "use client";
2
2
  import { createApiClient } from "./ApiCreateClient";
3
3
  import { useApi, useApiMutation } from "./ApiProvider";
4
+ function unwrap(res) {
5
+ if (res.error) {
6
+ throw Object.assign(res.error, {
7
+ status: res.response.status,
8
+ response: res.response,
9
+ });
10
+ }
11
+ return res.data;
12
+ }
4
13
  // API hook factory that converts a typed OpenAPI client into React hooks (`useApi` and `useApiMutation`) that the UI can use.
5
14
  // Provides a simple interface for making API calls and handling loading and error states in your React components.
6
15
  export function createApiHooks(client) {
@@ -10,53 +19,23 @@ export function createApiHooks(client) {
10
19
  // Using useQuery
11
20
  return {
12
21
  get: (path, input) => {
13
- return useApi(["get", path, JSON.stringify(input ?? {})], async () => {
14
- const res = await client.GET(path, input);
15
- if (res.error)
16
- throw res.error;
17
- return res.data;
18
- });
22
+ return useApi(["get", path, JSON.stringify(input ?? {})], async () => unwrap(await client.GET(path, input)));
19
23
  },
20
24
  post: (path, input) => {
21
- return useApi(["post", path, JSON.stringify(input ?? {})], async () => {
22
- const res = await client.POST(path, input);
23
- if (res.error)
24
- throw res.error;
25
- return res.data;
26
- });
25
+ return useApi(["post", path, JSON.stringify(input ?? {})], async () => unwrap(await client.POST(path, input)));
27
26
  },
28
27
  // Using useMutation
29
28
  postMutation: (path) => {
30
- return useApiMutation(async (input) => {
31
- const res = await client.POST(path, input);
32
- if (res.error)
33
- throw res.error;
34
- return res.data;
35
- });
29
+ return useApiMutation(async (input) => unwrap(await client.POST(path, input)));
36
30
  },
37
31
  putMutation: (path) => {
38
- return useApiMutation(async (input) => {
39
- const res = await client.PUT(path, input);
40
- if (res.error)
41
- throw res.error;
42
- return res.data;
43
- });
32
+ return useApiMutation(async (input) => unwrap(await client.PUT(path, input)));
44
33
  },
45
34
  patchMutation: (path) => {
46
- return useApiMutation(async (input) => {
47
- const res = await client.PATCH(path, input);
48
- if (res.error)
49
- throw res.error;
50
- return res.data;
51
- });
35
+ return useApiMutation(async (input) => unwrap(await client.PATCH(path, input)));
52
36
  },
53
37
  deleteMutation: (path) => {
54
- return useApiMutation(async (input) => {
55
- const res = await client.DELETE(path, input);
56
- if (res.error)
57
- throw res.error;
58
- return res.data;
59
- });
38
+ return useApiMutation(async (input) => unwrap(await client.DELETE(path, input)));
60
39
  },
61
40
  };
62
41
  }
@@ -10,7 +10,8 @@ interface TextareaInputProps extends Omit<JSX.IntrinsicElements["textarea"], "id
10
10
  labelSize?: LabelSize;
11
11
  rows?: number;
12
12
  className?: string;
13
+ characterLimit?: number;
13
14
  }
14
- export declare const TextAreaInput: ({ id, label, hint, error, labelAs, labelSize, rows, className, ...props }: TextareaInputProps) => JSX.Element;
15
+ export declare const TextAreaInput: ({ id, label, hint, error, labelAs, labelSize, rows, className, characterLimit, ...props }: TextareaInputProps) => JSX.Element;
15
16
  export {};
16
17
  //# sourceMappingURL=TextAreaInput.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TextAreaInput.d.ts","sourceRoot":"","sources":["../../../../src/components/Inputs/TextAreaInput/TextAreaInput.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAG5C,KAAK,OAAO,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AACzC,KAAK,SAAS,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,CAAC;AAExC,UAAU,kBACN,SAAQ,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC;IACrD,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,aAAa,GAAI,2EAU3B,kBAAkB,gBA6DpB,CAAC"}
1
+ {"version":3,"file":"TextAreaInput.d.ts","sourceRoot":"","sources":["../../../../src/components/Inputs/TextAreaInput/TextAreaInput.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAG5C,KAAK,OAAO,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AACzC,KAAK,SAAS,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,CAAC;AAExC,UAAU,kBACN,SAAQ,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC;IACrD,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,aAAa,GAAI,2FAW3B,kBAAkB,gBA+FpB,CAAC"}
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import clsx from "clsx";
4
- import { createElement } from "react";
5
- export const TextAreaInput = ({ id, label, hint, error, labelAs, labelSize = "l", rows = 5, className, ...props }) => {
4
+ import { createElement, useState } from "react";
5
+ export const TextAreaInput = ({ id, label, hint, error, labelAs, labelSize = "l", rows = 5, className, characterLimit, ...props }) => {
6
6
  const hasError = Boolean(error);
7
7
  const hintId = hint ? `${id}-hint` : undefined;
8
8
  const errorId = hasError ? `${id}-error` : undefined;
@@ -10,12 +10,25 @@ export const TextAreaInput = ({ id, label, hint, error, labelAs, labelSize = "l"
10
10
  hintId,
11
11
  errorId
12
12
  ].filter(Boolean).join(" ") || undefined;
13
+ const hasLimit = characterLimit !== undefined;
14
+ const [count, setCount] = useState(0);
13
15
  const labelElement = (_jsx("label", { htmlFor: id, className: clsx("govuk-label", {
14
16
  [`govuk-label--${labelSize}`]: true,
15
17
  }), children: label }));
18
+ const remaining = hasLimit ? characterLimit - count : undefined;
19
+ const isOverLimit = remaining !== undefined && remaining < 0;
20
+ const characterMessage = remaining !== undefined
21
+ ? (remaining < 0
22
+ ? `You have ${Math.abs(remaining)} character${Math.abs(remaining) === 1 ? "" : "s"} too many`
23
+ : `You have ${remaining} character${remaining === 1 ? "" : "s"} remaining`)
24
+ : undefined;
16
25
  return (_jsxs("div", { className: clsx("govuk-form-group", {
17
26
  "govuk-form-group--error": hasError,
18
- }), children: [labelAs
27
+ "govuk-character-count": hasLimit
28
+ }), ...(hasLimit ? { "data-module": "govuk-character-count", "data-maxlength": characterLimit } : {}), children: [labelAs
19
29
  ? createElement(labelAs, { className: "govuk-label-wrapper" }, labelElement)
20
- : labelElement, hint && (_jsx("div", { id: hintId, className: "govuk-hint", children: hint })), hasError && (_jsxs("p", { id: errorId, className: "govuk-error-message", children: [_jsx("span", { className: "govuk-visually-hidden", children: "Error:" }), " ", error] })), _jsx("textarea", { id: id, rows: rows, className: clsx("govuk-textarea", { "govuk-textarea--error": hasError }, className), "aria-describedby": ariaDescribedBy, ...props })] }));
30
+ : labelElement, hint && (_jsx("div", { id: hintId, className: "govuk-hint", children: hint })), hasError && (_jsxs("p", { id: errorId, className: "govuk-error-message", children: [_jsx("span", { className: "govuk-visually-hidden", children: "Error:" }), " ", error] })), _jsx("textarea", { id: id, rows: rows, className: clsx("govuk-textarea", { "govuk-textarea--error": hasError,
31
+ "govuk-js-character-count": hasLimit }, className), style: isOverLimit ? { borderColor: "#d4351c" } : {}, "aria-describedby": ariaDescribedBy, ...props, onChange: e => {
32
+ setCount(e.target.value.length);
33
+ } }), hasLimit && (_jsx("div", { id: "with-hint-info", className: clsx("govuk-hint govuk-character-count__message", { "govuk-error-message": isOverLimit }), style: isOverLimit ? { color: "#d4351c", fontWeight: "bold" } : {}, children: characterMessage }))] }));
21
34
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uktrade/react-component-library",
3
- "version": "0.22.11",
3
+ "version": "0.22.13",
4
4
  "description": "A collection of reusable React components following GOV.UK design patterns.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -55,13 +55,13 @@
55
55
  "@types/react": "^19.2.9",
56
56
  "@types/react-dom": "^19.2.3",
57
57
  "@typescript-eslint/eslint-plugin": "^8.59.0",
58
- "@typescript-eslint/parser": "^8.59.0",
58
+ "@typescript-eslint/parser": "^8.62.0",
59
59
  "@vitest/coverage-v8": "^4.0.18",
60
60
  "@vitest/ui": "^4.0.18",
61
61
  "eslint": "^9.39.2",
62
62
  "eslint-plugin-react": "^7.37.5",
63
63
  "globals": "^17.5.0",
64
- "jsdom": "^27.4.0",
64
+ "jsdom": "^29.1.1",
65
65
  "openapi-typescript": "^7.13.0",
66
66
  "typescript": "^5.9.3",
67
67
  "typescript-eslint": "^8.59.0",
@@ -5,6 +5,22 @@ import type { PathsWithMethod } from "openapi-typescript-helpers";
5
5
  import { createApiClient } from "./ApiCreateClient";
6
6
  import { useApi, useApiMutation } from "./ApiProvider";
7
7
 
8
+
9
+ function unwrap<T>(res: {
10
+ data?: T;
11
+ error?: any;
12
+ response: Response;
13
+ }): T {
14
+ if (res.error) {
15
+ throw Object.assign(res.error, {
16
+ status: res.response.status,
17
+ response: res.response,
18
+ });
19
+ }
20
+
21
+ return res.data as T;
22
+ }
23
+
8
24
  // API hook factory that converts a typed OpenAPI client into React hooks (`useApi` and `useApiMutation`) that the UI can use.
9
25
  // Provides a simple interface for making API calls and handling loading and error states in your React components.
10
26
  export function createApiHooks<Paths extends {}>(
@@ -22,11 +38,7 @@ export function createApiHooks<Paths extends {}>(
22
38
  ) => {
23
39
  return useApi(
24
40
  ["get", path, JSON.stringify(input ?? {})],
25
- async () => {
26
- const res = await client.GET(path, input);
27
- if (res.error) throw res.error;
28
- return res.data;
29
- }
41
+ async () => unwrap(await client.GET(path, input))
30
42
  );
31
43
  },
32
44
 
@@ -36,45 +48,30 @@ export function createApiHooks<Paths extends {}>(
36
48
  ) => {
37
49
  return useApi(
38
50
  ["post", path, JSON.stringify(input ?? {})],
39
- async () => {
40
- const res = await client.POST(path, input);
41
- if (res.error) throw res.error;
42
- return res.data;
43
- }
51
+ async () =>
52
+ unwrap(await client.POST(path, input))
44
53
  );
45
54
  },
46
55
 
47
56
  // Using useMutation
48
57
  postMutation: <P extends PathsWithMethod<Paths, "post">>(path: P) => {
49
- return useApiMutation(async (input: any) => {
50
- const res = await client.POST(path, input);
51
- if (res.error) throw res.error;
52
- return res.data;
53
- });
58
+ return useApiMutation(async (input: any) =>
59
+ unwrap(await client.POST(path, input)));
54
60
  },
55
61
 
56
62
  putMutation: <P extends PathsWithMethod<Paths, "put">>(path: P) => {
57
- return useApiMutation(async (input: any) => {
58
- const res = await client.PUT(path, input);
59
- if (res.error) throw res.error;
60
- return res.data;
61
- });
63
+ return useApiMutation(async (input: any) =>
64
+ unwrap(await client.PUT(path, input)));
62
65
  },
63
66
 
64
67
  patchMutation: <P extends PathsWithMethod<Paths, "patch">>(path: P) => {
65
- return useApiMutation(async (input: any) => {
66
- const res = await client.PATCH(path, input);
67
- if (res.error) throw res.error;
68
- return res.data;
69
- });
68
+ return useApiMutation(async (input: any)=>
69
+ unwrap(await client.PATCH(path, input)));
70
70
  },
71
71
 
72
72
  deleteMutation: <P extends PathsWithMethod<Paths, "delete">>(path: P) => {
73
- return useApiMutation(async (input: any) => {
74
- const res = await client.DELETE(path, input);
75
- if (res.error) throw res.error;
76
- return res.data;
77
- });
73
+ return useApiMutation(async (input: any) =>
74
+ unwrap(await client.DELETE(path, input)));
78
75
  },
79
76
  };
80
77
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import clsx from "clsx";
4
4
  import type { ReactNode, JSX } from "react";
5
- import { createElement } from "react";
5
+ import { createElement, useState } from "react";
6
6
 
7
7
  type LabelAs = "h1" | "h2" | "h3" | "h4";
8
8
  type LabelSize = "s" | "m" | "l" | "xl";
@@ -17,6 +17,7 @@ interface TextareaInputProps
17
17
  labelSize?: LabelSize;
18
18
  rows?: number;
19
19
  className?: string;
20
+ characterLimit?: number;
20
21
  }
21
22
 
22
23
  export const TextAreaInput = ({
@@ -28,9 +29,11 @@ export const TextAreaInput = ({
28
29
  labelSize = "l",
29
30
  rows = 5,
30
31
  className,
32
+ characterLimit,
31
33
  ...props
32
34
  }: TextareaInputProps) => {
33
35
  const hasError = Boolean(error);
36
+
34
37
 
35
38
  const hintId = hint ? `${id}-hint` : undefined;
36
39
  const errorId = hasError ? `${id}-error` : undefined;
@@ -40,6 +43,11 @@ export const TextAreaInput = ({
40
43
  errorId
41
44
  ].filter(Boolean).join(" ") || undefined;
42
45
 
46
+ const hasLimit = characterLimit !== undefined;
47
+
48
+
49
+ const [count, setCount] = useState(0);
50
+
43
51
  const labelElement = (
44
52
  <label
45
53
  htmlFor={id}
@@ -51,11 +59,21 @@ export const TextAreaInput = ({
51
59
  </label>
52
60
  );
53
61
 
62
+ const remaining = hasLimit ? characterLimit - count : undefined;
63
+ const isOverLimit = remaining !== undefined && remaining < 0;
64
+ const characterMessage = remaining !== undefined
65
+ ? (remaining < 0
66
+ ? `You have ${Math.abs(remaining)} character${Math.abs(remaining) === 1 ? "" : "s"} too many`
67
+ : `You have ${remaining} character${remaining === 1 ? "" : "s"} remaining`)
68
+ : undefined;
69
+
54
70
  return (
55
71
  <div
56
72
  className={clsx("govuk-form-group", {
57
73
  "govuk-form-group--error": hasError,
74
+ "govuk-character-count": hasLimit
58
75
  })}
76
+ {...(hasLimit ? { "data-module": "govuk-character-count", "data-maxlength": characterLimit } : {})}
59
77
  >
60
78
  {labelAs
61
79
  ? createElement(
@@ -82,12 +100,30 @@ export const TextAreaInput = ({
82
100
  rows={rows}
83
101
  className={clsx(
84
102
  "govuk-textarea",
85
- { "govuk-textarea--error": hasError },
103
+ { "govuk-textarea--error": hasError ,
104
+ "govuk-js-character-count": hasLimit},
86
105
  className
87
106
  )}
107
+ style={isOverLimit ? { borderColor: "#d4351c" }: {}}
88
108
  aria-describedby={ariaDescribedBy}
89
109
  {...props}
110
+ onChange={e => {
111
+ setCount(e.target.value.length);
112
+ }}
90
113
  />
114
+ {hasLimit && (
115
+ <div
116
+ id="with-hint-info"
117
+ className={clsx(
118
+ "govuk-hint govuk-character-count__message",
119
+ { "govuk-error-message": isOverLimit}
120
+ )}
121
+ style={isOverLimit ? { color: "#d4351c", fontWeight: "bold" }: {}}>
122
+ {characterMessage}
123
+ </div>
124
+ )}
125
+
126
+
91
127
  </div>
92
128
  );
93
129
  };