@uktrade/react-component-library 0.11.0 → 0.13.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 (34) hide show
  1. package/dist/components/ApiBoundary/ApiBoundary.d.ts.map +1 -1
  2. package/dist/components/ApiBoundary/ApiBoundary.js +1 -1
  3. package/dist/components/ApiQuery/ApiCreateHooks.d.ts +42 -2
  4. package/dist/components/ApiQuery/ApiCreateHooks.d.ts.map +1 -1
  5. package/dist/components/ApiQuery/ApiCreateHooks.js +44 -37
  6. package/dist/components/ApiQuery/ApiProvider.d.ts +5 -5
  7. package/dist/components/ApiQuery/ApiProvider.d.ts.map +1 -1
  8. package/dist/components/ApiQuery/ApiProvider.js +4 -2
  9. package/dist/components/Inputs/CheckBoxesInput/CheckBoxGroupInput.d.ts +21 -0
  10. package/dist/components/Inputs/CheckBoxesInput/CheckBoxGroupInput.d.ts.map +1 -0
  11. package/dist/components/Inputs/CheckBoxesInput/CheckBoxGroupInput.js +14 -0
  12. package/dist/components/Inputs/SelectInput/SelectInput.d.ts +21 -0
  13. package/dist/components/Inputs/SelectInput/SelectInput.d.ts.map +1 -0
  14. package/dist/components/Inputs/SelectInput/SelectInput.js +18 -0
  15. package/dist/components/Inputs/TextInput/TextInput.d.ts +17 -0
  16. package/dist/components/Inputs/TextInput/TextInput.d.ts.map +1 -0
  17. package/dist/components/Inputs/TextInput/TextInput.js +19 -0
  18. package/dist/components/Inputs/index.d.ts +4 -0
  19. package/dist/components/Inputs/index.d.ts.map +1 -0
  20. package/dist/components/Inputs/index.js +3 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -0
  24. package/package.json +1 -1
  25. package/src/components/ApiBoundary/ApiBoundary.tsx +5 -4
  26. package/src/components/ApiQuery/ApiCreateHooks.tsx +40 -36
  27. package/src/components/ApiQuery/ApiProvider.tsx +8 -3
  28. package/src/components/Inputs/CheckBoxesInput/CheckBoxGroupInput.tsx +93 -0
  29. package/src/components/Inputs/DateInput/.gitkeep +0 -0
  30. package/src/components/Inputs/RadioInput/.gitkeep +0 -0
  31. package/src/components/Inputs/SelectInput/SelectInput.tsx +102 -0
  32. package/src/components/Inputs/TextInput/TextInput.tsx +105 -0
  33. package/src/components/Inputs/index.ts +3 -0
  34. package/src/index.ts +1 -0
@@ -1 +1 @@
1
- {"version":3,"file":"ApiBoundary.d.ts","sourceRoot":"","sources":["../../../src/components/ApiBoundary/ApiBoundary.tsx"],"names":[],"mappings":"AAKA,UAAU,gBAAgB;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAGD,eAAO,MAAM,WAAW,GAAI,mDAMzB,gBAAgB,4CAuBlB,CAAC"}
1
+ {"version":3,"file":"ApiBoundary.d.ts","sourceRoot":"","sources":["../../../src/components/ApiBoundary/ApiBoundary.tsx"],"names":[],"mappings":"AAKA,UAAU,gBAAgB;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAGD,eAAO,MAAM,WAAW,GAAI,mDAMzB,gBAAgB,4CAwBlB,CAAC"}
@@ -4,4 +4,4 @@ import { LoadingSpinner } from "../LoadingSpinner/LoadingSpinner";
4
4
  import styles from "./ApiBoundary.module.css";
5
5
  import { Button } from "../Button";
6
6
  /* Boundary for API-related states: loading, success, and error */
7
- export const ApiBoundary = ({ isLoading, isError, error, onRetry, children, }) => (_jsxs("div", { className: styles.root, "aria-live": "polite", children: [isLoading && _jsx(LoadingSpinner, {}), !isLoading && !isError && children, isError && (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.errorBackdrop }), _jsxs("section", { className: styles.error, children: [_jsx("h2", { className: "govuk-heading-m", children: "Error!" }), _jsx("pre", { className: styles.description, children: typeof error === "string" ? error : JSON.stringify(error, null, 2) }), _jsx(Button, { variant: "warning", onClick: onRetry, children: "Retry" })] })] }))] }));
7
+ export const ApiBoundary = ({ isLoading, isError, error, onRetry, children, }) => (_jsxs("div", { className: styles.root, "aria-live": "polite", children: [isLoading && _jsx(LoadingSpinner, {}), !isLoading && !isError && children, isError && (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.errorBackdrop }), _jsxs("section", { className: styles.error, children: [_jsx("h2", { className: "govuk-heading-m", children: "Error!" }), _jsx("pre", { className: styles.description, children: typeof error === "string" ? error : JSON.stringify(error, null, 2) }), onRetry && (_jsx(Button, { variant: "warning", onClick: onRetry, children: "Retry" }))] })] }))] }));
@@ -1,7 +1,7 @@
1
1
  import type { PathsWithMethod } from "openapi-typescript-helpers";
