@etsoo/toolpad 1.0.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.
Files changed (202) hide show
  1. package/.github/workflows/main.yml +49 -0
  2. package/.vscode/settings.json +3 -0
  3. package/LICENSE +21 -0
  4. package/README.md +11 -0
  5. package/build/src/Account/Account.d.ts +71 -0
  6. package/build/src/Account/Account.js +117 -0
  7. package/build/src/Account/Account.test.d.ts +1 -0
  8. package/build/src/Account/Account.test.js +35 -0
  9. package/build/src/Account/AccountPopoverFooter.d.ts +6 -0
  10. package/build/src/Account/AccountPopoverFooter.js +21 -0
  11. package/build/src/Account/AccountPopoverHeader.d.ts +6 -0
  12. package/build/src/Account/AccountPopoverHeader.js +15 -0
  13. package/build/src/Account/AccountPreview.d.ts +67 -0
  14. package/build/src/Account/AccountPreview.js +76 -0
  15. package/build/src/Account/AccountPreview.test.d.ts +1 -0
  16. package/build/src/Account/AccountPreview.test.js +47 -0
  17. package/build/src/Account/SignInButton.d.ts +2 -0
  18. package/build/src/Account/SignInButton.js +26 -0
  19. package/build/src/Account/SignOutButton.d.ts +3 -0
  20. package/build/src/Account/SignOutButton.js +28 -0
  21. package/build/src/Account/index.d.ts +6 -0
  22. package/build/src/Account/index.js +6 -0
  23. package/build/src/AppProvider/AppProvider.d.ts +98 -0
  24. package/build/src/AppProvider/AppProvider.js +4 -0
  25. package/build/src/AppProvider/AppProvider.test.d.ts +4 -0
  26. package/build/src/AppProvider/AppProvider.test.js +16 -0
  27. package/build/src/AppProvider/AppProviderComponent.d.ts +17 -0
  28. package/build/src/AppProvider/AppProviderComponent.js +120 -0
  29. package/build/src/AppProvider/AppThemeProvider.d.ts +12 -0
  30. package/build/src/AppProvider/AppThemeProvider.js +73 -0
  31. package/build/src/AppProvider/index.d.ts +1 -0
  32. package/build/src/AppProvider/index.js +1 -0
  33. package/build/src/DashboardLayout/DashboardLayout.d.ts +83 -0
  34. package/build/src/DashboardLayout/DashboardLayout.js +286 -0
  35. package/build/src/DashboardLayout/DashboardLayout.test.d.ts +1 -0
  36. package/build/src/DashboardLayout/DashboardLayout.test.js +291 -0
  37. package/build/src/DashboardLayout/DashboardSidebarSubNavigation.d.ts +16 -0
  38. package/build/src/DashboardLayout/DashboardSidebarSubNavigation.js +149 -0
  39. package/build/src/DashboardLayout/ThemeSwitcher.d.ts +5 -0
  40. package/build/src/DashboardLayout/ThemeSwitcher.js +40 -0
  41. package/build/src/DashboardLayout/ToolbarActions.d.ts +5 -0
  42. package/build/src/DashboardLayout/ToolbarActions.js +9 -0
  43. package/build/src/DashboardLayout/index.d.ts +2 -0
  44. package/build/src/DashboardLayout/index.js +3 -0
  45. package/build/src/DashboardLayout/utils.d.ts +8 -0
  46. package/build/src/DashboardLayout/utils.js +16 -0
  47. package/build/src/PageContainer/PageContainer.d.ts +66 -0
  48. package/build/src/PageContainer/PageContainer.js +97 -0
  49. package/build/src/PageContainer/PageContainer.test.d.ts +1 -0
  50. package/build/src/PageContainer/PageContainer.test.js +127 -0
  51. package/build/src/PageContainer/PageContainerToolbar.d.ts +16 -0
  52. package/build/src/PageContainer/PageContainerToolbar.js +24 -0
  53. package/build/src/PageContainer/PageContainerToolbar.test.d.ts +1 -0
  54. package/build/src/PageContainer/PageContainerToolbar.test.js +10 -0
  55. package/build/src/PageContainer/index.d.ts +2 -0
  56. package/build/src/PageContainer/index.js +2 -0
  57. package/build/src/index.d.ts +8 -0
  58. package/build/src/index.js +8 -0
  59. package/build/src/nextjs/AppProvider.d.ts +6 -0
  60. package/build/src/nextjs/AppProvider.js +16 -0
  61. package/build/src/nextjs/AppProvider.test.d.ts +1 -0
  62. package/build/src/nextjs/AppProvider.test.js +36 -0
  63. package/build/src/nextjs/AppProviderNextApp.d.ts +5 -0
  64. package/build/src/nextjs/AppProviderNextApp.js +27 -0
  65. package/build/src/nextjs/AppProviderNextPages.d.ts +5 -0
  66. package/build/src/nextjs/AppProviderNextPages.js +37 -0
  67. package/build/src/nextjs/index.d.ts +1 -0
  68. package/build/src/nextjs/index.js +1 -0
  69. package/build/src/persistence/codec.d.ts +47 -0
  70. package/build/src/persistence/codec.js +57 -0
  71. package/build/src/persistence/index.d.ts +1 -0
  72. package/build/src/persistence/index.js +1 -0
  73. package/build/src/persistence/useStorageState.d.ts +50 -0
  74. package/build/src/persistence/useStorageState.js +116 -0
  75. package/build/src/persistence/useStorageState.test.d.ts +1 -0
  76. package/build/src/persistence/useStorageState.test.js +61 -0
  77. package/build/src/react-router-dom/AppProvider.d.ts +6 -0
  78. package/build/src/react-router-dom/AppProvider.js +29 -0
  79. package/build/src/react-router-dom/AppProvider.test.d.ts +1 -0
  80. package/build/src/react-router-dom/AppProvider.test.js +12 -0
  81. package/build/src/react-router-dom/index.d.ts +1 -0
  82. package/build/src/react-router-dom/index.js +1 -0
  83. package/build/src/shared/Link.d.ts +8 -0
  84. package/build/src/shared/Link.js +19 -0
  85. package/build/src/shared/branding.d.ts +1 -0
  86. package/build/src/shared/branding.js +6 -0
  87. package/build/src/shared/components.d.ts +5 -0
  88. package/build/src/shared/components.js +23 -0
  89. package/build/src/shared/context.d.ts +12 -0
  90. package/build/src/shared/context.js +10 -0
  91. package/build/src/shared/locales/LocaleContext.d.ts +19 -0
  92. package/build/src/shared/locales/LocaleContext.js +18 -0
  93. package/build/src/shared/locales/en.d.ts +6 -0
  94. package/build/src/shared/locales/en.js +8 -0
  95. package/build/src/shared/navigation.d.ts +16 -0
  96. package/build/src/shared/navigation.js +129 -0
  97. package/build/src/useActivePage/index.d.ts +1 -0
  98. package/build/src/useActivePage/index.js +1 -0
  99. package/build/src/useActivePage/useActivePage.d.ts +11 -0
  100. package/build/src/useActivePage/useActivePage.js +48 -0
  101. package/build/src/useLocalStorageState/index.d.ts +1 -0
  102. package/build/src/useLocalStorageState/index.js +1 -0
  103. package/build/src/useLocalStorageState/useLocalStorageState.d.ts +2 -0
  104. package/build/src/useLocalStorageState/useLocalStorageState.js +14 -0
  105. package/build/src/useSessionStorageState/index.d.ts +1 -0
  106. package/build/src/useSessionStorageState/index.js +1 -0
  107. package/build/src/useSessionStorageState/useSessionStorageState.d.ts +2 -0
  108. package/build/src/useSessionStorageState/useSessionStorageState.js +14 -0
  109. package/build/src/utils/collections.d.ts +44 -0
  110. package/build/src/utils/collections.js +43 -0
  111. package/build/src/utils/describeConformance.d.ts +7 -0
  112. package/build/src/utils/describeConformance.js +3 -0
  113. package/build/src/utils/events.d.ts +27 -0
  114. package/build/src/utils/events.js +54 -0
  115. package/build/src/utils/hooks/index.d.ts +2 -0
  116. package/build/src/utils/hooks/index.js +2 -0
  117. package/build/src/utils/hooks/useBoolean.d.ts +11 -0
  118. package/build/src/utils/hooks/useBoolean.js +11 -0
  119. package/build/src/utils/hooks/useDebounced.d.ts +8 -0
  120. package/build/src/utils/hooks/useDebounced.js +28 -0
  121. package/build/src/utils/hooks/useDebouncedHandler.d.ts +12 -0
  122. package/build/src/utils/hooks/useDebouncedHandler.js +41 -0
  123. package/build/src/utils/hooks/useLatest.d.ts +6 -0
  124. package/build/src/utils/hooks/useLatest.js +9 -0
  125. package/build/src/utils/hooks/usePageTitle.d.ts +4 -0
  126. package/build/src/utils/hooks/usePageTitle.js +13 -0
  127. package/build/src/utils/hooks/useSsr.d.ts +4 -0
  128. package/build/src/utils/hooks/useSsr.js +16 -0
  129. package/build/src/utils/hooks/useStorageState.d.ts +10 -0
  130. package/build/src/utils/hooks/useStorageState.js +51 -0
  131. package/build/src/utils/warnOnce.d.ts +5 -0
  132. package/build/src/utils/warnOnce.js +11 -0
  133. package/build/vitest.setup.d.ts +1 -0
  134. package/build/vitest.setup.js +19 -0
  135. package/package.json +60 -0
  136. package/src/Account/Account.test.tsx +56 -0
  137. package/src/Account/Account.tsx +226 -0
  138. package/src/Account/AccountPopoverFooter.tsx +33 -0
  139. package/src/Account/AccountPopoverHeader.tsx +20 -0
  140. package/src/Account/AccountPreview.test.tsx +79 -0
  141. package/src/Account/AccountPreview.tsx +189 -0
  142. package/src/Account/SignInButton.tsx +38 -0
  143. package/src/Account/SignOutButton.tsx +44 -0
  144. package/src/Account/index.ts +6 -0
  145. package/src/AppProvider/AppProvider.test.tsx +25 -0
  146. package/src/AppProvider/AppProvider.tsx +116 -0
  147. package/src/AppProvider/AppProviderComponent.tsx +162 -0
  148. package/src/AppProvider/AppThemeProvider.tsx +177 -0
  149. package/src/AppProvider/index.ts +1 -0
  150. package/src/DashboardLayout/DashboardLayout.test.tsx +429 -0
  151. package/src/DashboardLayout/DashboardLayout.tsx +558 -0
  152. package/src/DashboardLayout/DashboardSidebarSubNavigation.tsx +301 -0
  153. package/src/DashboardLayout/ThemeSwitcher.tsx +81 -0
  154. package/src/DashboardLayout/ToolbarActions.tsx +11 -0
  155. package/src/DashboardLayout/index.ts +4 -0
  156. package/src/DashboardLayout/utils.ts +23 -0
  157. package/src/PageContainer/PageContainer.test.tsx +194 -0
  158. package/src/PageContainer/PageContainer.tsx +201 -0
  159. package/src/PageContainer/PageContainerToolbar.test.tsx +11 -0
  160. package/src/PageContainer/PageContainerToolbar.tsx +31 -0
  161. package/src/PageContainer/index.ts +2 -0
  162. package/src/index.ts +15 -0
  163. package/src/nextjs/AppProvider.test.tsx +48 -0
  164. package/src/nextjs/AppProvider.tsx +18 -0
  165. package/src/nextjs/AppProviderNextApp.tsx +37 -0
  166. package/src/nextjs/AppProviderNextPages.tsx +52 -0
  167. package/src/nextjs/index.tsx +1 -0
  168. package/src/persistence/codec.tsx +81 -0
  169. package/src/persistence/index.ts +1 -0
  170. package/src/persistence/useStorageState.test.tsx +101 -0
  171. package/src/persistence/useStorageState.tsx +244 -0
  172. package/src/react-router-dom/AppProvider.test.tsx +17 -0
  173. package/src/react-router-dom/AppProvider.tsx +40 -0
  174. package/src/react-router-dom/index.tsx +1 -0
  175. package/src/shared/Link.tsx +37 -0
  176. package/src/shared/branding.ts +7 -0
  177. package/src/shared/components.tsx +46 -0
  178. package/src/shared/context.ts +21 -0
  179. package/src/shared/locales/LocaleContext.tsx +44 -0
  180. package/src/shared/locales/en.tsx +9 -0
  181. package/src/shared/navigation.tsx +197 -0
  182. package/src/useActivePage/index.ts +1 -0
  183. package/src/useActivePage/useActivePage.ts +66 -0
  184. package/src/useLocalStorageState/index.ts +1 -0
  185. package/src/useLocalStorageState/useLocalStorageState.tsx +23 -0
  186. package/src/useSessionStorageState/index.tsx +1 -0
  187. package/src/useSessionStorageState/useSessionStorageState.tsx +23 -0
  188. package/src/utils/collections.ts +125 -0
  189. package/src/utils/describeConformance.ts +14 -0
  190. package/src/utils/events.ts +81 -0
  191. package/src/utils/hooks/index.ts +2 -0
  192. package/src/utils/hooks/useBoolean.ts +12 -0
  193. package/src/utils/hooks/useDebounced.ts +39 -0
  194. package/src/utils/hooks/useDebouncedHandler.ts +71 -0
  195. package/src/utils/hooks/useLatest.ts +16 -0
  196. package/src/utils/hooks/usePageTitle.ts +14 -0
  197. package/src/utils/hooks/useSsr.ts +24 -0
  198. package/src/utils/hooks/useStorageState.ts +130 -0
  199. package/src/utils/warnOnce.ts +12 -0
  200. package/tsconfig.json +19 -0
  201. package/vite.config.ts +27 -0
  202. package/vitest.setup.ts +22 -0
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Returns true when serverside rendering, or when hydrating.
3
+ */
4
+ export default function useSsr(): boolean;
@@ -0,0 +1,16 @@
1
+ import * as React from "react";
2
+ function subscribe() {
3
+ return () => { };
4
+ }
5
+ function getSnapshot() {
6
+ return false;
7
+ }
8
+ function getServerSnapshot() {
9
+ return true;
10
+ }
11
+ /**
12
+ * Returns true when serverside rendering, or when hydrating.
13
+ */
14
+ export default function useSsr() {
15
+ return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
16
+ }
@@ -0,0 +1,10 @@
1
+ import * as React from "react";
2
+ type Initializer<T> = () => T;
3
+ type UseStorageStateHookResult<T> = [
4
+ T,
5
+ React.Dispatch<React.SetStateAction<T>>
6
+ ];
7
+ declare function useStorageStateServer(kind: "session" | "local", key: string, initializer: string | Initializer<string>): UseStorageStateHookResult<string>;
8
+ declare function useStorageStateServer(kind: "session" | "local", key: string, initializer?: string | null | Initializer<string | null>): UseStorageStateHookResult<string | null>;
9
+ declare const _default: typeof useStorageStateServer;
10
+ export default _default;
@@ -0,0 +1,51 @@
1
+ import * as React from "react";
2
+ import { Emitter } from "../events";
3
+ // storage events only work across windows, we'll use an event emitter to announce within the window
4
+ const emitter = new Emitter();
5
+ function subscribe(area, key, cb) {
6
+ const storageHandler = (event) => {
7
+ if (event.storageArea === area && event.key === key) {
8
+ cb();
9
+ }
10
+ };
11
+ window.addEventListener("storage", storageHandler);
12
+ emitter.on(key, cb);
13
+ return () => {
14
+ window.removeEventListener("storage", storageHandler);
15
+ emitter.off(key, cb);
16
+ };
17
+ }
18
+ function getSnapshot(area, key) {
19
+ return area.getItem(key);
20
+ }
21
+ function setValue(area, key, value) {
22
+ if (typeof window !== "undefined") {
23
+ if (value === null) {
24
+ area.removeItem(key);
25
+ }
26
+ else {
27
+ area.setItem(key, String(value));
28
+ }
29
+ emitter.emit(key, null);
30
+ }
31
+ }
32
+ function useStorageStateServer(kind, key, initializer = null) {
33
+ const [initialValue] = React.useState(initializer);
34
+ return [initialValue, () => { }];
35
+ }
36
+ function useStorageStateBrowser(kind, key, initializer = null) {
37
+ const [initialValue] = React.useState(initializer);
38
+ const area = kind === "session" ? window.sessionStorage : window.localStorage;
39
+ const subscribeKey = React.useCallback((cb) => subscribe(area, key, cb), [area, key]);
40
+ const getKeySnapshot = React.useCallback(() => getSnapshot(area, key) ?? initialValue, [area, initialValue, key]);
41
+ const getKeyServerSnapshot = React.useCallback(() => initialValue, [initialValue]);
42
+ const storedValue = React.useSyncExternalStore(subscribeKey, getKeySnapshot, getKeyServerSnapshot);
43
+ const setStoredValue = React.useCallback((value) => {
44
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
45
+ setValue(area, key, valueToStore);
46
+ }, [area, key, storedValue]);
47
+ return [storedValue, setStoredValue];
48
+ }
49
+ export default typeof window === "undefined"
50
+ ? useStorageStateServer
51
+ : useStorageStateBrowser;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Warns only once for a specific message.
3
+ * @param msg The message to warn.
4
+ */
5
+ export default function warnOnce(msg: string): void;
@@ -0,0 +1,11 @@
1
+ const history = new Set();
2
+ /**
3
+ * Warns only once for a specific message.
4
+ * @param msg The message to warn.
5
+ */
6
+ export default function warnOnce(msg) {
7
+ if (!history.has(msg)) {
8
+ history.add(msg);
9
+ console.warn(msg);
10
+ }
11
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import { afterEach, vi } from "vitest";
2
+ import { cleanup } from "@testing-library/react";
3
+ afterEach(cleanup);
4
+ // Mocks
5
+ if (typeof window !== "undefined" && !window.matchMedia) {
6
+ Object.defineProperty(window, "matchMedia", {
7
+ writable: true,
8
+ value: vi.fn().mockImplementation((query) => ({
9
+ matches: false,
10
+ media: query,
11
+ addListener: vi.fn(), // deprecated
12
+ removeListener: vi.fn(), // deprecated
13
+ onchange: null,
14
+ addEventListener: vi.fn(),
15
+ removeEventListener: vi.fn(),
16
+ dispatchEvent: vi.fn()
17
+ }))
18
+ });
19
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@etsoo/toolpad",
3
+ "version": "1.0.0",
4
+ "author": "ETSOO",
5
+ "description": "Dashboard framework extention based on Toolpad Core",
6
+ "main": "build/index.js",
7
+ "type": "module",
8
+ "types": "build/index.d.ts",
9
+ "sideEffects": false,
10
+ "keywords": [
11
+ "react",
12
+ "mui",
13
+ "toolpad",
14
+ "dashboard",
15
+ "Etsoo"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/ETSOO/toolpad.git"
20
+ },
21
+ "license": "MIT",
22
+ "bugs": {
23
+ "url": "https://github.com/ETSOO/toolpad/issues"
24
+ },
25
+ "homepage": "https://github.com/ETSOO/toolpad#readme",
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "test": "vitest",
29
+ "testb": "vitest --browser.enabled"
30
+ },
31
+ "dependencies": {
32
+ "@babel/runtime": "^7.25.7",
33
+ "@emotion/css": "^11.13.4",
34
+ "@emotion/react": "^11.13.3",
35
+ "@emotion/styled": "^11.13.0",
36
+ "@mui/lab": "6.0.0-beta.12",
37
+ "@mui/utils": "6.1.4",
38
+ "@vitejs/plugin-react": "4.3.3",
39
+ "invariant": "2.2.4",
40
+ "path-to-regexp": "6.3.0",
41
+ "prop-types": "15.8.1",
42
+ "react": "^18.3.1"
43
+ },
44
+ "devDependencies": {
45
+ "@mui/icons-material": "6.1.4",
46
+ "@mui/material": "6.1.4",
47
+ "@testing-library/jest-dom": "^6.6.2",
48
+ "@testing-library/react": "^16.0.1",
49
+ "@types/invariant": "2.2.37",
50
+ "@types/prop-types": "15.7.13",
51
+ "@types/react": "18.3.11",
52
+ "@types/react-dom": "18.3.1",
53
+ "@vitest/browser": "2.1.2",
54
+ "next": "^14.2.15",
55
+ "next-router-mock": "^0.9.13",
56
+ "playwright": "^1.47.2",
57
+ "react-router-dom": "6.26.2",
58
+ "vitest": "2.1.2"
59
+ }
60
+ }
@@ -0,0 +1,56 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { Account } from "./Account";
5
+ import describeConformance from "../utils/describeConformance";
6
+ import { AppProvider } from "../AppProvider/AppProviderComponent";
7
+
8
+ describe("AppProvider", () => {
9
+ describeConformance(<Account />, () => ({
10
+ skip: ["themeDefaultProps"]
11
+ }));
12
+
13
+ test("renders nothing in button when no authentication", async () => {
14
+ render(<Account />);
15
+
16
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
17
+ });
18
+
19
+ test("renders log in button when no session", async () => {
20
+ const auth = { signIn: vi.fn(), signOut: vi.fn() };
21
+ render(
22
+ <AppProvider authentication={auth}>
23
+ <Account />
24
+ </AppProvider>
25
+ );
26
+
27
+ const loginButton = screen.getByRole("button", { name: "Sign In" });
28
+
29
+ await userEvent.click(loginButton);
30
+
31
+ expect(auth.signIn).toHaveBeenCalled();
32
+ });
33
+
34
+ test("renders content correctly when there is a session", async () => {
35
+ const auth = { signIn: vi.fn(), signOut: vi.fn() };
36
+ const session = { user: { name: "John Doe", email: "john@example.com" } };
37
+ render(
38
+ <AppProvider authentication={auth} session={session}>
39
+ <Account />
40
+ </AppProvider>
41
+ );
42
+
43
+ const userButton = screen.getByRole("button", { name: "Current User" });
44
+
45
+ await userEvent.click(userButton);
46
+
47
+ expect(screen.getByText("John Doe")).toBeInTheDocument();
48
+ expect(screen.getByText("john@example.com")).toBeInTheDocument();
49
+
50
+ const signOutButton = screen.getByRole("button", { name: "Sign Out" });
51
+
52
+ await userEvent.click(signOutButton);
53
+
54
+ expect(auth.signOut).toHaveBeenCalled();
55
+ });
56
+ });
@@ -0,0 +1,226 @@
1
+ import * as React from "react";
2
+ import PropTypes from "prop-types";
3
+ import Button from "@mui/material/Button";
4
+ import Popover from "@mui/material/Popover";
5
+ import Divider from "@mui/material/Divider";
6
+ import Stack from "@mui/material/Stack";
7
+ import { SignInButton } from "./SignInButton";
8
+ import { SignOutButton } from "./SignOutButton";
9
+ import { AccountPreview, AccountPreviewProps } from "./AccountPreview";
10
+ import { AccountPopoverHeader } from "./AccountPopoverHeader";
11
+ import { AccountPopoverFooter } from "./AccountPopoverFooter";
12
+ import {
13
+ SessionContext,
14
+ AuthenticationContext
15
+ } from "../AppProvider/AppProvider";
16
+ import { LocaleProvider, useLocaleText } from "../shared/locales/LocaleContext";
17
+
18
+ export interface AccountSlots {
19
+ /**
20
+ * The component used for the account preview
21
+ * @default AccountPreview
22
+ */
23
+ preview?: React.ElementType;
24
+ /**
25
+ * The component used for the account popover menu
26
+ * @default Popover
27
+ */
28
+ popover?: React.ElementType;
29
+ /**
30
+ * The component used for the content of account popover
31
+ * @default Stack
32
+ */
33
+ popoverContent?: React.ElementType;
34
+ /**
35
+ * The component used for the sign in button.
36
+ * @default Button
37
+ */
38
+ signInButton?: React.ElementType;
39
+ /**
40
+ * The component used for the sign out button.
41
+ * @default Button
42
+ */
43
+ signOutButton?: React.ElementType;
44
+ }
45
+
46
+ export interface AccountProps {
47
+ /**
48
+ * The components used for each slot inside.
49
+ */
50
+ slots?: AccountSlots;
51
+ /**
52
+ * The props used for each slot inside.
53
+ */
54
+ slotProps?: {
55
+ preview?: AccountPreviewProps;
56
+ popover?: React.ComponentProps<typeof Popover>;
57
+ popoverContent?: React.ComponentProps<typeof Stack>;
58
+ signInButton?: React.ComponentProps<typeof SignInButton>;
59
+ signOutButton?: React.ComponentProps<typeof Button>;
60
+ };
61
+ /**
62
+ * The labels for the account component.
63
+ */
64
+ localeText?: Partial<ReturnType<typeof useLocaleText>>;
65
+ }
66
+ /**
67
+ *
68
+ * Demos:
69
+ *
70
+ * - [Account](https://mui.com/toolpad/core/react-account/)
71
+ * - [Dashboard Layout](https://mui.com/toolpad/core/react-dashboard-layout/)
72
+ * - [Sign-in Page](https://mui.com/toolpad/core/react-sign-in-page/)
73
+ *
74
+ * API:
75
+ *
76
+ * - [Account API](https://mui.com/toolpad/core/api/account)
77
+ */
78
+ function Account(props: AccountProps) {
79
+ const { localeText } = props;
80
+ const { slots, slotProps } = props;
81
+ const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
82
+ const session = React.useContext(SessionContext);
83
+ const authentication = React.useContext(AuthenticationContext);
84
+ const open = Boolean(anchorEl);
85
+
86
+ const handleClick = (event: React.MouseEvent<HTMLElement>) => {
87
+ setAnchorEl(event.currentTarget);
88
+ };
89
+
90
+ const handleClose = () => {
91
+ setAnchorEl(null);
92
+ };
93
+
94
+ if (!authentication) {
95
+ return null;
96
+ }
97
+
98
+ if (!session?.user) {
99
+ return (
100
+ <LocaleProvider localeText={localeText}>
101
+ {slots?.signInButton ? (
102
+ <slots.signInButton onClick={authentication.signIn} />
103
+ ) : (
104
+ <SignInButton {...slotProps?.signInButton} />
105
+ )}
106
+ </LocaleProvider>
107
+ );
108
+ }
109
+
110
+ return (
111
+ <LocaleProvider localeText={localeText}>
112
+ {slots?.preview ? (
113
+ <slots.preview handleClick={handleClick} open={open} />
114
+ ) : (
115
+ <AccountPreview
116
+ variant="condensed"
117
+ handleClick={handleClick}
118
+ open={open}
119
+ {...slotProps?.preview}
120
+ />
121
+ )}
122
+ {slots?.popover ? (
123
+ <slots.popover {...slotProps?.popover} />
124
+ ) : (
125
+ <Popover
126
+ anchorEl={anchorEl}
127
+ id="account-menu"
128
+ open={open}
129
+ onClose={handleClose}
130
+ onClick={handleClose}
131
+ transformOrigin={{ horizontal: "right", vertical: "top" }}
132
+ anchorOrigin={{ horizontal: "right", vertical: "bottom" }}
133
+ {...slotProps?.popover}
134
+ slotProps={{
135
+ paper: {
136
+ elevation: 0,
137
+ sx: {
138
+ overflow: "visible",
139
+ filter: "drop-shadow(0px 2px 8px rgba(0,0,0,0.32))",
140
+ mt: 1,
141
+ "&::before": {
142
+ content: '""',
143
+ display: "block",
144
+ position: "absolute",
145
+ top: 0,
146
+ right: 14,
147
+ width: 10,
148
+ height: 10,
149
+ bgcolor: "background.paper",
150
+ transform: "translateY(-50%) rotate(45deg)",
151
+ zIndex: 0
152
+ }
153
+ }
154
+ },
155
+ ...slotProps?.popover?.slotProps
156
+ }}
157
+ >
158
+ {slots?.popoverContent ? (
159
+ <slots.popoverContent {...slotProps?.popoverContent} />
160
+ ) : (
161
+ <Stack direction="column" {...slotProps?.popoverContent}>
162
+ <AccountPopoverHeader>
163
+ <AccountPreview variant="expanded" />
164
+ </AccountPopoverHeader>
165
+ <Divider />
166
+ <AccountPopoverFooter>
167
+ <SignOutButton {...slotProps?.signOutButton} />
168
+ </AccountPopoverFooter>
169
+ </Stack>
170
+ )}
171
+ </Popover>
172
+ )}
173
+ </LocaleProvider>
174
+ );
175
+ }
176
+
177
+ Account.propTypes /* remove-proptypes */ = {
178
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
179
+ // │ These PropTypes are generated from the TypeScript type definitions. │
180
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
181
+ // └─────────────────────────────────────────────────────────────────────┘
182
+ /**
183
+ * The labels for the account component.
184
+ */
185
+ localeText: PropTypes.shape({
186
+ iconButtonAriaLabel: PropTypes.string,
187
+ signInLabel: PropTypes.string,
188
+ signOutLabel: PropTypes.string
189
+ }),
190
+ /**
191
+ * The props used for each slot inside.
192
+ */
193
+ slotProps: PropTypes.shape({
194
+ popover: PropTypes.object,
195
+ popoverContent: PropTypes.object,
196
+ preview: PropTypes.shape({
197
+ handleClick: PropTypes.func,
198
+ open: PropTypes.bool,
199
+ slotProps: PropTypes.shape({
200
+ avatar: PropTypes.object,
201
+ avatarIconButton: PropTypes.object,
202
+ moreIconButton: PropTypes.object
203
+ }),
204
+ slots: PropTypes.shape({
205
+ avatar: PropTypes.elementType,
206
+ avatarIconButton: PropTypes.elementType,
207
+ moreIconButton: PropTypes.elementType
208
+ }),
209
+ variant: PropTypes.oneOf(["condensed", "expanded"])
210
+ }),
211
+ signInButton: PropTypes.object,
212
+ signOutButton: PropTypes.object
213
+ }),
214
+ /**
215
+ * The components used for each slot inside.
216
+ */
217
+ slots: PropTypes.shape({
218
+ popover: PropTypes.elementType,
219
+ popoverContent: PropTypes.elementType,
220
+ preview: PropTypes.elementType,
221
+ signInButton: PropTypes.elementType,
222
+ signOutButton: PropTypes.elementType
223
+ })
224
+ } as any;
225
+
226
+ export { Account };
@@ -0,0 +1,33 @@
1
+ import * as React from "react";
2
+ import Box, { BoxProps } from "@mui/material/Box";
3
+
4
+ export interface AccountPopoverFooterProps extends BoxProps {
5
+ children?: React.ReactNode;
6
+ }
7
+
8
+ export /**
9
+ *
10
+ * Demos:
11
+ *
12
+ * - [Account](https://mui.com/toolpad/core/react-account/)
13
+ *
14
+ * API:
15
+ *
16
+ * - [AccountPopoverFooter API](https://mui.com/toolpad/core/api/account-popover-footer)
17
+ */ function AccountPopoverFooter(props: AccountPopoverFooterProps) {
18
+ const { children, ...rest } = props;
19
+ return (
20
+ <Box
21
+ {...rest}
22
+ sx={{
23
+ display: "flex",
24
+ flexDirection: "row",
25
+ p: 1,
26
+ justifyContent: "flex-end",
27
+ ...rest.sx
28
+ }}
29
+ >
30
+ {children}
31
+ </Box>
32
+ );
33
+ }
@@ -0,0 +1,20 @@
1
+ import * as React from "react";
2
+ import Stack, { StackProps } from "@mui/material/Stack";
3
+
4
+ export interface AccountPopoverHeaderProps extends StackProps {
5
+ children?: React.ReactNode;
6
+ }
7
+
8
+ export /**
9
+ *
10
+ * Demos:
11
+ *
12
+ * - [Account](https://mui.com/toolpad/core/react-account/)
13
+ *
14
+ * API:
15
+ *
16
+ * - [AccountPopoverHeader API](https://mui.com/toolpad/core/api/account-popover-header)
17
+ */ function AccountPopoverHeader(props: AccountPopoverHeaderProps) {
18
+ const { children, ...rest } = props;
19
+ return <Stack {...rest}>{children}</Stack>;
20
+ }
@@ -0,0 +1,79 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { AccountPreview } from "./AccountPreview";
5
+ import { AppProvider } from "../AppProvider/AppProviderComponent";
6
+
7
+ describe("AccountPreview", () => {
8
+ const auth = { signIn: vi.fn(), signOut: vi.fn() };
9
+ const session = {
10
+ user: {
11
+ name: "John Doe",
12
+ email: "john@example.com",
13
+ image: "https://example.com/avatar.jpg"
14
+ }
15
+ };
16
+
17
+ test("renders nothing when no session is provided", () => {
18
+ render(
19
+ <AppProvider authentication={auth}>
20
+ <AccountPreview />
21
+ </AppProvider>
22
+ );
23
+
24
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
25
+ });
26
+
27
+ test("displays condensed variant by default", () => {
28
+ render(
29
+ <AppProvider authentication={auth} session={session}>
30
+ <AccountPreview />
31
+ </AppProvider>
32
+ );
33
+
34
+ const avatar = screen.getByRole("img", { name: "John Doe" });
35
+ expect(avatar).toBeInTheDocument();
36
+ expect(screen.queryByText("John Doe")).not.toBeInTheDocument();
37
+ expect(avatar).toHaveAttribute("src", "https://example.com/avatar.jpg");
38
+ });
39
+
40
+ test("displays user name, email, and avatar in expanded variant", () => {
41
+ render(
42
+ <AppProvider authentication={auth} session={session}>
43
+ <AccountPreview variant="expanded" />
44
+ </AppProvider>
45
+ );
46
+
47
+ expect(screen.getByText("John Doe")).toBeInTheDocument();
48
+ expect(screen.getByText("john@example.com")).toBeInTheDocument();
49
+ expect(screen.getByRole("img", { name: "John Doe" })).toBeInTheDocument();
50
+ });
51
+
52
+ test("calls handleClick when more icon button is clicked in expanded variant", async () => {
53
+ const handleClick = vi.fn();
54
+ render(
55
+ <AppProvider authentication={auth} session={session}>
56
+ <AccountPreview variant="expanded" handleClick={handleClick} />
57
+ </AppProvider>
58
+ );
59
+
60
+ const moreButton = screen.getByRole("button");
61
+ await userEvent.click(moreButton);
62
+
63
+ expect(handleClick).toHaveBeenCalled();
64
+ });
65
+
66
+ test("calls handleClick when avatar is clicked in condensed variant", async () => {
67
+ const handleClick = vi.fn();
68
+ render(
69
+ <AppProvider authentication={auth} session={session}>
70
+ <AccountPreview handleClick={handleClick} />
71
+ </AppProvider>
72
+ );
73
+
74
+ const avatarButton = screen.getByRole("button", { name: "Current User" });
75
+ await userEvent.click(avatarButton);
76
+
77
+ expect(handleClick).toHaveBeenCalled();
78
+ });
79
+ });