react-form-ux 0.0.1 → 0.1.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/LICENSE +21 -0
- package/README.md +183 -6
- package/dist/index.cjs +112 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.mjs +84 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +37 -8
- package/index.js +0 -2
- package/react-form-ux-0.0.1.tgz +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sayan Paul
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,11 +1,188 @@
|
|
|
1
1
|
# react-form-ux
|
|
2
2
|
|
|
3
|
-
UX
|
|
3
|
+
Fix common React form UX problems in seconds.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`react-form-ux` is a lightweight utility library that improves the **user experience of React forms** by handling common UX tasks like:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- focusing the first invalid field
|
|
8
|
+
- scrolling to validation errors
|
|
9
|
+
- generating accessible error summaries
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
The library is **headless and framework-agnostic**, so it works with any React form solution.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ✨ Features
|
|
16
|
+
|
|
17
|
+
- Focus the **first invalid input automatically**
|
|
18
|
+
- Scroll to validation errors in long forms
|
|
19
|
+
- Generate **accessible error summaries**
|
|
20
|
+
- Works with **React Hook Form, Formik, or custom forms**
|
|
21
|
+
- Tiny bundle size
|
|
22
|
+
- Headless API (bring your own UI)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 📦 Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install react-form-ux
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
or
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
yarn add react-form-ux
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## ⚡Quick Example
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
import { useFormUX } from "react-form-ux";
|
|
44
|
+
|
|
45
|
+
function MyForm({ errors }) {
|
|
46
|
+
const { focusFirstError } = useFormUX({ errors });
|
|
47
|
+
|
|
48
|
+
const handleSubmit = () => {
|
|
49
|
+
if (Object.keys(errors).length > 0) {
|
|
50
|
+
focusFirstError();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return <form onSubmit={handleSubmit}>{/* form inputs */}</form>;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 🧠 The Problem
|
|
61
|
+
|
|
62
|
+
Most React form libraries focus on **form state and validation**, but developers still need to manually implement UX behaviors such as:
|
|
63
|
+
|
|
64
|
+
- focusing the first invalid input after submit
|
|
65
|
+
- scrolling long forms to the first error
|
|
66
|
+
- showing accessible error summaries
|
|
67
|
+
- guiding the user to fix validation issues
|
|
68
|
+
|
|
69
|
+
Example of common ad-hoc code developers write repeatedly:
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
const firstError = document.querySelector("[aria-invalid='true']");
|
|
73
|
+
firstError?.focus();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This logic gets duplicated across projects.
|
|
77
|
+
|
|
78
|
+
`react-form-ux` provides **reusable UX primitives** so you don’t have to rewrite it.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 🛠 Usage
|
|
83
|
+
|
|
84
|
+
Basic usage with any form library:
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
import { useFormUX } from "react-form-ux";
|
|
88
|
+
|
|
89
|
+
const { focusFirstError, scrollToError } = useFormUX({
|
|
90
|
+
errors,
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Available helpers:
|
|
95
|
+
|
|
96
|
+
| Function | Description |
|
|
97
|
+
| ----------------- | --------------------------------------------- |
|
|
98
|
+
| focusFirstError() | Focus the first invalid input field |
|
|
99
|
+
| scrollToError() | Scroll smoothly to the first validation error |
|
|
100
|
+
| getErrorFields() | Get a list of fields with validation errors |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 📋 ErrorSummary Component
|
|
105
|
+
|
|
106
|
+
You can also display a summary of validation errors.
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
import { ErrorSummary } from "react-form-ux";
|
|
110
|
+
|
|
111
|
+
<ErrorSummary errors={errors} />
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This improves accessibility and helps users quickly identify issues in long forms.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 🔗 Example with React Hook Form
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
import { useForm } from "react-hook-form";
|
|
122
|
+
import { useFormUX } from "react-form-ux";
|
|
123
|
+
|
|
124
|
+
function SignupForm() {
|
|
125
|
+
const { register, handleSubmit, formState } = useForm();
|
|
126
|
+
|
|
127
|
+
const { focusFirstError } = useFormUX({
|
|
128
|
+
errors: formState.errors
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const onSubmit = () => {};
|
|
132
|
+
|
|
133
|
+
const onError = () => {
|
|
134
|
+
focusFirstError();
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<form onSubmit={handleSubmit(onSubmit, onError)}>
|
|
139
|
+
<input {...register("email")} />
|
|
140
|
+
<input {...register("password")} />
|
|
141
|
+
|
|
142
|
+
<button type="submit">Submit</button>
|
|
143
|
+
</form>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## ⚙️ Compatibility
|
|
151
|
+
|
|
152
|
+
`react-form-ux` works with:
|
|
153
|
+
|
|
154
|
+
- React 18+
|
|
155
|
+
- React Hook Form
|
|
156
|
+
- Formik
|
|
157
|
+
- Custom React forms
|
|
158
|
+
|
|
159
|
+
The library does not depend on any specific form framework.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 🚧 Status
|
|
164
|
+
|
|
165
|
+
Early development.
|
|
166
|
+
|
|
167
|
+
Current features:
|
|
168
|
+
|
|
169
|
+
- `focusFirstError`
|
|
170
|
+
- `scrollToError`
|
|
171
|
+
- `getErrorFields`
|
|
172
|
+
- `ErrorSummary` component
|
|
173
|
+
|
|
174
|
+
More improvements coming soon.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 📜 License
|
|
179
|
+
|
|
180
|
+
MIT License
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 🤝 Contributing
|
|
185
|
+
|
|
186
|
+
Contributions and ideas are welcome.
|
|
187
|
+
|
|
188
|
+
If you find a bug or have a feature suggestion, please open an issue.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ErrorSummary: () => ErrorSummary,
|
|
24
|
+
useFormUX: () => useFormUX
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/utils/getErrorFields.ts
|
|
29
|
+
function getErrorFields(errors, parent = "") {
|
|
30
|
+
const fields = [];
|
|
31
|
+
for (const key in errors) {
|
|
32
|
+
const value = errors[key];
|
|
33
|
+
const path = parent ? `${parent}.${key}` : key;
|
|
34
|
+
if (value && typeof value === "object" && !value.message) {
|
|
35
|
+
fields.push(...getErrorFields(value, path));
|
|
36
|
+
} else {
|
|
37
|
+
fields.push(path);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return fields;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/utils/focusFirstError.ts
|
|
44
|
+
function focusFirstError(errors) {
|
|
45
|
+
const fields = getErrorFields(errors);
|
|
46
|
+
if (!fields.length) return;
|
|
47
|
+
const firstField = fields[0];
|
|
48
|
+
const element = document.querySelector(
|
|
49
|
+
`[name="${firstField}"]`
|
|
50
|
+
);
|
|
51
|
+
element == null ? void 0 : element.focus();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/utils/scrollToError.ts
|
|
55
|
+
function scrollToError(errors) {
|
|
56
|
+
const fields = getErrorFields(errors);
|
|
57
|
+
if (!fields.length) return;
|
|
58
|
+
const firstField = fields[0];
|
|
59
|
+
const element = document.querySelector(
|
|
60
|
+
`[name="${firstField}"]`
|
|
61
|
+
);
|
|
62
|
+
element == null ? void 0 : element.scrollIntoView({
|
|
63
|
+
behavior: "smooth",
|
|
64
|
+
block: "center"
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/hooks/useFormUX.ts
|
|
69
|
+
function useFormUX(options) {
|
|
70
|
+
const { errors } = options;
|
|
71
|
+
return {
|
|
72
|
+
focusFirstError: () => focusFirstError(errors),
|
|
73
|
+
scrollToError: () => scrollToError(errors),
|
|
74
|
+
getErrorFields: () => getErrorFields(errors)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/components/ErrorSummary.tsx
|
|
79
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
80
|
+
function ErrorSummary({
|
|
81
|
+
errors,
|
|
82
|
+
title = "Please fix the following errors:"
|
|
83
|
+
}) {
|
|
84
|
+
const fields = getErrorFields(errors);
|
|
85
|
+
if (!fields.length) return null;
|
|
86
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { role: "alert", "aria-live": "assertive", children: [
|
|
87
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: title }),
|
|
88
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: fields.map((field) => {
|
|
89
|
+
var _a, _b;
|
|
90
|
+
const message = (_b = (_a = errors == null ? void 0 : errors[field]) == null ? void 0 : _a.message) != null ? _b : "Invalid field";
|
|
91
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
92
|
+
"button",
|
|
93
|
+
{
|
|
94
|
+
type: "button",
|
|
95
|
+
onClick: () => {
|
|
96
|
+
const element = document.querySelector(
|
|
97
|
+
`[name="${field}"]`
|
|
98
|
+
);
|
|
99
|
+
element == null ? void 0 : element.focus();
|
|
100
|
+
},
|
|
101
|
+
children: message
|
|
102
|
+
}
|
|
103
|
+
) }, field);
|
|
104
|
+
}) })
|
|
105
|
+
] });
|
|
106
|
+
}
|
|
107
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
108
|
+
0 && (module.exports = {
|
|
109
|
+
ErrorSummary,
|
|
110
|
+
useFormUX
|
|
111
|
+
});
|
|
112
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/utils/getErrorFields.ts","../src/utils/focusFirstError.ts","../src/utils/scrollToError.ts","../src/hooks/useFormUX.ts","../src/components/ErrorSummary.tsx"],"sourcesContent":["export * from \"./hooks/useFormUX\"\r\nexport * from \"./components/ErrorSummary\"","export function getErrorFields(\r\n errors: Record<string, any>,\r\n parent = \"\"\r\n): string[] {\r\n const fields: string[] = []\r\n\r\n for (const key in errors) {\r\n const value = errors[key]\r\n\r\n const path = parent ? `${parent}.${key}` : key\r\n\r\n if (value && typeof value === \"object\" && !value.message) {\r\n fields.push(...getErrorFields(value, path))\r\n } else {\r\n fields.push(path)\r\n }\r\n }\r\n\r\n return fields\r\n}","import { getErrorFields } from \"./getErrorFields\"\r\n\r\nexport function focusFirstError(errors: Record<string, unknown>) {\r\n const fields = getErrorFields(errors)\r\n\r\n if (!fields.length) return\r\n\r\n const firstField = fields[0]\r\n\r\n const element = document.querySelector(\r\n `[name=\"${firstField}\"]`\r\n ) as HTMLElement | null\r\n\r\n element?.focus()\r\n}","import { getErrorFields } from \"./getErrorFields\"\r\n\r\nexport function scrollToError(errors: Record<string, unknown>) {\r\n const fields = getErrorFields(errors)\r\n\r\n if (!fields.length) return\r\n\r\n const firstField = fields[0]\r\n\r\n const element = document.querySelector(\r\n `[name=\"${firstField}\"]`\r\n )\r\n\r\n element?.scrollIntoView({\r\n behavior: \"smooth\",\r\n block: \"center\"\r\n })\r\n}","import { UseFormUXOptions, UseFormUXReturn } from \"../types/formUX.types\"\r\nimport { focusFirstError } from \"../utils/focusFirstError\"\r\nimport { scrollToError } from \"../utils/scrollToError\"\r\nimport { getErrorFields } from \"../utils/getErrorFields\"\r\n\r\nexport function useFormUX(options: UseFormUXOptions): UseFormUXReturn {\r\n const { errors } = options\r\n\r\n return {\r\n focusFirstError: () => focusFirstError(errors),\r\n scrollToError: () => scrollToError(errors),\r\n getErrorFields: () => getErrorFields(errors)\r\n }\r\n}","import React from \"react\"\r\nimport { ErrorSummaryProps } from \"../types/formUX.types\"\r\nimport { getErrorFields } from \"../utils/getErrorFields\"\r\n\r\nexport function ErrorSummary({\r\n errors,\r\n title = \"Please fix the following errors:\"\r\n}: ErrorSummaryProps) {\r\n const fields = getErrorFields(errors)\r\n\r\n if (!fields.length) return null\r\n\r\n return (\r\n <div role=\"alert\" aria-live=\"assertive\">\r\n <strong>{title}</strong>\r\n\r\n <ul>\r\n {fields.map((field) => {\r\n const message = errors?.[field]?.message ?? \"Invalid field\"\r\n\r\n return (\r\n <li key={field}>\r\n <button\r\n type=\"button\"\r\n onClick={() => {\r\n const element = document.querySelector(\r\n `[name=\"${field}\"]`\r\n ) as HTMLElement | null\r\n\r\n element?.focus()\r\n }}\r\n >\r\n {message}\r\n </button>\r\n </li>\r\n )\r\n })}\r\n </ul>\r\n </div>\r\n )\r\n}"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAO,SAAS,eACd,QACA,SAAS,IACC;AACV,QAAM,SAAmB,CAAC;AAE1B,aAAW,OAAO,QAAQ;AACxB,UAAM,QAAQ,OAAO,GAAG;AAExB,UAAM,OAAO,SAAS,GAAG,MAAM,IAAI,GAAG,KAAK;AAE3C,QAAI,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,SAAS;AACxD,aAAO,KAAK,GAAG,eAAe,OAAO,IAAI,CAAC;AAAA,IAC5C,OAAO;AACL,aAAO,KAAK,IAAI;AAAA,IAClB;AAAA,EACF;AAEA,SAAO;AACT;;;ACjBO,SAAS,gBAAgB,QAAiC;AAC/D,QAAM,SAAS,eAAe,MAAM;AAEpC,MAAI,CAAC,OAAO,OAAQ;AAEpB,QAAM,aAAa,OAAO,CAAC;AAE3B,QAAM,UAAU,SAAS;AAAA,IACvB,UAAU,UAAU;AAAA,EACtB;AAEA,qCAAS;AACX;;;ACZO,SAAS,cAAc,QAAiC;AAC7D,QAAM,SAAS,eAAe,MAAM;AAEpC,MAAI,CAAC,OAAO,OAAQ;AAEpB,QAAM,aAAa,OAAO,CAAC;AAE3B,QAAM,UAAU,SAAS;AAAA,IACvB,UAAU,UAAU;AAAA,EACtB;AAEA,qCAAS,eAAe;AAAA,IACtB,UAAU;AAAA,IACV,OAAO;AAAA,EACT;AACF;;;ACZO,SAAS,UAAU,SAA4C;AACpE,QAAM,EAAE,OAAO,IAAI;AAEnB,SAAO;AAAA,IACL,iBAAiB,MAAM,gBAAgB,MAAM;AAAA,IAC7C,eAAe,MAAM,cAAc,MAAM;AAAA,IACzC,gBAAgB,MAAM,eAAe,MAAM;AAAA,EAC7C;AACF;;;ACAI;AATG,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA,QAAQ;AACV,GAAsB;AACpB,QAAM,SAAS,eAAe,MAAM;AAEpC,MAAI,CAAC,OAAO,OAAQ,QAAO;AAE3B,SACE,6CAAC,SAAI,MAAK,SAAQ,aAAU,aAC1B;AAAA,gDAAC,YAAQ,iBAAM;AAAA,IAEf,4CAAC,QACE,iBAAO,IAAI,CAAC,UAAU;AAjB/B;AAkBU,YAAM,WAAU,4CAAS,WAAT,mBAAiB,YAAjB,YAA4B;AAE5C,aACE,4CAAC,QACC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS,MAAM;AACb,kBAAM,UAAU,SAAS;AAAA,cACvB,UAAU,KAAK;AAAA,YACjB;AAEA,+CAAS;AAAA,UACX;AAAA,UAEC;AAAA;AAAA,MACH,KAZO,KAaT;AAAA,IAEJ,CAAC,GACH;AAAA,KACF;AAEJ;","names":[]}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface UseFormUXOptions {
|
|
4
|
+
errors: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
interface UseFormUXReturn {
|
|
7
|
+
focusFirstError: () => void;
|
|
8
|
+
scrollToError: () => void;
|
|
9
|
+
getErrorFields: () => string[];
|
|
10
|
+
}
|
|
11
|
+
interface ErrorSummaryProps {
|
|
12
|
+
errors: Record<string, any>;
|
|
13
|
+
title?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare function useFormUX(options: UseFormUXOptions): UseFormUXReturn;
|
|
17
|
+
|
|
18
|
+
declare function ErrorSummary({ errors, title }: ErrorSummaryProps): react_jsx_runtime.JSX.Element | null;
|
|
19
|
+
|
|
20
|
+
export { ErrorSummary, useFormUX };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface UseFormUXOptions {
|
|
4
|
+
errors: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
interface UseFormUXReturn {
|
|
7
|
+
focusFirstError: () => void;
|
|
8
|
+
scrollToError: () => void;
|
|
9
|
+
getErrorFields: () => string[];
|
|
10
|
+
}
|
|
11
|
+
interface ErrorSummaryProps {
|
|
12
|
+
errors: Record<string, any>;
|
|
13
|
+
title?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare function useFormUX(options: UseFormUXOptions): UseFormUXReturn;
|
|
17
|
+
|
|
18
|
+
declare function ErrorSummary({ errors, title }: ErrorSummaryProps): react_jsx_runtime.JSX.Element | null;
|
|
19
|
+
|
|
20
|
+
export { ErrorSummary, useFormUX };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/utils/getErrorFields.ts
|
|
2
|
+
function getErrorFields(errors, parent = "") {
|
|
3
|
+
const fields = [];
|
|
4
|
+
for (const key in errors) {
|
|
5
|
+
const value = errors[key];
|
|
6
|
+
const path = parent ? `${parent}.${key}` : key;
|
|
7
|
+
if (value && typeof value === "object" && !value.message) {
|
|
8
|
+
fields.push(...getErrorFields(value, path));
|
|
9
|
+
} else {
|
|
10
|
+
fields.push(path);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return fields;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/utils/focusFirstError.ts
|
|
17
|
+
function focusFirstError(errors) {
|
|
18
|
+
const fields = getErrorFields(errors);
|
|
19
|
+
if (!fields.length) return;
|
|
20
|
+
const firstField = fields[0];
|
|
21
|
+
const element = document.querySelector(
|
|
22
|
+
`[name="${firstField}"]`
|
|
23
|
+
);
|
|
24
|
+
element == null ? void 0 : element.focus();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/utils/scrollToError.ts
|
|
28
|
+
function scrollToError(errors) {
|
|
29
|
+
const fields = getErrorFields(errors);
|
|
30
|
+
if (!fields.length) return;
|
|
31
|
+
const firstField = fields[0];
|
|
32
|
+
const element = document.querySelector(
|
|
33
|
+
`[name="${firstField}"]`
|
|
34
|
+
);
|
|
35
|
+
element == null ? void 0 : element.scrollIntoView({
|
|
36
|
+
behavior: "smooth",
|
|
37
|
+
block: "center"
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/hooks/useFormUX.ts
|
|
42
|
+
function useFormUX(options) {
|
|
43
|
+
const { errors } = options;
|
|
44
|
+
return {
|
|
45
|
+
focusFirstError: () => focusFirstError(errors),
|
|
46
|
+
scrollToError: () => scrollToError(errors),
|
|
47
|
+
getErrorFields: () => getErrorFields(errors)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/components/ErrorSummary.tsx
|
|
52
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
53
|
+
function ErrorSummary({
|
|
54
|
+
errors,
|
|
55
|
+
title = "Please fix the following errors:"
|
|
56
|
+
}) {
|
|
57
|
+
const fields = getErrorFields(errors);
|
|
58
|
+
if (!fields.length) return null;
|
|
59
|
+
return /* @__PURE__ */ jsxs("div", { role: "alert", "aria-live": "assertive", children: [
|
|
60
|
+
/* @__PURE__ */ jsx("strong", { children: title }),
|
|
61
|
+
/* @__PURE__ */ jsx("ul", { children: fields.map((field) => {
|
|
62
|
+
var _a, _b;
|
|
63
|
+
const message = (_b = (_a = errors == null ? void 0 : errors[field]) == null ? void 0 : _a.message) != null ? _b : "Invalid field";
|
|
64
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
65
|
+
"button",
|
|
66
|
+
{
|
|
67
|
+
type: "button",
|
|
68
|
+
onClick: () => {
|
|
69
|
+
const element = document.querySelector(
|
|
70
|
+
`[name="${field}"]`
|
|
71
|
+
);
|
|
72
|
+
element == null ? void 0 : element.focus();
|
|
73
|
+
},
|
|
74
|
+
children: message
|
|
75
|
+
}
|
|
76
|
+
) }, field);
|
|
77
|
+
}) })
|
|
78
|
+
] });
|
|
79
|
+
}
|
|
80
|
+
export {
|
|
81
|
+
ErrorSummary,
|
|
82
|
+
useFormUX
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/getErrorFields.ts","../src/utils/focusFirstError.ts","../src/utils/scrollToError.ts","../src/hooks/useFormUX.ts","../src/components/ErrorSummary.tsx"],"sourcesContent":["export function getErrorFields(\r\n errors: Record<string, any>,\r\n parent = \"\"\r\n): string[] {\r\n const fields: string[] = []\r\n\r\n for (const key in errors) {\r\n const value = errors[key]\r\n\r\n const path = parent ? `${parent}.${key}` : key\r\n\r\n if (value && typeof value === \"object\" && !value.message) {\r\n fields.push(...getErrorFields(value, path))\r\n } else {\r\n fields.push(path)\r\n }\r\n }\r\n\r\n return fields\r\n}","import { getErrorFields } from \"./getErrorFields\"\r\n\r\nexport function focusFirstError(errors: Record<string, unknown>) {\r\n const fields = getErrorFields(errors)\r\n\r\n if (!fields.length) return\r\n\r\n const firstField = fields[0]\r\n\r\n const element = document.querySelector(\r\n `[name=\"${firstField}\"]`\r\n ) as HTMLElement | null\r\n\r\n element?.focus()\r\n}","import { getErrorFields } from \"./getErrorFields\"\r\n\r\nexport function scrollToError(errors: Record<string, unknown>) {\r\n const fields = getErrorFields(errors)\r\n\r\n if (!fields.length) return\r\n\r\n const firstField = fields[0]\r\n\r\n const element = document.querySelector(\r\n `[name=\"${firstField}\"]`\r\n )\r\n\r\n element?.scrollIntoView({\r\n behavior: \"smooth\",\r\n block: \"center\"\r\n })\r\n}","import { UseFormUXOptions, UseFormUXReturn } from \"../types/formUX.types\"\r\nimport { focusFirstError } from \"../utils/focusFirstError\"\r\nimport { scrollToError } from \"../utils/scrollToError\"\r\nimport { getErrorFields } from \"../utils/getErrorFields\"\r\n\r\nexport function useFormUX(options: UseFormUXOptions): UseFormUXReturn {\r\n const { errors } = options\r\n\r\n return {\r\n focusFirstError: () => focusFirstError(errors),\r\n scrollToError: () => scrollToError(errors),\r\n getErrorFields: () => getErrorFields(errors)\r\n }\r\n}","import React from \"react\"\r\nimport { ErrorSummaryProps } from \"../types/formUX.types\"\r\nimport { getErrorFields } from \"../utils/getErrorFields\"\r\n\r\nexport function ErrorSummary({\r\n errors,\r\n title = \"Please fix the following errors:\"\r\n}: ErrorSummaryProps) {\r\n const fields = getErrorFields(errors)\r\n\r\n if (!fields.length) return null\r\n\r\n return (\r\n <div role=\"alert\" aria-live=\"assertive\">\r\n <strong>{title}</strong>\r\n\r\n <ul>\r\n {fields.map((field) => {\r\n const message = errors?.[field]?.message ?? \"Invalid field\"\r\n\r\n return (\r\n <li key={field}>\r\n <button\r\n type=\"button\"\r\n onClick={() => {\r\n const element = document.querySelector(\r\n `[name=\"${field}\"]`\r\n ) as HTMLElement | null\r\n\r\n element?.focus()\r\n }}\r\n >\r\n {message}\r\n </button>\r\n </li>\r\n )\r\n })}\r\n </ul>\r\n </div>\r\n )\r\n}"],"mappings":";AAAO,SAAS,eACd,QACA,SAAS,IACC;AACV,QAAM,SAAmB,CAAC;AAE1B,aAAW,OAAO,QAAQ;AACxB,UAAM,QAAQ,OAAO,GAAG;AAExB,UAAM,OAAO,SAAS,GAAG,MAAM,IAAI,GAAG,KAAK;AAE3C,QAAI,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,SAAS;AACxD,aAAO,KAAK,GAAG,eAAe,OAAO,IAAI,CAAC;AAAA,IAC5C,OAAO;AACL,aAAO,KAAK,IAAI;AAAA,IAClB;AAAA,EACF;AAEA,SAAO;AACT;;;ACjBO,SAAS,gBAAgB,QAAiC;AAC/D,QAAM,SAAS,eAAe,MAAM;AAEpC,MAAI,CAAC,OAAO,OAAQ;AAEpB,QAAM,aAAa,OAAO,CAAC;AAE3B,QAAM,UAAU,SAAS;AAAA,IACvB,UAAU,UAAU;AAAA,EACtB;AAEA,qCAAS;AACX;;;ACZO,SAAS,cAAc,QAAiC;AAC7D,QAAM,SAAS,eAAe,MAAM;AAEpC,MAAI,CAAC,OAAO,OAAQ;AAEpB,QAAM,aAAa,OAAO,CAAC;AAE3B,QAAM,UAAU,SAAS;AAAA,IACvB,UAAU,UAAU;AAAA,EACtB;AAEA,qCAAS,eAAe;AAAA,IACtB,UAAU;AAAA,IACV,OAAO;AAAA,EACT;AACF;;;ACZO,SAAS,UAAU,SAA4C;AACpE,QAAM,EAAE,OAAO,IAAI;AAEnB,SAAO;AAAA,IACL,iBAAiB,MAAM,gBAAgB,MAAM;AAAA,IAC7C,eAAe,MAAM,cAAc,MAAM;AAAA,IACzC,gBAAgB,MAAM,eAAe,MAAM;AAAA,EAC7C;AACF;;;ACAI,SACE,KADF;AATG,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA,QAAQ;AACV,GAAsB;AACpB,QAAM,SAAS,eAAe,MAAM;AAEpC,MAAI,CAAC,OAAO,OAAQ,QAAO;AAE3B,SACE,qBAAC,SAAI,MAAK,SAAQ,aAAU,aAC1B;AAAA,wBAAC,YAAQ,iBAAM;AAAA,IAEf,oBAAC,QACE,iBAAO,IAAI,CAAC,UAAU;AAjB/B;AAkBU,YAAM,WAAU,4CAAS,WAAT,mBAAiB,YAAjB,YAA4B;AAE5C,aACE,oBAAC,QACC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS,MAAM;AACb,kBAAM,UAAU,SAAS;AAAA,cACvB,UAAU,KAAK;AAAA,YACjB;AAEA,+CAAS;AAAA,UACX;AAAA,UAEC;AAAA;AAAA,MACH,KAZO,KAaT;AAAA,IAEJ,CAAC,GACH;AAAA,KACF;AAEJ;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,20 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-form-ux",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "UX helpers for React forms (focus errors, scroll to validation, error summaries).",
|
|
5
|
-
"main": "index.
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
6
18
|
"scripts": {
|
|
7
|
-
"
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"dev": "tsup --watch",
|
|
21
|
+
"test": "vitest",
|
|
22
|
+
"test:ui": "vitest --ui"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"react": ">=18"
|
|
8
26
|
},
|
|
9
27
|
"keywords": [
|
|
10
28
|
"react",
|
|
11
|
-
"forms",
|
|
12
29
|
"react-hook-form",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
30
|
+
"forms",
|
|
31
|
+
"form-validation",
|
|
32
|
+
"form-ux",
|
|
33
|
+
"frontend",
|
|
34
|
+
"react-forms",
|
|
35
|
+
"validation",
|
|
36
|
+
"accessibility"
|
|
16
37
|
],
|
|
17
38
|
"author": "Sayan Paul",
|
|
18
39
|
"license": "MIT",
|
|
19
|
-
"
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
42
|
+
"@testing-library/react": "^16.3.2",
|
|
43
|
+
"@types/react": "^19.2.14",
|
|
44
|
+
"jsdom": "^28.1.0",
|
|
45
|
+
"tsup": "^8.5.1",
|
|
46
|
+
"typescript": "^5.9.3",
|
|
47
|
+
"vitest": "^4.0.18"
|
|
48
|
+
}
|
|
20
49
|
}
|
package/index.js
DELETED
package/react-form-ux-0.0.1.tgz
DELETED
|
Binary file
|