2
2
  import { createApiClient } from "./ApiCreateClient";
3
3
  export declare function createApiHooks<Paths extends {}>(client: ReturnType<typeof createApiClient<Paths>>): {
4
- get: <P extends PathsWithMethod<Paths, "get">>(path: P, params?: any) => {
4
+ get: <P extends PathsWithMethod<Paths, "get">>(path: P, input?: any) => {
5
5
  query: import("@tanstack/react-query").UseQueryResult<any, Error>;
6
6
  state: {
7
7
  data: any;
@@ -11,7 +11,7 @@ export declare function createApiHooks<Paths extends {}>(client: ReturnType<type
11
11
  refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<any, Error>>;
12
12
  };
13
13
  };
14
- post: <P extends PathsWithMethod<Paths, "post">>(path: P, params?: any) => {
14
+ post: <P extends PathsWithMethod<Paths, "post">>(path: P, input?: any) => {
15
15
  query: import("@tanstack/react-query").UseQueryResult<any, Error>;
16
16
  state: {
17
17
  data: any;
@@ -21,5 +21,45 @@ export declare function createApiHooks<Paths extends {}>(client: ReturnType<type
21
21
  refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<any, Error>>;
22
22
  };
23
23
  };
24
+ postMutation: <P extends PathsWithMethod<Paths, "post">>(path: P) => {
25
+ mutation: import("@tanstack/react-query").UseMutationResult<any, unknown, any, unknown>;
26
+ state: {
27
+ data: any;
28
+ isLoading: boolean;
29
+ isError: boolean;
30
+ error: unknown;
31
+ mutate: import("@tanstack/react-query").UseMutateFunction<any, unknown, any, unknown>;
32
+ };
33
+ };
34
+ putMutation: <P extends PathsWithMethod<Paths, "put">>(path: P) => {
35
+ mutation: import("@tanstack/react-query").UseMutationResult<any, unknown, any, unknown>;
36
+ state: {
37
+ data: any;
38
+ isLoading: boolean;
39
+ isError: boolean;
40
+ error: unknown;
41
+ mutate: import("@tanstack/react-query").UseMutateFunction<any, unknown, any, unknown>;
42
+ };
43
+ };
44
+ patchMutation: <P extends PathsWithMethod<Paths, "patch">>(path: P) => {
45
+ mutation: import("@tanstack/react-query").UseMutationResult<any, unknown, any, unknown>;
46
+ state: {
47
+ data: any;
48
+ isLoading: boolean;
49
+ isError: boolean;
50
+ error: unknown;
51
+ mutate: import("@tanstack/react-query").UseMutateFunction<any, unknown, any, unknown>;
52
+ };
53
+ };
54
+ deleteMutation: <P extends PathsWithMethod<Paths, "delete">>(path: P) => {
55
+ mutation: import("@tanstack/react-query").UseMutationResult<any, unknown, any, unknown>;
56
+ state: {
57
+ data: any;
58
+ isLoading: boolean;
59
+ isError: boolean;
60
+ error: unknown;
61
+ mutate: import("@tanstack/react-query").UseMutateFunction<any, unknown, any, unknown>;
62
+ };
63
+ };
24
64
  };
25
65
  //# sourceMappingURL=ApiCreateHooks.d.ts.map
@@ -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;AAGpD,wBAAgB,cAAc,CAAC,KAAK,SAAS,EAAE,EAC7C,MAAM,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;UAMzC,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,QACrC,CAAC,WACE,GAAG;;;;;;;;;;WAYP,CAAC,SAAS,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,QACvC,CAAC,WACE,GAAG;;;;;;;;;;EA6CjB"}
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,55 +1,62 @@
1
1
  "use client";
2
2
  import { createApiClient } from "./ApiCreateClient";
3
- import { useApi } from "./ApiProvider";
3
+ import { useApi, useApiMutation } from "./ApiProvider";
4
+ // API hook factory that converts a typed OpenAPI client into React hooks (`useApi` and `useApiMutation`) that the UI can use.
5
+ // Provides a simple interface for making API calls and handling loading and error states in your React components.
4
6
  export function createApiHooks(client) {
7
+ // ToDo: Generalize this (params?: any) and fight openapi-fetch’s internal generics to get correct types from PathsWithMethod. Same for the mutation methods.
5
8
  // type GetParams<P extends PathsWithMethod<Paths, "get">> =
6
9
  // Parameters<typeof client.GET<P>>[1];
10
+ // Using useQuery
7
11
  return {
8
- get: (path, params // GetParams<P> More work to be done to fight openapi-fetch’s internal generics!!!!
9
- ) => {
10
- return useApi(["get", path, JSON.stringify(params ?? {})], async () => {
11
- const res = await client.GET(path, params);
12
+ get: (path, input) => {
13
+ return useApi(["get", path, JSON.stringify(input ?? {})], async () => {
14
+ const res = await client.GET(path, input);
12
15
  if (res.error)
13
16
  throw res.error;
14
17
  return res.data;
15
18
  });
16
19
  },
17
- post: (path, params // GetParams<P> More work to be done to fight openapi-fetch’s internal generics!!!!
18
- ) => {
19
- return useApi(["post", path, JSON.stringify(params ?? {})], async () => {
20
- const res = await client.POST(path, params);
20
+ 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
+ });
27
+ },
28
+ // Using useMutation
29
+ 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
+ });
36
+ },
37
+ 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
+ });
44
+ },
45
+ 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
+ });
52
+ },
53
+ deleteMutation: (path) => {
54
+ return useApiMutation(async (input) => {
55
+ const res = await client.DELETE(path, input);
21
56
  if (res.error)
22
57
  throw res.error;
23
58
  return res.data;
24
59
  });
25
60
  },
