@sqrzro/ui 4.0.0-alpha.54 → 4.0.0-alpha.56

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.
@@ -8,6 +8,7 @@ export interface FormProps extends ClassNameProps<FormClassNames> {
8
8
  id?: string;
9
9
  onSubmit?: React.FormEventHandler<HTMLFormElement>;
10
10
  ref?: React.Ref<HTMLFormElement>;
11
+ uncaughtErrors: Record<string, string> | null;
11
12
  }
12
- declare function Form({ action, children, classNames, classNameProps, id, onSubmit, ref, }: Readonly<FormProps>): React.ReactElement;
13
+ declare function Form({ action, children, classNames, classNameProps, id, onSubmit, uncaughtErrors, ref, }: Readonly<FormProps>): React.ReactElement;
13
14
  export default Form;
@@ -1,9 +1,10 @@
1
1
  'use client';
2
- import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useClassNames } from '../../../styles/context';
4
4
  import tw from '../../../styles/classnames/utility/tw';
5
- function Form({ action, children, classNames, classNameProps, id, onSubmit, ref, }) {
5
+ import { InfoPanel } from '../../../components';
6
+ function Form({ action, children, classNames, classNameProps, id, onSubmit, uncaughtErrors, ref, }) {
6
7
  const componentClassNames = useClassNames('form', { props: classNameProps }, classNames);
7
- return (_jsx("form", { ref: ref, action: action, className: tw(componentClassNames?.root), id: id, onSubmit: onSubmit, children: children }));
8
+ return (_jsxs("form", { ref: ref, action: action, className: tw(componentClassNames?.root), id: id, onSubmit: onSubmit, children: [uncaughtErrors && Object.keys(uncaughtErrors).length ? (_jsx(InfoPanel, { variant: "error", children: Object.values(uncaughtErrors).map((item, index) => (_jsx("p", { children: item }, index))) })) : null, children] }));
8
9
  }
9
10
  export default Form;
@@ -11,9 +11,10 @@ export interface FormFieldClassNames {
11
11
  }
12
12
  export interface FormFieldProps<T, V extends T> extends ClassNameProps<FormFieldClassNames>, InputProps<T, V> {
13
13
  action?: SimpleActionObject | null;
14
- details?: string | null;
14
+ details?: React.ReactNode;
15
15
  error?: Record<string, string> | null;
16
16
  hasAssistiveError?: boolean;
17
+ hasAssistiveDetails?: boolean;
17
18
  hasAssistiveLabel?: boolean;
18
19
  isContentOnly?: boolean;
19
20
  isOptional?: boolean;
@@ -21,5 +22,5 @@ export interface FormFieldProps<T, V extends T> extends ClassNameProps<FormField
21
22
  onChange?: (event: InputEvent<T>) => void;
22
23
  render: (props: InputProps<T>) => React.ReactElement | null;
23
24
  }
24
- declare function FormField<T, V extends T>({ action, classNameProps, classNames, details, error, hasAssistiveError, hasAssistiveLabel, id, isContentOnly, isDisabled, isOptional, label, name, onChange, onKeyDown, render, value, }: Readonly<FormFieldProps<T, V>>): React.ReactElement | null;
25
+ declare function FormField<T, V extends T>({ action, classNameProps, classNames, details, error, hasAssistiveError, hasAssistiveDetails, hasAssistiveLabel, id, isContentOnly, isDisabled, isOptional, label, name, onChange, onKeyDown, render, value, }: Readonly<FormFieldProps<T, V>>): React.ReactElement | null;
25
26
  export default FormField;
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from 'react';
4
4
  import { useClassNames } from '../../../styles/context';
5
+ import tw from '../../../styles/classnames/utility/tw';
5
6
  import FormError from '../FormError';
6
7
  import FormLabel from '../FormLabel';
7
8
  import ActionButton from '../../../components/buttons/ActionButton';
@@ -10,7 +11,7 @@ function checkHasError(error) {
10
11
  Object.keys(error).length > 0 &&
11
12
  Object.values(error).some((item) => typeof item === 'string'));
12
13
  }
