kigumi 0.9.0 → 0.9.2
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/index.js +7 -11
- package/dist/skills/kigumi-react/references/event-mapping.md +36 -17
- package/dist/templates/AGENTS.md +19 -8
- package/dist/templates/react/Button/Button.jsx.hbs +4 -4
- package/dist/templates/react/Dialog/Dialog.jsx.hbs +47 -44
- package/dist/templates/react/Dialog/Dialog.tsx.hbs +73 -22
- package/dist/templates/react/Drawer/Drawer.jsx.hbs +46 -3
- package/dist/templates/react/Drawer/Drawer.tsx.hbs +63 -22
- package/dist/templates/react/FileInput/FileInput.jsx.hbs +16 -9
- package/dist/templates/react/Input/Input.jsx.hbs +10 -10
- package/dist/templates/react/Input/Input.tsx.hbs +6 -6
- package/dist/templates/react/NumberInput/NumberInput.jsx.hbs +10 -10
- package/dist/templates/vue/Dialog/Dialog.js.vue.hbs +45 -12
- package/dist/templates/vue/Dialog/Dialog.vue.hbs +50 -12
- package/dist/templates/vue/Drawer/Drawer.js.vue.hbs +44 -13
- package/dist/templates/vue/Drawer/Drawer.vue.hbs +47 -12
- package/package.json +3 -2
- package/skills/kigumi-react/references/event-mapping.md +36 -17
- package/templates/AGENTS.md +19 -8
- package/templates/react/Button/Button.jsx.hbs +4 -4
- package/templates/react/Dialog/Dialog.jsx.hbs +47 -44
- package/templates/react/Dialog/Dialog.tsx.hbs +73 -22
- package/templates/react/Drawer/Drawer.jsx.hbs +46 -3
- package/templates/react/Drawer/Drawer.tsx.hbs +63 -22
- package/templates/react/FileInput/FileInput.jsx.hbs +16 -9
- package/templates/react/Input/Input.jsx.hbs +10 -10
- package/templates/react/Input/Input.tsx.hbs +6 -6
- package/templates/react/NumberInput/NumberInput.jsx.hbs +10 -10
- package/templates/vue/Dialog/Dialog.js.vue.hbs +45 -12
- package/templates/vue/Dialog/Dialog.vue.hbs +50 -12
- package/templates/vue/Drawer/Drawer.js.vue.hbs +44 -13
- package/templates/vue/Drawer/Drawer.vue.hbs +47 -12
package/dist/index.js
CHANGED
|
@@ -517,7 +517,7 @@ var ConfigNotFoundError = class extends KigumiError {
|
|
|
517
517
|
// src/errors/validation.ts
|
|
518
518
|
var ValidationError = class extends KigumiError {
|
|
519
519
|
constructor(field, value, validValues, zodError) {
|
|
520
|
-
const errorMessages = zodError?.
|
|
520
|
+
const errorMessages = zodError?.issues.map((err) => {
|
|
521
521
|
return `${err.path.join(".")}: ${err.message}`;
|
|
522
522
|
}) || [];
|
|
523
523
|
const suggestions = [
|
|
@@ -779,10 +779,10 @@ function handleError(error, output) {
|
|
|
779
779
|
import { z } from "zod";
|
|
780
780
|
var FRAMEWORKS = ["react", "vue", "svelte", "angular"];
|
|
781
781
|
var frameworkSchema = z.enum(FRAMEWORKS, {
|
|
782
|
-
|
|
782
|
+
error: () => `Must be one of: ${FRAMEWORKS.join(", ")}`
|
|
783
783
|
});
|
|
784
784
|
var tierSchema = z.enum(["free", "pro"], {
|
|
785
|
-
|
|
785
|
+
error: () => 'Must be either "free" or "pro"'
|
|
786
786
|
});
|
|
787
787
|
var themeConfigSchema = z.object({
|
|
788
788
|
selected: z.string().min(1, "Theme name cannot be empty"),
|
|
@@ -796,7 +796,7 @@ var webAwesomeConfigSchema = z.object({
|
|
|
796
796
|
var kigumiConfigSchema = z.object({
|
|
797
797
|
framework: frameworkSchema,
|
|
798
798
|
typescript: z.boolean({
|
|
799
|
-
|
|
799
|
+
error: () => "Must be a boolean (true or false)"
|
|
800
800
|
}),
|
|
801
801
|
componentsDir: z.string().min(1, "Components directory cannot be empty"),
|
|
802
802
|
utilsDir: z.string().min(1, "Utils directory cannot be empty").optional(),
|
|
@@ -875,7 +875,7 @@ var listOptionsSchema = z2.object({
|
|
|
875
875
|
function validateOptions(schema, options) {
|
|
876
876
|
const result = schema.safeParse(options);
|
|
877
877
|
if (!result.success) {
|
|
878
|
-
const errors = result.error.
|
|
878
|
+
const errors = result.error.issues.map((err) => {
|
|
879
879
|
const path15 = err.path.join(".");
|
|
880
880
|
return path15 ? `${path15}: ${err.message}` : err.message;
|
|
881
881
|
});
|
|
@@ -917,14 +917,10 @@ var AVAILABLE_BRAND_COLORS = [
|
|
|
917
917
|
"gray"
|
|
918
918
|
];
|
|
919
919
|
var paletteSchema = z3.enum(AVAILABLE_PALETTES, {
|
|
920
|
-
|
|
921
|
-
message: `Palette must be one of: ${AVAILABLE_PALETTES.join(", ")}`
|
|
922
|
-
})
|
|
920
|
+
error: () => `Palette must be one of: ${AVAILABLE_PALETTES.join(", ")}`
|
|
923
921
|
});
|
|
924
922
|
var brandColorSchema = z3.enum(AVAILABLE_BRAND_COLORS, {
|
|
925
|
-
|
|
926
|
-
message: `Brand color must be one of: ${AVAILABLE_BRAND_COLORS.join(", ")}`
|
|
927
|
-
})
|
|
923
|
+
error: () => `Brand color must be one of: ${AVAILABLE_BRAND_COLORS.join(", ")}`
|
|
928
924
|
});
|
|
929
925
|
var componentNameSchema = z3.string().min(1, "Component name cannot be empty");
|
|
930
926
|
|
|
@@ -1,26 +1,45 @@
|
|
|
1
1
|
# Event Mapping
|
|
2
2
|
|
|
3
|
-
Web Awesome components
|
|
3
|
+
Web Awesome components use two different event systems depending on the component type.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Form Controls — Native DOM Events
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
| ----------------- | --------------------- | ------------------------------ |
|
|
9
|
-
| `wa-change` | `onChange` | `(event: CustomEvent) => void` |
|
|
10
|
-
| `wa-input` | `onInput` | `(event: CustomEvent) => void` |
|
|
11
|
-
| `wa-show` | `onShow` | `(event: CustomEvent) => void` |
|
|
12
|
-
| `wa-hide` | `onHide` | `(event: CustomEvent) => void` |
|
|
13
|
-
| `wa-after-show` | `onAfterShow` | `(event: CustomEvent) => void` |
|
|
14
|
-
| `wa-after-hide` | `onAfterHide` | `(event: CustomEvent) => void` |
|
|
15
|
-
| `wa-blur` | `onBlur` | `(event: FocusEvent) => void` |
|
|
16
|
-
| `wa-focus` | `onFocus` | `(event: FocusEvent) => void` |
|
|
17
|
-
| Standard events | Standard React events | Native types |
|
|
7
|
+
`wa-button`, `wa-input`, `wa-number-input`, `wa-file-input`, `wa-textarea`, `wa-select`, `wa-checkbox`, `wa-switch` emit **native browser events** (no `wa-` prefix). Event handlers receive standard `Event` / `FocusEvent` objects — use `e.target.value`, `e.target.files`, etc.
|
|
18
8
|
|
|
19
|
-
|
|
9
|
+
| Native Event | React Handler | Type |
|
|
10
|
+
| ------------ | ------------- | ------------ |
|
|
11
|
+
| `blur` | `onBlur` | `FocusEvent` |
|
|
12
|
+
| `focus` | `onFocus` | `FocusEvent` |
|
|
13
|
+
| `input` | `onInput` | `Event` |
|
|
14
|
+
| `change` | `onChange` | `Event` |
|
|
20
15
|
|
|
21
16
|
```tsx
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
// Correct: native events
|
|
18
|
+
<Input
|
|
19
|
+
onInput={(e) => console.log((e.target as HTMLInputElement).value)}
|
|
20
|
+
onChange={(e) => console.log((e.target as HTMLInputElement).value)}
|
|
21
|
+
/>
|
|
22
|
+
|
|
23
|
+
// Wrong: these events don't fire on form controls
|
|
24
|
+
<Input onInput={(e: CustomEvent) => console.log(e.detail.value)} />
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Overlay / Complex Components — Custom `wa-` Events
|
|
28
|
+
|
|
29
|
+
`wa-dialog`, `wa-drawer`, `wa-dropdown`, `wa-popup`, `wa-tooltip`, etc. emit **custom events** prefixed with `wa-`. Event handlers receive `CustomEvent` objects.
|
|
30
|
+
|
|
31
|
+
| Web Awesome Event | React Handler | Type |
|
|
32
|
+
| ----------------- | ------------- | ------------- |
|
|
33
|
+
| `wa-show` | `onShow` | `CustomEvent` |
|
|
34
|
+
| `wa-hide` | `onHide` | `CustomEvent` |
|
|
35
|
+
| `wa-after-show` | `onAfterShow` | `CustomEvent` |
|
|
36
|
+
| `wa-after-hide` | `onAfterHide` | `CustomEvent` |
|
|
37
|
+
| `wa-clear` | `onClear` | `CustomEvent` |
|
|
38
|
+
| `wa-invalid` | `onInvalid` | `CustomEvent` |
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { useState } from 'react';
|
|
42
|
+
import { Dialog, Button } from '@/components/ui';
|
|
24
43
|
|
|
25
44
|
function Example() {
|
|
26
45
|
const [open, setOpen] = useState(false);
|
|
@@ -31,7 +50,7 @@ function Example() {
|
|
|
31
50
|
<Dialog
|
|
32
51
|
open={open}
|
|
33
52
|
onHide={() => setOpen(false)}
|
|
34
|
-
onAfterShow={(e) => console.log(
|
|
53
|
+
onAfterShow={(e) => console.log('Dialog shown', e)}
|
|
35
54
|
>
|
|
36
55
|
Dialog content
|
|
37
56
|
</Dialog>
|
package/dist/templates/AGENTS.md
CHANGED
|
@@ -141,14 +141,25 @@ const { forwardRef, useRef, useEffect } = React;
|
|
|
141
141
|
|
|
142
142
|
### 3. Event Naming Convention
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
|
147
|
-
|
|
|
148
|
-
| `
|
|
149
|
-
| `
|
|
150
|
-
| `
|
|
151
|
-
| `
|
|
144
|
+
**Form controls** (wa-button, wa-input, wa-number-input, wa-file-input, wa-textarea, wa-select, wa-checkbox, wa-switch) emit **native DOM events** — no `wa-` prefix:
|
|
145
|
+
|
|
146
|
+
| Native Event | React Prop |
|
|
147
|
+
| ------------ | ---------- |
|
|
148
|
+
| `blur` | `onBlur` |
|
|
149
|
+
| `focus` | `onFocus` |
|
|
150
|
+
| `input` | `onInput` |
|
|
151
|
+
| `change` | `onChange` |
|
|
152
|
+
|
|
153
|
+
**Overlay/complex components** (wa-dialog, wa-drawer, wa-dropdown, wa-popup, etc.) emit **custom `wa-` events**:
|
|
154
|
+
|
|
155
|
+
| Web Awesome Event | React Prop |
|
|
156
|
+
| ----------------- | ------------- |
|
|
157
|
+
| `wa-show` | `onShow` |
|
|
158
|
+
| `wa-hide` | `onHide` |
|
|
159
|
+
| `wa-after-show` | `onAfterShow` |
|
|
160
|
+
| `wa-after-hide` | `onAfterHide` |
|
|
161
|
+
| `wa-clear` | `onClear` |
|
|
162
|
+
| `wa-invalid` | `onInvalid` |
|
|
152
163
|
|
|
153
164
|
### 4. Always Cleanup Event Listeners
|
|
154
165
|
|
|
@@ -82,12 +82,12 @@ export const Button = React.forwardRef(
|
|
|
82
82
|
if (onFocus) onFocus(e);
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
-
el.addEventListener('
|
|
86
|
-
el.addEventListener('
|
|
85
|
+
el.addEventListener('blur', handleBlur);
|
|
86
|
+
el.addEventListener('focus', handleFocus);
|
|
87
87
|
|
|
88
88
|
return () => {
|
|
89
|
-
el.removeEventListener('
|
|
90
|
-
el.removeEventListener('
|
|
89
|
+
el.removeEventListener('blur', handleBlur);
|
|
90
|
+
el.removeEventListener('focus', handleFocus);
|
|
91
91
|
};
|
|
92
92
|
}, [onBlur, onFocus]);
|
|
93
93
|
|
|
@@ -1,28 +1,40 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
2
3
|
import clsx from 'clsx';
|
|
3
4
|
import '{{{importPath}}}';
|
|
4
5
|
import './Dialog.css';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
+
* // Using data-dialog attribute (recommended)
|
|
9
|
+
* import { Dialog } from './components/ui';
|
|
10
|
+
* import { Button } from './components/ui';
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* <Button data-dialog="close">Close</Button>
|
|
16
|
-
* </div>
|
|
12
|
+
* <Button data-dialog="open dialog">Open FAQ</Button>
|
|
13
|
+
* <Dialog id="dialog" label="FAQ">
|
|
14
|
+
* <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
|
|
15
|
+
* <Button slot="footer" variant="brand" data-dialog="close">
|
|
16
|
+
* Close
|
|
17
|
+
* </Button>
|
|
17
18
|
* </Dialog>
|
|
18
|
-
*
|
|
19
|
+
* Dialogs display important prompts and information
|
|
19
20
|
*
|
|
21
|
+
* @example
|
|
20
22
|
* // Using open prop (recommended)
|
|
21
23
|
* const [open, setOpen] = React.useState(false);
|
|
22
24
|
* <Dialog open={open} label="Dialog Title" onHide={() => setOpen(false)}>
|
|
23
25
|
* <p>Dialog content</p>
|
|
26
|
+
* <Button slot="footer" variant="brand" onClick={() => setOpen(false)}>
|
|
27
|
+
* Close
|
|
28
|
+
* </Button>
|
|
24
29
|
* </Dialog>
|
|
25
30
|
*
|
|
31
|
+
* // Using ref methods (alternative)
|
|
32
|
+
* const dialogRef = React.useRef(null);
|
|
33
|
+
* <Dialog ref={dialogRef} label="Dialog Title">
|
|
34
|
+
* <p>Dialog content</p>
|
|
35
|
+
* </Dialog>
|
|
36
|
+
* dialogRef.current?.show();
|
|
37
|
+
*
|
|
26
38
|
* @param {Object} props
|
|
27
39
|
* @param {boolean} [props.open] - Indicates whether or not the dialog is open
|
|
28
40
|
* @param {string} props.label - The dialog's label as displayed in the header
|
|
@@ -32,33 +44,21 @@ import './Dialog.css';
|
|
|
32
44
|
* @param {function} [props.onAfterShow] - Event fired after the dialog is shown
|
|
33
45
|
* @param {function} [props.onHide] - Event fired when the dialog is about to hide
|
|
34
46
|
* @param {function} [props.onAfterHide] - Event fired after the dialog is hidden
|
|
47
|
+
* @param {string | string[]} [props['data-dialog']] - Data attribute for opening and closing declaratively
|
|
35
48
|
* @param {string} [props.className] - Additional CSS classes
|
|
36
49
|
* @param {React.ReactNode} [props.children] - Dialog content
|
|
37
50
|
* @param {React.Ref} ref - Ref with methods: show(), hide(), requestClose()
|
|
38
51
|
*/
|
|
39
52
|
export const Dialog = React.forwardRef(
|
|
40
|
-
({ children, className, onShow, onAfterShow, onHide, onAfterHide, ...props }, ref) => {
|
|
53
|
+
({ children, className, open, onShow, onAfterShow, onHide, onAfterHide, ...props }, ref) => {
|
|
41
54
|
const dialogRef = React.useRef(null);
|
|
42
55
|
|
|
43
56
|
React.useImperativeHandle(
|
|
44
57
|
ref,
|
|
45
58
|
() => ({
|
|
46
|
-
show: () =>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
hide: () => {
|
|
52
|
-
// hide is an alias for requestClose for consistency with other components
|
|
53
|
-
if (dialogRef.current && typeof dialogRef.current.requestClose === 'function') {
|
|
54
|
-
dialogRef.current.requestClose();
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
requestClose: () => {
|
|
58
|
-
if (dialogRef.current && typeof dialogRef.current.requestClose === 'function') {
|
|
59
|
-
dialogRef.current.requestClose();
|
|
60
|
-
}
|
|
61
|
-
},
|
|
59
|
+
show: () => dialogRef.current?.show?.(),
|
|
60
|
+
hide: () => dialogRef.current?.requestClose?.(),
|
|
61
|
+
requestClose: () => dialogRef.current?.requestClose?.(),
|
|
62
62
|
get element() {
|
|
63
63
|
return dialogRef.current;
|
|
64
64
|
},
|
|
@@ -66,26 +66,28 @@ export const Dialog = React.forwardRef(
|
|
|
66
66
|
[]
|
|
67
67
|
);
|
|
68
68
|
|
|
69
|
-
//
|
|
69
|
+
// Sync open prop with dialog element
|
|
70
70
|
React.useEffect(() => {
|
|
71
71
|
const el = dialogRef.current;
|
|
72
|
-
if (!el) return;
|
|
73
|
-
|
|
74
|
-
const handleShow = (e) => {
|
|
75
|
-
if (onShow) onShow(e);
|
|
76
|
-
};
|
|
72
|
+
if (!el || open === undefined) return;
|
|
77
73
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
const isOpen = el.open ?? false;
|
|
75
|
+
if (open && !isOpen) {
|
|
76
|
+
el.show?.();
|
|
77
|
+
} else if (!open && isOpen) {
|
|
78
|
+
el.requestClose?.();
|
|
79
|
+
}
|
|
80
|
+
}, [open]);
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
// Setup event listeners
|
|
83
|
+
React.useEffect(() => {
|
|
84
|
+
const el = dialogRef.current;
|
|
85
|
+
if (!el) return;
|
|
85
86
|
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
const handleShow = (e) => onShow?.(e);
|
|
88
|
+
const handleAfterShow = (e) => onAfterShow?.(e);
|
|
89
|
+
const handleHide = (e) => onHide?.(e);
|
|
90
|
+
const handleAfterHide = (e) => onAfterHide?.(e);
|
|
89
91
|
|
|
90
92
|
el.addEventListener('wa-show', handleShow);
|
|
91
93
|
el.addEventListener('wa-after-show', handleAfterShow);
|
|
@@ -100,14 +102,15 @@ export const Dialog = React.forwardRef(
|
|
|
100
102
|
};
|
|
101
103
|
}, [onShow, onAfterShow, onHide, onAfterHide]);
|
|
102
104
|
|
|
103
|
-
return (
|
|
105
|
+
return createPortal(
|
|
104
106
|
<wa-dialog
|
|
105
107
|
ref={dialogRef}
|
|
106
108
|
class={clsx('Dialog', className)}
|
|
107
109
|
{...props}
|
|
108
110
|
>
|
|
109
111
|
{children}
|
|
110
|
-
</wa-dialog
|
|
112
|
+
</wa-dialog>,
|
|
113
|
+
document.body
|
|
111
114
|
);
|
|
112
115
|
}
|
|
113
116
|
);
|
|
@@ -1,20 +1,55 @@
|
|
|
1
1
|
import { forwardRef, useRef, useImperativeHandle, useEffect, type HTMLAttributes } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
2
3
|
import clsx from 'clsx';
|
|
3
4
|
import '{{{importPath}}}';
|
|
4
5
|
import './Dialog.css';
|
|
5
6
|
|
|
6
7
|
/**
|
|
8
|
+
* // Using data-dialog attribute (recommended)
|
|
9
|
+
* import { Dialog } from './components/ui';
|
|
10
|
+
* import { Button } from './components/ui';
|
|
11
|
+
*
|
|
12
|
+
* <Button data-dialog="open dialog">Open FAQ</Button>
|
|
13
|
+
* <Dialog id="dialog" label="FAQ">
|
|
14
|
+
* <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
|
|
15
|
+
* <Button slot="footer" variant="brand" data-dialog="close">
|
|
16
|
+
* Close
|
|
17
|
+
* </Button>
|
|
18
|
+
* </Dialog>
|
|
7
19
|
* Dialogs display important prompts and information
|
|
8
20
|
*
|
|
9
21
|
* @example
|
|
10
22
|
* ```tsx
|
|
11
|
-
* //
|
|
12
|
-
*
|
|
23
|
+
* // Using open prop (recommended)
|
|
24
|
+
* import { useState } from 'react';
|
|
25
|
+
*
|
|
26
|
+
* function App() {
|
|
27
|
+
* const [open, setOpen] = useState(false);
|
|
13
28
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
29
|
+
* return (
|
|
30
|
+
* <>
|
|
31
|
+
* <Button onClick={() => setOpen(true)}>Open Dialog</Button>
|
|
32
|
+
* <Dialog open={open} label="Dialog Title" onHide={() => setOpen(false)}>
|
|
33
|
+
* <p>Dialog content</p>
|
|
34
|
+
* <Button slot="footer" variant="brand" onClick={() => setOpen(false)}>
|
|
35
|
+
* Close
|
|
36
|
+
* </Button>
|
|
37
|
+
* </Dialog>
|
|
38
|
+
* </>
|
|
39
|
+
* );
|
|
40
|
+
* }
|
|
17
41
|
*
|
|
42
|
+
* // Using ref methods (alternative)
|
|
43
|
+
* import { useRef } from 'react';
|
|
44
|
+
* const dialogRef = useRef<DialogRef>(null);
|
|
45
|
+
*
|
|
46
|
+
* <Button onClick={() => dialogRef.current?.show()}>Open Dialog</Button>
|
|
47
|
+
* <Dialog ref={dialogRef} label="Dialog Title">
|
|
48
|
+
* <p>Dialog content</p>
|
|
49
|
+
* <Button slot="footer" variant="brand" onClick={() => dialogRef.current?.hide()}>
|
|
50
|
+
* Close
|
|
51
|
+
* </Button>
|
|
52
|
+
* </Dialog>
|
|
18
53
|
* ```
|
|
19
54
|
*/
|
|
20
55
|
export interface DialogProps extends Omit<HTMLAttributes<HTMLElement>, 'onShow' | 'onAfterShow' | 'onHide' | 'onAfterHide' | 'dir'> {
|
|
@@ -42,21 +77,33 @@ export interface DialogProps extends Omit<HTMLAttributes<HTMLElement>, 'onShow'
|
|
|
42
77
|
|
|
43
78
|
/** Emitted after the dialog closes and all animations are complete. */
|
|
44
79
|
onAfterHide?: (event: CustomEvent) => void;
|
|
80
|
+
|
|
81
|
+
/** Data attribute for opening and closing declaratively. */
|
|
82
|
+
'data-dialog'?: string | string[];
|
|
45
83
|
}
|
|
46
84
|
|
|
47
85
|
export interface DialogRef {
|
|
86
|
+
show: () => void;
|
|
87
|
+
hide: () => void;
|
|
88
|
+
requestClose: () => void;
|
|
48
89
|
/** Reference to the underlying HTML element */
|
|
49
90
|
element: HTMLElement | null;
|
|
50
91
|
}
|
|
51
92
|
|
|
52
93
|
export const Dialog = forwardRef<DialogRef, DialogProps>(
|
|
53
|
-
({ children, className, onShow, onAfterShow, onHide, onAfterHide, ...props }, ref) => {
|
|
94
|
+
({ children, className, open, onShow, onAfterShow, onHide, onAfterHide, ...props }, ref) => {
|
|
54
95
|
const dialogRef = useRef<HTMLElement & {
|
|
96
|
+
show?: () => void;
|
|
97
|
+
requestClose?: () => void;
|
|
98
|
+
open?: boolean;
|
|
55
99
|
}>(null);
|
|
56
100
|
|
|
57
101
|
useImperativeHandle(
|
|
58
102
|
ref,
|
|
59
103
|
() => ({
|
|
104
|
+
show: () => dialogRef.current?.show?.(),
|
|
105
|
+
hide: () => dialogRef.current?.requestClose?.(),
|
|
106
|
+
requestClose: () => dialogRef.current?.requestClose?.(),
|
|
60
107
|
get element() {
|
|
61
108
|
return dialogRef.current;
|
|
62
109
|
},
|
|
@@ -64,25 +111,28 @@ export const Dialog = forwardRef<DialogRef, DialogProps>(
|
|
|
64
111
|
[]
|
|
65
112
|
);
|
|
66
113
|
|
|
114
|
+
// Sync open prop with dialog element
|
|
67
115
|
useEffect(() => {
|
|
68
116
|
const el = dialogRef.current;
|
|
69
|
-
if (!el) return;
|
|
117
|
+
if (!el || open === undefined) return;
|
|
70
118
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
119
|
+
const isOpen = el.open ?? false;
|
|
120
|
+
if (open && !isOpen) {
|
|
121
|
+
el.show?.();
|
|
122
|
+
} else if (!open && isOpen) {
|
|
123
|
+
el.requestClose?.();
|
|
124
|
+
}
|
|
125
|
+
}, [open]);
|
|
74
126
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const handleHide = (e: Event) => {
|
|
80
|
-
if (onHide) onHide(e as CustomEvent);
|
|
81
|
-
};
|
|
127
|
+
// Setup event listeners
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const el = dialogRef.current;
|
|
130
|
+
if (!el) return;
|
|
82
131
|
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
132
|
+
const handleShow = (e: Event) => onShow?.(e as CustomEvent);
|
|
133
|
+
const handleAfterShow = (e: Event) => onAfterShow?.(e as CustomEvent);
|
|
134
|
+
const handleHide = (e: Event) => onHide?.(e as CustomEvent);
|
|
135
|
+
const handleAfterHide = (e: Event) => onAfterHide?.(e as CustomEvent);
|
|
86
136
|
|
|
87
137
|
el.addEventListener('wa-show', handleShow);
|
|
88
138
|
el.addEventListener('wa-after-show', handleAfterShow);
|
|
@@ -97,14 +147,15 @@ export const Dialog = forwardRef<DialogRef, DialogProps>(
|
|
|
97
147
|
};
|
|
98
148
|
}, [onShow, onAfterShow, onHide, onAfterHide]);
|
|
99
149
|
|
|
100
|
-
return (
|
|
150
|
+
return createPortal(
|
|
101
151
|
<wa-dialog
|
|
102
152
|
ref={dialogRef}
|
|
103
153
|
class={clsx('Dialog', className)}
|
|
104
154
|
{...(props as Record<string, unknown>)}
|
|
105
155
|
>
|
|
106
156
|
{children}
|
|
107
|
-
</wa-dialog
|
|
157
|
+
</wa-dialog>,
|
|
158
|
+
document.body
|
|
108
159
|
);
|
|
109
160
|
}
|
|
110
161
|
);
|
|
@@ -1,15 +1,55 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
2
3
|
import clsx from 'clsx';
|
|
3
4
|
import '{{{importPath}}}';
|
|
4
5
|
import './Drawer.css';
|
|
5
6
|
|
|
6
7
|
/**
|
|
8
|
+
* // Using data-drawer attribute (recommended)
|
|
9
|
+
* import { Drawer } from './components/ui';
|
|
10
|
+
* import { Button } from './components/ui';
|
|
11
|
+
*
|
|
12
|
+
* <Button data-drawer="open drawer">Open Drawer</Button>
|
|
13
|
+
* <Drawer id="drawer" label="Drawer Title">
|
|
14
|
+
* <p>Drawer content</p>
|
|
15
|
+
* <Button slot="footer" variant="brand" data-drawer="close">Close</Button>
|
|
16
|
+
* </Drawer>
|
|
17
|
+
* Drawers slide in from a container edge to expose additional options
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // Using open prop (recommended)
|
|
21
|
+
* const [open, setOpen] = React.useState(false);
|
|
22
|
+
* <Drawer open={open} label="Drawer Title" onHide={() => setOpen(false)}>
|
|
23
|
+
* <p>Drawer content</p>
|
|
24
|
+
* </Drawer>
|
|
25
|
+
*
|
|
26
|
+
* // Using ref methods (alternative)
|
|
27
|
+
* const drawerRef = React.useRef(null);
|
|
28
|
+
* <Drawer ref={drawerRef} label="Drawer Title">
|
|
29
|
+
* <p>Drawer content</p>
|
|
30
|
+
* </Drawer>
|
|
31
|
+
* drawerRef.current?.show();
|
|
32
|
+
*
|
|
7
33
|
* @typedef {Object} DrawerRef
|
|
8
34
|
* @property {() => void} show
|
|
9
35
|
* @property {() => void} hide
|
|
10
36
|
* @property {HTMLElement | null} element
|
|
37
|
+
*
|
|
38
|
+
* @param {Object} props
|
|
39
|
+
* @param {boolean} [props.open] - Indicates whether the drawer is open
|
|
40
|
+
* @param {string} [props.label] - The drawer's label as displayed in the header
|
|
41
|
+
* @param {'top' | 'end' | 'bottom' | 'start'} [props.placement] - The direction from which the drawer will open
|
|
42
|
+
* @param {boolean} [props['light-dismiss']] - Closes the drawer when the user clicks outside of it
|
|
43
|
+
* @param {boolean} [props['without-header']] - Removes the header
|
|
44
|
+
* @param {function} [props.onShow] - Event fired when the drawer is shown
|
|
45
|
+
* @param {function} [props.onAfterShow] - Event fired after the drawer is shown
|
|
46
|
+
* @param {function} [props.onHide] - Event fired when the drawer is about to hide
|
|
47
|
+
* @param {function} [props.onAfterHide] - Event fired after the drawer is hidden
|
|
48
|
+
* @param {string | string[]} [props['data-drawer']] - Data attribute for opening and closing declaratively
|
|
49
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
50
|
+
* @param {React.ReactNode} [props.children] - Drawer content
|
|
51
|
+
* @param {React.Ref} ref - Ref with methods: show(), hide()
|
|
11
52
|
*/
|
|
12
|
-
|
|
13
53
|
export const Drawer = React.forwardRef(
|
|
14
54
|
({ children, className, open, onShow, onAfterShow, onHide, onAfterHide, ...props }, ref) => {
|
|
15
55
|
const drawerRef = React.useRef(null);
|
|
@@ -26,6 +66,7 @@ export const Drawer = React.forwardRef(
|
|
|
26
66
|
[]
|
|
27
67
|
);
|
|
28
68
|
|
|
69
|
+
// Sync open prop with drawer element
|
|
29
70
|
React.useEffect(() => {
|
|
30
71
|
const el = drawerRef.current;
|
|
31
72
|
if (!el || open === undefined) return;
|
|
@@ -38,6 +79,7 @@ export const Drawer = React.forwardRef(
|
|
|
38
79
|
}
|
|
39
80
|
}, [open]);
|
|
40
81
|
|
|
82
|
+
// Setup event listeners
|
|
41
83
|
React.useEffect(() => {
|
|
42
84
|
const el = drawerRef.current;
|
|
43
85
|
if (!el) return;
|
|
@@ -60,14 +102,15 @@ export const Drawer = React.forwardRef(
|
|
|
60
102
|
};
|
|
61
103
|
}, [onShow, onAfterShow, onHide, onAfterHide]);
|
|
62
104
|
|
|
63
|
-
return (
|
|
105
|
+
return createPortal(
|
|
64
106
|
<wa-drawer
|
|
65
107
|
ref={drawerRef}
|
|
66
108
|
class={clsx('Drawer', className)}
|
|
67
109
|
{...props}
|
|
68
110
|
>
|
|
69
111
|
{children}
|
|
70
|
-
</wa-drawer
|
|
112
|
+
</wa-drawer>,
|
|
113
|
+
document.body
|
|
71
114
|
);
|
|
72
115
|
}
|
|
73
116
|
);
|