26
- // TODO: implement other methods (put, delete), furthermore generalize the mutation method
27
- // for all methods (No GET) to avoid React Query’s useQuery from running automatically on mount
28
- // Same here (params?: any)
29
- // postMutation: <P extends PathsWithMethod<Paths, "post">>(path: P, params?: any) => {
30
- // return useApiMutation(async () => {
31
- // const res = await client.POST(path, params);
32
- // if (res.error) throw res.error;
33
- // return res.data;
34
- // });
35
- // },
36
- // mutation: <
37
- // M extends "post" | "put" | "patch" | "delete",
38
- // P extends PathsWithMethod<Paths, M>
39
- // >(
40
- // method: M,
41
- // path: P,
42
- // params?: any
43
- // ) => {
44
- // return useApiMutation(async () => {
45
- // const res = await client[method.toUpperCase() as "POST" | "PUT" | "PATCH" | "DELETE"](path, params);
46
- // if (res.error) throw res.error;
47
- // return res.data;
48
- // });
49
- // }
50
- // ToDo: Generalize this:
51
- // const res = await client.POST(path, params);
52
- // if (res.error) throw res.error;
53
- // return res.data;
54
61
  };
55
62
  }
@@ -21,14 +21,14 @@ export declare function useApi<T>(key: QueryKey, queryFn: () => Promise<T>): {
21
21
  refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<import("@tanstack/query-core").NoInfer<T>, Error>>;
22
22
  };
23
23
  };
