@enonic/ui-kit 0.3.0 → 0.5.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/README.md +78 -8
- package/dist/dialog/action-dialog-context.d.ts +22 -0
- package/dist/dialog/action-dialog-context.js +21 -0
- package/dist/dialog/action-dialog-context.js.map +1 -0
- package/dist/dialog/action-dialog.d.ts +135 -0
- package/dist/dialog/action-dialog.js +213 -0
- package/dist/dialog/action-dialog.js.map +1 -0
- package/dist/dialog/delete-confirm.d.ts +33 -0
- package/dist/dialog/delete-confirm.js +80 -0
- package/dist/dialog/delete-confirm.js.map +1 -0
- package/dist/dialog/delete-expectation.d.ts +17 -0
- package/dist/dialog/delete-expectation.js +13 -0
- package/dist/dialog/delete-expectation.js.map +1 -0
- package/dist/dialog/dialog-preset.d.ts +34 -0
- package/dist/dialog/dialog-preset.js +38 -0
- package/dist/dialog/dialog-preset.js.map +1 -0
- package/dist/dialog/gate-match.d.ts +6 -0
- package/dist/dialog/gate-match.js +11 -0
- package/dist/dialog/gate-match.js.map +1 -0
- package/dist/dialog/gate.d.ts +31 -0
- package/dist/dialog/gate.js +72 -0
- package/dist/dialog/gate.js.map +1 -0
- package/dist/dialog/use-close-guard.d.ts +34 -0
- package/dist/dialog/use-close-guard.js +40 -0
- package/dist/dialog/use-close-guard.js.map +1 -0
- package/dist/filter/filter-fields.d.ts +33 -0
- package/dist/filter/filter-fields.js +72 -0
- package/dist/filter/filter-fields.js.map +1 -0
- package/dist/filter/filter-input.d.ts +35 -0
- package/dist/filter/filter-input.js +441 -0
- package/dist/filter/filter-input.js.map +1 -0
- package/dist/filter/filter-input.phrases.d.ts +26 -0
- package/dist/filter/filter-input.phrases.js +26 -0
- package/dist/filter/filter-input.phrases.js.map +1 -0
- package/dist/filter/filter-query.d.ts +40 -0
- package/dist/filter/filter-query.js +61 -0
- package/dist/filter/filter-query.js.map +1 -0
- package/dist/i18n/fill-phrase.d.ts +10 -0
- package/dist/i18n/fill-phrase.js +25 -0
- package/dist/i18n/fill-phrase.js.map +1 -0
- package/dist/i18n/phrases.d.ts +14 -0
- package/dist/i18n/phrases.js +23 -0
- package/dist/i18n/phrases.js.map +1 -0
- package/dist/i18n/use-phrases.d.ts +9 -0
- package/dist/i18n/use-phrases.js +11 -0
- package/dist/i18n/use-phrases.js.map +1 -0
- package/dist/index.d.ts +16 -1
- package/dist/index.js +15 -0
- package/package.json +34 -6
- package/preset.css +6 -0
package/README.md
CHANGED
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
# @enonic/ui-kit
|
|
2
2
|
|
|
3
3
|
Composite React components for Enonic applications — what
|
|
4
|
-
[`@enonic/ui`](https://www.npmjs.com/package/@enonic/ui) would be if its parts carried behaviour
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
[`@enonic/ui`](https://www.npmjs.com/package/@enonic/ui) would be if its parts carried behaviour.
|
|
5
|
+
The dialog with an outcome and its confirmations are here, and the filter input of a browse screen;
|
|
6
|
+
layouts, toolbars and the rest of the browse screen follow.
|
|
7
7
|
|
|
8
8
|
A component belongs here when it holds state, coordinates several base components, or encodes a
|
|
9
9
|
screen pattern more than one application repeats. Props in, callbacks out: nothing here reaches
|
|
10
10
|
into a host application's configuration, stores, router or phrase keys.
|
|
11
11
|
|
|
12
|
-
> **Status**: scaffolding, pre-1.0. The build and release path work end to end; the components
|
|
13
|
-
> land package by package.
|
|
14
|
-
|
|
15
12
|
## Install
|
|
16
13
|
|
|
17
14
|
Pick one framework — the same contract as `@enonic/ui`:
|
|
18
15
|
|
|
19
16
|
```sh
|
|
20
|
-
pnpm add @enonic/ui-kit react react-dom # React
|
|
21
|
-
pnpm add @enonic/ui-kit preact # Preact
|
|
17
|
+
pnpm add @enonic/ui-kit @enonic/ui react react-dom # React
|
|
18
|
+
pnpm add @enonic/ui-kit @enonic/ui preact # Preact
|
|
22
19
|
```
|
|
23
20
|
|
|
24
21
|
The components are written as React code. On Preact, map React's names to `preact/compat` in the
|
|
@@ -35,4 +32,77 @@ resolve: {
|
|
|
35
32
|
},
|
|
36
33
|
```
|
|
37
34
|
|
|
35
|
+
## Styles
|
|
36
|
+
|
|
37
|
+
The components carry Tailwind classes and ship no CSS: the application's Tailwind build generates
|
|
38
|
+
what they use. Import the kit's preset after `@enonic/ui`'s — it points the build at the package's
|
|
39
|
+
`dist`, and the tokens the classes use are `@enonic/ui`'s:
|
|
40
|
+
|
|
41
|
+
```css
|
|
42
|
+
@import 'tailwindcss';
|
|
43
|
+
@import '@enonic/ui/preset.css';
|
|
44
|
+
@import '@enonic/ui-kit/preset.css';
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Labels
|
|
48
|
+
|
|
49
|
+
Every text a component renders on its own — a Cancel, a Confirm, the gate's hint — is English in
|
|
50
|
+
`uiKitPhrases`, keyed `enonic.uiKit.<area>.<name>`, and goes through the `Translate` the
|
|
51
|
+
application hands `@enonic/ui`'s `I18nProvider`; a key the application has no text for renders the
|
|
52
|
+
English. Titles, questions and target labels are props. `comparePhrases` from `@enonic/ui-utils`
|
|
53
|
+
tells an application which keys its bundle lacks.
|
|
54
|
+
|
|
55
|
+
## What is here
|
|
56
|
+
|
|
57
|
+
| Export | What it does |
|
|
58
|
+
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
59
|
+
| `ActionDialog` | `Dialog` from `@enonic/ui` plus an outcome: a `Content` that holds whether the action may be taken, a `Body` that goes inert while the footer asks, a `Footer` that takes the action or asks |
|
|
60
|
+
| `ActionDialog.Footer` | Cancel and Confirm when given `onConfirm`, or the controls it is given as children — a wizard's step indicator; `intent`, `error`, `confirmRef`; `question` asks in place of the controls |
|
|
61
|
+
| `ActionDialog.Action` | a further button of the footer, enabled by the content like Confirm; `intent`, `closeOnClick` |
|
|
62
|
+
| `useCloseGuard({ dirty, open, onOpenChange })` | the dirty-close question: the mask, the X, `Escape` and Cancel ask before a close that would lose something; `asking` feeds the footer's `question`, `keep` returns the focus |
|
|
63
|
+
| `Gate` | `Root`, `Hint` naming what to type back, `Input` that enables the confirm button on a match and locks |
|
|
64
|
+
| `DialogPreset.Confirm` | the plain question: title, question, two answers of equal weight |
|
|
65
|
+
| `DeleteConfirm` | the delete view: question, targets, the gate asking for the one name or the count, a red button — the first view, or the view after a list, with `closeOnCancel` off to go back to it |
|
|
66
|
+
| `deleteExpectation(targets)` | the one name, or the count |
|
|
67
|
+
| `matchesExpected(typed, expected)` | the gate's comparison, spaces around the entry forgiven |
|
|
68
|
+
| `fillPhrase(phrase, values)` | a phrase with its `{n}` placeholders replaced by nodes |
|
|
69
|
+
| `uiKitPhrases`, `useUiKitPhrases()` | the catalogue, and the hook that resolves it through the application's `Translate` |
|
|
70
|
+
| `useActionDialog()` | the content's state, for anything inside it that enables the action |
|
|
71
|
+
| `FilterInput` | one input for a list's filter: values of predefined `fields` combined with free text, every term a `Tag`; `value`/`onChange` or `defaultValue`; `disabled`, `readOnly` (the tags alone, no input), `error`; the ref is the text input's |
|
|
72
|
+
| `FilterField`, `FilterValue` | what the dropdown offers: a field with an id, a label, an icon, its values, `multiple` and a `notice`; a value with a `count`, a `group` and `disabled` |
|
|
73
|
+
| `FilterQuery`, `FilterTerm` | what the list is narrowed by: `FieldTerm` and `TextTerm`, discriminated by `kind`, ids never labels, in the order they were added; `fieldTerm`, `textTerm`, `termKey` |
|
|
74
|
+
| `textOf(query)`, `valuesOf(query, field)` | the two readings a list takes of a query: the free text as one string, and every value picked for a field — empty narrows nothing |
|
|
75
|
+
| `withTerm`, `withoutTerm`, `toggledTerm` | the pure operations on a query the input is built from, with `sameTerm`, `isTextTerm`, `isFieldTerm` and `EMPTY_FILTER` |
|
|
76
|
+
| `filterInputPhrases` | the input's English, for an application's `comparePhrases` |
|
|
77
|
+
|
|
78
|
+
## The filter
|
|
79
|
+
|
|
80
|
+
`FilterInput` is the GitHub-style filter of a browse screen. Its dropdown offers the `fields` the
|
|
81
|
+
caller passes — each with an id, a label, an icon and the values it can take; a value carries a
|
|
82
|
+
`count` where the list knows its hits, a `group` where values are listed under a labelled heading,
|
|
83
|
+
and `disabled` where it is withheld for another reason; a field with `multiple: false` holds one
|
|
84
|
+
value at a time, one with a `notice` says above its values why they may be short or stale, and one
|
|
85
|
+
with `loading` says its values are still on their way. The
|
|
86
|
+
dropdown opens on a click, a keystroke or an arrow, not on focus. The fields come first; picking
|
|
87
|
+
one, or typing its label and a colon — or pasting `Field: value` whole — opens its values ordered by
|
|
88
|
+
hits, the empty ones last and disabled, with a `No matches` row when the typed text finds none.
|
|
89
|
+
Enter picks the highlighted value, and the values stay open so several can be ticked; a tick picked
|
|
90
|
+
again, or the tag's cross, takes the term back. Typed text with no field entered becomes a tag on
|
|
91
|
+
Enter. Backspace in the empty input steps out of a field, or takes the last tag back; ArrowLeft at
|
|
92
|
+
the start of the input walks onto the tags, where arrows move between them and Backspace or Delete
|
|
93
|
+
removes one. The right-hand cross clears everything, and leaving the filter drops a field no value
|
|
94
|
+
was picked for. Every add, remove and clearing is announced to assistive technology.
|
|
95
|
+
|
|
96
|
+
The query is a list of terms, and the component decides nothing about what they mean. The reading
|
|
97
|
+
the helpers encode, and the one the list should match by, is: values of one field are ORed, fields
|
|
98
|
+
are ANDed, and free text is ANDed with everything, word by word, without regard to case.
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
const [query, setQuery] = useState<FilterQuery>(EMPTY_FILTER);
|
|
102
|
+
const providers = valuesOf(query, 'idProvider'); // a Set of ids; empty narrows nothing
|
|
103
|
+
const text = textOf(query); // 'alice ward'
|
|
104
|
+
|
|
105
|
+
<FilterInput fields={fields} value={query} onChange={setQuery} placeholder="Search users" />;
|
|
106
|
+
```
|
|
107
|
+
|
|
38
108
|
Part of the [Enonic UI Toolkit](https://github.com/enonic/npm-enonic-ui-toolkit).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MutableRefObject, ReactElement, ReactNode } from "react";
|
|
2
|
+
//#region src/dialog/action-dialog-context.d.ts
|
|
3
|
+
type ActionDialogContextValue = {
|
|
4
|
+
/** Whether the action may be taken: anything inside the content may decide, a gate does. */
|
|
5
|
+
confirmEnabled: boolean;
|
|
6
|
+
setConfirmEnabled: (next: boolean) => void;
|
|
7
|
+
/** Whether the footer is asking its question, during which the body is inert. */
|
|
8
|
+
asking: boolean;
|
|
9
|
+
setAsking: (next: boolean) => void;
|
|
10
|
+
/** The way back from the question, for `Escape`. */
|
|
11
|
+
keepRef: MutableRefObject<(() => void) | undefined>;
|
|
12
|
+
/** The element inside the content that last had the focus, for the way back from the question. */
|
|
13
|
+
lastFocusRef: MutableRefObject<HTMLElement | null>;
|
|
14
|
+
};
|
|
15
|
+
declare const useActionDialog: () => ActionDialogContextValue;
|
|
16
|
+
/** Why `onOpenChange` closes: `action` when an `Action` or the footer's Confirm took the outcome. */
|
|
17
|
+
type ActionDialogOpenChangeDetails = {
|
|
18
|
+
reason: 'action';
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
export { ActionDialogContextValue, ActionDialogOpenChangeDetails, useActionDialog };
|
|
22
|
+
//# sourceMappingURL=action-dialog-context.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
//#region src/dialog/action-dialog-context.tsx
|
|
4
|
+
const ActionDialogContext = createContext(void 0);
|
|
5
|
+
const ActionDialogProvider = ({ value, children }) => /* @__PURE__ */ jsx(ActionDialogContext.Provider, {
|
|
6
|
+
value,
|
|
7
|
+
children
|
|
8
|
+
});
|
|
9
|
+
ActionDialogProvider.displayName = "ActionDialogProvider";
|
|
10
|
+
const useActionDialog = () => {
|
|
11
|
+
const context = useContext(ActionDialogContext);
|
|
12
|
+
if (context === void 0) throw new Error("useActionDialog must be used within ActionDialog.Content");
|
|
13
|
+
return context;
|
|
14
|
+
};
|
|
15
|
+
const ActionDialogRootContext = createContext(void 0);
|
|
16
|
+
const ActionDialogRootProvider = ActionDialogRootContext.Provider;
|
|
17
|
+
const useActionDialogRoot = () => useContext(ActionDialogRootContext);
|
|
18
|
+
//#endregion
|
|
19
|
+
export { ActionDialogProvider, ActionDialogRootProvider, useActionDialog, useActionDialogRoot };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=action-dialog-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action-dialog-context.js","names":[],"sources":["../../src/dialog/action-dialog-context.tsx"],"sourcesContent":["import {\n createContext,\n type MutableRefObject,\n type ReactElement,\n type ReactNode,\n useContext,\n} from 'react';\n\nexport type ActionDialogContextValue = {\n /** Whether the action may be taken: anything inside the content may decide, a gate does. */\n confirmEnabled: boolean;\n setConfirmEnabled: (next: boolean) => void;\n /** Whether the footer is asking its question, during which the body is inert. */\n asking: boolean;\n setAsking: (next: boolean) => void;\n /** The way back from the question, for `Escape`. */\n keepRef: MutableRefObject<(() => void) | undefined>;\n /** The element inside the content that last had the focus, for the way back from the question. */\n lastFocusRef: MutableRefObject<HTMLElement | null>;\n};\n\nconst ActionDialogContext = createContext<ActionDialogContextValue | undefined>(undefined);\n\nexport type ActionDialogProviderProps = {\n value: ActionDialogContextValue;\n children?: ReactNode;\n};\n\nexport const ActionDialogProvider = ({\n value,\n children,\n}: ActionDialogProviderProps): ReactElement => (\n <ActionDialogContext.Provider value={value}>{children}</ActionDialogContext.Provider>\n);\nActionDialogProvider.displayName = 'ActionDialogProvider';\n\nexport const useActionDialog = (): ActionDialogContextValue => {\n const context = useContext(ActionDialogContext);\n if (context === undefined) {\n throw new Error('useActionDialog must be used within ActionDialog.Content');\n }\n return context;\n};\n\n/** Why `onOpenChange` closes: `action` when an `Action` or the footer's Confirm took the outcome. */\nexport type ActionDialogOpenChangeDetails = { reason: 'action' };\n\nexport type ActionDialogRootContextValue = {\n /** Set by an action that closes, read by the close it causes. */\n actionCloseRef: MutableRefObject<boolean>;\n};\n\nconst ActionDialogRootContext = createContext<ActionDialogRootContextValue | undefined>(undefined);\n\nexport const ActionDialogRootProvider = ActionDialogRootContext.Provider;\n\nexport const useActionDialogRoot = (): ActionDialogRootContextValue | undefined =>\n useContext(ActionDialogRootContext);\n"],"mappings":";;;AAqBA,MAAM,sBAAsB,cAAoD,KAAA,CAAS;AAOzF,MAAa,wBAAwB,EACnC,OACA,eAEA,oBAAC,oBAAoB,UAArB;CAAqC;CAAQ;AAAuC,CAAA;AAEtF,qBAAqB,cAAc;AAEnC,MAAa,wBAAkD;CAC7D,MAAM,UAAU,WAAW,mBAAmB;CAC9C,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,MAAM,0DAA0D;CAE5E,OAAO;AACT;AAUA,MAAM,0BAA0B,cAAwD,KAAA,CAAS;AAEjG,MAAa,2BAA2B,wBAAwB;AAEhE,MAAa,4BACX,WAAW,uBAAuB"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { ActionDialogOpenChangeDetails } from "./action-dialog-context.js";
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactElement, ReactNode, Ref } from "react";
|
|
3
|
+
import { Button, ButtonVariant, Dialog, DialogRootProps } from "@enonic/ui";
|
|
4
|
+
//#region src/dialog/action-dialog.d.ts
|
|
5
|
+
type DialogIntent = 'default' | 'danger';
|
|
6
|
+
/** A question needs no more room than its own text; `medium` carries a control, `wide` a form. */
|
|
7
|
+
type DialogSize = 'default' | 'medium' | 'wide';
|
|
8
|
+
type ActionDialogRootProps = Omit<DialogRootProps, 'onOpenChange'> & {
|
|
9
|
+
/** `details` tells a close an action took from one that would leave without it. */
|
|
10
|
+
onOpenChange?: (open: boolean, details?: ActionDialogOpenChangeDetails) => void;
|
|
11
|
+
};
|
|
12
|
+
declare const ActionDialogRoot: {
|
|
13
|
+
({ onOpenChange, ...props }: ActionDialogRootProps): ReactElement;
|
|
14
|
+
displayName: string;
|
|
15
|
+
};
|
|
16
|
+
type ActionDialogContentProps = {
|
|
17
|
+
size?: DialogSize;
|
|
18
|
+
/** Whether the action starts enabled; a content with a gate starts it disabled. */
|
|
19
|
+
defaultConfirmEnabled?: boolean;
|
|
20
|
+
} & ComponentPropsWithoutRef<typeof Dialog.Content>;
|
|
21
|
+
type ActionDialogBodyProps = ComponentPropsWithoutRef<typeof Dialog.Body>;
|
|
22
|
+
type ActionDialogActionProps = {
|
|
23
|
+
label: string;
|
|
24
|
+
/** `danger` paints the button red, for an action there is no undoing. */
|
|
25
|
+
intent?: DialogIntent;
|
|
26
|
+
/** Enabled while the content allows the action — a gate matched, a form valid — unless disabled outright. */
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
/** Close the dialog on click; off for a caller that moves on to a progress view instead. */
|
|
29
|
+
closeOnClick?: boolean;
|
|
30
|
+
variant?: ButtonVariant;
|
|
31
|
+
} & Omit<ComponentPropsWithoutRef<typeof Button>, 'label' | 'variant' | 'disabled'>;
|
|
32
|
+
/** A question the footer asks in place of its controls. */
|
|
33
|
+
type FooterQuestion = {
|
|
34
|
+
text: ReactNode;
|
|
35
|
+
intent?: DialogIntent;
|
|
36
|
+
/** Defaults to the kit's "Confirm". */
|
|
37
|
+
confirmLabel?: string;
|
|
38
|
+
/** The way back; defaults to the kit's "Keep editing". */
|
|
39
|
+
keepLabel?: string;
|
|
40
|
+
onConfirm: () => void;
|
|
41
|
+
onKeep: () => void;
|
|
42
|
+
};
|
|
43
|
+
type ActionDialogFooterProps = {
|
|
44
|
+
/** Given `onConfirm`, the footer renders Cancel and Confirm; without it, its children are the controls. */
|
|
45
|
+
onConfirm?: () => void;
|
|
46
|
+
onCancel?: () => void;
|
|
47
|
+
/** Defaults to the kit's "Confirm". */
|
|
48
|
+
confirmLabel?: string;
|
|
49
|
+
/** Defaults to the kit's "Cancel". */
|
|
50
|
+
cancelLabel?: string;
|
|
51
|
+
/** `danger` paints the confirm button red, for an action there is no undoing. */
|
|
52
|
+
intent?: DialogIntent;
|
|
53
|
+
/** Why the dialog is still open, shown beside its controls — a rejected save is the case. */
|
|
54
|
+
error?: string;
|
|
55
|
+
/** For a caller that moves the focus onto the confirm button once it means something. */
|
|
56
|
+
confirmRef?: Ref<HTMLButtonElement>;
|
|
57
|
+
/** `outline` gives the two answers of a question equal weight; a form's Cancel stays quiet. */
|
|
58
|
+
cancelVariant?: ButtonVariant;
|
|
59
|
+
/** Close the dialog on Cancel; off for a caller that closes it itself. */
|
|
60
|
+
closeOnCancel?: boolean;
|
|
61
|
+
/** Close the dialog on Confirm; off for a caller that moves on to a progress view instead. */
|
|
62
|
+
closeOnConfirm?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* While set, the footer asks this instead of showing its controls: the body goes inert, `Escape`
|
|
65
|
+
* is `onKeep`, and the focus goes to the way back. `useCloseGuard` is the usual source.
|
|
66
|
+
*/
|
|
67
|
+
question?: FooterQuestion;
|
|
68
|
+
} & ComponentPropsWithoutRef<typeof Dialog.Footer>;
|
|
69
|
+
/**
|
|
70
|
+
* `Dialog` from `@enonic/ui` plus an outcome: a `Content` that holds whether the action may be
|
|
71
|
+
* taken, a `Body` that goes inert while the footer asks a question, a `Footer` that takes the
|
|
72
|
+
* action or asks the question, and `Action` for a further button the content enables. Stacks
|
|
73
|
+
* nothing: a question goes to the footer, content with room of its own is another `Content` under
|
|
74
|
+
* the same `Root`.
|
|
75
|
+
*/
|
|
76
|
+
declare const ActionDialog: {
|
|
77
|
+
displayName: string;
|
|
78
|
+
Trigger: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").DialogTriggerProps> & {
|
|
79
|
+
ref?: import('preact').Ref<HTMLButtonElement> | undefined;
|
|
80
|
+
}>;
|
|
81
|
+
Portal: {
|
|
82
|
+
({ children, container, forceMount }: import("@enonic/ui").DialogPortalProps): ReactElement | null;
|
|
83
|
+
displayName: string;
|
|
84
|
+
};
|
|
85
|
+
Overlay: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").DialogOverlayProps> & {
|
|
86
|
+
ref?: import('preact').Ref<HTMLDivElement> | undefined;
|
|
87
|
+
}>;
|
|
88
|
+
Header: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").DialogHeaderProps> & {
|
|
89
|
+
ref?: import('preact').Ref<HTMLElement> | undefined;
|
|
90
|
+
}>;
|
|
91
|
+
Close: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").DialogCloseProps> & {
|
|
92
|
+
ref?: import('preact').Ref<HTMLButtonElement> | undefined;
|
|
93
|
+
}>;
|
|
94
|
+
Title: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").DialogTitleProps> & {
|
|
95
|
+
ref?: import('preact').Ref<HTMLHeadingElement> | undefined;
|
|
96
|
+
}>;
|
|
97
|
+
Description: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").DialogDescriptionProps> & {
|
|
98
|
+
ref?: import('preact').Ref<HTMLParagraphElement> | undefined;
|
|
99
|
+
}>;
|
|
100
|
+
DefaultClose: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<Partial<ComponentPropsWithoutRef<import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").IconButtonProps> & {
|
|
101
|
+
ref?: import('preact').Ref<HTMLButtonElement> | undefined;
|
|
102
|
+
}>>>> & {
|
|
103
|
+
ref?: import('preact').Ref<HTMLButtonElement> | undefined;
|
|
104
|
+
}>;
|
|
105
|
+
DefaultHeader: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").DialogDefaultHeaderProps> & {
|
|
106
|
+
ref?: import('preact').Ref<HTMLElement> | undefined;
|
|
107
|
+
}>;
|
|
108
|
+
StepHeader: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").DialogStepHeaderProps> & {
|
|
109
|
+
ref?: import('preact').Ref<HTMLElement> | undefined;
|
|
110
|
+
}>;
|
|
111
|
+
StepContent: {
|
|
112
|
+
({ step, locked, children }: import("@enonic/ui").DialogStepContentProps): ReactElement;
|
|
113
|
+
displayName: string;
|
|
114
|
+
};
|
|
115
|
+
StepIndicator: {
|
|
116
|
+
({ previousLabel, nextLabel, lastStepLabel, onLastStep, dots, disabled, pending, renderDot }: import("@enonic/ui").DialogStepIndicatorProps): ReactElement;
|
|
117
|
+
displayName: string;
|
|
118
|
+
};
|
|
119
|
+
Root: typeof ActionDialogRoot;
|
|
120
|
+
Content: import("preact").FunctionalComponent<import("preact/compat").PropsWithoutRef<ActionDialogContentProps> & {
|
|
121
|
+
ref?: Ref<HTMLDivElement> | undefined;
|
|
122
|
+
}>;
|
|
123
|
+
Body: import("preact").FunctionalComponent<import("preact/compat").PropsWithoutRef<ActionDialogBodyProps> & {
|
|
124
|
+
ref?: Ref<HTMLDivElement> | undefined;
|
|
125
|
+
}>;
|
|
126
|
+
Footer: import("preact").FunctionalComponent<import("preact/compat").PropsWithoutRef<ActionDialogFooterProps> & {
|
|
127
|
+
ref?: Ref<HTMLElement> | undefined;
|
|
128
|
+
}>;
|
|
129
|
+
Action: import("preact").FunctionalComponent<import("preact/compat").PropsWithoutRef<ActionDialogActionProps> & {
|
|
130
|
+
ref?: Ref<HTMLButtonElement> | undefined;
|
|
131
|
+
}>;
|
|
132
|
+
};
|
|
133
|
+
//#endregion
|
|
134
|
+
export { ActionDialog, ActionDialogActionProps, ActionDialogBodyProps, ActionDialogContentProps, ActionDialogFooterProps, ActionDialogRootProps, DialogIntent, DialogSize, FooterQuestion };
|
|
135
|
+
//# sourceMappingURL=action-dialog.d.ts.map
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { ActionDialogProvider, ActionDialogRootProvider, useActionDialog, useActionDialogRoot } from "./action-dialog-context.js";
|
|
2
|
+
import { useUiKitPhrases } from "../i18n/use-phrases.js";
|
|
3
|
+
import { forwardRef, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { Button, Dialog, cn, usePrefixedId } from "@enonic/ui";
|
|
6
|
+
import { TriangleAlert } from "lucide-react";
|
|
7
|
+
//#region src/dialog/action-dialog.tsx
|
|
8
|
+
const SIZES = {
|
|
9
|
+
default: "max-w-lg",
|
|
10
|
+
medium: "max-w-180 sm:min-w-152",
|
|
11
|
+
wide: "max-w-4xl"
|
|
12
|
+
};
|
|
13
|
+
const DANGER_BUTTON_CLASS_NAME = "bg-btn-error text-alt hover:bg-btn-error-hover active:bg-btn-error-active focus-visible:ring-error/50";
|
|
14
|
+
const ActionDialogRoot = ({ onOpenChange, ...props }) => {
|
|
15
|
+
const actionCloseRef = useRef(false);
|
|
16
|
+
const value = useMemo(() => ({ actionCloseRef }), []);
|
|
17
|
+
return /* @__PURE__ */ jsx(ActionDialogRootProvider, {
|
|
18
|
+
value,
|
|
19
|
+
children: /* @__PURE__ */ jsx(Dialog.Root, {
|
|
20
|
+
...props,
|
|
21
|
+
onOpenChange: (open) => {
|
|
22
|
+
const byAction = actionCloseRef.current;
|
|
23
|
+
actionCloseRef.current = false;
|
|
24
|
+
onOpenChange?.(open, !open && byAction ? { reason: "action" } : void 0);
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
ActionDialogRoot.displayName = "ActionDialog.Root";
|
|
30
|
+
const CONTENT_NAME = "ActionDialog.Content";
|
|
31
|
+
const QUESTION_NAME = "ActionDialog.Question";
|
|
32
|
+
const ActionDialogContent = forwardRef(({ size = "default", defaultConfirmEnabled = true, className, children, onEscapeKeyDown, onFocusCapture, ...props }, ref) => {
|
|
33
|
+
const [confirmEnabled, setConfirmEnabled] = useState(defaultConfirmEnabled);
|
|
34
|
+
const [asking, setAsking] = useState(false);
|
|
35
|
+
const keepRef = useRef(void 0);
|
|
36
|
+
const lastFocusRef = useRef(null);
|
|
37
|
+
const wasAskingRef = useRef(false);
|
|
38
|
+
const context = useMemo(() => ({
|
|
39
|
+
confirmEnabled,
|
|
40
|
+
setConfirmEnabled,
|
|
41
|
+
asking,
|
|
42
|
+
setAsking,
|
|
43
|
+
keepRef,
|
|
44
|
+
lastFocusRef
|
|
45
|
+
}), [confirmEnabled, asking]);
|
|
46
|
+
useLayoutEffect(() => {
|
|
47
|
+
const target = lastFocusRef.current;
|
|
48
|
+
const answered = wasAskingRef.current && !asking;
|
|
49
|
+
wasAskingRef.current = asking;
|
|
50
|
+
if (!answered || target === null) return;
|
|
51
|
+
const frame = requestAnimationFrame(() => target.focus());
|
|
52
|
+
return () => cancelAnimationFrame(frame);
|
|
53
|
+
}, [asking]);
|
|
54
|
+
return /* @__PURE__ */ jsx(Dialog.Content, {
|
|
55
|
+
ref,
|
|
56
|
+
"data-component": CONTENT_NAME,
|
|
57
|
+
className: cn("gap-5 p-5 md:p-7.5", SIZES[size], className),
|
|
58
|
+
onFocusCapture: (event) => {
|
|
59
|
+
onFocusCapture?.(event);
|
|
60
|
+
if (event.target instanceof HTMLElement && event.target.closest(`[data-component="${QUESTION_NAME}"]`) === null) lastFocusRef.current = event.target;
|
|
61
|
+
},
|
|
62
|
+
onEscapeKeyDown: (event) => {
|
|
63
|
+
onEscapeKeyDown?.(event);
|
|
64
|
+
if (asking && !event.defaultPrevented) {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
keepRef.current?.();
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
...props,
|
|
70
|
+
children: /* @__PURE__ */ jsx(ActionDialogProvider, {
|
|
71
|
+
value: context,
|
|
72
|
+
children
|
|
73
|
+
})
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
ActionDialogContent.displayName = CONTENT_NAME;
|
|
77
|
+
const BODY_NAME = "ActionDialog.Body";
|
|
78
|
+
const ActionDialogBody = forwardRef(({ className, ...props }, ref) => {
|
|
79
|
+
const { asking } = useActionDialog();
|
|
80
|
+
return /* @__PURE__ */ jsx(Dialog.Body, {
|
|
81
|
+
ref,
|
|
82
|
+
"data-component": BODY_NAME,
|
|
83
|
+
className: cn("-mx-2 flex flex-col gap-7 px-2 py-1 transition-opacity", asking && "opacity-50", className),
|
|
84
|
+
...props,
|
|
85
|
+
inert: asking || props.inert
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
ActionDialogBody.displayName = BODY_NAME;
|
|
89
|
+
const ACTION_NAME = "ActionDialog.Action";
|
|
90
|
+
/** An action of the footer: enabled by the content, red for a destructive one. */
|
|
91
|
+
const ActionDialogAction = forwardRef(({ label, intent = "default", disabled = false, closeOnClick = true, variant = "solid", className, onClick, ...props }, ref) => {
|
|
92
|
+
const { confirmEnabled } = useActionDialog();
|
|
93
|
+
const root = useActionDialogRoot();
|
|
94
|
+
const button = /* @__PURE__ */ jsx(Button, {
|
|
95
|
+
ref,
|
|
96
|
+
"data-component": ACTION_NAME,
|
|
97
|
+
variant,
|
|
98
|
+
size: "lg",
|
|
99
|
+
label,
|
|
100
|
+
disabled: disabled || !confirmEnabled,
|
|
101
|
+
className: cn(intent === "danger" && DANGER_BUTTON_CLASS_NAME, className),
|
|
102
|
+
onClick: (event) => {
|
|
103
|
+
onClick?.(event);
|
|
104
|
+
if (closeOnClick && root !== void 0 && !event.defaultPrevented) root.actionCloseRef.current = true;
|
|
105
|
+
},
|
|
106
|
+
...props
|
|
107
|
+
});
|
|
108
|
+
return closeOnClick ? /* @__PURE__ */ jsx(Dialog.Close, {
|
|
109
|
+
asChild: true,
|
|
110
|
+
children: button
|
|
111
|
+
}) : button;
|
|
112
|
+
});
|
|
113
|
+
ActionDialogAction.displayName = ACTION_NAME;
|
|
114
|
+
const FOOTER_NAME = "ActionDialog.Footer";
|
|
115
|
+
/**
|
|
116
|
+
* The footer: Cancel and Confirm, or the controls it is given. While it asks a question, every
|
|
117
|
+
* control hides — hidden, not unmounted, so the one the focus returns to is still there.
|
|
118
|
+
*/
|
|
119
|
+
const ActionDialogFooter = forwardRef(({ onConfirm, onCancel, confirmLabel, cancelLabel, intent = "default", error, confirmRef, cancelVariant = "text", closeOnCancel = true, closeOnConfirm = true, question, className, children, ...props }, ref) => {
|
|
120
|
+
const t = useUiKitPhrases();
|
|
121
|
+
const { setAsking, keepRef } = useActionDialog();
|
|
122
|
+
const asking = question !== void 0;
|
|
123
|
+
const keepButtonRef = useRef(null);
|
|
124
|
+
const questionId = usePrefixedId();
|
|
125
|
+
keepRef.current = question?.onKeep;
|
|
126
|
+
useLayoutEffect(() => {
|
|
127
|
+
setAsking(asking);
|
|
128
|
+
return () => setAsking(false);
|
|
129
|
+
}, [asking, setAsking]);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (asking) keepButtonRef.current?.focus();
|
|
132
|
+
}, [asking]);
|
|
133
|
+
const cancel = /* @__PURE__ */ jsx(Button, {
|
|
134
|
+
variant: cancelVariant,
|
|
135
|
+
size: "lg",
|
|
136
|
+
label: cancelLabel ?? t("enonic.uiKit.dialog.cancel"),
|
|
137
|
+
onClick: onCancel
|
|
138
|
+
});
|
|
139
|
+
return /* @__PURE__ */ jsxs(Dialog.Footer, {
|
|
140
|
+
ref,
|
|
141
|
+
"data-component": FOOTER_NAME,
|
|
142
|
+
className: cn("items-center", className),
|
|
143
|
+
...props,
|
|
144
|
+
children: [question !== void 0 && /* @__PURE__ */ jsxs("div", {
|
|
145
|
+
"data-component": QUESTION_NAME,
|
|
146
|
+
className: "flex flex-1 flex-wrap items-center justify-end gap-x-5 gap-y-2.5",
|
|
147
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
148
|
+
id: questionId,
|
|
149
|
+
role: "alert",
|
|
150
|
+
className: "mr-auto flex items-center gap-2 text-sm",
|
|
151
|
+
children: [/* @__PURE__ */ jsx(TriangleAlert, { className: cn("size-4 shrink-0", question.intent === "danger" ? "text-error" : "text-subtle") }), question.text]
|
|
152
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
153
|
+
className: "flex shrink-0 gap-2.5",
|
|
154
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
155
|
+
ref: keepButtonRef,
|
|
156
|
+
"aria-describedby": questionId,
|
|
157
|
+
variant: "outline",
|
|
158
|
+
size: "lg",
|
|
159
|
+
label: question.keepLabel ?? t("enonic.uiKit.dialog.keep"),
|
|
160
|
+
onClick: question.onKeep
|
|
161
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
162
|
+
"aria-describedby": questionId,
|
|
163
|
+
variant: "solid",
|
|
164
|
+
size: "lg",
|
|
165
|
+
label: question.confirmLabel ?? t("enonic.uiKit.dialog.confirm"),
|
|
166
|
+
className: cn(question.intent === "danger" && DANGER_BUTTON_CLASS_NAME),
|
|
167
|
+
onClick: question.onConfirm
|
|
168
|
+
})]
|
|
169
|
+
})]
|
|
170
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
171
|
+
className: cn("flex flex-1 items-center justify-end gap-2.5", asking && "hidden"),
|
|
172
|
+
children: [
|
|
173
|
+
error !== void 0 && /* @__PURE__ */ jsx("p", {
|
|
174
|
+
className: "text-error mr-auto text-sm",
|
|
175
|
+
role: "alert",
|
|
176
|
+
children: error
|
|
177
|
+
}),
|
|
178
|
+
children,
|
|
179
|
+
onConfirm !== void 0 && (closeOnCancel ? /* @__PURE__ */ jsx(Dialog.Close, {
|
|
180
|
+
asChild: true,
|
|
181
|
+
children: cancel
|
|
182
|
+
}) : cancel),
|
|
183
|
+
onConfirm !== void 0 && /* @__PURE__ */ jsx(ActionDialogAction, {
|
|
184
|
+
ref: confirmRef,
|
|
185
|
+
label: confirmLabel ?? t("enonic.uiKit.dialog.confirm"),
|
|
186
|
+
intent,
|
|
187
|
+
closeOnClick: closeOnConfirm,
|
|
188
|
+
onClick: onConfirm
|
|
189
|
+
})
|
|
190
|
+
]
|
|
191
|
+
})]
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
ActionDialogFooter.displayName = FOOTER_NAME;
|
|
195
|
+
/**
|
|
196
|
+
* `Dialog` from `@enonic/ui` plus an outcome: a `Content` that holds whether the action may be
|
|
197
|
+
* taken, a `Body` that goes inert while the footer asks a question, a `Footer` that takes the
|
|
198
|
+
* action or asks the question, and `Action` for a further button the content enables. Stacks
|
|
199
|
+
* nothing: a question goes to the footer, content with room of its own is another `Content` under
|
|
200
|
+
* the same `Root`.
|
|
201
|
+
*/
|
|
202
|
+
const ActionDialog = {
|
|
203
|
+
...Dialog,
|
|
204
|
+
Root: ActionDialogRoot,
|
|
205
|
+
Content: ActionDialogContent,
|
|
206
|
+
Body: ActionDialogBody,
|
|
207
|
+
Footer: ActionDialogFooter,
|
|
208
|
+
Action: ActionDialogAction
|
|
209
|
+
};
|
|
210
|
+
//#endregion
|
|
211
|
+
export { ActionDialog };
|
|
212
|
+
|
|
213
|
+
//# sourceMappingURL=action-dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action-dialog.js","names":[],"sources":["../../src/dialog/action-dialog.tsx"],"sourcesContent":["import {\n Button,\n type ButtonVariant,\n cn,\n Dialog,\n type DialogRootProps,\n usePrefixedId,\n} from '@enonic/ui';\nimport { TriangleAlert } from 'lucide-react';\nimport {\n type ComponentPropsWithoutRef,\n forwardRef,\n type ReactElement,\n type ReactNode,\n type Ref,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { useUiKitPhrases } from '../i18n/use-phrases';\nimport {\n type ActionDialogContextValue,\n type ActionDialogOpenChangeDetails,\n ActionDialogProvider,\n ActionDialogRootProvider,\n useActionDialog,\n useActionDialogRoot,\n} from './action-dialog-context';\n\nexport type DialogIntent = 'default' | 'danger';\n\n/** A question needs no more room than its own text; `medium` carries a control, `wide` a form. */\nexport type DialogSize = 'default' | 'medium' | 'wide';\n\nconst SIZES: Record<DialogSize, string> = {\n default: 'max-w-lg',\n medium: 'max-w-180 sm:min-w-152',\n wide: 'max-w-4xl',\n};\n\nconst DANGER_BUTTON_CLASS_NAME =\n 'bg-btn-error text-alt hover:bg-btn-error-hover active:bg-btn-error-active focus-visible:ring-error/50';\n\n//\n// * Root\n//\n\nexport type ActionDialogRootProps = Omit<DialogRootProps, 'onOpenChange'> & {\n /** `details` tells a close an action took from one that would leave without it. */\n onOpenChange?: (open: boolean, details?: ActionDialogOpenChangeDetails) => void;\n};\n\nconst ActionDialogRoot = ({ onOpenChange, ...props }: ActionDialogRootProps): ReactElement => {\n const actionCloseRef = useRef(false);\n const value = useMemo(() => ({ actionCloseRef }), []);\n\n return (\n <ActionDialogRootProvider value={value}>\n <Dialog.Root\n {...props}\n onOpenChange={(open) => {\n const byAction = actionCloseRef.current;\n actionCloseRef.current = false;\n onOpenChange?.(open, !open && byAction ? { reason: 'action' } : undefined);\n }}\n />\n </ActionDialogRootProvider>\n );\n};\nActionDialogRoot.displayName = 'ActionDialog.Root';\n\n//\n// * Content\n//\n\nexport type ActionDialogContentProps = {\n size?: DialogSize;\n /** Whether the action starts enabled; a content with a gate starts it disabled. */\n defaultConfirmEnabled?: boolean;\n} & ComponentPropsWithoutRef<typeof Dialog.Content>;\n\nconst CONTENT_NAME = 'ActionDialog.Content';\nconst QUESTION_NAME = 'ActionDialog.Question';\n\nconst ActionDialogContent = forwardRef<HTMLDivElement, ActionDialogContentProps>(\n (\n {\n size = 'default',\n defaultConfirmEnabled = true,\n className,\n children,\n onEscapeKeyDown,\n onFocusCapture,\n ...props\n },\n ref,\n ): ReactElement => {\n const [confirmEnabled, setConfirmEnabled] = useState(defaultConfirmEnabled);\n const [asking, setAsking] = useState(false);\n const keepRef = useRef<(() => void) | undefined>(undefined);\n const lastFocusRef = useRef<HTMLElement | null>(null);\n const wasAskingRef = useRef(false);\n\n const context = useMemo<ActionDialogContextValue>(\n () => ({ confirmEnabled, setConfirmEnabled, asking, setAsking, keepRef, lastFocusRef }),\n [confirmEnabled, asking],\n );\n\n // Answered, the focus goes back to the control the question left. The last focus, not the active\n // element: a mask click has already blurred the field to the body.\n useLayoutEffect(() => {\n const target = lastFocusRef.current;\n const answered = wasAskingRef.current && !asking;\n wasAskingRef.current = asking;\n if (!answered || target === null) {\n return;\n }\n // ! A frame later: under Preact the body reads `asking` through context and lifts its `inert`\n // ! in a render after this one, and an inert field takes no focus.\n const frame = requestAnimationFrame(() => target.focus());\n return () => cancelAnimationFrame(frame);\n }, [asking]);\n\n return (\n <Dialog.Content\n ref={ref}\n data-component={CONTENT_NAME}\n className={cn('gap-5 p-5 md:p-7.5', SIZES[size], className)}\n onFocusCapture={(event) => {\n onFocusCapture?.(event);\n if (\n event.target instanceof HTMLElement &&\n event.target.closest(`[data-component=\"${QUESTION_NAME}\"]`) === null\n ) {\n lastFocusRef.current = event.target;\n }\n }}\n onEscapeKeyDown={(event) => {\n onEscapeKeyDown?.(event);\n // While the footer asks its question, `Escape` is the way back, not the way out.\n if (asking && !event.defaultPrevented) {\n event.preventDefault();\n keepRef.current?.();\n }\n }}\n {...props}\n >\n <ActionDialogProvider value={context}>{children}</ActionDialogProvider>\n </Dialog.Content>\n );\n },\n);\nActionDialogContent.displayName = CONTENT_NAME;\n\n//\n// * Body\n//\n\nexport type ActionDialogBodyProps = ComponentPropsWithoutRef<typeof Dialog.Body>;\n\nconst BODY_NAME = 'ActionDialog.Body';\n\nconst ActionDialogBody = forwardRef<HTMLDivElement, ActionDialogBodyProps>(\n ({ className, ...props }, ref): ReactElement => {\n const { asking } = useActionDialog();\n\n return (\n <Dialog.Body\n ref={ref}\n data-component={BODY_NAME}\n className={cn(\n '-mx-2 flex flex-col gap-7 px-2 py-1 transition-opacity',\n asking && 'opacity-50',\n className,\n )}\n {...props}\n // ! `inert`, not a disabled overlay: the form stays in sight but takes no click, no focus.\n inert={asking || props.inert}\n />\n );\n },\n);\nActionDialogBody.displayName = BODY_NAME;\n\n//\n// * Action — a button that takes the dialog's outcome\n//\n\nexport type ActionDialogActionProps = {\n label: string;\n /** `danger` paints the button red, for an action there is no undoing. */\n intent?: DialogIntent;\n /** Enabled while the content allows the action — a gate matched, a form valid — unless disabled outright. */\n disabled?: boolean;\n /** Close the dialog on click; off for a caller that moves on to a progress view instead. */\n closeOnClick?: boolean;\n variant?: ButtonVariant;\n} & Omit<ComponentPropsWithoutRef<typeof Button>, 'label' | 'variant' | 'disabled'>;\n\nconst ACTION_NAME = 'ActionDialog.Action';\n\n/** An action of the footer: enabled by the content, red for a destructive one. */\nconst ActionDialogAction = forwardRef<HTMLButtonElement, ActionDialogActionProps>(\n (\n {\n label,\n intent = 'default',\n disabled = false,\n closeOnClick = true,\n variant = 'solid',\n className,\n onClick,\n ...props\n },\n ref,\n ): ReactElement => {\n const { confirmEnabled } = useActionDialog();\n const root = useActionDialogRoot();\n const button = (\n <Button\n ref={ref}\n data-component={ACTION_NAME}\n variant={variant}\n size=\"lg\"\n label={label}\n disabled={disabled || !confirmEnabled}\n className={cn(intent === 'danger' && DANGER_BUTTON_CLASS_NAME, className)}\n onClick={(event) => {\n onClick?.(event);\n // ! Runs before `Dialog.Close` closes: the close guard reads this to let the action through.\n if (closeOnClick && root !== undefined && !event.defaultPrevented) {\n root.actionCloseRef.current = true;\n }\n }}\n {...props}\n />\n );\n\n return closeOnClick ? <Dialog.Close asChild>{button}</Dialog.Close> : button;\n },\n);\nActionDialogAction.displayName = ACTION_NAME;\n\n//\n// * Footer\n//\n\n/** A question the footer asks in place of its controls. */\nexport type FooterQuestion = {\n text: ReactNode;\n intent?: DialogIntent;\n /** Defaults to the kit's \"Confirm\". */\n confirmLabel?: string;\n /** The way back; defaults to the kit's \"Keep editing\". */\n keepLabel?: string;\n onConfirm: () => void;\n onKeep: () => void;\n};\n\nexport type ActionDialogFooterProps = {\n /** Given `onConfirm`, the footer renders Cancel and Confirm; without it, its children are the controls. */\n onConfirm?: () => void;\n onCancel?: () => void;\n /** Defaults to the kit's \"Confirm\". */\n confirmLabel?: string;\n /** Defaults to the kit's \"Cancel\". */\n cancelLabel?: string;\n /** `danger` paints the confirm button red, for an action there is no undoing. */\n intent?: DialogIntent;\n /** Why the dialog is still open, shown beside its controls — a rejected save is the case. */\n error?: string;\n /** For a caller that moves the focus onto the confirm button once it means something. */\n confirmRef?: Ref<HTMLButtonElement>;\n /** `outline` gives the two answers of a question equal weight; a form's Cancel stays quiet. */\n cancelVariant?: ButtonVariant;\n /** Close the dialog on Cancel; off for a caller that closes it itself. */\n closeOnCancel?: boolean;\n /** Close the dialog on Confirm; off for a caller that moves on to a progress view instead. */\n closeOnConfirm?: boolean;\n /**\n * While set, the footer asks this instead of showing its controls: the body goes inert, `Escape`\n * is `onKeep`, and the focus goes to the way back. `useCloseGuard` is the usual source.\n */\n question?: FooterQuestion;\n} & ComponentPropsWithoutRef<typeof Dialog.Footer>;\n\nconst FOOTER_NAME = 'ActionDialog.Footer';\n\n/**\n * The footer: Cancel and Confirm, or the controls it is given. While it asks a question, every\n * control hides — hidden, not unmounted, so the one the focus returns to is still there.\n */\nconst ActionDialogFooter = forwardRef<HTMLElement, ActionDialogFooterProps>(\n (\n {\n onConfirm,\n onCancel,\n confirmLabel,\n cancelLabel,\n intent = 'default',\n error,\n confirmRef,\n cancelVariant = 'text',\n closeOnCancel = true,\n closeOnConfirm = true,\n question,\n className,\n children,\n ...props\n },\n ref,\n ): ReactElement => {\n const t = useUiKitPhrases();\n const { setAsking, keepRef } = useActionDialog();\n const asking = question !== undefined;\n const keepButtonRef = useRef<HTMLButtonElement>(null);\n const questionId = usePrefixedId();\n\n keepRef.current = question?.onKeep;\n\n useLayoutEffect(() => {\n setAsking(asking);\n return () => setAsking(false);\n }, [asking, setAsking]);\n\n useEffect(() => {\n if (asking) {\n keepButtonRef.current?.focus();\n }\n }, [asking]);\n\n const cancel = (\n <Button\n variant={cancelVariant}\n size=\"lg\"\n label={cancelLabel ?? t('enonic.uiKit.dialog.cancel')}\n onClick={onCancel}\n />\n );\n\n return (\n <Dialog.Footer\n ref={ref}\n data-component={FOOTER_NAME}\n className={cn('items-center', className)}\n {...props}\n >\n {question !== undefined && (\n <div\n data-component={QUESTION_NAME}\n className=\"flex flex-1 flex-wrap items-center justify-end gap-x-5 gap-y-2.5\"\n >\n {/* Announced as it appears, and read with either answer the focus lands on. */}\n <div id={questionId} role=\"alert\" className=\"mr-auto flex items-center gap-2 text-sm\">\n <TriangleAlert\n className={cn(\n 'size-4 shrink-0',\n question.intent === 'danger' ? 'text-error' : 'text-subtle',\n )}\n />\n {question.text}\n </div>\n <div className=\"flex shrink-0 gap-2.5\">\n <Button\n ref={keepButtonRef}\n aria-describedby={questionId}\n variant=\"outline\"\n size=\"lg\"\n label={question.keepLabel ?? t('enonic.uiKit.dialog.keep')}\n onClick={question.onKeep}\n />\n <Button\n aria-describedby={questionId}\n variant=\"solid\"\n size=\"lg\"\n label={question.confirmLabel ?? t('enonic.uiKit.dialog.confirm')}\n className={cn(question.intent === 'danger' && DANGER_BUTTON_CLASS_NAME)}\n onClick={question.onConfirm}\n />\n </div>\n </div>\n )}\n <div className={cn('flex flex-1 items-center justify-end gap-2.5', asking && 'hidden')}>\n {error !== undefined && (\n <p className=\"text-error mr-auto text-sm\" role=\"alert\">\n {error}\n </p>\n )}\n {children}\n {onConfirm !== undefined &&\n (closeOnCancel ? <Dialog.Close asChild>{cancel}</Dialog.Close> : cancel)}\n {onConfirm !== undefined && (\n <ActionDialogAction\n ref={confirmRef}\n label={confirmLabel ?? t('enonic.uiKit.dialog.confirm')}\n intent={intent}\n closeOnClick={closeOnConfirm}\n onClick={onConfirm}\n />\n )}\n </div>\n </Dialog.Footer>\n );\n },\n);\nActionDialogFooter.displayName = FOOTER_NAME;\n\n//\n// * Compound\n//\n\n/**\n * `Dialog` from `@enonic/ui` plus an outcome: a `Content` that holds whether the action may be\n * taken, a `Body` that goes inert while the footer asks a question, a `Footer` that takes the\n * action or asks the question, and `Action` for a further button the content enables. Stacks\n * nothing: a question goes to the footer, content with room of its own is another `Content` under\n * the same `Root`.\n */\nexport const ActionDialog = {\n ...Dialog,\n Root: ActionDialogRoot,\n Content: ActionDialogContent,\n Body: ActionDialogBody,\n Footer: ActionDialogFooter,\n Action: ActionDialogAction,\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DialogSize } from "./action-dialog.js";
|
|
2
|
+
import { DeleteTarget } from "./delete-expectation.js";
|
|
3
|
+
import { ReactElement, ReactNode } from "react";
|
|
4
|
+
//#region src/dialog/delete-confirm.d.ts
|
|
5
|
+
type DeleteConfirmProps = {
|
|
6
|
+
title: string;
|
|
7
|
+
/** The question, already worded for one target or many; the targets are listed under it. */
|
|
8
|
+
question: ReactNode;
|
|
9
|
+
targets: readonly DeleteTarget[];
|
|
10
|
+
/**
|
|
11
|
+
* What is typed back before the button enables: `deleteExpectation(targets)` by default, the
|
|
12
|
+
* one name or the count. `null` asks for nothing, and the button is enabled from the start.
|
|
13
|
+
*/
|
|
14
|
+
expected?: string | number | null;
|
|
15
|
+
/** Defaults to the kit's "Delete". */
|
|
16
|
+
confirmLabel?: string;
|
|
17
|
+
size?: DialogSize;
|
|
18
|
+
onConfirm: () => void;
|
|
19
|
+
onCancel?: () => void;
|
|
20
|
+
/** Off for a caller that moves on to a progress view instead of closing. */
|
|
21
|
+
closeOnConfirm?: boolean;
|
|
22
|
+
/** Off for a caller whose Cancel goes back to the view before — a list the delete was picked from. */
|
|
23
|
+
closeOnCancel?: boolean;
|
|
24
|
+
'data-component'?: string;
|
|
25
|
+
};
|
|
26
|
+
declare const DELETE_CONFIRM_NAME = "DeleteConfirm";
|
|
27
|
+
declare function DeleteConfirm({ title, question, targets, expected, confirmLabel, size, onConfirm, onCancel, closeOnConfirm, closeOnCancel, 'data-component': componentName }: DeleteConfirmProps): ReactElement;
|
|
28
|
+
declare namespace DeleteConfirm {
|
|
29
|
+
export { DELETE_CONFIRM_NAME as displayName };
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { DeleteConfirm, DeleteConfirmProps };
|
|
33
|
+
//# sourceMappingURL=delete-confirm.d.ts.map
|