13
- function FormField({ action, classNameProps, classNames, details, error, hasAssistiveError, hasAssistiveLabel, id, isContentOnly, isDisabled, isOptional, label, name, onChange, onKeyDown, render, value, }) {
14
+ function FormField({ action, classNameProps, classNames, details, error, hasAssistiveError, hasAssistiveDetails, hasAssistiveLabel, id, isContentOnly, isDisabled, isOptional, label, name, onChange, onKeyDown, render, value, }) {
14
15
  const componentClassNames = useClassNames('formField', { props: classNameProps, states: { isError: checkHasError(error) } }, classNames);
15
16
  const inputId = id || `ff_${name}`;
16
17
  const [inputError, setInputError] = useState(null);
@@ -19,6 +20,7 @@ function FormField({ action, classNameProps, classNames, details, error, hasAssi
19
20
  }
20
21
  const renderProps = {
21
22
  classNameProps,
23
+ details,
22
24
  error,
23
25
  hasError: checkHasError(error),
24
26
  id: inputId,
@@ -34,6 +36,6 @@ function FormField({ action, classNameProps, classNames, details, error, hasAssi
34
36
  if (isContentOnly) {
35
37
  return render(renderProps);
36
38
  }
37
- return (_jsxs("div", { className: componentClassNames?.root, children: [label ? (_jsx(FormLabel, { classNameProps: classNameProps, htmlFor: inputId, isAssistive: hasAssistiveLabel, isOptional: isOptional, children: label })) : null, details && label ? (_jsx("div", { className: componentClassNames?.details, children: details })) : null, _jsx("div", { className: componentClassNames?.field, children: render(renderProps) }), action ? (_jsx("div", { children: _jsx(ActionButton, { ...action, isDisabled: Boolean(isDisabled || action.isDisabled) }) })) : null, inputError || error?.[name] ? (_jsx(FormError, { classNameProps: classNameProps, id: inputId, isAssistive: hasAssistiveError, children: inputError || error?.[name] })) : null] }));
39
+ return (_jsxs("div", { className: componentClassNames?.root, children: [label ? (_jsx(FormLabel, { classNameProps: classNameProps, htmlFor: inputId, isAssistive: hasAssistiveLabel, isOptional: isOptional, children: label })) : null, details && label ? (_jsx("div", { className: tw(componentClassNames?.details, hasAssistiveDetails ? 'sr-only' : null), children: details })) : null, _jsx("div", { className: componentClassNames?.field, children: render(renderProps) }), action ? (_jsx("div", { children: _jsx(ActionButton, { ...action, isDisabled: Boolean(isDisabled || action.isDisabled) }) })) : null, inputError || error?.[name] ? (_jsx(FormError, { classNameProps: classNameProps, id: inputId, isAssistive: hasAssistiveError, children: inputError || error?.[name] })) : null] }));
38
40
  }
39
41
  export default FormField;
@@ -4,6 +4,7 @@ import type { DropdownComponentProps } from '../Dropdown';
4
4
  import type { PointsInputComponentProps } from '../PointsInput';
5
5
  import type { NumberInputComponentProps } from '../NumberInput';
6
6
  import type { PasswordInputComponentProps } from '../PasswordInput';
7
+ import type { SwitchComponentProps } from '../Switch';
7
8
  import type { TextAreaComponentProps } from '../TextArea';
8
9
  import type { TextInputComponentProps } from '../TextInput';
9
10
  export type AutocompleteFormFieldProps<T> = FormFieldComponentProps<T | null> & AutocompleteComponentProps<T>;
@@ -18,6 +19,8 @@ export type NumberFormFieldProps = FormFieldComponentProps<number> & NumberInput
18
19
  export declare function NumberFormField(props: Readonly<NumberFormFieldProps>): React.ReactElement;
19
20
  export type PasswordFormFieldProps = FormFieldComponentProps<string> & PasswordInputComponentProps;
20
21
  export declare function PasswordFormField(props: Readonly<PasswordFormFieldProps>): React.ReactElement;
22
+ export type SwitchFormFieldProps = FormFieldComponentProps<boolean> & SwitchComponentProps;
23
+ export declare function SwitchFormField(props: SwitchFormFieldProps): React.ReactElement;
21
24
  export type TextFormFieldProps = FormFieldComponentProps<string> & TextInputComponentProps;
22
25
  export declare function TextFormField(props: Readonly<TextFormFieldProps>): React.ReactElement;
23
26
  export type TextAreaFormFieldProps = FormFieldComponentProps<string> & TextAreaComponentProps;
@@ -8,6 +8,7 @@ import FormField from '../FormField';
8
8
  import PointsInput from '../PointsInput';
9
9
  import NumberInput from '../NumberInput';
10
10
  import PasswordInput from '../PasswordInput';
11
+ import Switch from '../Switch';
11
12
  import TextArea from '../TextArea';
12
13
  import TextInput from '../TextInput';
13
14
  export function AutocompleteFormField(props) {
@@ -40,6 +41,11 @@ export function PasswordFormField(props) {
40
41
  const renderInput = useCallback((renderProps) => (_jsx(PasswordInput, { ...renderProps, ...inputProps })), [inputProps]);
41
42
  return _jsx(FormField, { ...fieldProps, render: renderInput });
42
43
  }
44
+ export function SwitchFormField(props) {
45
+ const { fieldProps, inputProps } = extractInputProps(props);
46
+ const renderInput = useCallback((renderProps) => (_jsx(Switch, { ...renderProps, ...inputProps })), [inputProps]);
47
+ return _jsx(FormField, { ...fieldProps, hasAssistiveLabel: true, hasAssistiveDetails: true, render: renderInput });
48
+ }
43
49
  export function TextFormField(props) {
44
50
  const { fieldProps, inputProps } = extractInputProps(props);
45
51
  const renderInput = useCallback((renderProps) => (_jsx(TextInput, { ...renderProps, ...inputProps })), [inputProps]);
@@ -3,11 +3,12 @@ import type { FormProps } from '../Form';
3
3
  export interface ModalFormProps {
4
4
  children: React.ReactNode;
5
5
  formProps: Omit<FormProps, 'children'>;
6
- hasServerError?: boolean;
7
6
  hasSubmit?: boolean;
8
7
  isDisabled?: boolean;
9
8
  modalProps: Omit<ModalProps, 'children'>;
9
+ serverError?: string | null;
10
10
  submitLabel?: string;
11
+ validationErrors?: string[];
11
12
  }
12
- declare function ModalForm({ children, formProps, hasServerError, hasSubmit, isDisabled, modalProps, submitLabel, }: Readonly<ModalFormProps>): React.ReactElement;
13
+ declare function ModalForm({ children, formProps, hasSubmit, isDisabled, modalProps, serverError, submitLabel, validationErrors, }: Readonly<ModalFormProps>): React.ReactElement;
13
14
  export default ModalForm;
@@ -4,12 +4,13 @@ import Modal from '../../../components/modals/Modal';
4
4
  import ModalActions from '../../../components/modals/ModalActions';
5
5
  import useSearchParamsHref from '../../../hooks/useSearchParamsHref';
6
6
  import Form from '../Form';
7
- function ModalForm({ children, formProps, hasServerError, hasSubmit = true, isDisabled, modalProps, submitLabel, }) {
7
+ import { InfoPanel } from '../../../components';
8
+ function ModalForm({ children, formProps, hasSubmit = true, isDisabled, modalProps, serverError, submitLabel, validationErrors, }) {
8
9
  const { setSearchParamsHref } = useSearchParamsHref();
9
10
  function handleCancel() {
10
11
  setSearchParamsHref('action', null);
11
12
  }
12
- return (_jsx(Modal, { ...modalProps, children: _jsxs(Form, { ...formProps, children: [children, hasServerError ? _jsx("div", { children: "SERVER ERROR" }) : null, _jsx(ModalActions, { actions: [
13
+ return (_jsx(Modal, { ...modalProps, children: _jsxs(Form, { ...formProps, children: [serverError ? _jsx(InfoPanel, { variant: "error", children: serverError }) : null, validationErrors?.length ? (_jsx(InfoPanel, { variant: "error", children: JSON.stringify(validationErrors) })) : null, children, _jsx(ModalActions, { actions: [
13
14
  { label: 'Cancel', onClick: handleCancel },
14
15
  ...(modalProps.actions || []),
15
16
  hasSubmit
@@ -5,7 +5,13 @@ export interface SwitchClassNames {
5
5
  control: string;
6
6
  input: CheckableClassName;
7
7
  icon: CheckableClassName;
8
+ label: string;
9
+ details: string;
8
10
  }
9
- export type SwitchProps = ClassNameProps<SwitchClassNames> & InputProps<boolean>;
10
- declare function Switch({ classNameProps, classNames, id, isDisabled, name, onChange, value, }: Readonly<SwitchProps>): React.ReactElement;
11
+ export type SwitchComponentProps = {
12
+ details?: React.ReactNode;
13
+ label?: React.ReactNode;
14
+ };
15
+ export type SwitchProps = ClassNameProps<SwitchClassNames> & InputProps<boolean> & SwitchComponentProps;
16
+ declare function Switch({ classNameProps, classNames, details, id, isDisabled, label, name, onChange, value, }: Readonly<SwitchProps>): React.ReactElement;
11
17
  export default Switch;
@@ -1,9 +1,10 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Fragment } from 'react';
4
+ import Assistive from '../../../components/utility/Assistive';
4
5
  import { useClassNames } from '../../../styles/context';
5
6
  import tw from '../../../styles/classnames/utility/tw';
6
- function Switch({ classNameProps, classNames, id, isDisabled, name, onChange, value, }) {
7
+ function Switch({ classNameProps, classNames, details, id, isDisabled, label, name, onChange, value, }) {
7
8
  const componentClassNames = useClassNames('switch', { props: classNameProps, states: { isChecked: Boolean(value) } }, classNames);
8
9
  function handleChange(event) {
9
10
  if (onChange) {
@@ -13,6 +14,6 @@ function Switch({ classNameProps, classNames, id, isDisabled, name, onChange, va
13
14
  onChange(inputEvent);
14
15
  }
15
16
  }
16
- return (_jsxs(Fragment, { children: [_jsx("input", { name: name, type: "hidden", value: "false" }), _jsx("div", { className: tw('block', componentClassNames?.root), children: _jsxs("div", { className: tw('relative', componentClassNames?.control), children: [_jsx("input", { "aria-checked": Boolean(value), checked: Boolean(value), className: tw('appearance-none', componentClassNames?.input), disabled: isDisabled, id: id || name, name: name, onChange: handleChange, type: "checkbox", value: "true" }), _jsx("i", { className: componentClassNames?.icon })] }) })] }));
17
+ return (_jsxs(Fragment, { children: [_jsx("input", { name: name, type: "hidden", value: "false" }), _jsxs("div", { className: tw('block', componentClassNames?.root), children: [_jsxs("div", { className: tw('relative', componentClassNames?.control), children: [_jsx("input", { "aria-checked": Boolean(value), checked: Boolean(value), className: tw('appearance-none', componentClassNames?.input), disabled: isDisabled, id: id || name, name: name, onChange: handleChange, type: "checkbox", value: "true" }), _jsx("i", { className: componentClassNames?.icon })] }), label ? (_jsxs("label", { htmlFor: id || name, children: [_jsx("span", { className: tw('', componentClassNames?.label), children: label }), details ? (_jsx("small", { className: tw('', componentClassNames?.details), children: details })) : null] })) : (_jsx(Assistive, { children: "Yes?" }))] })] }));
17
18
  }
18
19
  export default Switch;
@@ -24,7 +24,7 @@ export interface UseFormArgs<Request, Response> extends UseSuccessArgs<Response>
24
24
  defaults?: Default<Request>;
25
25
  onError?: (message: string) => void;
26
26
  onSubmit?: (formData: Request) => FormResponse<Response>;
27
- onValidation?: (errors: Record<string, string>) => void;
27
+ onValidation?: (errors: Record<string, string>, uncaughtErrors?: Record<string, string>) => void;
28
28
  toasts?: ToastsArgs | false;
29
29
  }
30
30
  export interface UseFormReturn<Request> {
@@ -38,6 +38,7 @@ export interface UseFormReturn<Request> {
38
38
  setFormData: <K extends keyof Request>(key: K, value: Request[K]) => void;
39
39
  submitForm: () => void;
40
40
  }
41
+ export declare const DEFAULT_TOAST_MESSAGES: Record<keyof ToastsArgs, string>;
41
42
  /**
42
43
  * ## Overview
43
44
  *
@@ -4,7 +4,7 @@ import { formatTitle } from '@sqrzro/utility';
4
4
  import useDeepCompareEffect from 'use-deep-compare-effect';
5
5
  import useSuccess from '../../hooks/useSuccess';
6
6
  import useToast from '../../hooks/useToast';
7
- const DEFAULT_TOAST_MESSAGES = {
7
+ export const DEFAULT_TOAST_MESSAGES = {
8
8
  server: 'There was a problem submitting your request. Please try again later.',
9
9
  success: 'Your request was submitted successfully.',
10
10
  validation: 'There was a problem with your submission. Please check the form and try again.',
@@ -121,9 +121,11 @@ function getToastMessage(key, toasts) {
121
121
  */
122
122
  function useForm({ defaults = {}, onError, onSubmit, onSuccess, onValidation, redirectOnSuccess, refreshOnSuccess, toasts, }) {
123
123
  const ref = useRef(null);
124
+ const usedFields = useRef(new Set());
124
125
  const { handleSuccess } = useSuccess({ onSuccess, redirectOnSuccess, refreshOnSuccess });
125
126
  const [isLoading] = useTransition();
126
127
  const [errors, setErrors] = useState(null);
128
+ const [uncaughtErrors, setUncaughtErrors] = useState(null);
127
129
  const [data, setData] = useState(defaults);
128
130
  const { toastError, toastSuccess } = useToast();
129
131
  function setFormData(key, value) {
@@ -141,8 +143,12 @@ function useForm({ defaults = {}, onError, onSubmit, onSuccess, onValidation, re
141
143
  toastError(getToastMessage('server', toasts));
142
144
  }
143
145
  function handleFormValidation(messages) {
144
- onValidation?.(messages);
146
+ const uncaughtMessages = Object.keys(messages)
147
+ .filter((key) => !usedFields.current.has(key))
148
+ .reduce((acc, key) => ({ ...acc, [key]: messages[key] }), {});
149
+ onValidation?.(messages, uncaughtMessages);
145
150
  setErrors(messages);
151
+ setUncaughtErrors(uncaughtMessages);
146
152
  toastError(getToastMessage('validation', toasts));
147
153
  }
148
154
  async function handleSubmit() {
@@ -162,6 +168,7 @@ function useForm({ defaults = {}, onError, onSubmit, onSuccess, onValidation, re
162
168
  }
163
169
  }
164
170
  function fieldProps(name, label) {
171
+ usedFields.current.add(name);
165
172
  return {
166
173
  error: getErrorsForField(errors, name),
167
174
  label: getLabel(name, label),
@@ -186,6 +193,7 @@ function useForm({ defaults = {}, onError, onSubmit, onSuccess, onValidation, re
186
193
  formProps: {
187
194
  action: handleSubmit,
188
195
  ref,
196
+ uncaughtErrors,
189
197
  },
190
198
  isLoading,
191
199
  resetForm,
@@ -1,8 +1,10 @@
1
1
  'use client';
2
+ import { useState } from 'react';
2
3
  import { useRouter } from 'next/navigation';
3
4
  import useSearchParamsHref from '../../hooks/useSearchParamsHref';
4
- import useForm from './useForm';
5
+ import useForm, { DEFAULT_TOAST_MESSAGES } from './useForm';
5
6
  function useModalForm({ actions, icon, submitLabel, title, ...useFormArgs }) {
7
+ const [serverError, setServerError] = useState(null);
6
8
  const { setSearchParamsHref } = useSearchParamsHref();
7
9
  const router = useRouter();
8
10
  function handleSuccess(response) {
@@ -14,6 +16,10 @@ function useModalForm({ actions, icon, submitLabel, title, ...useFormArgs }) {
14
16
  }
15
17
  const useFormReturn = useForm({
16
18
  ...useFormArgs,
19
+ onError: (message) => {
20
+ setServerError(DEFAULT_TOAST_MESSAGES.server);
21
+ useFormArgs.onError?.(message);
22
+ },
17
23
  onSuccess: handleSuccess,
18
24
  toasts: false,
19
25
  });
@@ -22,6 +28,7 @@ function useModalForm({ actions, icon, submitLabel, title, ...useFormArgs }) {
22
28
  formProps: {
23
29
  formProps: useFormReturn.formProps,
24
30
  modalProps: { actions, icon, title },
31
+ serverError,
25
32
  submitLabel,
26
33
  },
27
34
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sqrzro/ui",
3
3
  "type": "module",
4
- "version": "4.0.0-alpha.54",
4
+ "version": "4.0.0-alpha.56",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "license": "ISC",