24
- export declare function useApiMutation<T>(mutationFn: () => Promise<T>): {
25
- mutation: import("@tanstack/react-query").UseMutationResult<T, Error, void, unknown>;
24
+ export declare function useApiMutation<TData, TVariables = void>(mutationFn: (variables: TVariables) => Promise<TData>): {
25
+ mutation: import("@tanstack/react-query").UseMutationResult<TData, unknown, TVariables, unknown>;
26
26
  state: {
27
- data: T | undefined;
27
+ data: TData | undefined;
28
28
  isLoading: boolean;
29
29
  isError: boolean;
30
- error: Error | null;
31
- mutate: import("@tanstack/react-query").UseMutateFunction<T, Error, void, unknown>;
30
+ error: unknown;
31
+ mutate: import("@tanstack/react-query").UseMutateFunction<TData, unknown, TVariables, unknown>;
32
32
  };
33
33
  };
34
34
  //# sourceMappingURL=ApiProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ApiProvider.d.ts","sourceRoot":"","sources":["../../../src/components/ApiQuery/ApiProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,WAAW,EAIX,KAAK,QAAQ,EACd,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AASvC,wBAAgB,cAAc,gBAa7B;AAMD,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CAMhE;AAED,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI;IAC7B,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAYF,wBAAgB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC;;;;;;;;;EAgBjE;AAYD,wBAAgB,cAAc,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC;;;;;;;;;EAa7D"}
1
+ {"version":3,"file":"ApiProvider.d.ts","sourceRoot":"","sources":["../../../src/components/ApiQuery/ApiProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,WAAW,EAIX,KAAK,QAAQ,EACd,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AASvC,wBAAgB,cAAc,gBAa7B;AAMD,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CAMhE;AAED,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI;IAC7B,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAYF,wBAAgB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC;;;;;;;;;EAgBjE;AAYD,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,EACrD,UAAU,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,OAAO,CAAC,KAAK,CAAC;;;;;;;;;EAgBtD"}
@@ -65,9 +65,11 @@ It accepts `mutationFn` as function that performs the mutation request, and It r
65
65
  - `state` — a simplified structure with commonly used properties like `data`, `isLoading`, `isError`
66
66
  */
67
67
  export function useApiMutation(mutationFn) {
68
- const mutation = useMutation({ mutationFn });
68
+ const mutation = useMutation({
69
+ mutationFn,
70
+ });
69
71
  return {
70
- mutation, // Full React Query object
72
+ mutation,
71
73
  state: {
72
74
  data: mutation.data,
73
75
  isLoading: mutation.isPending,
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from "react";
2
+ type LegendAs = "h1" | "h2" | "h3" | "h4";
3
+ type LegendSize = "s" | "m" | "l" | "xl";
4
+ interface CheckboxOption {
5
+ id: string;
6
+ value: string;
7
+ label: ReactNode;
8
+ checked?: boolean;
9
+ }
10
+ interface CheckboxGroupProps {
11
+ legend: ReactNode;
12
+ hint?: ReactNode;
13
+ error?: string;
14
+ options: CheckboxOption[];
15
+ name: string;
16
+ legendAs?: LegendAs;
17
+ legendSize?: LegendSize;
18
+ }
19
+ export declare const CheckboxGroup: ({ legend, hint, error, options, name, legendAs, legendSize, }: CheckboxGroupProps) => import("react/jsx-runtime").JSX.Element;
20
+ export {};
21
+ //# sourceMappingURL=CheckBoxGroupInput.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CheckBoxGroupInput.d.ts","sourceRoot":"","sources":["../../../../src/components/Inputs/CheckBoxesInput/CheckBoxGroupInput.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,KAAK,QAAQ,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAC1C,KAAK,UAAU,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,CAAC;AAEzC,UAAU,cAAc;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,UAAU,kBAAkB;IACxB,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;CAC3B;AAED,eAAO,MAAM,aAAa,GAAI,+DAQ3B,kBAAkB,4CA0DpB,CAAC"}
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import clsx from "clsx";
4
+ import { createElement } from "react";
5
+ export const CheckboxGroup = ({ legend, hint, error, options, name, legendAs = "h4", legendSize = "m", }) => {
6
+ const hasError = Boolean(error);
7
+ const errorId = hasError ? `${name}-error` : undefined;
8
+ const legendHeading = createElement(legendAs, { className: "govuk-fieldset__heading" }, legend);
9
+ return (_jsx("div", { className: clsx("govuk-form-group", {
10
+ "govuk-form-group--error": hasError,
11
+ }), children: _jsxs("fieldset", { className: "govuk-fieldset", "aria-describedby": errorId, children: [_jsx("legend", { className: clsx("govuk-fieldset__legend", {
12
+ [`govuk-fieldset__legend--${legendSize}`]: legendSize !== "m",
13
+ }), children: legendHeading }), hint && _jsx("div", { 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("div", { className: "govuk-checkboxes", "data-module": "govuk-checkboxes", children: options.map((opt) => (_jsxs("div", { className: "govuk-checkboxes__item", children: [_jsx("input", { className: "govuk-checkboxes__input", id: opt.id, name: name, type: "checkbox", value: opt.value, defaultChecked: opt.checked }), _jsx("label", { className: "govuk-label govuk-checkboxes__label", htmlFor: opt.id, children: opt.label })] }, opt.id))) })] }) }));
14
+ };
@@ -0,0 +1,21 @@
1
+ import type { JSX, ReactNode } from "react";
2
+ type LabelAs = "h1" | "h2" | "h3" | "h4";
3
+ type LabelSize = "s" | "m" | "l" | "xl";
4
+ type SelectProps = Omit<JSX.IntrinsicElements["select"], "id">;
5
+ interface SelectInputProps extends SelectProps {
6
+ id: string;
7
+ label: ReactNode;
8
+ hint?: ReactNode;
9
+ error?: string;
10
+ options: {
11
+ value: string;
12
+ label: string;
13
+ }[];
14
+ placeholder?: string;
15
+ labelAs?: LabelAs;
16
+ labelSize?: LabelSize;
17
+ className?: string;
18
+ }
19
+ export declare const SelectInput: ({ id, label, hint, error, options, placeholder, labelAs, labelSize, className, ...props }: SelectInputProps) => import("react/jsx-runtime").JSX.Element;
20
+ export {};
21
+ //# sourceMappingURL=SelectInput.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SelectInput.d.ts","sourceRoot":"","sources":["../../../../src/components/Inputs/SelectInput/SelectInput.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,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;AACxC,KAAK,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC;AAE/D,UAAU,gBAAiB,SAAQ,WAAW;IAC1C,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,WAAW,GAAI,2FAWzB,gBAAgB,4CAoElB,CAAC"}
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import clsx from "clsx";
4
+ import { createElement } from "react";
5
+ export const SelectInput = ({ id, label, hint, error, options, placeholder, labelAs, labelSize = "m", className, ...props }) => {
6
+ const hasError = Boolean(error);
7
+ const hintId = hint ? `${id}-hint` : undefined;
8
+ const errorId = hasError ? `${id}-error` : undefined;
9
+ const ariaDescribedBy = [hintId, errorId].filter(Boolean).join(" ") || undefined;
10
+ const labelElement = (_jsx("label", { htmlFor: id, className: clsx("govuk-label", {
11
+ [`govuk-label--${labelSize}`]: labelSize !== "m",
12
+ }), children: label }));
13
+ return (_jsxs("div", { className: clsx("govuk-form-group", {
14
+ "govuk-form-group--error": hasError,
15
+ }), children: [labelAs
16
+ ? createElement(labelAs, { className: "govuk-label-wrapper" }, labelElement)
17
+ : 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] })), _jsxs("select", { id: id, className: clsx("govuk-select", { "govuk-select--error": hasError }, className), "aria-describedby": ariaDescribedBy, ...props, children: [placeholder && (_jsx("option", { value: "", disabled: true, hidden: true, children: placeholder })), options.map((opt) => (_jsx("option", { value: opt.value, children: opt.label }, opt.value)))] })] }));
18
+ };
@@ -0,0 +1,17 @@
1
+ import type { JSX, ReactNode } from "react";
2
+ type LabelAs = "h1" | "h2" | "h3" | "h4";
3
+ type InputProps = Omit<JSX.IntrinsicElements["input"], "id">;
4
+ interface TextInputProps extends InputProps {
5
+ id: string;
6
+ label: ReactNode;
7
+ hint?: ReactNode;
8
+ error?: string;
9
+ width?: "10" | "20" | "30";
10
+ labelSize?: "s" | "m" | "l" | "xl";
11
+ labelAs?: LabelAs;
12
+ placeholder?: string;
13
+ suffix?: ReactNode;
14
+ }
15
+ export declare const TextInput: ({ id, label, hint, error, width, labelSize, labelAs, className, placeholder, suffix, ...props }: TextInputProps) => import("react/jsx-runtime").JSX.Element;
16
+ export {};
17
+ //# sourceMappingURL=TextInput.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TextInput.d.ts","sourceRoot":"","sources":["../../../../src/components/Inputs/TextInput/TextInput.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAG5C,KAAK,OAAO,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AACzC,KAAK,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC;AAE7D,UAAU,cAAe,SAAQ,UAAU;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC3B,SAAS,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,CAAC;IACnC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,eAAO,MAAM,SAAS,GAAI,iGAYvB,cAAc,4CAuEhB,CAAC"}
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import clsx from "clsx";
4
+ import { createElement } from "react";
5
+ export const TextInput = ({ id, label, hint, error, width = "20", labelSize = "m", labelAs, className, placeholder, suffix, ...props }) => {
6
+ const hasError = Boolean(error);
7
+ const hintId = hint ? `${id}-hint` : undefined;
8
+ const errorId = hasError ? `${id}-error` : undefined;
9
+ const describedBy = [hintId, errorId].filter(Boolean).join(" ") || undefined;
10
+ const labelElement = (_jsx("label", { htmlFor: id, className: clsx("govuk-label", {
11
+ [`govuk-label--${labelSize}`]: labelSize !== "m",
12
+ }), children: label }));
13
+ const input = (_jsx("input", { id: id, className: clsx("govuk-input", `govuk-input--width-${width}`, className), "aria-invalid": hasError, "aria-describedby": describedBy, placeholder: placeholder, ...props }));
14
+ return (_jsxs("div", { className: clsx("govuk-form-group", {
15
+ "govuk-form-group--error": hasError,
16
+ }), children: [labelAs
17
+ ? createElement(labelAs, { className: "govuk-label-wrapper" }, labelElement)
18
+ : 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] })), suffix ? (_jsxs("div", { className: "govuk-input__wrapper", children: [input, _jsx("div", { className: "govuk-input__suffix", "aria-hidden": "true", children: suffix })] })) : (input)] }));
19
+ };
@@ -0,0 +1,4 @@
1
+ export * from "./TextInput/TextInput";
2
+ export * from "./CheckBoxesInput/CheckBoxGroupInput";
3
+ export * from "./SelectInput/SelectInput";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Inputs/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,sCAAsC,CAAC;AACrD,cAAc,2BAA2B,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from "./TextInput/TextInput";
2
+ export * from "./CheckBoxesInput/CheckBoxGroupInput";
3
+ export * from "./SelectInput/SelectInput";
package/dist/index.d.ts CHANGED
@@ -11,4 +11,5 @@ export * from "./components/BackLink";
11
11
  export * from "./components/LoadingSpinner/LoadingSpinner";
