@servicetitan/anvil2-ext-common 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/main.ts +35 -0
- package/.storybook/manager-head.html +1 -0
- package/.storybook/preview-head.html +0 -0
- package/.storybook/preview.tsx +203 -0
- package/.storybook/vitest.setup.ts +7 -0
- package/CHANGELOG.md +12 -0
- package/README.md +0 -0
- package/eslint.config.js +5 -0
- package/package.json +77 -0
- package/public/favicon.ico +0 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useConfirmDialog/ConfirmDialog.tsx +131 -0
- package/src/hooks/useConfirmDialog/index.ts +4 -0
- package/src/hooks/useConfirmDialog/useConfirmDialog.stories.tsx +177 -0
- package/src/hooks/useConfirmDialog/useConfirmDialog.tsx +89 -0
- package/src/index.ts +4 -0
- package/src/types/index.ts +7 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +25 -0
- package/tsconfig.node.json +10 -0
- package/vite.config.ts +126 -0
- package/vitest-setup.ts +46 -0
- package/vitest.shims.d.ts +1 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { StorybookConfig } from "@storybook/react-vite";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This function is used to resolve the absolute path of a package.
|
|
6
|
+
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
|
|
7
|
+
*/
|
|
8
|
+
function getAbsolutePath(value: string): string {
|
|
9
|
+
return dirname(require.resolve(join(value, "package.json")));
|
|
10
|
+
}
|
|
11
|
+
const config: StorybookConfig = {
|
|
12
|
+
stories: [
|
|
13
|
+
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)",
|
|
14
|
+
"../src/**/*.stories.mdx",
|
|
15
|
+
],
|
|
16
|
+
features: {
|
|
17
|
+
backgrounds: true,
|
|
18
|
+
viewport: true,
|
|
19
|
+
},
|
|
20
|
+
addons: [
|
|
21
|
+
getAbsolutePath("@storybook/addon-a11y"),
|
|
22
|
+
getAbsolutePath("@storybook/addon-links"),
|
|
23
|
+
getAbsolutePath("@chromatic-com/storybook"),
|
|
24
|
+
getAbsolutePath("@storybook/addon-vitest"),
|
|
25
|
+
],
|
|
26
|
+
framework: {
|
|
27
|
+
name: getAbsolutePath("@storybook/react-vite"),
|
|
28
|
+
options: {},
|
|
29
|
+
},
|
|
30
|
+
refs: {
|
|
31
|
+
"@servicetitan/anvil2": { disable: true },
|
|
32
|
+
"@servicetitan/design-system": { disable: true },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
export default config;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
File without changes
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { Preview } from "@storybook/react-vite";
|
|
2
|
+
import * as anvil2 from "@servicetitan/anvil2";
|
|
3
|
+
import { core } from "@servicetitan/anvil2/token";
|
|
4
|
+
|
|
5
|
+
const { AnvilProvider, Flex } = anvil2;
|
|
6
|
+
|
|
7
|
+
const layoutProps = [
|
|
8
|
+
"flex",
|
|
9
|
+
"flexGrow",
|
|
10
|
+
"flexShrink",
|
|
11
|
+
"flexBasis",
|
|
12
|
+
"alignSelf",
|
|
13
|
+
"justifySelf",
|
|
14
|
+
"order",
|
|
15
|
+
"gridArea",
|
|
16
|
+
"gridColumn",
|
|
17
|
+
"gridRow",
|
|
18
|
+
"gridTemplate",
|
|
19
|
+
"gridColumnStart",
|
|
20
|
+
"gridColumnEnd",
|
|
21
|
+
"gridRowStart",
|
|
22
|
+
"gridRowEnd",
|
|
23
|
+
"sm",
|
|
24
|
+
"md",
|
|
25
|
+
"lg",
|
|
26
|
+
"xl",
|
|
27
|
+
"xxl",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// we /could/ detect the actual scrollbar width here, but this is good enough
|
|
31
|
+
function addScrollbarWidth(s?: string) {
|
|
32
|
+
return s ? parseInt(s) + 16 + "px" : s;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const viewports = {
|
|
36
|
+
breakpointSmall: {
|
|
37
|
+
name: "Breakpoint Small",
|
|
38
|
+
styles: {
|
|
39
|
+
width: addScrollbarWidth(core.primitive?.BreakpointSm?.value),
|
|
40
|
+
height: "100%",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
breakpointMedium: {
|
|
44
|
+
name: "Breakpoint Medium",
|
|
45
|
+
styles: {
|
|
46
|
+
width: addScrollbarWidth(core.primitive?.BreakpointMd?.value),
|
|
47
|
+
height: "100%",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
breakpointLarge: {
|
|
51
|
+
name: "Breakpoint Large",
|
|
52
|
+
styles: {
|
|
53
|
+
width: addScrollbarWidth(core.primitive?.BreakpointLg?.value),
|
|
54
|
+
height: "100%",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
breakpointXLarge: {
|
|
58
|
+
name: "Breakpoint X-Large",
|
|
59
|
+
styles: {
|
|
60
|
+
width: addScrollbarWidth(core.primitive?.BreakpointXl?.value),
|
|
61
|
+
height: "100%",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
breakpointXXLarge: {
|
|
65
|
+
name: "Breakpoint XX-Large",
|
|
66
|
+
styles: {
|
|
67
|
+
width: addScrollbarWidth(core.primitive?.BreakpointXxl?.value),
|
|
68
|
+
height: "100%",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const preview: Preview = {
|
|
74
|
+
globalTypes: {
|
|
75
|
+
shadow: {
|
|
76
|
+
description: "Whether or not to use a shadow DOM",
|
|
77
|
+
toolbar: {
|
|
78
|
+
title: "Shadow",
|
|
79
|
+
icon: "mirror",
|
|
80
|
+
items: [
|
|
81
|
+
{
|
|
82
|
+
value: "fragment",
|
|
83
|
+
icon: "switchalt",
|
|
84
|
+
title: "Fragment",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
value: "shadow",
|
|
88
|
+
icon: "contrast",
|
|
89
|
+
title: "Shadow",
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
dynamicTitle: true,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
dir: {
|
|
96
|
+
description: "Indicates the directionality of the element's text",
|
|
97
|
+
toolbar: {
|
|
98
|
+
title: "dir",
|
|
99
|
+
items: [
|
|
100
|
+
{
|
|
101
|
+
value: "ltr",
|
|
102
|
+
title: "LTR",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
value: "rtl",
|
|
106
|
+
title: "RTL",
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
dynamicTitle: true,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
parameters: {
|
|
114
|
+
actions: { argTypesRegex: "^on.*" },
|
|
115
|
+
|
|
116
|
+
backgrounds: {
|
|
117
|
+
options: {
|
|
118
|
+
dark: { name: "Dark", value: "#141414" },
|
|
119
|
+
light: { name: "Light", value: "#FFFFFF" },
|
|
120
|
+
system: { name: "System", value: undefined },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
chromatic: {
|
|
125
|
+
prefersReducedMotion: "reduce",
|
|
126
|
+
pauseAnimationAtEnd: true,
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
docs: {
|
|
130
|
+
argTypes: {
|
|
131
|
+
sort: "requiredFirst",
|
|
132
|
+
},
|
|
133
|
+
codePanel: true,
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
controls: {
|
|
137
|
+
sort: "requiredFirst",
|
|
138
|
+
matchers: {
|
|
139
|
+
color: /(background|color)$/i,
|
|
140
|
+
date: /Date$/i,
|
|
141
|
+
},
|
|
142
|
+
expanded: true,
|
|
143
|
+
exclude: layoutProps,
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
layout: "fullscreen",
|
|
147
|
+
viewport: { options: viewports },
|
|
148
|
+
|
|
149
|
+
a11y: {
|
|
150
|
+
// 'todo' - show a11y violations in the test UI only
|
|
151
|
+
// 'error' - fail CI on a11y violations
|
|
152
|
+
// 'off' - skip a11y checks entirely
|
|
153
|
+
test: "todo",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
initialGlobals: {
|
|
157
|
+
shadow: "shadow",
|
|
158
|
+
dir: "ltr",
|
|
159
|
+
backgrounds: { value: "system" },
|
|
160
|
+
},
|
|
161
|
+
decorators: [
|
|
162
|
+
(Story, context) => {
|
|
163
|
+
const { minHeight, noPadding } = context.parameters;
|
|
164
|
+
const mode =
|
|
165
|
+
context.globals.backgrounds.value === "system"
|
|
166
|
+
? undefined
|
|
167
|
+
: context.globals.backgrounds.value;
|
|
168
|
+
|
|
169
|
+
if (context.title.startsWith("Anvil2")) {
|
|
170
|
+
return (
|
|
171
|
+
<AnvilProvider themeData={{ mode }} dir={context.globals.dir}>
|
|
172
|
+
<Flex
|
|
173
|
+
className="bootstrap"
|
|
174
|
+
alignItems="flex-start"
|
|
175
|
+
style={{
|
|
176
|
+
backgroundColor: `var(--background-color)`,
|
|
177
|
+
padding: noPadding ? undefined : "1rem",
|
|
178
|
+
minHeight,
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
<Story />
|
|
182
|
+
</Flex>
|
|
183
|
+
</AnvilProvider>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return (
|
|
187
|
+
<Flex
|
|
188
|
+
dir={context.globals.dir}
|
|
189
|
+
alignItems="flex-start"
|
|
190
|
+
style={{
|
|
191
|
+
backgroundColor: `var(--background-color)`,
|
|
192
|
+
padding: "1rem",
|
|
193
|
+
minHeight,
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
<Story />
|
|
197
|
+
</Flex>
|
|
198
|
+
);
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export default preview;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
|
|
2
|
+
import { setProjectAnnotations } from "@storybook/react-vite";
|
|
3
|
+
import * as projectAnnotations from "./preview";
|
|
4
|
+
|
|
5
|
+
// This is an important step to apply the right configuration when testing your stories.
|
|
6
|
+
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
|
|
7
|
+
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# @servicetitan/anvil2-ext-common
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#1566](https://github.com/servicetitan/hammer/pull/1566) [`5822b65`](https://github.com/servicetitan/hammer/commit/5822b65debe42dce860a2174f91bef2b42da0370) Thanks [@rgdelato](https://github.com/rgdelato)! - [useConfirmDialog] Add `useConfirmDialog` to new `@servicetitan/anvil2-ext-common` package
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`fc8b30c`](https://github.com/servicetitan/hammer/commit/fc8b30cc6a8cb7ec99f9630e9ba9d9a4e5af5e4b), [`3daa6f6`](https://github.com/servicetitan/hammer/commit/3daa6f67670bcdb5e978ad083056db4d29892b16), [`3df1040`](https://github.com/servicetitan/hammer/commit/3df1040566f627a937f2f7f7e889c3c2bcfb230a)]:
|
|
12
|
+
- @servicetitan/anvil2@1.46.8
|
package/README.md
ADDED
|
File without changes
|
package/eslint.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@servicetitan/anvil2-ext-common",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"keywords": [],
|
|
8
|
+
"author": "",
|
|
9
|
+
"license": "ISC",
|
|
10
|
+
"description": "",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@react-hook/merged-ref": "^1.3.2",
|
|
13
|
+
"@react-hook/resize-observer": "^2.0.1",
|
|
14
|
+
"classnames": "^2.5.1",
|
|
15
|
+
"tabbable": "^6.2.0",
|
|
16
|
+
"tinycolor2": "^1.6.0",
|
|
17
|
+
"uuid": "^10.0.0",
|
|
18
|
+
"@servicetitan/anvil2": "1.46.8"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@types/react": "^18",
|
|
22
|
+
"@types/react-dom": "^18",
|
|
23
|
+
"react": "^18",
|
|
24
|
+
"react-dom": "^18"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@chromatic-com/storybook": "^4.1.1",
|
|
28
|
+
"@storybook/addon-a11y": "9.1.5",
|
|
29
|
+
"@storybook/addon-docs": "9.1.5",
|
|
30
|
+
"@storybook/addon-links": "9.1.5",
|
|
31
|
+
"@storybook/addon-vitest": "9.1.5",
|
|
32
|
+
"@storybook/react-vite": "9.1.5",
|
|
33
|
+
"@testing-library/jest-dom": "^5.17.0",
|
|
34
|
+
"@testing-library/react": "^16.1.0",
|
|
35
|
+
"@testing-library/user-event": "^14.5.2",
|
|
36
|
+
"@types/crypto-js": "^4.2.2",
|
|
37
|
+
"@types/luxon": "^3.4.2",
|
|
38
|
+
"@types/react": "18.3.18",
|
|
39
|
+
"@types/react-dom": "18.3.5",
|
|
40
|
+
"@types/react-window": "^1.8.8",
|
|
41
|
+
"@types/testing-library__jest-dom": "^5.14.9",
|
|
42
|
+
"@types/tinycolor2": "^1.4.6",
|
|
43
|
+
"@types/uuid": "^10.0.0",
|
|
44
|
+
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
|
45
|
+
"@typescript-eslint/parser": "^6.14.0",
|
|
46
|
+
"@vitest/browser": "^3.2.4",
|
|
47
|
+
"@vitest/coverage-v8": "^3.0.8",
|
|
48
|
+
"chromatic": "^11.20.2",
|
|
49
|
+
"commander": "^12.1.0",
|
|
50
|
+
"globals": "^15.15.0",
|
|
51
|
+
"happy-dom": "^17.4.4",
|
|
52
|
+
"mdast-util-to-string": "^4.0.0",
|
|
53
|
+
"playwright": "^1.52.0",
|
|
54
|
+
"react": "18.2.0",
|
|
55
|
+
"react-dom": "18.2.0",
|
|
56
|
+
"remark-gfm": "^4.0.0",
|
|
57
|
+
"remark-parse": "^11.0.0",
|
|
58
|
+
"remark-stringify": "^11.0.0",
|
|
59
|
+
"sass": "1.87.0",
|
|
60
|
+
"storybook": "9.1.5",
|
|
61
|
+
"svgo": "^3.3.2",
|
|
62
|
+
"typescript": "~5.7.2",
|
|
63
|
+
"unified": "^11.0.5",
|
|
64
|
+
"vite": "6.1.6",
|
|
65
|
+
"vitest": "^3.2.4",
|
|
66
|
+
"vitest-axe": "^0.1.0"
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"dev": "vite",
|
|
70
|
+
"build": "vite build",
|
|
71
|
+
"lint": "eslint .",
|
|
72
|
+
"preview": "vite preview",
|
|
73
|
+
"storybook": "storybook dev -p 8008",
|
|
74
|
+
"build-storybook": "storybook build --stats-json",
|
|
75
|
+
"test-storybook": "vitest --project=storybook"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./useConfirmDialog";
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { ComponentProps, ComponentPropsWithRef, FC, ReactNode } from "react";
|
|
2
|
+
import { Button, Dialog } from "@servicetitan/anvil2";
|
|
3
|
+
import { Prettify } from "../../types";
|
|
4
|
+
|
|
5
|
+
export interface PrimaryButtonInternalProps {
|
|
6
|
+
appearance?: ComponentProps<typeof Button>["appearance"];
|
|
7
|
+
onClick?: () => void;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ConfirmDialogInternalProps {
|
|
12
|
+
/**
|
|
13
|
+
* Callback function when the confirm button is clicked
|
|
14
|
+
*/
|
|
15
|
+
onConfirm: () => void;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Content to be displayed in the dialog
|
|
19
|
+
* @defaultValue 'Are you sure?'
|
|
20
|
+
*/
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Appearance of the primary button
|
|
25
|
+
* @defaultValue 'primary'
|
|
26
|
+
*/
|
|
27
|
+
appearance?: PrimaryButtonInternalProps["appearance"];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Header content of the dialog
|
|
31
|
+
* @defaultValue 'Confirm'
|
|
32
|
+
*/
|
|
33
|
+
header?: ReactNode;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Content of the primary button
|
|
37
|
+
* @defaultValue 'OK'
|
|
38
|
+
*/
|
|
39
|
+
primaryButton?: ReactNode;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Content of the cancel button
|
|
43
|
+
* @defaultValue 'Cancel'
|
|
44
|
+
*/
|
|
45
|
+
cancelButton?: ReactNode;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Props to be passed to the internal <Dialog.Header>
|
|
49
|
+
*/
|
|
50
|
+
headerProps?: Omit<ComponentPropsWithRef<typeof Dialog.Header>, "children">;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Props to be passed to the internal <Dialog.Content>
|
|
54
|
+
*/
|
|
55
|
+
contentProps?: Omit<ComponentPropsWithRef<typeof Dialog.Content>, "children">;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Props to be passed to the internal <Dialog.Footer>
|
|
59
|
+
*/
|
|
60
|
+
footerProps?: Omit<ComponentPropsWithRef<typeof Dialog.Footer>, "children">;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Props to be passed to the internal <Dialog.CancelButton>
|
|
64
|
+
*/
|
|
65
|
+
cancelButtonProps?: Omit<
|
|
66
|
+
ComponentPropsWithRef<typeof Dialog.CancelButton>,
|
|
67
|
+
"children"
|
|
68
|
+
>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Props to be passed to the internal primary <Button>
|
|
72
|
+
*/
|
|
73
|
+
primaryButtonProps?: Omit<
|
|
74
|
+
ComponentPropsWithRef<typeof Button>,
|
|
75
|
+
keyof PrimaryButtonInternalProps
|
|
76
|
+
>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type ConfirmDialogProps = Prettify<
|
|
80
|
+
ConfirmDialogInternalProps &
|
|
81
|
+
Omit<ComponentPropsWithRef<typeof Dialog>, keyof ConfirmDialogInternalProps>
|
|
82
|
+
>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* ConfirmDialog component for displaying a simple confirmation dialog.
|
|
86
|
+
*
|
|
87
|
+
* @remarks
|
|
88
|
+
* - Any property of Anvil's <Dialog> can be passed directly to <ConfirmDialog>
|
|
89
|
+
*
|
|
90
|
+
* - header, primaryButton, and cancelButton are typed as ReactNode,
|
|
91
|
+
* so arbitrary React markup can be passed in addition to strings.
|
|
92
|
+
*
|
|
93
|
+
* - headerProps, contentProps, footerProps, cancelButtonProps, and primaryButtonProps
|
|
94
|
+
* can be used to customize the internal components. They will be forwarded directly
|
|
95
|
+
* to <Dialog.Header>, <Dialog.Content>, <Dialog.Footer>, etc.
|
|
96
|
+
*/
|
|
97
|
+
export const ConfirmDialog: FC<ConfirmDialogProps> = ({
|
|
98
|
+
appearance = "primary",
|
|
99
|
+
header = "Confirm",
|
|
100
|
+
children = "Are you sure?",
|
|
101
|
+
primaryButton = "OK",
|
|
102
|
+
cancelButton = "Cancel",
|
|
103
|
+
onConfirm,
|
|
104
|
+
cancelButtonProps,
|
|
105
|
+
primaryButtonProps,
|
|
106
|
+
headerProps,
|
|
107
|
+
contentProps,
|
|
108
|
+
footerProps,
|
|
109
|
+
...dialogProps
|
|
110
|
+
}: ConfirmDialogProps) => {
|
|
111
|
+
const primaryButtonInternalProps: PrimaryButtonInternalProps = {
|
|
112
|
+
appearance,
|
|
113
|
+
onClick: onConfirm,
|
|
114
|
+
children: primaryButton,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Dialog {...dialogProps}>
|
|
119
|
+
<Dialog.Header {...headerProps}>{header}</Dialog.Header>
|
|
120
|
+
<Dialog.Content {...contentProps}>{children}</Dialog.Content>
|
|
121
|
+
<Dialog.Footer {...footerProps}>
|
|
122
|
+
<Dialog.CancelButton {...cancelButtonProps}>
|
|
123
|
+
{cancelButton}
|
|
124
|
+
</Dialog.CancelButton>
|
|
125
|
+
|
|
126
|
+
{/* allow primaryButtonProps object to override the explicit top-level props - primaryButton, appearance, onClick */}
|
|
127
|
+
<Button {...primaryButtonInternalProps} {...primaryButtonProps} />
|
|
128
|
+
</Dialog.Footer>
|
|
129
|
+
</Dialog>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { Meta, StoryObj, StoryFn } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { action } from "storybook/actions";
|
|
4
|
+
import { ComponentProps } from "react";
|
|
5
|
+
import { Button, Flex } from "@servicetitan/anvil2";
|
|
6
|
+
import { ConfirmDialog } from "./ConfirmDialog";
|
|
7
|
+
import { useConfirmDialog } from "./useConfirmDialog";
|
|
8
|
+
|
|
9
|
+
type UseConfirmDialogWrapperProps = Omit<
|
|
10
|
+
ComponentProps<typeof ConfirmDialog>,
|
|
11
|
+
"onCancel" | "onConfirm"
|
|
12
|
+
> & {
|
|
13
|
+
onConfirm: () => void;
|
|
14
|
+
onCancel?: () => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const UseConfirmDialogWrapper = ({
|
|
18
|
+
children,
|
|
19
|
+
onConfirm,
|
|
20
|
+
onCancel,
|
|
21
|
+
...rest
|
|
22
|
+
}: UseConfirmDialogWrapperProps) => {
|
|
23
|
+
const [ConfirmDialog, openConfirm] = useConfirmDialog(onConfirm, onCancel);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Flex justifyContent="center" alignItems="center" style={{ height: 400 }}>
|
|
27
|
+
<Button onClick={() => openConfirm()}>Do Something</Button>
|
|
28
|
+
<ConfirmDialog {...rest}>{children}</ConfirmDialog>
|
|
29
|
+
</Flex>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
|
34
|
+
const meta = {
|
|
35
|
+
title: "Hooks/UseConfirmDialog",
|
|
36
|
+
component: UseConfirmDialogWrapper,
|
|
37
|
+
parameters: {
|
|
38
|
+
layout: "fullscreen",
|
|
39
|
+
minHeight: "100dvh",
|
|
40
|
+
},
|
|
41
|
+
args: {
|
|
42
|
+
header: undefined,
|
|
43
|
+
children: undefined,
|
|
44
|
+
primaryButton: undefined,
|
|
45
|
+
primaryButtonProps: {},
|
|
46
|
+
appearance: undefined,
|
|
47
|
+
cancelButton: undefined,
|
|
48
|
+
cancelButtonProps: {},
|
|
49
|
+
headerProps: {},
|
|
50
|
+
contentProps: {},
|
|
51
|
+
footerProps: {},
|
|
52
|
+
onConfirm: action("onConfirm"),
|
|
53
|
+
onCancel: action("onCancel"),
|
|
54
|
+
},
|
|
55
|
+
argTypes: {
|
|
56
|
+
header: {
|
|
57
|
+
control: "text",
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "The header content of the confirmation dialog",
|
|
60
|
+
},
|
|
61
|
+
children: {
|
|
62
|
+
control: "text",
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "The content of the confirmation dialog",
|
|
65
|
+
},
|
|
66
|
+
primaryButton: {
|
|
67
|
+
control: "text",
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "The content of the primary button",
|
|
70
|
+
},
|
|
71
|
+
primaryButtonProps: {
|
|
72
|
+
control: "object",
|
|
73
|
+
description:
|
|
74
|
+
"Props to be passed to the primary button. See Button for details.",
|
|
75
|
+
},
|
|
76
|
+
appearance: {
|
|
77
|
+
control: "select",
|
|
78
|
+
options: ["primary", "secondary", "ghost", "danger", "danger-secondary"],
|
|
79
|
+
description: "The appearance of the primary button",
|
|
80
|
+
type: "string",
|
|
81
|
+
},
|
|
82
|
+
cancelButton: {
|
|
83
|
+
control: "text",
|
|
84
|
+
description: "The content of the cancel button",
|
|
85
|
+
type: "string",
|
|
86
|
+
},
|
|
87
|
+
cancelButtonProps: {
|
|
88
|
+
control: "object",
|
|
89
|
+
description:
|
|
90
|
+
"Props to be passed to the cancel button. See Button for details.",
|
|
91
|
+
},
|
|
92
|
+
headerProps: {
|
|
93
|
+
control: "object",
|
|
94
|
+
description:
|
|
95
|
+
"Props to be passed to the header. See Dialog.Header for details.",
|
|
96
|
+
},
|
|
97
|
+
contentProps: {
|
|
98
|
+
control: "object",
|
|
99
|
+
description:
|
|
100
|
+
"Props to be passed to the content. See Dialog.Content for details.",
|
|
101
|
+
},
|
|
102
|
+
footerProps: {
|
|
103
|
+
control: "object",
|
|
104
|
+
description:
|
|
105
|
+
"Props to be passed to the footer. See Dialog.Footer for details.",
|
|
106
|
+
},
|
|
107
|
+
onConfirm: {
|
|
108
|
+
action: "onConfirm",
|
|
109
|
+
type: "function",
|
|
110
|
+
description: "Callback fired when the primary button is clicked",
|
|
111
|
+
},
|
|
112
|
+
onCancel: {
|
|
113
|
+
action: "onCancel",
|
|
114
|
+
type: "function",
|
|
115
|
+
description: "Callback fired when the cancel button is clicked",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// More on argTypes: https://storybook.js.org/docs/api/argtypes
|
|
120
|
+
} satisfies Meta<typeof UseConfirmDialogWrapper>;
|
|
121
|
+
|
|
122
|
+
export default meta;
|
|
123
|
+
|
|
124
|
+
const renderUseConfirmDialog: StoryFn<typeof UseConfirmDialogWrapper> = ({
|
|
125
|
+
header,
|
|
126
|
+
children,
|
|
127
|
+
primaryButton,
|
|
128
|
+
primaryButtonProps,
|
|
129
|
+
appearance,
|
|
130
|
+
cancelButton,
|
|
131
|
+
cancelButtonProps,
|
|
132
|
+
headerProps,
|
|
133
|
+
contentProps,
|
|
134
|
+
footerProps,
|
|
135
|
+
onConfirm,
|
|
136
|
+
onCancel,
|
|
137
|
+
}) => {
|
|
138
|
+
return (
|
|
139
|
+
<UseConfirmDialogWrapper
|
|
140
|
+
header={header}
|
|
141
|
+
primaryButton={primaryButton}
|
|
142
|
+
primaryButtonProps={primaryButtonProps}
|
|
143
|
+
appearance={appearance}
|
|
144
|
+
cancelButton={cancelButton}
|
|
145
|
+
cancelButtonProps={cancelButtonProps}
|
|
146
|
+
headerProps={headerProps}
|
|
147
|
+
contentProps={contentProps}
|
|
148
|
+
footerProps={footerProps}
|
|
149
|
+
onConfirm={onConfirm}
|
|
150
|
+
onCancel={onCancel}
|
|
151
|
+
>
|
|
152
|
+
{children}
|
|
153
|
+
</UseConfirmDialogWrapper>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const UseConfirmDefaults: StoryFn<typeof UseConfirmDialogWrapper> =
|
|
158
|
+
renderUseConfirmDialog;
|
|
159
|
+
|
|
160
|
+
export const UseConfirmDanger: StoryObj<typeof UseConfirmDialogWrapper> = {
|
|
161
|
+
args: {
|
|
162
|
+
header: "Please Confirm",
|
|
163
|
+
children: "Are you sure you want to do this?",
|
|
164
|
+
primaryButton: "Yes, do it",
|
|
165
|
+
primaryButtonProps: {},
|
|
166
|
+
appearance: "danger",
|
|
167
|
+
cancelButton: undefined,
|
|
168
|
+
cancelButtonProps: {},
|
|
169
|
+
headerProps: {},
|
|
170
|
+
contentProps: {},
|
|
171
|
+
footerProps: {},
|
|
172
|
+
onConfirm: action("onConfirm"),
|
|
173
|
+
onCancel: action("onCancel"),
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
render: renderUseConfirmDialog,
|
|
177
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { ConfirmDialog, ConfirmDialogProps } from "./ConfirmDialog";
|
|
3
|
+
import { Optional, Prettify } from "../../types";
|
|
4
|
+
|
|
5
|
+
export type UseConfirmDialogProps = Prettify<
|
|
6
|
+
Omit<Optional<ConfirmDialogProps, "open">, "onClose" | "onConfirm">
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* useConfirmDialog hook for displaying a confirmation dialog.
|
|
11
|
+
* - simpler markup than building a whole <Dialog>
|
|
12
|
+
* - no need to explicitly manage its `open` state
|
|
13
|
+
*
|
|
14
|
+
* @param onConfirm (required) - Function to call when the confirmation action is taken.
|
|
15
|
+
* @param onClose (optional) - Function to call when the dialog is closed.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const [ConfirmDelete, onDelete] = useConfirmDialog(store.onDelete);
|
|
20
|
+
*
|
|
21
|
+
* // Then in markup below:
|
|
22
|
+
* <ConfirmDelete header="Confirm Deletion" primaryButton="Delete" appearance="danger">
|
|
23
|
+
* Are you sure you want to delete this item?
|
|
24
|
+
* </ConfirmDelete>
|
|
25
|
+
*
|
|
26
|
+
* // If no children are supplied, the default message 'Are you sure?' will be used.
|
|
27
|
+
* <ConfirmDelete />
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* If you want to manually control the `open` state of the ConfirmDialog:
|
|
32
|
+
* ```ts
|
|
33
|
+
* const [ConfirmDelete] = useConfirmDialog(store.onDelete);
|
|
34
|
+
*
|
|
35
|
+
* // Then in markup below:
|
|
36
|
+
* <ConfirmDelete open={showConfirmDelete} header="Confirm Deletion" primaryButton="Delete" appearance="danger">
|
|
37
|
+
* Are you sure you want to delete this item?
|
|
38
|
+
* </ConfirmDelete>
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* - The returned `ConfirmDialog` component will handle its own open/close state,
|
|
43
|
+
* or you can override it by passing the `open` prop to it.
|
|
44
|
+
*
|
|
45
|
+
* - The `onConfirm` function will be called when the user confirms the action.
|
|
46
|
+
*
|
|
47
|
+
* - If provided, the `onCancel` function will be called when the dialog is cancelled
|
|
48
|
+
* (either by clicking the cancel button, the close button, or outside the dialog).
|
|
49
|
+
*
|
|
50
|
+
* - If you need to customize the dialog further:
|
|
51
|
+
* - Any property of Anvil2's <Dialog> can be passed to <ConfirmDialog> except `open` and `onClose`
|
|
52
|
+
*
|
|
53
|
+
* - The `header`, `primaryButton`, and `cancelButton` properties are typed as ReactNode,
|
|
54
|
+
* so arbitrary React markup can be passed in addition to strings.
|
|
55
|
+
*
|
|
56
|
+
* - The following properties are passed to the internal components:
|
|
57
|
+
* `headerProps`, `contentProps`, `footerProps`, `cancelButtonProps`, `primaryButtonProps`
|
|
58
|
+
*/
|
|
59
|
+
export const useConfirmDialog = (
|
|
60
|
+
onConfirm: () => void,
|
|
61
|
+
onCancel?: () => void,
|
|
62
|
+
) => {
|
|
63
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
64
|
+
|
|
65
|
+
const onConfirmInner = () => {
|
|
66
|
+
onConfirm();
|
|
67
|
+
setIsOpen(false);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const closeDialogInner = () => {
|
|
71
|
+
onCancel?.();
|
|
72
|
+
setIsOpen(false);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const UseConfirmDialogInner = (props: UseConfirmDialogProps) => (
|
|
76
|
+
<ConfirmDialog
|
|
77
|
+
open={isOpen}
|
|
78
|
+
{...props} // allow `open` passed to <ConfirmDialog> to override internal `isOpen`
|
|
79
|
+
onConfirm={onConfirmInner}
|
|
80
|
+
onClose={closeDialogInner}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
UseConfirmDialogInner,
|
|
86
|
+
() => setIsOpen(true),
|
|
87
|
+
() => setIsOpen(false),
|
|
88
|
+
] as const;
|
|
89
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Make a select list of properties optional
|
|
2
|
+
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
3
|
+
|
|
4
|
+
// Improves display of complex types in Intellisense (VS Code and Visual Studio)
|
|
5
|
+
export type Prettify<T> = {
|
|
6
|
+
[K in keyof T]: T[K];
|
|
7
|
+
} & {};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES6",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"jsx": "react-jsx",
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["src"],
|
|
24
|
+
"references": [{ "path": "./tsconfig.node.json" }]
|
|
25
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/// <reference types="vitest/config" />
|
|
2
|
+
import { defineConfig } from "vite";
|
|
3
|
+
import react from "@vitejs/plugin-react";
|
|
4
|
+
import dts from "vite-plugin-dts";
|
|
5
|
+
import { libInjectCss } from "vite-plugin-lib-inject-css";
|
|
6
|
+
import svgr from "vite-plugin-svgr";
|
|
7
|
+
import { readdirSync } from "node:fs";
|
|
8
|
+
import packageJson from "./package.json";
|
|
9
|
+
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
|
|
13
|
+
|
|
14
|
+
const dirname =
|
|
15
|
+
typeof __dirname !== "undefined"
|
|
16
|
+
? __dirname
|
|
17
|
+
: path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
const entries = ["hooks", "types"]
|
|
20
|
+
.map((folderName) =>
|
|
21
|
+
readdirSync(path.resolve(__dirname, `src/${folderName}`), {
|
|
22
|
+
withFileTypes: true,
|
|
23
|
+
})
|
|
24
|
+
.filter((dirent) => dirent.isDirectory())
|
|
25
|
+
.reduce(
|
|
26
|
+
(acc, dirent) => ({
|
|
27
|
+
...acc,
|
|
28
|
+
[`${dirent.name}`]: `src/${folderName}/${dirent.name}/index.ts`,
|
|
29
|
+
}),
|
|
30
|
+
{},
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
.reduce((acc, folderEntries) => ({ ...acc, ...folderEntries }), {});
|
|
34
|
+
|
|
35
|
+
export default defineConfig({
|
|
36
|
+
plugins: [
|
|
37
|
+
react(),
|
|
38
|
+
libInjectCss(),
|
|
39
|
+
svgr({
|
|
40
|
+
include: "**/*.svg",
|
|
41
|
+
svgrOptions: {
|
|
42
|
+
icon: true,
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
dts({
|
|
46
|
+
entryRoot: "src",
|
|
47
|
+
tsconfigPath: path.join(__dirname, "tsconfig.json"),
|
|
48
|
+
exclude: ["**/*.test.tsx", "**/*.test.ts", "**/*.stories.tsx"],
|
|
49
|
+
insertTypesEntry: true,
|
|
50
|
+
}),
|
|
51
|
+
],
|
|
52
|
+
build: {
|
|
53
|
+
target: "esnext",
|
|
54
|
+
minify: false,
|
|
55
|
+
cssMinify: false,
|
|
56
|
+
outDir: "./dist",
|
|
57
|
+
emptyOutDir: true,
|
|
58
|
+
reportCompressedSize: true,
|
|
59
|
+
sourcemap: true,
|
|
60
|
+
commonjsOptions: {
|
|
61
|
+
transformMixedEsModules: true,
|
|
62
|
+
},
|
|
63
|
+
lib: {
|
|
64
|
+
entry: {
|
|
65
|
+
...entries,
|
|
66
|
+
index: "src/index.ts",
|
|
67
|
+
},
|
|
68
|
+
formats: ["es"],
|
|
69
|
+
},
|
|
70
|
+
rollupOptions: {
|
|
71
|
+
external: [
|
|
72
|
+
...Object.keys(packageJson.peerDependencies),
|
|
73
|
+
"@servicetitan/anvil2",
|
|
74
|
+
/react\/jsx-runtime/,
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
css: {
|
|
79
|
+
postcss: "../../",
|
|
80
|
+
},
|
|
81
|
+
test: {
|
|
82
|
+
environment: "happy-dom",
|
|
83
|
+
exclude: ["**/node_modules/**"],
|
|
84
|
+
css: {
|
|
85
|
+
modules: {
|
|
86
|
+
classNameStrategy: "non-scoped",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
projects: [
|
|
90
|
+
{
|
|
91
|
+
extends: true,
|
|
92
|
+
test: {
|
|
93
|
+
name: "unit",
|
|
94
|
+
setupFiles: ["vitest-setup.ts"],
|
|
95
|
+
include: ["**/*.test.ts", "**/*.test.tsx"],
|
|
96
|
+
// coverage: {
|
|
97
|
+
// provider: "istanbul",
|
|
98
|
+
// include: ["src/**"],
|
|
99
|
+
// exclude: ["**/*.stories.tsx", "**/*.test.ts", "**/*.test.tsx"],
|
|
100
|
+
// reporter: ["text", "json", "html"],
|
|
101
|
+
// },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
extends: true,
|
|
106
|
+
plugins: [
|
|
107
|
+
// The plugin will run tests for the stories defined in your Storybook config
|
|
108
|
+
// See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
|
|
109
|
+
storybookTest({
|
|
110
|
+
configDir: path.join(dirname, ".storybook"),
|
|
111
|
+
}),
|
|
112
|
+
],
|
|
113
|
+
test: {
|
|
114
|
+
name: "storybook",
|
|
115
|
+
browser: {
|
|
116
|
+
enabled: true,
|
|
117
|
+
headless: true,
|
|
118
|
+
provider: "playwright",
|
|
119
|
+
instances: [{ browser: "chromium" }],
|
|
120
|
+
},
|
|
121
|
+
setupFiles: [".storybook/vitest.setup.ts"],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
});
|
package/vitest-setup.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as axeMatchers from "vitest-axe/matchers";
|
|
2
|
+
import matchers from "@testing-library/jest-dom/matchers";
|
|
3
|
+
import { vi, afterEach, beforeAll, expect } from "vitest";
|
|
4
|
+
import "vitest-axe/extend-expect";
|
|
5
|
+
import { cleanup } from "@testing-library/react";
|
|
6
|
+
|
|
7
|
+
expect.extend(matchers);
|
|
8
|
+
expect.extend(axeMatchers);
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
cleanup();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
HTMLDialogElement.prototype.showModal = vi.fn();
|
|
16
|
+
HTMLDialogElement.prototype.close = vi.fn();
|
|
17
|
+
HTMLElement.prototype.showPopover = vi.fn();
|
|
18
|
+
HTMLElement.prototype.hidePopover = vi.fn();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* mocking window.matchMedia in JSDOM
|
|
23
|
+
* https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
|
|
24
|
+
*
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(window, "matchMedia", {
|
|
27
|
+
writable: true,
|
|
28
|
+
value: vi.fn().mockImplementation((query) => ({
|
|
29
|
+
matches: false,
|
|
30
|
+
media: query,
|
|
31
|
+
onchange: null,
|
|
32
|
+
addListener: vi.fn(), // deprecated
|
|
33
|
+
removeListener: vi.fn(), // deprecated
|
|
34
|
+
addEventListener: vi.fn(),
|
|
35
|
+
removeEventListener: vi.fn(),
|
|
36
|
+
dispatchEvent: vi.fn(),
|
|
37
|
+
})),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Fixes a testing throw from axe-core
|
|
42
|
+
* https://github.com/NickColley/jest-axe/issues/147
|
|
43
|
+
*
|
|
44
|
+
*/
|
|
45
|
+
const { getComputedStyle } = window;
|
|
46
|
+
window.getComputedStyle = (elt) => getComputedStyle(elt);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="@vitest/browser/providers/playwright" />
|