@okta/odyssey-react-mui 1.27.0 → 1.28.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 +23 -0
- package/dist/{Button.js → Buttons/BaseButton.js} +11 -10
- package/dist/Buttons/BaseButton.js.map +1 -0
- package/dist/{MenuButton.js → Buttons/BaseMenuButton.js} +30 -10
- package/dist/Buttons/BaseMenuButton.js.map +1 -0
- package/dist/Buttons/Button.js +24 -0
- package/dist/Buttons/Button.js.map +1 -0
- package/dist/Buttons/ButtonContext.js.map +1 -0
- package/dist/Buttons/MenuButton.js +25 -0
- package/dist/Buttons/MenuButton.js.map +1 -0
- package/dist/Buttons/MenuContext.js.map +1 -0
- package/dist/Buttons/MenuItem.js.map +1 -0
- package/dist/Buttons/index.js +18 -0
- package/dist/Buttons/index.js.map +1 -0
- package/dist/Card.js +1 -2
- package/dist/Card.js.map +1 -1
- package/dist/DataTable/DataTable.js +1 -2
- package/dist/DataTable/DataTable.js.map +1 -1
- package/dist/DataTable/DataTableRowActions.js +1 -2
- package/dist/DataTable/DataTableRowActions.js.map +1 -1
- package/dist/DataTable/DataTableSettings.js +1 -2
- package/dist/DataTable/DataTableSettings.js.map +1 -1
- package/dist/Dialog.js +1 -1
- package/dist/Dialog.js.map +1 -1
- package/dist/Drawer.js +1 -1
- package/dist/Drawer.js.map +1 -1
- package/dist/FileUploader/FileUploader.js +1 -1
- package/dist/FileUploader/FileUploader.js.map +1 -1
- package/dist/Form.js.map +1 -1
- package/dist/Pagination/Pagination.js +1 -1
- package/dist/Pagination/Pagination.js.map +1 -1
- package/dist/Toast.js +1 -1
- package/dist/Toast.js.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/index.scss +1 -1
- package/dist/labs/AppSwitcher/AppSwitcher.js +76 -0
- package/dist/labs/AppSwitcher/AppSwitcher.js.map +1 -0
- package/dist/labs/AppSwitcher/AppSwitcherApp.js +112 -0
- package/dist/labs/AppSwitcher/AppSwitcherApp.js.map +1 -0
- package/dist/labs/{SideNav → AppSwitcher}/OktaAura.js +16 -3
- package/dist/labs/AppSwitcher/OktaAura.js.map +1 -0
- package/dist/labs/AppSwitcher/index.js +13 -0
- package/dist/labs/AppSwitcher/index.js.map +1 -0
- package/dist/labs/AppTile.js +102 -65
- package/dist/labs/AppTile.js.map +1 -1
- package/dist/labs/DataFilters.js +1 -1
- package/dist/labs/DataFilters.js.map +1 -1
- package/dist/labs/DataTable.js.map +1 -1
- package/dist/labs/DataTablePagination.js +1 -1
- package/dist/labs/DataTablePagination.js.map +1 -1
- package/dist/labs/DataView/BulkActionsMenu.js +1 -2
- package/dist/labs/DataView/BulkActionsMenu.js.map +1 -1
- package/dist/labs/DataView/DataCard.js +53 -42
- package/dist/labs/DataView/DataCard.js.map +1 -1
- package/dist/labs/DataView/DataView.js +1 -1
- package/dist/labs/DataView/DataView.js.map +1 -1
- package/dist/labs/DataView/LayoutSwitcher.js +1 -2
- package/dist/labs/DataView/LayoutSwitcher.js.map +1 -1
- package/dist/labs/DataView/RowActions.js +1 -1
- package/dist/labs/DataView/RowActions.js.map +1 -1
- package/dist/labs/DataView/TableLayoutContent.js +1 -2
- package/dist/labs/DataView/TableLayoutContent.js.map +1 -1
- package/dist/labs/DataView/TableSettings.js +1 -2
- package/dist/labs/DataView/TableSettings.js.map +1 -1
- package/dist/labs/DatePicker.js +1 -1
- package/dist/labs/DatePicker.js.map +1 -1
- package/dist/labs/SideNav/SideNav.js +5 -4
- package/dist/labs/SideNav/SideNav.js.map +1 -1
- package/dist/labs/TopNav/UserProfile.js +16 -3
- package/dist/labs/TopNav/UserProfile.js.map +1 -1
- package/dist/labs/TopNav/UserProfileMenuButton.js +41 -0
- package/dist/labs/TopNav/UserProfileMenuButton.js.map +1 -0
- package/dist/labs/TopNav/index.js +1 -0
- package/dist/labs/TopNav/index.js.map +1 -1
- package/dist/labs/UiShell/UiShell.js +6 -5
- package/dist/labs/UiShell/UiShell.js.map +1 -1
- package/dist/labs/UiShell/UiShellContent.js +53 -13
- package/dist/labs/UiShell/UiShellContent.js.map +1 -1
- package/dist/labs/UiShell/renderUiShell.js +4 -0
- package/dist/labs/UiShell/renderUiShell.js.map +1 -1
- package/dist/labs/index.js +1 -0
- package/dist/labs/index.js.map +1 -1
- package/dist/src/{Button.d.ts → Buttons/BaseButton.d.ts} +12 -34
- package/dist/src/Buttons/BaseButton.d.ts.map +1 -0
- package/dist/src/{MenuButton.d.ts → Buttons/BaseMenuButton.d.ts} +37 -14
- package/dist/src/Buttons/BaseMenuButton.d.ts.map +1 -0
- package/dist/src/Buttons/Button.d.ts +16 -0
- package/dist/src/Buttons/Button.d.ts.map +1 -0
- package/dist/src/Buttons/ButtonContext.d.ts.map +1 -0
- package/dist/src/Buttons/MenuButton.d.ts +17 -0
- package/dist/src/Buttons/MenuButton.d.ts.map +1 -0
- package/dist/src/Buttons/MenuContext.d.ts.map +1 -0
- package/dist/src/{MenuItem.d.ts → Buttons/MenuItem.d.ts} +1 -1
- package/dist/src/Buttons/MenuItem.d.ts.map +1 -0
- package/dist/src/Buttons/index.d.ts +18 -0
- package/dist/src/Buttons/index.d.ts.map +1 -0
- package/dist/src/Card.d.ts +1 -2
- package/dist/src/Card.d.ts.map +1 -1
- package/dist/src/DataTable/DataTable.d.ts +1 -1
- package/dist/src/DataTable/DataTable.d.ts.map +1 -1
- package/dist/src/DataTable/DataTableRowActions.d.ts +1 -2
- package/dist/src/DataTable/DataTableRowActions.d.ts.map +1 -1
- package/dist/src/DataTable/DataTableSettings.d.ts.map +1 -1
- package/dist/src/Dialog.d.ts +1 -1
- package/dist/src/Dialog.d.ts.map +1 -1
- package/dist/src/Drawer.d.ts +1 -1
- package/dist/src/Drawer.d.ts.map +1 -1
- package/dist/src/Form.d.ts +1 -1
- package/dist/src/Form.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/labs/AppSwitcher/AppSwitcher.d.ts +20 -0
- package/dist/src/labs/AppSwitcher/AppSwitcher.d.ts.map +1 -0
- package/dist/src/labs/AppSwitcher/AppSwitcherApp.d.ts +24 -0
- package/dist/src/labs/AppSwitcher/AppSwitcherApp.d.ts.map +1 -0
- package/dist/src/labs/AppSwitcher/OktaAura.d.ts.map +1 -0
- package/dist/src/labs/AppSwitcher/index.d.ts +13 -0
- package/dist/src/labs/AppSwitcher/index.d.ts.map +1 -0
- package/dist/src/labs/AppTile.d.ts +6 -4
- package/dist/src/labs/AppTile.d.ts.map +1 -1
- package/dist/src/labs/DataTable.d.ts +1 -1
- package/dist/src/labs/DataTable.d.ts.map +1 -1
- package/dist/src/labs/DataView/BulkActionsMenu.d.ts.map +1 -1
- package/dist/src/labs/DataView/DataCard.d.ts +1 -2
- package/dist/src/labs/DataView/DataCard.d.ts.map +1 -1
- package/dist/src/labs/DataView/LayoutSwitcher.d.ts.map +1 -1
- package/dist/src/labs/DataView/RowActions.d.ts +1 -2
- package/dist/src/labs/DataView/RowActions.d.ts.map +1 -1
- package/dist/src/labs/DataView/TableLayoutContent.d.ts.map +1 -1
- package/dist/src/labs/DataView/TableSettings.d.ts.map +1 -1
- package/dist/src/labs/SideNav/SideNav.d.ts.map +1 -1
- package/dist/src/labs/TopNav/UserProfile.d.ts +5 -1
- package/dist/src/labs/TopNav/UserProfile.d.ts.map +1 -1
- package/dist/src/labs/TopNav/UserProfileMenuButton.d.ts +17 -0
- package/dist/src/labs/TopNav/UserProfileMenuButton.d.ts.map +1 -0
- package/dist/src/labs/TopNav/index.d.ts +1 -0
- package/dist/src/labs/TopNav/index.d.ts.map +1 -1
- package/dist/src/labs/UiShell/UiShell.d.ts +2 -2
- package/dist/src/labs/UiShell/UiShell.d.ts.map +1 -1
- package/dist/src/labs/UiShell/UiShellContent.d.ts +19 -2
- package/dist/src/labs/UiShell/UiShellContent.d.ts.map +1 -1
- package/dist/src/labs/UiShell/renderUiShell.d.ts +2 -2
- package/dist/src/labs/UiShell/renderUiShell.d.ts.map +1 -1
- package/dist/src/labs/index.d.ts +1 -0
- package/dist/src/labs/index.d.ts.map +1 -1
- package/dist/src/theme/components.d.ts.map +1 -1
- package/dist/src/web-component/renderReactInWebComponent.d.ts +2 -2
- package/dist/src/web-component/renderReactInWebComponent.d.ts.map +1 -1
- package/dist/theme/components.js +25 -27
- package/dist/theme/components.js.map +1 -1
- package/dist/tsconfig.production.tsbuildinfo +1 -1
- package/dist/web-component/renderReactInWebComponent.js +6 -7
- package/dist/web-component/renderReactInWebComponent.js.map +1 -1
- package/package.json +3 -3
- package/src/{Button.tsx → Buttons/BaseButton.tsx} +48 -68
- package/src/{MenuButton.tsx → Buttons/BaseMenuButton.tsx} +94 -32
- package/src/Buttons/Button.tsx +30 -0
- package/src/Buttons/MenuButton.tsx +35 -0
- package/src/{MenuItem.tsx → Buttons/MenuItem.tsx} +1 -1
- package/src/Buttons/index.ts +22 -0
- package/src/Card.tsx +1 -3
- package/src/DataTable/DataTable.tsx +1 -2
- package/src/DataTable/DataTableRowActions.tsx +1 -3
- package/src/DataTable/DataTableSettings.tsx +1 -2
- package/src/Dialog.tsx +1 -1
- package/src/Drawer.tsx +1 -1
- package/src/FileUploader/FileUploader.tsx +1 -1
- package/src/Form.tsx +1 -1
- package/src/Pagination/Pagination.test.tsx +58 -36
- package/src/Pagination/Pagination.tsx +1 -1
- package/src/Toast.tsx +1 -1
- package/src/index.ts +1 -3
- package/src/labs/AppSwitcher/AppSwitcher.tsx +94 -0
- package/src/labs/AppSwitcher/AppSwitcherApp.tsx +146 -0
- package/src/labs/{SideNav → AppSwitcher}/OktaAura.tsx +19 -4
- package/src/labs/AppSwitcher/index.ts +13 -0
- package/src/labs/AppTile.tsx +171 -85
- package/src/labs/DataFilters.tsx +1 -1
- package/src/labs/DataTable.tsx +1 -1
- package/src/labs/DataTablePagination.tsx +1 -1
- package/src/labs/DataView/BulkActionsMenu.tsx +1 -2
- package/src/labs/DataView/DataCard.tsx +56 -31
- package/src/labs/DataView/DataView.tsx +1 -1
- package/src/labs/DataView/LayoutSwitcher.tsx +1 -2
- package/src/labs/DataView/RowActions.tsx +1 -3
- package/src/labs/DataView/TableLayoutContent.tsx +1 -2
- package/src/labs/DataView/TableSettings.tsx +1 -2
- package/src/labs/DatePicker.tsx +1 -1
- package/src/labs/SideNav/SideNav.tsx +10 -4
- package/src/labs/TopNav/UserProfile.tsx +26 -2
- package/src/labs/TopNav/UserProfileMenuButton.tsx +57 -0
- package/src/labs/TopNav/index.ts +1 -0
- package/src/labs/UiShell/UiShell.test.tsx +23 -38
- package/src/labs/UiShell/UiShell.tsx +14 -6
- package/src/labs/UiShell/UiShellContent.tsx +85 -16
- package/src/labs/UiShell/renderUiShell.test.tsx +21 -15
- package/src/labs/UiShell/renderUiShell.tsx +8 -1
- package/src/labs/index.ts +1 -0
- package/src/theme/components.tsx +25 -28
- package/src/web-component/renderReactInWebComponent.ts +10 -5
- package/dist/Button.js.map +0 -1
- package/dist/ButtonContext.js.map +0 -1
- package/dist/MenuButton.js.map +0 -1
- package/dist/MenuContext.js.map +0 -1
- package/dist/MenuItem.js.map +0 -1
- package/dist/labs/SideNav/OktaAura.js.map +0 -1
- package/dist/src/Button.d.ts.map +0 -1
- package/dist/src/ButtonContext.d.ts.map +0 -1
- package/dist/src/MenuButton.d.ts.map +0 -1
- package/dist/src/MenuContext.d.ts.map +0 -1
- package/dist/src/MenuItem.d.ts.map +0 -1
- package/dist/src/labs/SideNav/OktaAura.d.ts.map +0 -1
- /package/dist/{ButtonContext.js → Buttons/ButtonContext.js} +0 -0
- /package/dist/{MenuContext.js → Buttons/MenuContext.js} +0 -0
- /package/dist/{MenuItem.js → Buttons/MenuItem.js} +0 -0
- /package/dist/src/{ButtonContext.d.ts → Buttons/ButtonContext.d.ts} +0 -0
- /package/dist/src/{MenuContext.d.ts → Buttons/MenuContext.d.ts} +0 -0
- /package/dist/src/labs/{SideNav → AppSwitcher}/OktaAura.d.ts +0 -0
- /package/src/{ButtonContext.tsx → Buttons/ButtonContext.tsx} +0 -0
- /package/src/{MenuContext.ts → Buttons/MenuContext.ts} +0 -0
|
@@ -301,7 +301,13 @@ const SideNav = ({
|
|
|
301
301
|
const { t } = useTranslation();
|
|
302
302
|
const [sideNavItemsList, updateSideNavItemsList] = useState(sideNavItems);
|
|
303
303
|
|
|
304
|
+
// The default value (sideNavItems) passed to useState is ONLY used by the useState hook for
|
|
305
|
+
// the very first value. Subsequent updates to the prop (sideNavItems) need to cause the state
|
|
306
|
+
// to update!
|
|
307
|
+
useEffect(() => updateSideNavItemsList(sideNavItems), [sideNavItems]);
|
|
308
|
+
|
|
304
309
|
useEffect(() => {
|
|
310
|
+
// This is called directly in this effect AND perhaps as a result of the ResizeObserver
|
|
305
311
|
const updateIsContentScrollable = () => {
|
|
306
312
|
if (
|
|
307
313
|
scrollableContentRef.current &&
|
|
@@ -381,7 +387,7 @@ const SideNav = ({
|
|
|
381
387
|
}
|
|
382
388
|
cancelAnimationFrame(resizeObserverDebounceTimer); // Ensure timer is cleared on component unmount
|
|
383
389
|
};
|
|
384
|
-
}, [
|
|
390
|
+
}, [sideNavItemsList]);
|
|
385
391
|
|
|
386
392
|
const scrollIntoViewRef = useRef<HTMLLIElement>(null);
|
|
387
393
|
/**
|
|
@@ -390,7 +396,7 @@ const SideNav = ({
|
|
|
390
396
|
* call scrollIntoView in the effect
|
|
391
397
|
*/
|
|
392
398
|
const firstSideNavItemIdWithIsSelected = useMemo(() => {
|
|
393
|
-
const flattenedItems =
|
|
399
|
+
const flattenedItems = sideNavItemsList.flatMap((sideNavItem) =>
|
|
394
400
|
sideNavItem.nestedNavItems
|
|
395
401
|
? [sideNavItem, ...sideNavItem.nestedNavItems]
|
|
396
402
|
: sideNavItem,
|
|
@@ -399,7 +405,7 @@ const SideNav = ({
|
|
|
399
405
|
(sideNavItem) => sideNavItem.isSelected,
|
|
400
406
|
);
|
|
401
407
|
return firstItemWithIsSelected?.id;
|
|
402
|
-
}, [
|
|
408
|
+
}, [sideNavItemsList]);
|
|
403
409
|
/**
|
|
404
410
|
* Once we've rendered and if we have an item to scroll to, do the scroll action.
|
|
405
411
|
* This must rely on checking firstSideNavItemIdWithIsSelected or it will not run
|
|
@@ -409,7 +415,7 @@ const SideNav = ({
|
|
|
409
415
|
if (firstSideNavItemIdWithIsSelected && scrollIntoViewRef.current) {
|
|
410
416
|
scrollIntoViewRef.current.scrollIntoView();
|
|
411
417
|
}
|
|
412
|
-
}, [firstSideNavItemIdWithIsSelected
|
|
418
|
+
}, [firstSideNavItemIdWithIsSelected]);
|
|
413
419
|
|
|
414
420
|
/**
|
|
415
421
|
* We only want to put a ref on a node iff it is the first selected node.
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
useOdysseyDesignTokens,
|
|
19
19
|
} from "../../OdysseyDesignTokensContext";
|
|
20
20
|
import { Subordinate } from "../../Typography";
|
|
21
|
+
import { Box } from "../../Box";
|
|
21
22
|
|
|
22
23
|
const UserProfileContainer = styled("div", {
|
|
23
24
|
shouldForwardProp: (prop) => prop !== "odysseyDesignTokens",
|
|
@@ -37,6 +38,7 @@ const UserProfileIconContainer = styled("div", {
|
|
|
37
38
|
const UserProfileInfoContainer = styled("div")(() => ({
|
|
38
39
|
display: "flex",
|
|
39
40
|
flexDirection: "column",
|
|
41
|
+
textAlign: "left",
|
|
40
42
|
}));
|
|
41
43
|
|
|
42
44
|
export type UserProfileProps = {
|
|
@@ -52,9 +54,18 @@ export type UserProfileProps = {
|
|
|
52
54
|
* Org name of the logged in user
|
|
53
55
|
*/
|
|
54
56
|
orgName: string;
|
|
57
|
+
/**
|
|
58
|
+
* The icon element to display after the username
|
|
59
|
+
*/
|
|
60
|
+
userNameEndIcon?: ReactElement;
|
|
55
61
|
};
|
|
56
62
|
|
|
57
|
-
const UserProfile = ({
|
|
63
|
+
const UserProfile = ({
|
|
64
|
+
profileIcon,
|
|
65
|
+
userName,
|
|
66
|
+
orgName,
|
|
67
|
+
userNameEndIcon,
|
|
68
|
+
}: UserProfileProps) => {
|
|
58
69
|
const odysseyDesignTokens = useOdysseyDesignTokens();
|
|
59
70
|
|
|
60
71
|
return (
|
|
@@ -66,7 +77,20 @@ const UserProfile = ({ profileIcon, userName, orgName }: UserProfileProps) => {
|
|
|
66
77
|
)}
|
|
67
78
|
|
|
68
79
|
<UserProfileInfoContainer>
|
|
69
|
-
|
|
80
|
+
{userNameEndIcon ? (
|
|
81
|
+
<Box
|
|
82
|
+
sx={{
|
|
83
|
+
display: "flex",
|
|
84
|
+
flexDirection: "row",
|
|
85
|
+
gap: odysseyDesignTokens.Spacing2,
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<Subordinate color="textPrimary">{userName}</Subordinate>
|
|
89
|
+
{userNameEndIcon}
|
|
90
|
+
</Box>
|
|
91
|
+
) : (
|
|
92
|
+
<Subordinate color="textPrimary">{userName}</Subordinate>
|
|
93
|
+
)}
|
|
70
94
|
<Subordinate color="textSecondary">{orgName}</Subordinate>
|
|
71
95
|
</UserProfileInfoContainer>
|
|
72
96
|
</UserProfileContainer>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
|
|
3
|
+
* The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
|
|
4
|
+
*
|
|
5
|
+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
|
|
6
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
7
|
+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
8
|
+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
9
|
+
*
|
|
10
|
+
* See the License for the specific language governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { memo } from "react";
|
|
14
|
+
import { UserProfile, UserProfileProps } from "./UserProfile";
|
|
15
|
+
import { ChevronDownIcon } from "../../icons.generated";
|
|
16
|
+
import {
|
|
17
|
+
AdditionalBaseMenuButtonProps,
|
|
18
|
+
BaseMenuButton,
|
|
19
|
+
BaseMenuButtonProps,
|
|
20
|
+
} from "../../Buttons/BaseMenuButton";
|
|
21
|
+
|
|
22
|
+
export type UserProfileMenuButtonProps = Omit<
|
|
23
|
+
BaseMenuButtonProps,
|
|
24
|
+
"endIcon" | "variant"
|
|
25
|
+
> &
|
|
26
|
+
AdditionalBaseMenuButtonProps &
|
|
27
|
+
UserProfileProps;
|
|
28
|
+
|
|
29
|
+
const UserProfileMenuButton = (props: UserProfileMenuButtonProps) => {
|
|
30
|
+
const {
|
|
31
|
+
profileIcon,
|
|
32
|
+
userName,
|
|
33
|
+
orgName,
|
|
34
|
+
userNameEndIcon,
|
|
35
|
+
...menuButtonProps
|
|
36
|
+
} = props;
|
|
37
|
+
return (
|
|
38
|
+
<BaseMenuButton
|
|
39
|
+
{...menuButtonProps}
|
|
40
|
+
buttonVariant="floating"
|
|
41
|
+
omitEndIcon={true}
|
|
42
|
+
buttonChildren={
|
|
43
|
+
<UserProfile
|
|
44
|
+
profileIcon={profileIcon}
|
|
45
|
+
userName={userName}
|
|
46
|
+
orgName={orgName}
|
|
47
|
+
userNameEndIcon={userNameEndIcon ?? <ChevronDownIcon />}
|
|
48
|
+
/>
|
|
49
|
+
}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const MemoizedUserProfileMenuButton = memo(UserProfileMenuButton);
|
|
55
|
+
MemoizedUserProfileMenuButton.displayName = "UserProfileMenuButton";
|
|
56
|
+
|
|
57
|
+
export { MemoizedUserProfileMenuButton as UserProfileMenuButton };
|
package/src/labs/TopNav/index.ts
CHANGED
|
@@ -10,43 +10,13 @@
|
|
|
10
10
|
* See the License for the specific language governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { render, within } from "@testing-library/react";
|
|
13
|
+
import { render, waitFor, within } from "@testing-library/react";
|
|
14
14
|
|
|
15
|
-
import { Dialog } from "../../Dialog";
|
|
16
15
|
import { defaultComponentProps, UiShell, UiShellProps } from "./UiShell";
|
|
17
16
|
import { ReactElement } from "react";
|
|
18
17
|
|
|
19
18
|
describe("UiShell", () => {
|
|
20
|
-
test("renders `
|
|
21
|
-
const appRootElement = document.createElement("div");
|
|
22
|
-
|
|
23
|
-
render(
|
|
24
|
-
<UiShell
|
|
25
|
-
appComponent={<div />}
|
|
26
|
-
appRootElement={appRootElement}
|
|
27
|
-
onSubscriptionCreated={() => {}}
|
|
28
|
-
optionalComponents={{
|
|
29
|
-
sideNavFooter: <div />,
|
|
30
|
-
topNavLeftSide: <div />,
|
|
31
|
-
topNavRightSide: (
|
|
32
|
-
<Dialog
|
|
33
|
-
children={undefined}
|
|
34
|
-
title="Hello World!"
|
|
35
|
-
isOpen
|
|
36
|
-
onClose={() => {}}
|
|
37
|
-
/>
|
|
38
|
-
),
|
|
39
|
-
}}
|
|
40
|
-
stylesRootElement={document.createElement("div")}
|
|
41
|
-
subscribeToPropChanges={() => () => {}}
|
|
42
|
-
/>,
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
expect(Array.from(appRootElement.children)).toHaveLength(1);
|
|
46
|
-
expect(appRootElement).toHaveTextContent("Hello World!");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("renders `stylesRootElement`", async () => {
|
|
19
|
+
test("renders `stylesRootElement`", () => {
|
|
50
20
|
const rootElement = document.createElement("div");
|
|
51
21
|
|
|
52
22
|
// If this isn't appended to the DOM, the React app won't exist because of how Web Components run.
|
|
@@ -80,7 +50,7 @@ describe("UiShell", () => {
|
|
|
80
50
|
/>,
|
|
81
51
|
);
|
|
82
52
|
|
|
83
|
-
expect(within(container).getByTestId(testId)).
|
|
53
|
+
expect(within(container).getByTestId(testId)).toBeVisible();
|
|
84
54
|
});
|
|
85
55
|
|
|
86
56
|
test("renders always-available `componentSlots`", async () => {
|
|
@@ -88,6 +58,19 @@ describe("UiShell", () => {
|
|
|
88
58
|
keyof Required<UiShellProps>["optionalComponents"]
|
|
89
59
|
> = ["banners", "topNavLeftSide", "topNavRightSide"];
|
|
90
60
|
|
|
61
|
+
// This is the subscription we give the component, and then once subscribed, we're going to immediately call it with new props.
|
|
62
|
+
// TopNav won't render unless we pass something into it.
|
|
63
|
+
const subscribeToPropChanges: UiShellProps["subscribeToPropChanges"] = (
|
|
64
|
+
subscriber,
|
|
65
|
+
) => {
|
|
66
|
+
subscriber({
|
|
67
|
+
...defaultComponentProps,
|
|
68
|
+
topNavProps: {},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return () => {};
|
|
72
|
+
};
|
|
73
|
+
|
|
91
74
|
const { container } = render(
|
|
92
75
|
<UiShell
|
|
93
76
|
appComponent={<div />}
|
|
@@ -102,12 +85,14 @@ describe("UiShell", () => {
|
|
|
102
85
|
) as Record<keyof UiShellProps["optionalComponents"], ReactElement>
|
|
103
86
|
}
|
|
104
87
|
stylesRootElement={document.createElement("div")}
|
|
105
|
-
subscribeToPropChanges={
|
|
88
|
+
subscribeToPropChanges={subscribeToPropChanges}
|
|
106
89
|
/>,
|
|
107
90
|
);
|
|
108
91
|
|
|
109
|
-
|
|
110
|
-
|
|
92
|
+
await waitFor(() => {
|
|
93
|
+
optionalComponentTestIds.forEach((testId) => {
|
|
94
|
+
expect(within(container).getByTestId(testId)).toBeVisible();
|
|
95
|
+
});
|
|
111
96
|
});
|
|
112
97
|
});
|
|
113
98
|
|
|
@@ -151,7 +136,7 @@ describe("UiShell", () => {
|
|
|
151
136
|
);
|
|
152
137
|
|
|
153
138
|
optionalComponentTestIds.forEach((testId) => {
|
|
154
|
-
expect(within(container).getByTestId(testId)).
|
|
139
|
+
expect(within(container).getByTestId(testId)).toBeVisible();
|
|
155
140
|
});
|
|
156
141
|
});
|
|
157
142
|
|
|
@@ -247,7 +232,7 @@ describe("UiShell", () => {
|
|
|
247
232
|
/>,
|
|
248
233
|
);
|
|
249
234
|
|
|
250
|
-
expect(container).
|
|
235
|
+
expect(container).toBeVisible();
|
|
251
236
|
});
|
|
252
237
|
|
|
253
238
|
test("has previous state in prop change subscription", async () => {
|
|
@@ -23,11 +23,8 @@ import {
|
|
|
23
23
|
import { type ReactRootElements } from "../../web-component";
|
|
24
24
|
|
|
25
25
|
export const defaultComponentProps: UiShellNavComponentProps = {
|
|
26
|
-
sideNavProps:
|
|
27
|
-
|
|
28
|
-
sideNavItems: [],
|
|
29
|
-
},
|
|
30
|
-
topNavProps: {},
|
|
26
|
+
sideNavProps: undefined,
|
|
27
|
+
topNavProps: undefined,
|
|
31
28
|
} as const;
|
|
32
29
|
|
|
33
30
|
export type UiShellProps = {
|
|
@@ -49,7 +46,14 @@ export type UiShellProps = {
|
|
|
49
46
|
) => void,
|
|
50
47
|
) => () => void;
|
|
51
48
|
} & Pick<ReactRootElements, "appRootElement" | "stylesRootElement"> &
|
|
52
|
-
Pick<
|
|
49
|
+
Pick<
|
|
50
|
+
UiShellContentProps,
|
|
51
|
+
| "appBackgroundContrastMode"
|
|
52
|
+
| "appComponent"
|
|
53
|
+
| "initialVisibleSections"
|
|
54
|
+
| "onError"
|
|
55
|
+
| "optionalComponents"
|
|
56
|
+
>;
|
|
53
57
|
|
|
54
58
|
/**
|
|
55
59
|
* Our new Unified Platform UI Shell.
|
|
@@ -59,8 +63,10 @@ export type UiShellProps = {
|
|
|
59
63
|
* If an error occurs, this will revert to only showing the app.
|
|
60
64
|
*/
|
|
61
65
|
const UiShell = ({
|
|
66
|
+
appBackgroundContrastMode,
|
|
62
67
|
appComponent,
|
|
63
68
|
appRootElement,
|
|
69
|
+
initialVisibleSections,
|
|
64
70
|
onError = console.error,
|
|
65
71
|
onSubscriptionCreated,
|
|
66
72
|
optionalComponents,
|
|
@@ -93,7 +99,9 @@ const UiShell = ({
|
|
|
93
99
|
|
|
94
100
|
<UiShellContent
|
|
95
101
|
{...componentProps}
|
|
102
|
+
appBackgroundContrastMode={appBackgroundContrastMode}
|
|
96
103
|
appComponent={appComponent}
|
|
104
|
+
initialVisibleSections={initialVisibleSections}
|
|
97
105
|
onError={onError}
|
|
98
106
|
optionalComponents={optionalComponents}
|
|
99
107
|
/>
|
|
@@ -14,6 +14,7 @@ import styled from "@emotion/styled";
|
|
|
14
14
|
import { memo, type ReactElement, type ReactNode } from "react";
|
|
15
15
|
import { ErrorBoundary, ErrorBoundaryProps } from "react-error-boundary";
|
|
16
16
|
|
|
17
|
+
import { AppSwitcher, type AppSwitcherProps } from "../AppSwitcher";
|
|
17
18
|
import { SideNav, type SideNavProps } from "../SideNav";
|
|
18
19
|
import { TopNav, type TopNavProps } from "../TopNav";
|
|
19
20
|
import {
|
|
@@ -21,23 +22,36 @@ import {
|
|
|
21
22
|
type DesignTokens,
|
|
22
23
|
} from "../../OdysseyDesignTokensContext";
|
|
23
24
|
import { useScrollState } from "./useScrollState";
|
|
25
|
+
import { ContrastMode } from "../../useContrastMode";
|
|
26
|
+
|
|
27
|
+
const emptySideNavItems = [] satisfies SideNavProps["sideNavItems"];
|
|
24
28
|
|
|
25
29
|
const StyledAppContainer = styled("div", {
|
|
26
|
-
shouldForwardProp: (prop) =>
|
|
30
|
+
shouldForwardProp: (prop) =>
|
|
31
|
+
prop !== "odysseyDesignTokens" && prop !== "appBackgroundContrastMode",
|
|
27
32
|
})<{
|
|
33
|
+
appBackgroundContrastMode: ContrastMode;
|
|
28
34
|
odysseyDesignTokens: DesignTokens;
|
|
29
|
-
}>(({ odysseyDesignTokens }) => ({
|
|
35
|
+
}>(({ appBackgroundContrastMode, odysseyDesignTokens }) => ({
|
|
30
36
|
gridArea: "app-content",
|
|
31
37
|
overflowX: "hidden",
|
|
32
38
|
overflowY: "auto",
|
|
33
39
|
paddingBlock: odysseyDesignTokens.Spacing5,
|
|
34
40
|
paddingInline: odysseyDesignTokens.Spacing8,
|
|
41
|
+
backgroundColor:
|
|
42
|
+
appBackgroundContrastMode === "highContrast"
|
|
43
|
+
? odysseyDesignTokens.HueNeutralWhite
|
|
44
|
+
: odysseyDesignTokens.HueNeutral50,
|
|
35
45
|
}));
|
|
36
46
|
|
|
37
47
|
const StyledBannersContainer = styled("div")(() => ({
|
|
38
48
|
gridArea: "banners",
|
|
39
49
|
}));
|
|
40
50
|
|
|
51
|
+
const StyledAppSwitcherContainer = styled("div")(() => ({
|
|
52
|
+
gridArea: "app-switcher",
|
|
53
|
+
}));
|
|
54
|
+
|
|
41
55
|
const StyledSideNavContainer = styled("div")(() => ({
|
|
42
56
|
gridArea: "side-nav",
|
|
43
57
|
}));
|
|
@@ -51,11 +65,11 @@ const StyledShellContainer = styled("div", {
|
|
|
51
65
|
display: "grid",
|
|
52
66
|
gridGap: 0,
|
|
53
67
|
gridTemplateAreas: `
|
|
54
|
-
"banners banners"
|
|
55
|
-
"side-nav top-nav"
|
|
56
|
-
"side-nav app-content"
|
|
68
|
+
"banners banners banners"
|
|
69
|
+
"app-switcher side-nav top-nav"
|
|
70
|
+
"app-switcher side-nav app-content"
|
|
57
71
|
`,
|
|
58
|
-
gridTemplateColumns: "auto 1fr",
|
|
72
|
+
gridTemplateColumns: "auto auto 1fr",
|
|
59
73
|
gridTemplateRows: "auto auto 1fr",
|
|
60
74
|
height: "100vh",
|
|
61
75
|
width: "100vw",
|
|
@@ -65,7 +79,14 @@ const StyledTopNavContainer = styled("div")(() => ({
|
|
|
65
79
|
gridArea: "top-nav",
|
|
66
80
|
}));
|
|
67
81
|
|
|
82
|
+
export const subComponentNames = ["TopNav", "SideNav", "AppSwitcher"] as const;
|
|
83
|
+
export type SubComponentName = (typeof subComponentNames)[number];
|
|
84
|
+
|
|
68
85
|
export type UiShellNavComponentProps = {
|
|
86
|
+
/**
|
|
87
|
+
* Object that gets pass directly to the app switcher component.
|
|
88
|
+
*/
|
|
89
|
+
appSwitcherProps?: AppSwitcherProps;
|
|
69
90
|
/**
|
|
70
91
|
* Object that gets pass directly to the side nav component.
|
|
71
92
|
*/
|
|
@@ -73,14 +94,23 @@ export type UiShellNavComponentProps = {
|
|
|
73
94
|
/**
|
|
74
95
|
* Object that gets pass directly to the top nav component.
|
|
75
96
|
*/
|
|
76
|
-
topNavProps
|
|
97
|
+
topNavProps?: Omit<TopNavProps, "leftSideComponent" | "rightSideComponent">;
|
|
77
98
|
};
|
|
78
99
|
|
|
79
100
|
export type UiShellContentProps = {
|
|
101
|
+
/**
|
|
102
|
+
* Sets the background color for the app content area.
|
|
103
|
+
*/
|
|
104
|
+
appBackgroundContrastMode?: ContrastMode;
|
|
80
105
|
/**
|
|
81
106
|
* React app component that renders as children in the correct location of the shell.
|
|
82
107
|
*/
|
|
83
108
|
appComponent: ReactNode;
|
|
109
|
+
/**
|
|
110
|
+
* Which parts of the UI Shell should be visible initially? For example,
|
|
111
|
+
* if sideNavProps is undefined, should the space for the sidenav be initially visible?
|
|
112
|
+
*/
|
|
113
|
+
initialVisibleSections?: SubComponentName[];
|
|
84
114
|
/**
|
|
85
115
|
* Notifies when a React rendering error occurs. This could be useful for logging, flagging "p0"s, and recovering UI Shell when errors occur.
|
|
86
116
|
*/
|
|
@@ -104,9 +134,12 @@ export type UiShellContentProps = {
|
|
|
104
134
|
* If an error occurs, this will revert to only showing the app.
|
|
105
135
|
*/
|
|
106
136
|
const UiShellContent = ({
|
|
137
|
+
appBackgroundContrastMode = "lowContrast",
|
|
107
138
|
appComponent,
|
|
139
|
+
initialVisibleSections = ["TopNav", "SideNav", "AppSwitcher"],
|
|
108
140
|
onError = console.error,
|
|
109
141
|
optionalComponents,
|
|
142
|
+
appSwitcherProps,
|
|
110
143
|
sideNavProps,
|
|
111
144
|
topNavProps,
|
|
112
145
|
}: UiShellContentProps) => {
|
|
@@ -119,7 +152,32 @@ const UiShellContent = ({
|
|
|
119
152
|
{optionalComponents?.banners}
|
|
120
153
|
</StyledBannersContainer>
|
|
121
154
|
|
|
155
|
+
<StyledAppSwitcherContainer>
|
|
156
|
+
{
|
|
157
|
+
/* If AppSwitcher should be initially visible and we have not yet received props, render AppSwitcher in the loading state */
|
|
158
|
+
initialVisibleSections?.includes("AppSwitcher") &&
|
|
159
|
+
!appSwitcherProps && (
|
|
160
|
+
<ErrorBoundary fallback={null} onError={onError}>
|
|
161
|
+
<AppSwitcher isLoading appIcons={[]} selectedAppName="" />
|
|
162
|
+
</ErrorBoundary>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
{appSwitcherProps && (
|
|
166
|
+
<ErrorBoundary fallback={null} onError={onError}>
|
|
167
|
+
<AppSwitcher {...appSwitcherProps} />
|
|
168
|
+
</ErrorBoundary>
|
|
169
|
+
)}
|
|
170
|
+
</StyledAppSwitcherContainer>
|
|
171
|
+
|
|
122
172
|
<StyledSideNavContainer>
|
|
173
|
+
{
|
|
174
|
+
/* If SideNav should be initially visible and we have not yet received props, render SideNav with minimal inputs */
|
|
175
|
+
initialVisibleSections?.includes("SideNav") && !sideNavProps && (
|
|
176
|
+
<ErrorBoundary fallback={null} onError={onError}>
|
|
177
|
+
<SideNav isLoading appName="" sideNavItems={emptySideNavItems} />
|
|
178
|
+
</ErrorBoundary>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
123
181
|
{sideNavProps && (
|
|
124
182
|
<ErrorBoundary fallback={null} onError={onError}>
|
|
125
183
|
<SideNav
|
|
@@ -143,20 +201,31 @@ const UiShellContent = ({
|
|
|
143
201
|
</StyledSideNavContainer>
|
|
144
202
|
|
|
145
203
|
<StyledTopNavContainer>
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
204
|
+
{
|
|
205
|
+
/* If TopNav should be initially visible and we have not yet received props, render Topnav with minimal inputs */
|
|
206
|
+
initialVisibleSections?.includes("TopNav") && !topNavProps && (
|
|
207
|
+
<ErrorBoundary fallback={null} onError={onError}>
|
|
208
|
+
<TopNav />
|
|
209
|
+
</ErrorBoundary>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
{topNavProps && (
|
|
213
|
+
<ErrorBoundary fallback={null} onError={onError}>
|
|
214
|
+
<TopNav
|
|
215
|
+
{...topNavProps}
|
|
216
|
+
isScrolled={isContentScrolled}
|
|
217
|
+
leftSideComponent={optionalComponents?.topNavLeftSide}
|
|
218
|
+
rightSideComponent={optionalComponents?.topNavRightSide}
|
|
219
|
+
/>
|
|
220
|
+
</ErrorBoundary>
|
|
221
|
+
)}
|
|
154
222
|
</StyledTopNavContainer>
|
|
155
223
|
|
|
156
224
|
<StyledAppContainer
|
|
157
225
|
odysseyDesignTokens={odysseyDesignTokens}
|
|
158
|
-
|
|
226
|
+
appBackgroundContrastMode={appBackgroundContrastMode}
|
|
159
227
|
ref={scrollableContentRef}
|
|
228
|
+
tabIndex={0}
|
|
160
229
|
>
|
|
161
230
|
{appComponent}
|
|
162
231
|
</StyledAppContainer>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* See the License for the specific language governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { act } from "@testing-library/react";
|
|
13
|
+
import { act, waitFor } from "@testing-library/react";
|
|
14
14
|
|
|
15
15
|
import { renderUiShell } from "./renderUiShell";
|
|
16
16
|
import {
|
|
@@ -126,9 +126,11 @@ describe("renderUiShell", () => {
|
|
|
126
126
|
});
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
await waitFor(() => {
|
|
130
|
+
expect(
|
|
131
|
+
rootElement.querySelector(reactWebComponentElementName)!.shadowRoot,
|
|
132
|
+
).toHaveTextContent(appName);
|
|
133
|
+
});
|
|
132
134
|
});
|
|
133
135
|
|
|
134
136
|
test("renders `UiShell` with immediately updated props", async () => {
|
|
@@ -153,9 +155,11 @@ describe("renderUiShell", () => {
|
|
|
153
155
|
});
|
|
154
156
|
});
|
|
155
157
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
await waitFor(() => {
|
|
159
|
+
expect(
|
|
160
|
+
rootElement.querySelector(reactWebComponentElementName)!.shadowRoot,
|
|
161
|
+
).toHaveTextContent(appName);
|
|
162
|
+
});
|
|
159
163
|
});
|
|
160
164
|
|
|
161
165
|
test("renders `<slot>` in the event of an error", async () => {
|
|
@@ -184,14 +188,16 @@ describe("renderUiShell", () => {
|
|
|
184
188
|
);
|
|
185
189
|
});
|
|
186
190
|
|
|
187
|
-
|
|
191
|
+
await waitFor(() => {
|
|
192
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
193
|
+
expect(consoleError).toHaveBeenCalledTimes(1);
|
|
194
|
+
expect(
|
|
195
|
+
rootElement
|
|
196
|
+
.querySelector(reactWebComponentElementName)!
|
|
197
|
+
.shadowRoot?.querySelector("slot"),
|
|
198
|
+
).toBeInstanceOf(HTMLSlotElement);
|
|
199
|
+
});
|
|
188
200
|
|
|
189
|
-
|
|
190
|
-
expect(consoleError).toHaveBeenCalledTimes(1);
|
|
191
|
-
expect(
|
|
192
|
-
rootElement
|
|
193
|
-
.querySelector(reactWebComponentElementName)!
|
|
194
|
-
.shadowRoot?.querySelector("slot"),
|
|
195
|
-
).toBeInstanceOf(HTMLSlotElement);
|
|
201
|
+
consoleErrorSpy.mockRestore();
|
|
196
202
|
});
|
|
197
203
|
});
|
|
@@ -41,7 +41,9 @@ export const optionalComponentSlotNames: Record<
|
|
|
41
41
|
* It also provides you with other elements fitted to slots in the web component. **In React, you can portal to these components.**
|
|
42
42
|
*/
|
|
43
43
|
export const renderUiShell = ({
|
|
44
|
+
appBackgroundContrastMode,
|
|
44
45
|
appRootElement: explicitAppRootElement,
|
|
46
|
+
initialVisibleSections,
|
|
45
47
|
onError = console.error,
|
|
46
48
|
uiShellRootElement,
|
|
47
49
|
}: {
|
|
@@ -57,7 +59,10 @@ export const renderUiShell = ({
|
|
|
57
59
|
* HTML element used as the root for UI Shell.
|
|
58
60
|
*/
|
|
59
61
|
uiShellRootElement: HTMLElement;
|
|
60
|
-
}
|
|
62
|
+
} & Pick<
|
|
63
|
+
UiShellProps,
|
|
64
|
+
"appBackgroundContrastMode" | "initialVisibleSections"
|
|
65
|
+
>) => {
|
|
61
66
|
const appRootElement =
|
|
62
67
|
explicitAppRootElement || document.createElement("div");
|
|
63
68
|
|
|
@@ -101,8 +106,10 @@ export const renderUiShell = ({
|
|
|
101
106
|
getReactComponent: (reactRootElements) => (
|
|
102
107
|
<ErrorBoundary fallback={appComponent} onError={onError}>
|
|
103
108
|
<UiShell
|
|
109
|
+
appBackgroundContrastMode={appBackgroundContrastMode}
|
|
104
110
|
appComponent={appComponent}
|
|
105
111
|
appRootElement={reactRootElements.appRootElement}
|
|
112
|
+
initialVisibleSections={initialVisibleSections}
|
|
106
113
|
onError={onError}
|
|
107
114
|
onSubscriptionCreated={publishSubscriptionCreated}
|
|
108
115
|
// `optionalComponents` doesn't need to be memoized because gets passed in once.
|