12
12
  export * from "./components/ApiBoundary/ApiBoundary";
13
13
  export * from "./components/ApiQuery";
14
+ export * from "./components/Inputs";
14
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sCAAsC,CAAC;AACrD,cAAc,sCAAsC,CAAC;AACrD,cAAc,wCAAwC,CAAC;AACvD,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,4CAA4C,CAAC;AAC3D,cAAc,sCAAsC,CAAC;AACrD,cAAc,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sCAAsC,CAAC;AACrD,cAAc,sCAAsC,CAAC;AACrD,cAAc,wCAAwC,CAAC;AACvD,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,4CAA4C,CAAC;AAC3D,cAAc,sCAAsC,CAAC;AACrD,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -11,3 +11,4 @@ export * from "./components/BackLink";
11
11
  export * from "./components/LoadingSpinner/LoadingSpinner";
12
12
  export * from "./components/ApiBoundary/ApiBoundary";
13
13
  export * from "./components/ApiQuery";
14
+ export * from "./components/Inputs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uktrade/react-component-library",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
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",
@@ -33,10 +33,11 @@ export const ApiBoundary = ({
33
33
  {typeof error === "string" ? error : JSON.stringify(error, null, 2)}
34
34
  </pre>
35
35
 
36
- {/* TODO: Make this button optional since it cannot always be required */}
37
- <Button variant="warning" onClick={onRetry}>
38
- Retry
39
- </Button>
36
+ {onRetry && (
37
+ <Button variant="warning" onClick={onRetry}>
38
+ Retry
39
+ </Button>
40
+ )}
40
41
  </section>
41
42
  </>
42
43
  )}
