@payfit/unity-components 2.1.4 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/avatar/Avatar.context.d.ts +2 -1
- package/dist/esm/components/avatar/Avatar.context.js +13 -11
- package/dist/esm/components/avatar/Avatar.d.ts +126 -0
- package/dist/esm/components/avatar/Avatar.js +34 -20
- package/dist/esm/components/avatar/Avatar.variants.d.ts +39 -0
- package/dist/esm/components/avatar/Avatar.variants.js +22 -4
- package/dist/esm/components/avatar/parts/AvatarFallback.d.ts +52 -0
- package/dist/esm/components/avatar/parts/AvatarIcon.d.ts +31 -0
- package/dist/esm/components/avatar/parts/AvatarIcon.js +40 -0
- package/dist/esm/components/button/Button.js +8 -7
- package/dist/esm/components/dialog/Dialog.js +32 -31
- package/dist/esm/components/dialog/test-utils.d.ts +7 -1
- package/dist/esm/components/dialog/test-utils.js +39 -28
- package/dist/esm/components/inline-field-group/InlineFieldGroup.context.d.ts +23 -0
- package/dist/esm/components/inline-field-group/InlineFieldGroup.context.js +6 -0
- package/dist/esm/components/inline-field-group/InlineFieldGroup.d.ts +119 -0
- package/dist/esm/components/inline-field-group/InlineFieldGroup.js +185 -0
- package/dist/esm/components/inline-field-group/hooks/useInlineFieldGroupMode.d.ts +46 -0
- package/dist/esm/components/inline-field-group/hooks/useInlineFieldGroupMode.js +27 -0
- package/dist/esm/components/inline-field-group/parts/InlineFieldGroupEditView.d.ts +64 -0
- package/dist/esm/components/inline-field-group/parts/InlineFieldGroupEditView.js +56 -0
- package/dist/esm/components/inline-field-group/parts/InlineFieldGroupHeader.d.ts +95 -0
- package/dist/esm/components/inline-field-group/parts/InlineFieldGroupHeader.js +106 -0
- package/dist/esm/components/inline-field-group/parts/InlineFieldGroupReadView.d.ts +56 -0
- package/dist/esm/components/inline-field-group/parts/InlineFieldGroupReadView.js +28 -0
- package/dist/esm/components/side-panel/parts/SidePanelFooter.js +19 -10
- package/dist/esm/hooks/tanstack-form-context.d.ts +1 -1
- package/dist/esm/hooks/use-tanstack-form.d.ts +32 -8
- package/dist/esm/hooks/use-tanstack-form.js +71 -48
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +445 -443
- package/i18n/en-GB.json +6 -0
- package/i18n/es-ES.json +6 -0
- package/i18n/fr-FR.json +6 -0
- package/package.json +21 -21
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as o, jsxs as
|
|
1
|
+
import { jsx as o, jsxs as p } from "react/jsx-runtime";
|
|
2
2
|
import { Children as l, isValidElement as i } from "react";
|
|
3
3
|
import { uyTv as f } from "@payfit/unity-themes";
|
|
4
4
|
import { useId as w } from "react-aria";
|
|
@@ -28,7 +28,7 @@ const T = f({
|
|
|
28
28
|
"uy:data-[entering]:animate-slide-up-fade-in uy:data-[exiting]:animate-slide-down-fade-out",
|
|
29
29
|
"uy:md:[animation-delay:100ms] uy:md:data-[entering]:animate-zoom-in uy:md:data-[exiting]:animate-zoom-out"
|
|
30
30
|
],
|
|
31
|
-
content: ["uy:group/dialog", "uy:outline-none
|
|
31
|
+
content: ["uy:group/dialog", "uy:outline-none"],
|
|
32
32
|
dismissIcon: ["uy:absolute uy:right-200 uy:top-200 uy:z-20"]
|
|
33
33
|
},
|
|
34
34
|
variants: {
|
|
@@ -62,7 +62,7 @@ function B({
|
|
|
62
62
|
size: n = "md",
|
|
63
63
|
...d
|
|
64
64
|
}) {
|
|
65
|
-
const m = z(), u = w(), { overlay: y, wrapper: s, content: g, dismissIcon:
|
|
65
|
+
const m = z(), u = w(), { overlay: y, wrapper: s, content: g, dismissIcon: c } = T({ size: n });
|
|
66
66
|
return /* @__PURE__ */ o(
|
|
67
67
|
x,
|
|
68
68
|
{
|
|
@@ -70,34 +70,35 @@ function B({
|
|
|
70
70
|
isOpen: t,
|
|
71
71
|
onOpenChange: a,
|
|
72
72
|
className: y(),
|
|
73
|
-
children: /* @__PURE__ */
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
73
|
+
children: /* @__PURE__ */ o(v, { className: s(), "data-unity-dialog": !0, "data-dd-privacy": "allow", children: /* @__PURE__ */ o(h.Provider, { value: { "aria-describedby": u }, children: /* @__PURE__ */ p(
|
|
74
|
+
b,
|
|
75
|
+
{
|
|
76
|
+
role: j(e) ? "alertdialog" : "dialog",
|
|
77
|
+
"aria-modal": "true",
|
|
78
|
+
className: g(),
|
|
79
|
+
"aria-label": d["aria-label"],
|
|
80
|
+
"aria-describedby": u,
|
|
81
|
+
"data-unity-slot": "dialog",
|
|
82
|
+
"data-unity-size": n,
|
|
83
|
+
children: [
|
|
84
|
+
r && /* @__PURE__ */ o(
|
|
85
|
+
I,
|
|
86
|
+
{
|
|
87
|
+
icon: "CloseOutlined",
|
|
88
|
+
color: "content.neutral.low",
|
|
89
|
+
title: m.formatMessage({
|
|
90
|
+
id: "unity:component:common:close:label"
|
|
91
|
+
}),
|
|
92
|
+
className: c(),
|
|
93
|
+
slot: "close",
|
|
94
|
+
size: "large",
|
|
95
|
+
asElement: "button"
|
|
96
|
+
}
|
|
97
|
+
),
|
|
98
|
+
e
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
) }) })
|
|
101
102
|
}
|
|
102
103
|
);
|
|
103
104
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { IntlShape } from 'react-intl';
|
|
1
2
|
import { PlayCtx } from '../../types/testing.js';
|
|
2
3
|
/**
|
|
3
4
|
* Factory function returning a set of helpers to test Unity Dialog components.
|
|
4
5
|
* It wires Storybook's `Play` context with convenient utilities to query,
|
|
5
6
|
* assert, and interact with dialogs rendered on the canvas.
|
|
6
7
|
* @param context The Storybook play context used to group steps and actions.
|
|
8
|
+
* @param globalIntl The global intl instance to use for localization.
|
|
7
9
|
* @returns An object of helpers: `findDialog`, `closeDialog`, `assertDialogIsClosed`, `assertElementExistsInDialog`, and `triggerPrimaryAction`.
|
|
8
10
|
*/
|
|
9
|
-
export declare const getTestingUtilsDialog: (context: PlayCtx) => {
|
|
11
|
+
export declare const getTestingUtilsDialog: (context: PlayCtx, globalIntl: IntlShape) => {
|
|
10
12
|
findDialog: ({ title }: {
|
|
11
13
|
title: string;
|
|
12
14
|
}) => Promise<HTMLElement>;
|
|
@@ -14,6 +16,10 @@ export declare const getTestingUtilsDialog: (context: PlayCtx) => {
|
|
|
14
16
|
title: string;
|
|
15
17
|
closeButtonLabel: string;
|
|
16
18
|
}) => Promise<void>;
|
|
19
|
+
closeDialogWithDismissButton: ({ title, dismissButtonLabel, }: {
|
|
20
|
+
title: string;
|
|
21
|
+
dismissButtonLabel?: string;
|
|
22
|
+
}) => Promise<void>;
|
|
17
23
|
assertDialogIsClosed: (dialog: HTMLElement) => Promise<void>;
|
|
18
24
|
assertElementExistsInDialog: ({ title, content, primaryActionLabel, secondaryActionLabel, }: {
|
|
19
25
|
title: string;
|
|
@@ -1,31 +1,42 @@
|
|
|
1
|
-
import { userEvent as
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const t =
|
|
1
|
+
import { userEvent as r, within as s, expect as n, waitFor as w, screen as y } from "storybook/test";
|
|
2
|
+
const p = (e, m) => {
|
|
3
|
+
const o = async ({ title: a }) => await w(() => {
|
|
4
|
+
const t = y.queryByRole("dialog", { name: a }) ?? y.queryByRole("alertdialog", { name: a });
|
|
5
5
|
if (!t)
|
|
6
6
|
throw new Error(`Dialog with title: "${a}" not found in the page`);
|
|
7
7
|
return t;
|
|
8
|
-
}),
|
|
9
|
-
await
|
|
10
|
-
|
|
8
|
+
}), c = async (a, t) => {
|
|
9
|
+
await r.click(
|
|
10
|
+
s(a).getByRole("button", {
|
|
11
11
|
name: t
|
|
12
12
|
}),
|
|
13
13
|
{ delay: 100 }
|
|
14
14
|
);
|
|
15
15
|
};
|
|
16
16
|
return {
|
|
17
|
-
findDialog:
|
|
17
|
+
findDialog: o,
|
|
18
18
|
closeDialog: async ({
|
|
19
19
|
title: a,
|
|
20
20
|
closeButtonLabel: t
|
|
21
21
|
}) => {
|
|
22
|
-
await
|
|
23
|
-
const
|
|
24
|
-
await
|
|
22
|
+
await e.step("Close dialog", async () => {
|
|
23
|
+
const i = await o({ title: a });
|
|
24
|
+
await c(i, t);
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
closeDialogWithDismissButton: async ({
|
|
28
|
+
title: a,
|
|
29
|
+
dismissButtonLabel: t = m.formatMessage({
|
|
30
|
+
id: "unity:component:common:close:label"
|
|
31
|
+
})
|
|
32
|
+
}) => {
|
|
33
|
+
await e.step("Close dialog", async () => {
|
|
34
|
+
const i = await o({ title: a });
|
|
35
|
+
await c(i, t);
|
|
25
36
|
});
|
|
26
37
|
},
|
|
27
38
|
assertDialogIsClosed: async (a) => {
|
|
28
|
-
await
|
|
39
|
+
await w(
|
|
29
40
|
async () => {
|
|
30
41
|
await n(a).not.toBeInTheDocument();
|
|
31
42
|
},
|
|
@@ -35,24 +46,24 @@ const f = (i) => {
|
|
|
35
46
|
assertElementExistsInDialog: async ({
|
|
36
47
|
title: a,
|
|
37
48
|
content: t,
|
|
38
|
-
primaryActionLabel:
|
|
39
|
-
secondaryActionLabel:
|
|
49
|
+
primaryActionLabel: i,
|
|
50
|
+
secondaryActionLabel: l
|
|
40
51
|
}) => {
|
|
41
|
-
const
|
|
42
|
-
t && await
|
|
43
|
-
await n(
|
|
44
|
-
}),
|
|
45
|
-
`Check if "${
|
|
52
|
+
const g = await o({ title: a });
|
|
53
|
+
t && await e.step(`Check if "${t}" is in the dialog`, async () => {
|
|
54
|
+
await n(s(g).getByText(t)).toBeInTheDocument();
|
|
55
|
+
}), i && await e.step(
|
|
56
|
+
`Check if "${i}" is in the dialog`,
|
|
46
57
|
async () => {
|
|
47
58
|
await n(
|
|
48
|
-
|
|
59
|
+
s(g).getByRole("button", { name: i })
|
|
49
60
|
).toBeInTheDocument();
|
|
50
61
|
}
|
|
51
|
-
),
|
|
52
|
-
`Check if "${
|
|
62
|
+
), l && await e.step(
|
|
63
|
+
`Check if "${l}" is in the dialog`,
|
|
53
64
|
async () => {
|
|
54
65
|
await n(
|
|
55
|
-
|
|
66
|
+
s(g).getByRole("button", { name: l })
|
|
56
67
|
).toBeInTheDocument();
|
|
57
68
|
}
|
|
58
69
|
);
|
|
@@ -61,12 +72,12 @@ const f = (i) => {
|
|
|
61
72
|
title: a,
|
|
62
73
|
primaryActionLabel: t
|
|
63
74
|
}) => {
|
|
64
|
-
const
|
|
65
|
-
await
|
|
75
|
+
const i = await o({ title: a });
|
|
76
|
+
await e.step(
|
|
66
77
|
`Trigger the primary action "${t}" of the dialog`,
|
|
67
78
|
async () => {
|
|
68
|
-
await
|
|
69
|
-
|
|
79
|
+
await r.click(
|
|
80
|
+
s(i).getByRole("button", { name: t })
|
|
70
81
|
);
|
|
71
82
|
}
|
|
72
83
|
);
|
|
@@ -74,5 +85,5 @@ const f = (i) => {
|
|
|
74
85
|
};
|
|
75
86
|
};
|
|
76
87
|
export {
|
|
77
|
-
|
|
88
|
+
p as getTestingUtilsDialog
|
|
78
89
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
export type InlineFieldGroupMode = 'read' | 'edit';
|
|
3
|
+
export interface InlineFieldGroupContextValue {
|
|
4
|
+
/** Current mode: read or edit */
|
|
5
|
+
mode: InlineFieldGroupMode;
|
|
6
|
+
/** Switch to edit mode */
|
|
7
|
+
enterEditMode: () => void;
|
|
8
|
+
/** Exit edit mode (cancel or save) */
|
|
9
|
+
exitEditMode: () => void;
|
|
10
|
+
/** Unique ID for the field group (for aria attributes) */
|
|
11
|
+
groupId: string;
|
|
12
|
+
/** ID for the header title (for aria-labelledby) */
|
|
13
|
+
headerId: string;
|
|
14
|
+
/** ID for the edit view container (for aria-controls) */
|
|
15
|
+
editViewId: string;
|
|
16
|
+
/** Whether the component is in a loading state */
|
|
17
|
+
isLoading?: boolean;
|
|
18
|
+
/** Ref to the edit button for focus management */
|
|
19
|
+
editButtonRef?: RefObject<HTMLButtonElement>;
|
|
20
|
+
/** Ref to the edit view container for focus management */
|
|
21
|
+
editViewRef?: RefObject<HTMLFieldSetElement>;
|
|
22
|
+
}
|
|
23
|
+
export declare const InlineFieldGroupContext: import('react').Context<InlineFieldGroupContextValue | undefined>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { default as React, PropsWithChildren } from 'react';
|
|
2
|
+
import { InlineFieldGroupMode } from './InlineFieldGroup.context.js';
|
|
3
|
+
/**
|
|
4
|
+
* Imperative handle for InlineFieldGroup component.
|
|
5
|
+
* Provides methods for programmatic control when needed for advanced use cases.
|
|
6
|
+
*/
|
|
7
|
+
export interface InlineFieldGroupHandle {
|
|
8
|
+
/**
|
|
9
|
+
* Focuses the first invalid field in the edit view.
|
|
10
|
+
* Useful for custom validation error handling.
|
|
11
|
+
*/
|
|
12
|
+
focusFirstInvalidField: () => void;
|
|
13
|
+
/**
|
|
14
|
+
* Exits edit mode programmatically.
|
|
15
|
+
*/
|
|
16
|
+
exitEditMode: () => void;
|
|
17
|
+
/**
|
|
18
|
+
* Enters edit mode programmatically.
|
|
19
|
+
*/
|
|
20
|
+
enterEditMode: () => void;
|
|
21
|
+
}
|
|
22
|
+
export interface InlineFieldGroupProps extends PropsWithChildren {
|
|
23
|
+
/**
|
|
24
|
+
* Controlled mode value. When provided, the component operates in controlled mode.
|
|
25
|
+
*/
|
|
26
|
+
mode?: InlineFieldGroupMode;
|
|
27
|
+
/**
|
|
28
|
+
* Default mode value for uncontrolled mode.
|
|
29
|
+
* @default 'read'
|
|
30
|
+
*/
|
|
31
|
+
defaultMode?: InlineFieldGroupMode;
|
|
32
|
+
/**
|
|
33
|
+
* Callback fired when mode changes.
|
|
34
|
+
*/
|
|
35
|
+
onModeChange?: (mode: InlineFieldGroupMode) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Callback to intercept and potentially prevent mode changes.
|
|
38
|
+
* Return `false` to prevent the mode transition.
|
|
39
|
+
* Return `true` or `undefined` to allow the transition.
|
|
40
|
+
*/
|
|
41
|
+
shouldModeChange?: (fromMode: InlineFieldGroupMode, toMode: InlineFieldGroupMode) => boolean | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Whether the component is in a loading state (e.g., during async save).
|
|
44
|
+
*/
|
|
45
|
+
isLoading?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Optional className for custom styling
|
|
48
|
+
*/
|
|
49
|
+
className?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Optional aria-label for the form element.
|
|
52
|
+
* If not provided, aria-labelledby will reference the header title.
|
|
53
|
+
*/
|
|
54
|
+
'aria-label'?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Success message to announce when save succeeds.
|
|
57
|
+
* If not provided, no success announcement is made.
|
|
58
|
+
*/
|
|
59
|
+
successMessage?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Error message to announce when save or validation fails.
|
|
62
|
+
* If not provided, generic validation errors are announced.
|
|
63
|
+
*/
|
|
64
|
+
errorMessage?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* InlineFieldGroup enables group-level inline editing with read/edit mode switching.
|
|
68
|
+
* It integrates with TanStack Form for validation and state management, providing
|
|
69
|
+
* a complete pattern for displaying data in read mode and editing it in edit mode.
|
|
70
|
+
* The component handles the full edit lifecycle: mode transitions, form submission,
|
|
71
|
+
* validation error handling, and accessibility announcements.
|
|
72
|
+
* @example
|
|
73
|
+
* ```tsx
|
|
74
|
+
* import { useTanstackUnityForm } from '@payfit/unity-components'
|
|
75
|
+
*
|
|
76
|
+
* function ContactForm() {
|
|
77
|
+
* const form = useTanstackUnityForm({
|
|
78
|
+
* defaultValues: { email: 'user@example.com', phone: '+1234567890' },
|
|
79
|
+
* onSubmit: async ({ value }) => {
|
|
80
|
+
* await saveContact(value)
|
|
81
|
+
* }
|
|
82
|
+
* })
|
|
83
|
+
*
|
|
84
|
+
* return (
|
|
85
|
+
* <form.AppForm>
|
|
86
|
+
* <form.InlineFieldGroup successMessage="Contact saved!">
|
|
87
|
+
* <form.InlineFieldGroupHeader title="Contact Information" />
|
|
88
|
+
* <form.InlineFieldGroupReadView>
|
|
89
|
+
* <DefinitionList>
|
|
90
|
+
* <DefinitionItem term="Email" description={form.state.values.email} />
|
|
91
|
+
* </DefinitionList>
|
|
92
|
+
* </form.InlineFieldGroupReadView>
|
|
93
|
+
* <form.InlineFieldGroupEditView legend="Edit contact">
|
|
94
|
+
* <form.AppField name="email">
|
|
95
|
+
* {field => <field.TextField label="Email" />}
|
|
96
|
+
* </form.AppField>
|
|
97
|
+
* </form.InlineFieldGroupEditView>
|
|
98
|
+
* </form.InlineFieldGroup>
|
|
99
|
+
* </form.AppForm>
|
|
100
|
+
* )
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
* @remarks
|
|
104
|
+
* - The component automatically exits edit mode on successful form submission
|
|
105
|
+
* - Press Escape to cancel editing and reset form values
|
|
106
|
+
* - Focus moves to the first form field when entering edit mode
|
|
107
|
+
* - Focus returns to the edit button when exiting edit mode
|
|
108
|
+
* - Focus is retained under a scope when in edit mode, to prevent users for leaving unfinished changes
|
|
109
|
+
* - Use `shouldModeChange` to intercept and conditionally prevent mode transitions
|
|
110
|
+
* - Use `successMessage` and `errorMessage` for accessible announcements via live regions
|
|
111
|
+
* @see {@link InlineFieldGroupProps} for all available props
|
|
112
|
+
* @see {@link InlineFieldGroupHandle} for imperative handle methods
|
|
113
|
+
* @see {@link InlineFieldGroupHeader} for the header component with action buttons
|
|
114
|
+
* @see {@link InlineFieldGroupReadView} for read mode content
|
|
115
|
+
* @see {@link InlineFieldGroupEditView} for edit mode form fields
|
|
116
|
+
* @see Source code in {@link https://github.com/PayFit/hr-apps/tree/master/libs/shared/unity/components/src/components/inline-field-group GitHub}
|
|
117
|
+
* @see Developer docs in {@link https://unity-components.payfit.io/?path=/docs/forms-reference-inlinefieldgroup--docs unity-components.payfit.io}
|
|
118
|
+
*/
|
|
119
|
+
export declare const InlineFieldGroup: React.ForwardRefExoticComponent<InlineFieldGroupProps & React.RefAttributes<InlineFieldGroupHandle>>;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { jsx as x, jsxs as z } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef as J, useRef as l, useState as V, useEffect as r, useCallback as P, useImperativeHandle as Q, useMemo as W } from "react";
|
|
3
|
+
import { uyTv as X } from "@payfit/unity-themes";
|
|
4
|
+
import { useStore as Y } from "@tanstack/react-form";
|
|
5
|
+
import { useId as Z, useKeyboard as L, FocusScope as ee } from "react-aria";
|
|
6
|
+
import { useIntl as te } from "react-intl";
|
|
7
|
+
import { useFormContext as ie } from "../../hooks/tanstack-form-context.js";
|
|
8
|
+
import { useInlineFieldGroupMode as re } from "./hooks/useInlineFieldGroupMode.js";
|
|
9
|
+
import { InlineFieldGroupContext as oe } from "./InlineFieldGroup.context.js";
|
|
10
|
+
const se = X({
|
|
11
|
+
slots: {
|
|
12
|
+
form: "uy:flex uy:flex-col uy:gap-300"
|
|
13
|
+
}
|
|
14
|
+
}), D = (a) => {
|
|
15
|
+
if (!a.current) return;
|
|
16
|
+
const c = a.current.querySelector(
|
|
17
|
+
'[aria-invalid="true"]'
|
|
18
|
+
);
|
|
19
|
+
c && c.focus();
|
|
20
|
+
}, k = 5e3, ne = J(
|
|
21
|
+
({
|
|
22
|
+
children: a,
|
|
23
|
+
mode: c,
|
|
24
|
+
defaultMode: j,
|
|
25
|
+
onModeChange: q,
|
|
26
|
+
shouldModeChange: m,
|
|
27
|
+
isLoading: N,
|
|
28
|
+
className: K,
|
|
29
|
+
"aria-label": _,
|
|
30
|
+
successMessage: g,
|
|
31
|
+
errorMessage: d
|
|
32
|
+
}, O) => {
|
|
33
|
+
const s = Z(), A = `unity-InlineFieldGroup-${s}__header`, C = `unity-InlineFieldGroup-${s}__edit-view`, E = l(null), n = l(null), M = l(!0), [G, R] = V(""), [h, f] = V(""), p = te(), u = ie(), { mode: t, enterEditMode: b, exitEditMode: o } = re({
|
|
34
|
+
mode: c,
|
|
35
|
+
defaultMode: j,
|
|
36
|
+
onModeChange: q,
|
|
37
|
+
shouldModeChange: m
|
|
38
|
+
}), { isSubmitting: i, isValid: v, isSubmitSuccessful: S, submissionAttempts: y } = Y(u.store, (e) => ({
|
|
39
|
+
isSubmitting: e.isSubmitting,
|
|
40
|
+
isValid: e.isValid,
|
|
41
|
+
isSubmitSuccessful: e.isSubmitSuccessful,
|
|
42
|
+
submissionAttempts: e.submissionAttempts
|
|
43
|
+
})), F = l(i), T = l(y);
|
|
44
|
+
r(() => {
|
|
45
|
+
i && !F.current && (R(""), f(""));
|
|
46
|
+
}, [i]), r(() => {
|
|
47
|
+
if (y > T.current && !v) {
|
|
48
|
+
const I = d ?? p.formatMessage({
|
|
49
|
+
id: "unity:component:inline-field-group:validation-error",
|
|
50
|
+
defaultMessage: "Please fix the errors before saving."
|
|
51
|
+
});
|
|
52
|
+
f(I), D(n);
|
|
53
|
+
}
|
|
54
|
+
}, [y, v, d, p]), r(() => {
|
|
55
|
+
F.current && !i && S && (g && R(g), o());
|
|
56
|
+
}, [i, S, g, o]), r(() => {
|
|
57
|
+
if (F.current && !i && !S && v) {
|
|
58
|
+
const I = d ?? p.formatMessage({
|
|
59
|
+
id: "unity:component:inline-field-group:save-error",
|
|
60
|
+
defaultMessage: "An error occurred while saving. Please try again."
|
|
61
|
+
});
|
|
62
|
+
f(I);
|
|
63
|
+
}
|
|
64
|
+
}, [i, S, v, d, p]), r(() => {
|
|
65
|
+
F.current = i, T.current = y;
|
|
66
|
+
});
|
|
67
|
+
const $ = P(
|
|
68
|
+
(e) => {
|
|
69
|
+
e.preventDefault(), e.stopPropagation(), u.handleSubmit();
|
|
70
|
+
},
|
|
71
|
+
[u]
|
|
72
|
+
), w = P(() => {
|
|
73
|
+
m !== void 0 && !m(t, "read") || (u.reset(), o());
|
|
74
|
+
}, [u, t, o, m]);
|
|
75
|
+
Q(
|
|
76
|
+
O,
|
|
77
|
+
() => ({
|
|
78
|
+
focusFirstInvalidField: () => {
|
|
79
|
+
D(n);
|
|
80
|
+
},
|
|
81
|
+
exitEditMode: o,
|
|
82
|
+
enterEditMode: b
|
|
83
|
+
}),
|
|
84
|
+
[o, b]
|
|
85
|
+
), r(() => {
|
|
86
|
+
if (M.current) {
|
|
87
|
+
M.current = !1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (t === "edit") {
|
|
91
|
+
if (n.current) {
|
|
92
|
+
const e = n.current.querySelectorAll(
|
|
93
|
+
'input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
94
|
+
);
|
|
95
|
+
e.length > 0 && e[0].focus();
|
|
96
|
+
}
|
|
97
|
+
} else
|
|
98
|
+
E.current && E.current.focus();
|
|
99
|
+
}, [t]);
|
|
100
|
+
const { keyboardProps: B } = L({
|
|
101
|
+
onKeyDown: (e) => {
|
|
102
|
+
e.key === "Escape" && t === "edit" && (e.preventDefault(), w());
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
r(() => {
|
|
106
|
+
if (G) {
|
|
107
|
+
const e = setTimeout(() => {
|
|
108
|
+
R("");
|
|
109
|
+
}, k);
|
|
110
|
+
return () => {
|
|
111
|
+
clearTimeout(e);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}, [G]), r(() => {
|
|
115
|
+
if (h) {
|
|
116
|
+
const e = setTimeout(() => {
|
|
117
|
+
f("");
|
|
118
|
+
}, k);
|
|
119
|
+
return () => {
|
|
120
|
+
clearTimeout(e);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}, [h]);
|
|
124
|
+
const H = W(
|
|
125
|
+
() => ({
|
|
126
|
+
mode: t,
|
|
127
|
+
enterEditMode: b,
|
|
128
|
+
exitEditMode: w,
|
|
129
|
+
groupId: s,
|
|
130
|
+
headerId: A,
|
|
131
|
+
editViewId: C,
|
|
132
|
+
isLoading: N,
|
|
133
|
+
editButtonRef: E,
|
|
134
|
+
editViewRef: n
|
|
135
|
+
}),
|
|
136
|
+
[
|
|
137
|
+
t,
|
|
138
|
+
b,
|
|
139
|
+
w,
|
|
140
|
+
s,
|
|
141
|
+
A,
|
|
142
|
+
C,
|
|
143
|
+
N
|
|
144
|
+
]
|
|
145
|
+
), { form: U } = se();
|
|
146
|
+
return /* @__PURE__ */ x(oe.Provider, { value: H, children: /* @__PURE__ */ x(
|
|
147
|
+
"form",
|
|
148
|
+
{
|
|
149
|
+
...B,
|
|
150
|
+
id: s,
|
|
151
|
+
className: U({ className: K }),
|
|
152
|
+
onSubmit: $,
|
|
153
|
+
"aria-label": _,
|
|
154
|
+
"aria-labelledby": _ ? void 0 : A,
|
|
155
|
+
children: /* @__PURE__ */ z(ee, { contain: t === "edit", children: [
|
|
156
|
+
a,
|
|
157
|
+
/* @__PURE__ */ x(
|
|
158
|
+
"div",
|
|
159
|
+
{
|
|
160
|
+
role: "status",
|
|
161
|
+
"aria-live": "polite",
|
|
162
|
+
"aria-atomic": "true",
|
|
163
|
+
className: "uy:sr-only",
|
|
164
|
+
children: G
|
|
165
|
+
}
|
|
166
|
+
),
|
|
167
|
+
/* @__PURE__ */ x(
|
|
168
|
+
"div",
|
|
169
|
+
{
|
|
170
|
+
role: "alert",
|
|
171
|
+
"aria-live": "assertive",
|
|
172
|
+
"aria-atomic": "true",
|
|
173
|
+
className: "uy:sr-only",
|
|
174
|
+
children: h
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
] })
|
|
178
|
+
}
|
|
179
|
+
) });
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
ne.displayName = "InlineFieldGroup";
|
|
183
|
+
export {
|
|
184
|
+
ne as InlineFieldGroup
|
|
185
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { InlineFieldGroupMode } from '../InlineFieldGroup.context.js';
|
|
2
|
+
export interface UseInlineFieldGroupModeProps {
|
|
3
|
+
/**
|
|
4
|
+
* Controlled mode value. When provided, the component operates in controlled mode.
|
|
5
|
+
*/
|
|
6
|
+
mode?: InlineFieldGroupMode;
|
|
7
|
+
/**
|
|
8
|
+
* Default mode value for uncontrolled mode.
|
|
9
|
+
* @default 'read'
|
|
10
|
+
*/
|
|
11
|
+
defaultMode?: InlineFieldGroupMode;
|
|
12
|
+
/**
|
|
13
|
+
* Callback fired when mode changes.
|
|
14
|
+
*/
|
|
15
|
+
onModeChange?: (mode: InlineFieldGroupMode) => void;
|
|
16
|
+
/**
|
|
17
|
+
* Callback to intercept and potentially prevent mode changes.
|
|
18
|
+
* Return `false` to prevent the mode transition.
|
|
19
|
+
* Return `true` or `undefined` to allow the transition.
|
|
20
|
+
*/
|
|
21
|
+
shouldModeChange?: (fromMode: InlineFieldGroupMode, toMode: InlineFieldGroupMode) => boolean | undefined;
|
|
22
|
+
}
|
|
23
|
+
export interface UseInlineFieldGroupModeReturn {
|
|
24
|
+
/** Current mode value */
|
|
25
|
+
mode: InlineFieldGroupMode;
|
|
26
|
+
/** Switch to edit mode */
|
|
27
|
+
enterEditMode: () => void;
|
|
28
|
+
/** Exit edit mode (return to read mode) */
|
|
29
|
+
exitEditMode: () => void;
|
|
30
|
+
/** Check if component is in controlled mode */
|
|
31
|
+
isControlled: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Hook to manage the mode state (read/edit) for InlineFieldGroup.
|
|
35
|
+
* Supports both controlled and uncontrolled modes, following the pattern
|
|
36
|
+
* established by NavGroup.tsx
|
|
37
|
+
* @param props - Configuration for the mode hook
|
|
38
|
+
* @param props.mode - Controlled mode value. When provided, the component operates in controlled mode.
|
|
39
|
+
* @param props.defaultMode - Default mode value for uncontrolled mode.
|
|
40
|
+
* @param props.onModeChange - Callback fired when mode changes.
|
|
41
|
+
* @param props.shouldModeChange - Callback to intercept and potentially prevent mode changes.
|
|
42
|
+
* Return `false` to prevent the mode transition.
|
|
43
|
+
* Return `true` or `undefined` to allow the transition.
|
|
44
|
+
* @returns Mode state and handlers
|
|
45
|
+
*/
|
|
46
|
+
export declare function useInlineFieldGroupMode({ mode: controlledMode, defaultMode, onModeChange, shouldModeChange, }: UseInlineFieldGroupModeProps): UseInlineFieldGroupModeReturn;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useState as m, useCallback as i } from "react";
|
|
2
|
+
function x({
|
|
3
|
+
mode: d,
|
|
4
|
+
defaultMode: c = "read",
|
|
5
|
+
onModeChange: s,
|
|
6
|
+
shouldModeChange: o
|
|
7
|
+
}) {
|
|
8
|
+
const [l, u] = m(c), t = d !== void 0, n = t ? d : l, e = i(
|
|
9
|
+
(r) => {
|
|
10
|
+
o && o(n, r) === !1 || (t || u(r), s?.(r));
|
|
11
|
+
},
|
|
12
|
+
[t, n, s, o]
|
|
13
|
+
), a = i(() => {
|
|
14
|
+
e("edit");
|
|
15
|
+
}, [e]), f = i(() => {
|
|
16
|
+
e("read");
|
|
17
|
+
}, [e]);
|
|
18
|
+
return {
|
|
19
|
+
mode: n,
|
|
20
|
+
enterEditMode: a,
|
|
21
|
+
exitEditMode: f,
|
|
22
|
+
isControlled: t
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
x as useInlineFieldGroupMode
|
|
27
|
+
};
|