@forge/react 11.17.0 → 11.18.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/out/components/global/index.d.ts +1 -1
- package/out/components/global/index.d.ts.map +1 -1
- package/out/components/global/index.js +2 -2
- package/out/hooks/__test__/useTheme.test.js +34 -10
- package/out/hooks/useTheme.d.ts.map +1 -1
- package/out/hooks/useTheme.js +42 -12
- package/out/router/components/ParamsContext.d.ts +3 -0
- package/out/router/components/ParamsContext.d.ts.map +1 -0
- package/out/router/components/ParamsContext.js +5 -0
- package/out/router/components/Route.d.ts +20 -0
- package/out/router/components/Route.d.ts.map +1 -0
- package/out/router/components/Route.js +30 -0
- package/out/router/components/Router.d.ts +19 -0
- package/out/router/components/Router.d.ts.map +1 -0
- package/out/router/components/Router.js +58 -0
- package/out/router/components/RouterContext.d.ts +9 -0
- package/out/router/components/RouterContext.d.ts.map +1 -0
- package/out/router/components/RouterContext.js +5 -0
- package/out/router/components/__test__/Router.test.d.ts +2 -0
- package/out/router/components/__test__/Router.test.d.ts.map +1 -0
- package/out/router/components/__test__/Router.test.js +77 -0
- package/out/router/components/index.d.ts +3 -0
- package/out/router/components/index.d.ts.map +1 -0
- package/out/router/components/index.js +7 -0
- package/out/router/hooks/__test__/useLocation.test.d.ts +2 -0
- package/out/router/hooks/__test__/useLocation.test.d.ts.map +1 -0
- package/out/router/hooks/__test__/useLocation.test.js +59 -0
- package/out/router/hooks/__test__/useNavigate.test.d.ts +2 -0
- package/out/router/hooks/__test__/useNavigate.test.d.ts.map +1 -0
- package/out/router/hooks/__test__/useNavigate.test.js +159 -0
- package/out/router/hooks/__test__/useParams.test.d.ts +2 -0
- package/out/router/hooks/__test__/useParams.test.d.ts.map +1 -0
- package/out/router/hooks/__test__/useParams.test.js +69 -0
- package/out/router/hooks/useLocation.d.ts +9 -0
- package/out/router/hooks/useLocation.d.ts.map +1 -0
- package/out/router/hooks/useLocation.js +19 -0
- package/out/router/hooks/useNavigate.d.ts +11 -0
- package/out/router/hooks/useNavigate.d.ts.map +1 -0
- package/out/router/hooks/useNavigate.js +50 -0
- package/out/router/hooks/useParams.d.ts +8 -0
- package/out/router/hooks/useParams.d.ts.map +1 -0
- package/out/router/hooks/useParams.js +19 -0
- package/out/router/index.d.ts +5 -0
- package/out/router/index.d.ts.map +1 -0
- package/out/router/index.js +12 -0
- package/out/router/utils/__test__/matchPath.test.d.ts +2 -0
- package/out/router/utils/__test__/matchPath.test.d.ts.map +1 -0
- package/out/router/utils/__test__/matchPath.test.js +56 -0
- package/out/router/utils/matchPath.d.ts +5 -0
- package/out/router/utils/matchPath.d.ts.map +1 -0
- package/out/router/utils/matchPath.js +34 -0
- package/out/router/utils/test-utils.d.ts +10 -0
- package/out/router/utils/test-utils.d.ts.map +1 -0
- package/out/router/utils/test-utils.js +23 -0
- package/package.json +7 -2
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @forge/react
|
|
2
2
|
|
|
3
|
+
## 11.18.0-next.1
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8dc5a0c: Add routing components and hooks for use in full-page apps.
|
|
8
|
+
|
|
9
|
+
Added the following:
|
|
10
|
+
|
|
11
|
+
- `Router`
|
|
12
|
+
- `Route`
|
|
13
|
+
- `useNavigate`
|
|
14
|
+
- `useLocation`
|
|
15
|
+
- `useParams`
|
|
16
|
+
|
|
17
|
+
## 11.18.0-next.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- 01550cf: Fix flaky unit test
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 7504dd5: Fix naming on PersonalSettingsItem
|
|
26
|
+
- Updated dependencies [b0b69a2]
|
|
27
|
+
- Updated dependencies [561f8f4]
|
|
28
|
+
- @forge/bridge@5.18.0-next.0
|
|
29
|
+
|
|
3
30
|
## 11.17.0
|
|
4
31
|
|
|
5
32
|
### Minor Changes
|
|
@@ -10,6 +10,6 @@ export declare const CreateButton: TCreateButton<ForgeElement<Record<string, any
|
|
|
10
10
|
export declare const CreateMenuItem: TCreateMenuItem<ForgeElement<Record<string, any>>>;
|
|
11
11
|
export declare const HelpLink: THelpLink<ForgeElement<Record<string, any>>>;
|
|
12
12
|
export declare const PersonalSettings: TPersonalSettings<ForgeElement<Record<string, any>>>;
|
|
13
|
-
export declare const
|
|
13
|
+
export declare const PersonalSettingsItem: TPersonalSettingsItem<ForgeElement<Record<string, any>>>;
|
|
14
14
|
export type { GlobalProps, MainProps, SidebarProps, LinkMenuItemProps, ExpandableMenuItemProps, FlyOutMenuItemProps, CreateButtonProps, CreateMenuItemProps, HelpLinkProps, PersonalSettingsProps, PersonalSettingsItemProps } from '@atlaskit/forge-react-types/global';
|
|
15
15
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/global/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,QAAQ,EACR,KAAK,EACL,OAAO,EACP,aAAa,EACb,eAAe,EACf,SAAS,EACT,iBAAiB,EACjB,qBAAqB,EACtB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,MAAM,4CAA+C,CAAC;AACnE,eAAO,MAAM,IAAI,0CAA2C,CAAC;AAC7D,eAAO,MAAM,OAAO,6CAAiD,CAAC;AACtE,eAAO,MAAM,YAAY,kDAA2D,CAAC;AACrF,eAAO,MAAM,kBAAkB,wDAAuE,CAAC;AACvG,eAAO,MAAM,cAAc,oDAA+D,CAAC;AAC3F,eAAO,MAAM,YAAY,kDAA2D,CAAC;AACrF,eAAO,MAAM,cAAc,oDAA+D,CAAC;AAC3F,eAAO,MAAM,QAAQ,8CAAmD,CAAC;AACzE,eAAO,MAAM,gBAAgB,sDAAmE,CAAC;AACjG,eAAO,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/global/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,QAAQ,EACR,KAAK,EACL,OAAO,EACP,aAAa,EACb,eAAe,EACf,SAAS,EACT,iBAAiB,EACjB,qBAAqB,EACtB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,MAAM,4CAA+C,CAAC;AACnE,eAAO,MAAM,IAAI,0CAA2C,CAAC;AAC7D,eAAO,MAAM,OAAO,6CAAiD,CAAC;AACtE,eAAO,MAAM,YAAY,kDAA2D,CAAC;AACrF,eAAO,MAAM,kBAAkB,wDAAuE,CAAC;AACvG,eAAO,MAAM,cAAc,oDAA+D,CAAC;AAC3F,eAAO,MAAM,YAAY,kDAA2D,CAAC;AACrF,eAAO,MAAM,cAAc,oDAA+D,CAAC;AAC3F,eAAO,MAAM,QAAQ,8CAAmD,CAAC;AACzE,eAAO,MAAM,gBAAgB,sDAAmE,CAAC;AACjG,eAAO,MAAM,oBAAoB,0DAA2E,CAAC;AAG7G,YAAY,EACV,WAAW,EACX,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACvB,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,yBAAyB,EAC1B,MAAM,oCAAoC,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.PersonalSettingsItem = exports.PersonalSettings = exports.HelpLink = exports.CreateMenuItem = exports.CreateButton = exports.FlyOutMenuItem = exports.ExpandableMenuItem = exports.LinkMenuItem = exports.Sidebar = exports.Main = exports.Global = void 0;
|
|
4
4
|
exports.Global = 'Global';
|
|
5
5
|
exports.Main = 'Main';
|
|
6
6
|
exports.Sidebar = 'Sidebar';
|
|
@@ -11,4 +11,4 @@ exports.CreateButton = 'CreateButton';
|
|
|
11
11
|
exports.CreateMenuItem = 'CreateMenuItem';
|
|
12
12
|
exports.HelpLink = 'HelpLink';
|
|
13
13
|
exports.PersonalSettings = 'PersonalSettings';
|
|
14
|
-
exports.
|
|
14
|
+
exports.PersonalSettingsItem = 'PersonalSettingsItem';
|
|
@@ -26,14 +26,28 @@ const MOCK_CONTEXT_NO_THEME = {
|
|
|
26
26
|
extension: {}
|
|
27
27
|
};
|
|
28
28
|
const themeListener = jest.fn();
|
|
29
|
+
const flushUpdates = () => reconcilerTestRenderer_1.default.act(async () => {
|
|
30
|
+
await Promise.resolve();
|
|
31
|
+
await (0, utils_1.sleep)();
|
|
32
|
+
});
|
|
33
|
+
const emitThemeChanged = async (theme) => {
|
|
34
|
+
await reconcilerTestRenderer_1.default.act(async () => {
|
|
35
|
+
utils_1.simpleBridgeEvents.emit('FORGE_CORE_THEME_CHANGED', { theme });
|
|
36
|
+
await (0, utils_1.sleep)();
|
|
37
|
+
});
|
|
38
|
+
};
|
|
29
39
|
// react app fragment to load useTheme hook
|
|
30
|
-
const renderTest = async () => {
|
|
40
|
+
const renderTest = async (options) => {
|
|
41
|
+
const flushAfterMount = options?.flushAfterMount ?? true;
|
|
31
42
|
const Test = () => {
|
|
32
43
|
const theme = (0, useTheme_1.useTheme)();
|
|
33
44
|
(0, react_1.useEffect)(() => themeListener(theme), [theme]);
|
|
34
45
|
return (0, jsx_runtime_1.jsx)(react_1.default.Fragment, {});
|
|
35
46
|
};
|
|
36
47
|
const { update } = await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(Test, {}));
|
|
48
|
+
if (flushAfterMount) {
|
|
49
|
+
await flushUpdates();
|
|
50
|
+
}
|
|
37
51
|
return {
|
|
38
52
|
update: async () => {
|
|
39
53
|
await update((0, jsx_runtime_1.jsx)(Test, {}));
|
|
@@ -62,17 +76,19 @@ describe('useTheme', () => {
|
|
|
62
76
|
await renderTest();
|
|
63
77
|
expect(themeListener).toHaveBeenCalledWith(expect.objectContaining(MOCK_THEME));
|
|
64
78
|
const newTheme = { colorMode: 'light', colorScheme: 'red' };
|
|
65
|
-
|
|
66
|
-
await (0, utils_1.sleep)();
|
|
79
|
+
await emitThemeChanged(newTheme);
|
|
67
80
|
expect(themeListener).toHaveBeenCalledWith(expect.objectContaining(newTheme));
|
|
68
81
|
});
|
|
69
82
|
it('does not cause re-render when theme content is the same', async () => {
|
|
70
83
|
mockGetContext.mockResolvedValue(MOCK_CONTEXT_WITH_THEME);
|
|
71
84
|
await renderTest();
|
|
72
|
-
expect(themeListener).toHaveBeenCalledTimes(2); //
|
|
85
|
+
expect(themeListener).toHaveBeenCalledTimes(2); // null, then theme
|
|
73
86
|
// Emit same theme content but different object reference
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
await reconcilerTestRenderer_1.default.act(async () => {
|
|
88
|
+
utils_1.simpleBridgeEvents.emit('FORGE_CORE_THEME_CHANGED', {
|
|
89
|
+
theme: { ...MOCK_THEME } // New object, same content
|
|
90
|
+
});
|
|
91
|
+
await (0, utils_1.sleep)();
|
|
76
92
|
});
|
|
77
93
|
// Should not trigger re-render due to isEqual check
|
|
78
94
|
expect(themeListener).toHaveBeenCalledTimes(2);
|
|
@@ -84,13 +100,21 @@ describe('useTheme', () => {
|
|
|
84
100
|
resolveGetContext = resolve;
|
|
85
101
|
});
|
|
86
102
|
mockGetContext.mockReturnValue(getContextPromise);
|
|
87
|
-
const renderPromise = renderTest();
|
|
103
|
+
const renderPromise = renderTest({ flushAfterMount: false });
|
|
104
|
+
await renderPromise;
|
|
88
105
|
// Fire event before getContext resolves
|
|
89
106
|
const eventTheme = { colorMode: 'light', colorScheme: 'green' };
|
|
90
|
-
|
|
107
|
+
await reconcilerTestRenderer_1.default.act(async () => {
|
|
108
|
+
utils_1.simpleBridgeEvents.emit('FORGE_CORE_THEME_CHANGED', { theme: eventTheme });
|
|
109
|
+
await (0, utils_1.sleep)();
|
|
110
|
+
});
|
|
91
111
|
// Now resolve getContext
|
|
92
|
-
|
|
93
|
-
|
|
112
|
+
await reconcilerTestRenderer_1.default.act(async () => {
|
|
113
|
+
resolveGetContext?.(MOCK_CONTEXT_WITH_THEME);
|
|
114
|
+
await getContextPromise;
|
|
115
|
+
await (0, utils_1.sleep)();
|
|
116
|
+
});
|
|
117
|
+
await flushUpdates();
|
|
94
118
|
// Should have the event theme, not the getContext theme
|
|
95
119
|
expect(themeListener).toHaveBeenCalledWith(expect.objectContaining(eventTheme));
|
|
96
120
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useTheme.d.ts","sourceRoot":"","sources":["../../src/hooks/useTheme.ts"],"names":[],"mappings":"AACA,OAAO,EAAU,KAAK,WAAW,EAAQ,MAAM,eAAe,CAAC;AAG/D,aAAK,KAAK,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;AAElC,eAAO,MAAM,QAAQ,QAAO,KAAK,GAAG,
|
|
1
|
+
{"version":3,"file":"useTheme.d.ts","sourceRoot":"","sources":["../../src/hooks/useTheme.ts"],"names":[],"mappings":"AACA,OAAO,EAAU,KAAK,WAAW,EAAQ,MAAM,eAAe,CAAC;AAG/D,aAAK,KAAK,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;AAElC,eAAO,MAAM,QAAQ,QAAO,KAAK,GAAG,IAkEnC,CAAC"}
|
package/out/hooks/useTheme.js
CHANGED
|
@@ -7,31 +7,61 @@ const bridge_1 = require("@forge/bridge");
|
|
|
7
7
|
const isEqual_1 = tslib_1.__importDefault(require("lodash/isEqual"));
|
|
8
8
|
const useTheme = () => {
|
|
9
9
|
const [theme, setTheme] = (0, react_1.useState)(null);
|
|
10
|
+
const themeRef = (0, react_1.useRef)(null);
|
|
10
11
|
const themeLoadedRef = (0, react_1.useRef)(false);
|
|
12
|
+
const loadingContextRef = (0, react_1.useRef)(false);
|
|
13
|
+
const applyTheme = (0, react_1.useCallback)((nextTheme) => {
|
|
14
|
+
if ((0, isEqual_1.default)(themeRef.current, nextTheme)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
themeRef.current = nextTheme;
|
|
18
|
+
setTheme((current) => ((0, isEqual_1.default)(current, nextTheme) ? current : nextTheme));
|
|
19
|
+
}, []);
|
|
11
20
|
(0, react_1.useEffect)(() => {
|
|
21
|
+
let cancelled = false;
|
|
12
22
|
void (async () => {
|
|
13
|
-
if (
|
|
23
|
+
if (themeLoadedRef.current || loadingContextRef.current) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
loadingContextRef.current = true;
|
|
27
|
+
try {
|
|
14
28
|
const context = await bridge_1.view.getContext();
|
|
15
|
-
if (context?.theme
|
|
16
|
-
|
|
17
|
-
return (0, isEqual_1.default)(currentTheme, context.theme) ? currentTheme : context.theme;
|
|
18
|
-
});
|
|
19
|
-
themeLoadedRef.current = true;
|
|
29
|
+
if (cancelled || !context?.theme || themeLoadedRef.current) {
|
|
30
|
+
return;
|
|
20
31
|
}
|
|
32
|
+
applyTheme(context.theme);
|
|
33
|
+
themeLoadedRef.current = true;
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
loadingContextRef.current = false;
|
|
21
37
|
}
|
|
22
38
|
})();
|
|
23
|
-
|
|
39
|
+
return () => {
|
|
40
|
+
cancelled = true;
|
|
41
|
+
};
|
|
42
|
+
}, [applyTheme]);
|
|
24
43
|
(0, react_1.useEffect)(() => {
|
|
44
|
+
let unsubscribeFn = null;
|
|
45
|
+
let cancelled = false;
|
|
25
46
|
const sub = bridge_1.events.on('FORGE_CORE_THEME_CHANGED', ({ theme: updatedTheme }) => {
|
|
26
47
|
themeLoadedRef.current = true;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
48
|
+
applyTheme(updatedTheme);
|
|
49
|
+
});
|
|
50
|
+
void sub.then((subscription) => {
|
|
51
|
+
if (cancelled) {
|
|
52
|
+
subscription.unsubscribe();
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
unsubscribeFn = () => subscription.unsubscribe();
|
|
56
|
+
}
|
|
30
57
|
});
|
|
31
58
|
return () => {
|
|
32
|
-
|
|
59
|
+
cancelled = true;
|
|
60
|
+
if (unsubscribeFn) {
|
|
61
|
+
unsubscribeFn();
|
|
62
|
+
}
|
|
33
63
|
};
|
|
34
|
-
}, []);
|
|
64
|
+
}, [applyTheme]);
|
|
35
65
|
return theme;
|
|
36
66
|
};
|
|
37
67
|
exports.useTheme = useTheme;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ParamsContext.d.ts","sourceRoot":"","sources":["../../../src/router/components/ParamsContext.ts"],"names":[],"mappings":";AAEA,eAAO,MAAM,aAAa,wDAAqD,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ForgeChildren } from '../../types';
|
|
2
|
+
export interface RouteProps {
|
|
3
|
+
/**
|
|
4
|
+
* The URL path pattern to match against the current location. Supports static segments, dynamic parameters (e.g. :id), and a catch-all wildcard (*).
|
|
5
|
+
* @type string
|
|
6
|
+
*/
|
|
7
|
+
path: string;
|
|
8
|
+
/**
|
|
9
|
+
* The content to render when the path matches the current location.
|
|
10
|
+
* @type ForgeElement
|
|
11
|
+
*/
|
|
12
|
+
children: ForgeChildren;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Route conditionally renders its children when the current URL path matches its path prop. It must be used within a Router component.
|
|
16
|
+
*
|
|
17
|
+
* @see [Route](https://developer.atlassian.com/platform/forge/ui-kit/components/router/#route) in UI Kit documentation for more information
|
|
18
|
+
*/
|
|
19
|
+
export declare const Route: ({ path, children }: RouteProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
20
|
+
//# sourceMappingURL=Route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Route.d.ts","sourceRoot":"","sources":["../../../src/router/components/Route.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,QAAQ,EAAE,aAAa,CAAC;CACzB;AAED;;;;GAIG;AACH,eAAO,MAAM,KAAK,uBAAwB,UAAU,mDAoBnD,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Route = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const RouterContext_1 = require("./RouterContext");
|
|
7
|
+
const ParamsContext_1 = require("./ParamsContext");
|
|
8
|
+
const matchPath_1 = require("../utils/matchPath");
|
|
9
|
+
/**
|
|
10
|
+
* Route conditionally renders its children when the current URL path matches its path prop. It must be used within a Router component.
|
|
11
|
+
*
|
|
12
|
+
* @see [Route](https://developer.atlassian.com/platform/forge/ui-kit/components/router/#route) in UI Kit documentation for more information
|
|
13
|
+
*/
|
|
14
|
+
const Route = ({ path, children }) => {
|
|
15
|
+
const context = (0, react_1.useContext)(RouterContext_1.RouterContext);
|
|
16
|
+
if (!context) {
|
|
17
|
+
throw new Error('Route must be used within a Router component');
|
|
18
|
+
}
|
|
19
|
+
const { location, hasMatchedRouteRef } = context;
|
|
20
|
+
if (hasMatchedRouteRef.current) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const match = (0, matchPath_1.matchPath)(path, location.pathname);
|
|
24
|
+
if (!match) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
hasMatchedRouteRef.current = true;
|
|
28
|
+
return (0, jsx_runtime_1.jsx)(ParamsContext_1.ParamsContext.Provider, { value: match.params, children: children });
|
|
29
|
+
};
|
|
30
|
+
exports.Route = Route;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ForgeChildren, ForgeNode } from '../../types';
|
|
2
|
+
export interface RouterProps {
|
|
3
|
+
/**
|
|
4
|
+
* @type Route
|
|
5
|
+
* The child elements to render within the router. Typically contains one or more Route components.
|
|
6
|
+
*/
|
|
7
|
+
children: ForgeChildren;
|
|
8
|
+
/**
|
|
9
|
+
* A fallback value to render when no Route child has a path matching the current path.
|
|
10
|
+
*/
|
|
11
|
+
fallback?: ForgeNode | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Router provides client-side routing by allowing content to be rendered based on the current URL path.
|
|
15
|
+
*
|
|
16
|
+
* @see [Router](https://developer.atlassian.com/platform/forge/ui-kit/components/router/) in UI Kit documentation for more information
|
|
17
|
+
*/
|
|
18
|
+
export declare const Router: ({ children, fallback }: RouterProps) => import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
//# sourceMappingURL=Router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Router.d.ts","sourceRoot":"","sources":["../../../src/router/components/Router.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAevD,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,QAAQ,EAAE,aAAa,CAAC;IACxB;;OAEG;IACH,QAAQ,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;CAC7B;AAED;;;;GAIG;AACH,eAAO,MAAM,MAAM,2BAAmC,WAAW,4CAiEhE,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Router = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const bridge_1 = require("@forge/bridge");
|
|
7
|
+
const RouterContext_1 = require("./RouterContext");
|
|
8
|
+
const components_1 = require("../../components");
|
|
9
|
+
const Fallback = ({ hasMatchedRouteRef, fallback }) => {
|
|
10
|
+
if (hasMatchedRouteRef.current) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: fallback });
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Router provides client-side routing by allowing content to be rendered based on the current URL path.
|
|
17
|
+
*
|
|
18
|
+
* @see [Router](https://developer.atlassian.com/platform/forge/ui-kit/components/router/) in UI Kit documentation for more information
|
|
19
|
+
*/
|
|
20
|
+
const Router = ({ children, fallback = null }) => {
|
|
21
|
+
const [history, setHistory] = (0, react_1.useState)(null);
|
|
22
|
+
const [location, setLocation] = (0, react_1.useState)(null);
|
|
23
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
24
|
+
const hasMatchedRouteRef = (0, react_1.useRef)(false);
|
|
25
|
+
hasMatchedRouteRef.current = false;
|
|
26
|
+
(0, react_1.useEffect)(() => {
|
|
27
|
+
let unlisten;
|
|
28
|
+
bridge_1.view
|
|
29
|
+
.createHistory()
|
|
30
|
+
.then((history) => {
|
|
31
|
+
setHistory(history);
|
|
32
|
+
setLocation(history.location);
|
|
33
|
+
unlisten = history.listen((location) => {
|
|
34
|
+
// @ts-ignore - The history object returned by the bridge does not conform to the v5 types.
|
|
35
|
+
// Instead it uses v4 types, so we need to ignore this type error.
|
|
36
|
+
setLocation(location);
|
|
37
|
+
});
|
|
38
|
+
})
|
|
39
|
+
.catch(setError);
|
|
40
|
+
return () => {
|
|
41
|
+
unlisten?.();
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
const routerContext = (0, react_1.useMemo)(() => {
|
|
45
|
+
if (!history || !location) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return { history, location, hasMatchedRouteRef };
|
|
49
|
+
}, [history, location]);
|
|
50
|
+
if (error) {
|
|
51
|
+
return ((0, jsx_runtime_1.jsx)(components_1.SectionMessage, { appearance: "error", children: (0, jsx_runtime_1.jsx)(components_1.Text, { children: error.message }) }));
|
|
52
|
+
}
|
|
53
|
+
if (!routerContext) {
|
|
54
|
+
return ((0, jsx_runtime_1.jsx)(components_1.SectionMessage, { appearance: "error", children: (0, jsx_runtime_1.jsxs)(components_1.Text, { children: ["History is not defined. Check the list of", ' ', (0, jsx_runtime_1.jsx)(components_1.Link, { href: "https://developer.atlassian.com/platform/forge/ui-kit/components/router/#supported-modules", children: "supported modules" }), ' ', "for Router"] }) }));
|
|
55
|
+
}
|
|
56
|
+
return ((0, jsx_runtime_1.jsxs)(RouterContext_1.RouterContext.Provider, { value: routerContext, children: [children, (0, jsx_runtime_1.jsx)(Fallback, { hasMatchedRouteRef: hasMatchedRouteRef, fallback: fallback })] }));
|
|
57
|
+
};
|
|
58
|
+
exports.Router = Router;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { MutableRefObject } from 'react';
|
|
2
|
+
import type { History, Location } from 'history';
|
|
3
|
+
export interface RouterContextValue {
|
|
4
|
+
history: History;
|
|
5
|
+
location: Location;
|
|
6
|
+
hasMatchedRouteRef: MutableRefObject<boolean>;
|
|
7
|
+
}
|
|
8
|
+
export declare const RouterContext: import("react").Context<RouterContextValue | null>;
|
|
9
|
+
//# sourceMappingURL=RouterContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RouterContext.d.ts","sourceRoot":"","sources":["../../../src/router/components/RouterContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,gBAAgB,EAAE,MAAM,OAAO,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEjD,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,kBAAkB,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;CAC/C;AAED,eAAO,MAAM,aAAa,oDAAiD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Router.test.d.ts","sourceRoot":"","sources":["../../../../src/router/components/__test__/Router.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const testUtils_1 = require("../../../__test__/testUtils");
|
|
6
|
+
const reconcilerTestRenderer_1 = tslib_1.__importDefault(require("../../../__test__/reconcilerTestRenderer"));
|
|
7
|
+
const Router_1 = require("../Router");
|
|
8
|
+
const Route_1 = require("../Route");
|
|
9
|
+
const __1 = require("../../..");
|
|
10
|
+
const test_utils_1 = require("../../utils/test-utils");
|
|
11
|
+
const mockCreateHistory = jest.fn(async () => (0, test_utils_1.createMockHistory)());
|
|
12
|
+
jest.mock('@forge/bridge', () => ({
|
|
13
|
+
view: {
|
|
14
|
+
createHistory: () => mockCreateHistory()
|
|
15
|
+
}
|
|
16
|
+
}));
|
|
17
|
+
describe('Router', () => {
|
|
18
|
+
let bridgeCalls;
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
bridgeCalls = (0, testUtils_1.setupBridge)();
|
|
21
|
+
});
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
bridgeCalls.length = 0;
|
|
24
|
+
mockCreateHistory.mockClear();
|
|
25
|
+
mockCreateHistory.mockImplementation(async () => (0, test_utils_1.createMockHistory)());
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => jest.clearAllMocks());
|
|
28
|
+
it('renders children after history is initialized', async () => {
|
|
29
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(Router_1.Router, { children: (0, jsx_runtime_1.jsx)(__1.Text, { children: "Home Page" }) }));
|
|
30
|
+
const forgeDoc = (0, testUtils_1.getLastBridgeCallForgeDoc)(bridgeCalls);
|
|
31
|
+
expect(forgeDoc).toHaveProperty('children[0].type', 'Text');
|
|
32
|
+
expect(forgeDoc).toHaveProperty('children[0].children[0].props.text', 'Home Page');
|
|
33
|
+
});
|
|
34
|
+
it('renders children when Route path matches', async () => {
|
|
35
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(Router_1.Router, { children: (0, jsx_runtime_1.jsx)(Route_1.Route, { path: "/", children: (0, jsx_runtime_1.jsx)(__1.Text, { children: "Home Page" }) }) }));
|
|
36
|
+
const forgeDoc = (0, testUtils_1.getLastBridgeCallForgeDoc)(bridgeCalls);
|
|
37
|
+
expect(forgeDoc).toHaveProperty('children[0].type', 'Text');
|
|
38
|
+
expect(forgeDoc).toHaveProperty('children[0].children[0].props.text', 'Home Page');
|
|
39
|
+
});
|
|
40
|
+
it('does not render Route children when path does not match', async () => {
|
|
41
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(Router_1.Router, { children: (0, jsx_runtime_1.jsx)(Route_1.Route, { path: "/settings", children: (0, jsx_runtime_1.jsx)(__1.Text, { children: "Settings Page" }) }) }));
|
|
42
|
+
const forgeDoc = (0, testUtils_1.getLastBridgeCallForgeDoc)(bridgeCalls);
|
|
43
|
+
expect(forgeDoc?.children).toHaveLength(0);
|
|
44
|
+
});
|
|
45
|
+
it('throws when Route is used outside of Router', async () => {
|
|
46
|
+
await expect(reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(Route_1.Route, { path: "/", children: (0, jsx_runtime_1.jsx)(__1.Text, { children: "Home" }) }))).rejects.toThrow('Route must be used within a Router component');
|
|
47
|
+
});
|
|
48
|
+
it('renders only the first matching Route when multiple routes match', async () => {
|
|
49
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsxs)(Router_1.Router, { children: [(0, jsx_runtime_1.jsx)(Route_1.Route, { path: "/", children: (0, jsx_runtime_1.jsx)(__1.Text, { children: "First" }) }), (0, jsx_runtime_1.jsx)(Route_1.Route, { path: "/", children: (0, jsx_runtime_1.jsx)(__1.Text, { children: "Second" }) })] }));
|
|
50
|
+
const forgeDoc = (0, testUtils_1.getLastBridgeCallForgeDoc)(bridgeCalls);
|
|
51
|
+
expect(forgeDoc).toHaveProperty('children[0].type', 'Text');
|
|
52
|
+
expect(forgeDoc).toHaveProperty('children[0].children[0].props.text', 'First');
|
|
53
|
+
expect(forgeDoc?.children).toHaveLength(1);
|
|
54
|
+
});
|
|
55
|
+
it('renders fallback when no Route matches', async () => {
|
|
56
|
+
mockCreateHistory.mockImplementation(async () => (0, test_utils_1.createMockHistory)('/unknown'));
|
|
57
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsxs)(Router_1.Router, { fallback: (0, jsx_runtime_1.jsx)(__1.Text, { children: "No match" }), children: [(0, jsx_runtime_1.jsx)(Route_1.Route, { path: "/", children: (0, jsx_runtime_1.jsx)(__1.Text, { children: "Home" }) }), (0, jsx_runtime_1.jsx)(Route_1.Route, { path: "/settings", children: (0, jsx_runtime_1.jsx)(__1.Text, { children: "Settings" }) })] }));
|
|
58
|
+
const forgeDoc = (0, testUtils_1.getLastBridgeCallForgeDoc)(bridgeCalls);
|
|
59
|
+
expect(forgeDoc).toHaveProperty('children[0].type', 'Text');
|
|
60
|
+
expect(forgeDoc).toHaveProperty('children[0].children[0].props.text', 'No match');
|
|
61
|
+
});
|
|
62
|
+
it('renders the catchall Route when no other route matches', async () => {
|
|
63
|
+
mockCreateHistory.mockImplementation(async () => (0, test_utils_1.createMockHistory)('/unknown'));
|
|
64
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsxs)(Router_1.Router, { children: [(0, jsx_runtime_1.jsx)(Route_1.Route, { path: "/", children: (0, jsx_runtime_1.jsx)(__1.Text, { children: "Home" }) }), (0, jsx_runtime_1.jsx)(Route_1.Route, { path: "*", children: (0, jsx_runtime_1.jsx)(__1.Text, { children: "Not Found" }) })] }));
|
|
65
|
+
const forgeDoc = (0, testUtils_1.getLastBridgeCallForgeDoc)(bridgeCalls);
|
|
66
|
+
expect(forgeDoc).toHaveProperty('children[0].type', 'Text');
|
|
67
|
+
expect(forgeDoc).toHaveProperty('children[0].children[0].props.text', 'Not Found');
|
|
68
|
+
expect(forgeDoc?.children).toHaveLength(1);
|
|
69
|
+
});
|
|
70
|
+
it('renders a specific Route over catchall when it matches', async () => {
|
|
71
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsxs)(Router_1.Router, { children: [(0, jsx_runtime_1.jsx)(Route_1.Route, { path: "/", children: (0, jsx_runtime_1.jsx)(__1.Text, { children: "Home" }) }), (0, jsx_runtime_1.jsx)(Route_1.Route, { path: "*", children: (0, jsx_runtime_1.jsx)(__1.Text, { children: "Not Found" }) })] }));
|
|
72
|
+
const forgeDoc = (0, testUtils_1.getLastBridgeCallForgeDoc)(bridgeCalls);
|
|
73
|
+
expect(forgeDoc).toHaveProperty('children[0].type', 'Text');
|
|
74
|
+
expect(forgeDoc).toHaveProperty('children[0].children[0].props.text', 'Home');
|
|
75
|
+
expect(forgeDoc?.children).toHaveLength(1);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/router/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Route = exports.Router = void 0;
|
|
4
|
+
var Router_1 = require("./Router");
|
|
5
|
+
Object.defineProperty(exports, "Router", { enumerable: true, get: function () { return Router_1.Router; } });
|
|
6
|
+
var Route_1 = require("./Route");
|
|
7
|
+
Object.defineProperty(exports, "Route", { enumerable: true, get: function () { return Route_1.Route; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLocation.test.d.ts","sourceRoot":"","sources":["../../../../src/router/hooks/__test__/useLocation.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const testUtils_1 = require("../../../__test__/testUtils");
|
|
7
|
+
const reconcilerTestRenderer_1 = tslib_1.__importDefault(require("../../../__test__/reconcilerTestRenderer"));
|
|
8
|
+
const Router_1 = require("../../components/Router");
|
|
9
|
+
const useNavigate_1 = require("../useNavigate");
|
|
10
|
+
const useLocation_1 = require("../useLocation");
|
|
11
|
+
const components_1 = require("../../../components");
|
|
12
|
+
const test_utils_1 = require("../../utils/test-utils");
|
|
13
|
+
const mockCreateHistory = jest.fn(async () => (0, test_utils_1.createMockHistory)());
|
|
14
|
+
jest.mock('@forge/bridge', () => ({
|
|
15
|
+
view: {
|
|
16
|
+
createHistory: () => mockCreateHistory()
|
|
17
|
+
}
|
|
18
|
+
}));
|
|
19
|
+
describe('useLocation', () => {
|
|
20
|
+
let bridgeCalls;
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
bridgeCalls = (0, testUtils_1.setupBridge)();
|
|
23
|
+
});
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
bridgeCalls.length = 0;
|
|
26
|
+
mockCreateHistory.mockClear();
|
|
27
|
+
mockCreateHistory.mockImplementation(async () => (0, test_utils_1.createMockHistory)());
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => jest.clearAllMocks());
|
|
30
|
+
it('returns the current location', async () => {
|
|
31
|
+
const locationListener = jest.fn();
|
|
32
|
+
const LocationTest = () => {
|
|
33
|
+
const location = (0, useLocation_1.useLocation)();
|
|
34
|
+
(0, react_1.useEffect)(() => locationListener(location), [location]);
|
|
35
|
+
return (0, jsx_runtime_1.jsx)(components_1.Text, { children: "Location Test" });
|
|
36
|
+
};
|
|
37
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(Router_1.Router, { children: (0, jsx_runtime_1.jsx)(LocationTest, {}) }));
|
|
38
|
+
expect(locationListener).toHaveBeenCalledWith(expect.objectContaining({
|
|
39
|
+
pathname: '/',
|
|
40
|
+
search: '',
|
|
41
|
+
hash: ''
|
|
42
|
+
}));
|
|
43
|
+
});
|
|
44
|
+
it('updates when navigation occurs', async () => {
|
|
45
|
+
const locationListener = jest.fn();
|
|
46
|
+
const LocationTest = () => {
|
|
47
|
+
const location = (0, useLocation_1.useLocation)();
|
|
48
|
+
const navigate = (0, useNavigate_1.useNavigate)();
|
|
49
|
+
(0, react_1.useEffect)(() => locationListener(location), [location]);
|
|
50
|
+
(0, react_1.useEffect)(() => {
|
|
51
|
+
navigate('/about');
|
|
52
|
+
}, [navigate]);
|
|
53
|
+
return (0, jsx_runtime_1.jsx)(components_1.Text, { children: "Location Test" });
|
|
54
|
+
};
|
|
55
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(Router_1.Router, { children: (0, jsx_runtime_1.jsx)(LocationTest, {}) }));
|
|
56
|
+
expect(locationListener).toHaveBeenCalledWith(expect.objectContaining({ pathname: '/' }));
|
|
57
|
+
expect(locationListener).toHaveBeenCalledWith(expect.objectContaining({ pathname: '/about' }));
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useNavigate.test.d.ts","sourceRoot":"","sources":["../../../../src/router/hooks/__test__/useNavigate.test.tsx"],"names":[],"mappings":""}
|