@@ -3,23 +3,27 @@
3
3
  import type { PathsWithMethod } from "openapi-typescript-helpers";
4
4
 
5
5
  import { createApiClient } from "./ApiCreateClient";
6
- import { useApi } from "./ApiProvider";
6
+ import { useApi, useApiMutation } from "./ApiProvider";
7
7
 
8
+ // API hook factory that converts a typed OpenAPI client into React hooks (`useApi` and `useApiMutation`) that the UI can use.
9
+ // Provides a simple interface for making API calls and handling loading and error states in your React components.
8
10
  export function createApiHooks<Paths extends {}>(
9
11
  client: ReturnType<typeof createApiClient<Paths>>
10
12
  ) {
13
+ // ToDo: Generalize this (params?: any) and fight openapi-fetch’s internal generics to get correct types from PathsWithMethod. Same for the mutation methods.
11
14
  // type GetParams<P extends PathsWithMethod<Paths, "get">> =
12
15
  // Parameters<typeof client.GET<P>>[1];
13
16
 
17
+ // Using useQuery
14
18
  return {
15
19
  get: <P extends PathsWithMethod<Paths, "get">>(
16
20
  path: P,
17
- params?: any // GetParams<P> More work to be done to fight openapi-fetch’s internal generics!!!!
21
+ input?: any
18
22
  ) => {
19
23
  return useApi(
20
- ["get", path, JSON.stringify(params ?? {})],
24
+ ["get", path, JSON.stringify(input ?? {})],
21
25
  async () => {
22
- const res = await client.GET(path, params);
26
+ const res = await client.GET(path, input);
23
27
  if (res.error) throw res.error;
24
28
  return res.data;
25
29
  }
@@ -28,49 +32,49 @@ export function createApiHooks<Paths extends {}>(
28
32
 
29
33
  post: <P extends PathsWithMethod<Paths, "post">>(
30
34
  path: P,
31
- params?: any // GetParams<P> More work to be done to fight openapi-fetch’s internal generics!!!!
35
+ input?: any
32
36
  ) => {
33
37
  return useApi(
34
- ["post", path, JSON.stringify(params ?? {})],
38
+ ["post", path, JSON.stringify(input ?? {})],
35
39
  async () => {
36
- const res = await client.POST(path, params);
40
+ const res = await client.POST(path, input);
37
41
  if (res.error) throw res.error;
38
42
  return res.data;
39
43
  }
40
44
  );
41
45
  },
42
46
 
43
- // TODO: implement other methods (put, delete), furthermore generalize the mutation method
44
- // for all methods (No GET) to avoid React Query’s useQuery from running automatically on mount
45
-
46
- // Same here (params?: any)
47
- // postMutation: <P extends PathsWithMethod<Paths, "post">>(path: P, params?: any) => {
48
- // return useApiMutation(async () => {
49
- // const res = await client.POST(path, params);
50
- // if (res.error) throw res.error;
51
- // return res.data;
52
- // });
53
- // },
47
+ // Using useMutation
48
+ 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
+ });
54
+ },
54
55
 
55
- // mutation: <
56
- // M extends "post" | "put" | "patch" | "delete",
57
- // P extends PathsWithMethod<Paths, M>
58
- // >(
59
- // method: M,
60
- // path: P,
61
- // params?: any
62
- // ) => {
63
- // return useApiMutation(async () => {
64
- // const res = await client[method.toUpperCase() as "POST" | "PUT" | "PATCH" | "DELETE"](path, params);
65
- // if (res.error) throw res.error;
66
- // return res.data;
67
- // });
68
- // }
56
+ 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
+ });
62
+ },
69
63
 
70
- // ToDo: Generalize this:
71
- // const res = await client.POST(path, params);
72
- // if (res.error) throw res.error;
73
- // return res.data;
64
+ 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
+ });
70
+ },
74
71
 
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
+ });
78
+ },
75
79
  };
76
80
  }
@@ -89,11 +89,15 @@ It accepts `mutationFn` as function that performs the mutation request, and It r
89
89
  - `mutation` — the full React Query mutation object
90
90
  - `state` — a simplified structure with commonly used properties like `data`, `isLoading`, `isError`
91
91
  */
92
- export function useApiMutation<T>(mutationFn: () => Promise<T>) {
93
- const mutation = useMutation({ mutationFn });
92
+ export function useApiMutation<TData, TVariables = void>(
93
+ mutationFn: (variables: TVariables) => Promise<TData>
94
+ ) {
95
+ const mutation = useMutation<TData, unknown, TVariables>({
96
+ mutationFn,
97
+ });
94
98
 
95
99
  return {
96
- mutation, // Full React Query object
100
+ mutation,
97
101
  state: {
98
102
  data: mutation.data,
99
103
  isLoading: mutation.isPending,
@@ -103,3 +107,4 @@ export function useApiMutation<T>(mutationFn: () => Promise<T>) {
103
107
  },
104
108
  };
105
109
  }
110
+
@@ -0,0 +1,93 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import type { ReactNode } from "react";
5
+ import { createElement } from "react";
6
+
7
+ type LegendAs = "h1" | "h2" | "h3" | "h4";
8
+ type LegendSize = "s" | "m" | "l" | "xl";
9
+
10
+ interface CheckboxOption {
11
+ id: string;
12
+ value: string;
13
+ label: ReactNode;
14
+ checked?: boolean;
15
+ }
16
+
17
+ interface CheckboxGroupProps {
18
+ legend: ReactNode;
19
+ hint?: ReactNode;
20
+ error?: string;
21
+ options: CheckboxOption[];
22
+ name: string;
23
+ legendAs?: LegendAs;
24
+ legendSize?: LegendSize;
25
+ }
26
+
27
+ export const CheckboxGroup = ({
28
+ legend,
29
+ hint,
30
+ error,
31
+ options,
32
+ name,
33
+ legendAs = "h4",
34
+ legendSize = "m",
35
+ }: CheckboxGroupProps) => {
36
+ const hasError = Boolean(error);
37
+ const errorId = hasError ? `${name}-error` : undefined;
38
+
39
+ const legendHeading = createElement(
40
+ legendAs,
41
+ { className: "govuk-fieldset__heading" },
42
+ legend
43
+ );
44
+
45
+ return (
46
+ <div
47
+ className={clsx("govuk-form-group", {
48
+ "govuk-form-group--error": hasError,
49
+ })}
50
+ >
51
+ <fieldset className="govuk-fieldset" aria-describedby={errorId}>
52
+ <legend
53
+ className={clsx("govuk-fieldset__legend", {
54
+ [`govuk-fieldset__legend--${legendSize}`]:
55
+ legendSize !== "m",
56
+ })}
57
+ >
58
+ {legendHeading}
59
+ </legend>
60
+
61
+ {hint && <div className="govuk-hint">{hint}</div>}
62
+
63
+ {hasError && (
64
+ <p id={errorId} className="govuk-error-message">
65
+ <span className="govuk-visually-hidden">Error:</span>{" "}
66
+ {error}
67
+ </p>
68
+ )}
69
+
70
+ <div className="govuk-checkboxes" data-module="govuk-checkboxes">
71
+ {options.map((opt) => (
72
+ <div key={opt.id} className="govuk-checkboxes__item">
73
+ <input
74
+ className="govuk-checkboxes__input"
75
+ id={opt.id}
76
+ name={name}
77
+ type="checkbox"
78
+ value={opt.value}
79
+ defaultChecked={opt.checked}
80
+ />
81
+ <label
82
+ className="govuk-label govuk-checkboxes__label"
83
+ htmlFor={opt.id}
84
+ >
85
+ {opt.label}
86
+ </label>
87
+ </div>
88
+ ))}
89
+ </div>
90
+ </fieldset>
91
+ </div>
92
+ );
93
+ };
File without changes
File without changes
@@ -0,0 +1,102 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import type { JSX, ReactNode } from "react";
5
+ import { createElement } from "react";
6
+
7
+ type LabelAs = "h1" | "h2" | "h3" | "h4";
8
+ type LabelSize = "s" | "m" | "l" | "xl";
9
+ type SelectProps = Omit<JSX.IntrinsicElements["select"], "id">;
10
+
11
+ interface SelectInputProps extends SelectProps {
12
+ id: string;
13
+ label: ReactNode;
14
+ hint?: ReactNode;
15
+ error?: string;
16
+ options: { value: string; label: string }[];
17
+ placeholder?: string;
18
+ labelAs?: LabelAs;
19
+ labelSize?: LabelSize;
20
+ className?: string;
21
+ }
22
+
23
+ export const SelectInput = ({
24
+ id,
25
+ label,
26
+ hint,
27
+ error,
28
+ options,
29
+ placeholder,
30
+ labelAs,
31
+ labelSize = "m",
32
+ className,
33
+ ...props
34
+ }: SelectInputProps) => {
35
+ const hasError = Boolean(error);
36
+ const hintId = hint ? `${id}-hint` : undefined;
37
+ const errorId = hasError ? `${id}-error` : undefined;
38
+ const ariaDescribedBy =
39
+ [hintId, errorId].filter(Boolean).join(" ") || undefined;
40
+
41
+ const labelElement = (
42
+ <label
43
+ htmlFor={id}
44
+ className={clsx("govuk-label", {
45
+ [`govuk-label--${labelSize}`]: labelSize !== "m",
46
+ })}
47
+ >
48
+ {label}
49
+ </label>
50
+ );
51
+
52
+ return (
53
+ <div
54
+ className={clsx("govuk-form-group", {
55
+ "govuk-form-group--error": hasError,
56
+ })}
57
+ >
58
+ {labelAs
59
+ ? createElement(
60
+ labelAs,
61
+ { className: "govuk-label-wrapper" },
62
+ labelElement
63
+ )
64
+ : labelElement}
65
+
66
+ {hint && (
67
+ <div id={hintId} className="govuk-hint">
68
+ {hint}
69
+ </div>
70
+ )}
71
+
72
+ {hasError && (
73
+ <p id={errorId} className="govuk-error-message">
74
+ <span className="govuk-visually-hidden">Error:</span> {error}
75
+ </p>
76
+ )}
77
+
78
+ <select
79
+ id={id}
80
+ className={clsx(
81
+ "govuk-select",
82
+ { "govuk-select--error": hasError },
83
+ className
84
+ )}
85
+ aria-describedby={ariaDescribedBy}
86
+ {...props}
87
+ >
88
+ {placeholder && (
89
+ <option value="" disabled hidden>
90
+ {placeholder}
91
+ </option>
92
+ )}
93
+
94
+ {options.map((opt) => (
95
+ <option key={opt.value} value={opt.value}>
96
+ {opt.label}
97
+ </option>
98
+ ))}
99
+ </select>
100
+ </div>
101
+ );
102
+ };
@@ -0,0 +1,105 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import type { JSX, ReactNode } from "react";
5
+ import { createElement } from "react";
6
+
7
+ type LabelAs = "h1" | "h2" | "h3" | "h4";
8
+ type InputProps = Omit<JSX.IntrinsicElements["input"], "id">;
9
+
10
+ interface TextInputProps extends InputProps {
11
+ id: string;
12
+ label: ReactNode;
13
+ hint?: ReactNode;
14
+ error?: string;
15
+ width?: "10" | "20" | "30";
16
+ labelSize?: "s" | "m" | "l" | "xl";
17
+ labelAs?: LabelAs;
18
+ placeholder?: string;
19
+ suffix?: ReactNode;
20
+ }
21
+
22
+ export const TextInput = ({
23
+ id,
24
+ label,
25
+ hint,
26
+ error,
27
+ width = "20",
28
+ labelSize = "m",
29
+ labelAs,
30
+ className,
31
+ placeholder,
32
+ suffix,
33
+ ...props
34
+ }: TextInputProps) => {
35
+ const hasError = Boolean(error);
36
+
37
+ const hintId = hint ? `${id}-hint` : undefined;
38
+ const errorId = hasError ? `${id}-error` : undefined;
39
+ const describedBy = [hintId, errorId].filter(Boolean).join(" ") || undefined;
40
+
41
+ const labelElement = (
42
+ <label
43
+ htmlFor={id}
44
+ className={clsx("govuk-label", {
45
+ [`govuk-label--${labelSize}`]: labelSize !== "m",
46
+ })}
47
+ >
48
+ {label}
49
+ </label>
50
+ );
51
+
52
+ const input = (
53
+ <input
54
+ id={id}
55
+ className={clsx(
56
+ "govuk-input",
57
+ `govuk-input--width-${width}`,
58
+ className
59
+ )}
60
+ aria-invalid={hasError}
61
+ aria-describedby={describedBy}
62
+ placeholder={placeholder}
63
+ {...props}
64
+ />
65
+ );
66
+
67
+ return (
68
+ <div
69
+ className={clsx("govuk-form-group", {
70
+ "govuk-form-group--error": hasError,
71
+ })}
72
+ >
73
+ {labelAs
74
+ ? createElement(
75
+ labelAs,
76
+ { className: "govuk-label-wrapper" },
77
+ labelElement
78
+ )
79
+ : labelElement}
80
+
81
+ {hint && (
82
+ <div id={hintId} className="govuk-hint">
83
+ {hint}
84
+ </div>
85
+ )}
86
+
87
+ {hasError && (
88
+ <p id={errorId} className="govuk-error-message">
89
+ <span className="govuk-visually-hidden">Error:</span> {error}
90
+ </p>
91
+ )}
92
+
93
+ {suffix ? (
94
+ <div className="govuk-input__wrapper">
95
+ {input}
96
+ <div className="govuk-input__suffix" aria-hidden="true">
97
+ {suffix}
98
+ </div>
99
+ </div>
100
+ ) : (
101
+ input
102
+ )}
103
+ </div>
104
+ );
105
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./TextInput/TextInput";
2
+ export * from "./CheckBoxesInput/CheckBoxGroupInput";
3
+ export * from "./SelectInput/SelectInput";
package/src/index.ts CHANGED
@@ -11,3 +11,4 @@ export * from "./components/BackLink";
11
11
  export * from "./components/LoadingSpinner/LoadingSpinner";
12
12
  export * from "./components/ApiBoundary/ApiBoundary";
13
13
  export * from "./components/ApiQuery";
14
+ export * from "./components/Inputs";