@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.
Files changed (221) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/{Button.js → Buttons/BaseButton.js} +11 -10
  3. package/dist/Buttons/BaseButton.js.map +1 -0
  4. package/dist/{MenuButton.js → Buttons/BaseMenuButton.js} +30 -10
  5. package/dist/Buttons/BaseMenuButton.js.map +1 -0
  6. package/dist/Buttons/Button.js +24 -0
  7. package/dist/Buttons/Button.js.map +1 -0
  8. package/dist/Buttons/ButtonContext.js.map +1 -0
  9. package/dist/Buttons/MenuButton.js +25 -0
  10. package/dist/Buttons/MenuButton.js.map +1 -0
  11. package/dist/Buttons/MenuContext.js.map +1 -0
  12. package/dist/Buttons/MenuItem.js.map +1 -0
  13. package/dist/Buttons/index.js +18 -0
  14. package/dist/Buttons/index.js.map +1 -0
  15. package/dist/Card.js +1 -2
  16. package/dist/Card.js.map +1 -1
  17. package/dist/DataTable/DataTable.js +1 -2
  18. package/dist/DataTable/DataTable.js.map +1 -1
  19. package/dist/DataTable/DataTableRowActions.js +1 -2
  20. package/dist/DataTable/DataTableRowActions.js.map +1 -1
  21. package/dist/DataTable/DataTableSettings.js +1 -2
  22. package/dist/DataTable/DataTableSettings.js.map +1 -1
  23. package/dist/Dialog.js +1 -1
  24. package/dist/Dialog.js.map +1 -1
  25. package/dist/Drawer.js +1 -1
  26. package/dist/Drawer.js.map +1 -1
  27. package/dist/FileUploader/FileUploader.js +1 -1
  28. package/dist/FileUploader/FileUploader.js.map +1 -1
  29. package/dist/Form.js.map +1 -1
  30. package/dist/Pagination/Pagination.js +1 -1
  31. package/dist/Pagination/Pagination.js.map +1 -1
  32. package/dist/Toast.js +1 -1
  33. package/dist/Toast.js.map +1 -1
  34. package/dist/index.js +1 -3
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.scss +1 -1
  37. package/dist/labs/AppSwitcher/AppSwitcher.js +76 -0
  38. package/dist/labs/AppSwitcher/AppSwitcher.js.map +1 -0
  39. package/dist/labs/AppSwitcher/AppSwitcherApp.js +112 -0
  40. package/dist/labs/AppSwitcher/AppSwitcherApp.js.map +1 -0
  41. package/dist/labs/{SideNav → AppSwitcher}/OktaAura.js +16 -3
  42. package/dist/labs/AppSwitcher/OktaAura.js.map +1 -0
  43. package/dist/labs/AppSwitcher/index.js +13 -0
  44. package/dist/labs/AppSwitcher/index.js.map +1 -0
  45. package/dist/labs/AppTile.js +102 -65
  46. package/dist/labs/AppTile.js.map +1 -1
  47. package/dist/labs/DataFilters.js +1 -1
  48. package/dist/labs/DataFilters.js.map +1 -1
  49. package/dist/labs/DataTable.js.map +1 -1
  50. package/dist/labs/DataTablePagination.js +1 -1
  51. package/dist/labs/DataTablePagination.js.map +1 -1
  52. package/dist/labs/DataView/BulkActionsMenu.js +1 -2
  53. package/dist/labs/DataView/BulkActionsMenu.js.map +1 -1
  54. package/dist/labs/DataView/DataCard.js +53 -42
  55. package/dist/labs/DataView/DataCard.js.map +1 -1
  56. package/dist/labs/DataView/DataView.js +1 -1
  57. package/dist/labs/DataView/DataView.js.map +1 -1
  58. package/dist/labs/DataView/LayoutSwitcher.js +1 -2
  59. package/dist/labs/DataView/LayoutSwitcher.js.map +1 -1
  60. package/dist/labs/DataView/RowActions.js +1 -1
  61. package/dist/labs/DataView/RowActions.js.map +1 -1
  62. package/dist/labs/DataView/TableLayoutContent.js +1 -2
  63. package/dist/labs/DataView/TableLayoutContent.js.map +1 -1
  64. package/dist/labs/DataView/TableSettings.js +1 -2
  65. package/dist/labs/DataView/TableSettings.js.map +1 -1
  66. package/dist/labs/DatePicker.js +1 -1
  67. package/dist/labs/DatePicker.js.map +1 -1
  68. package/dist/labs/SideNav/SideNav.js +5 -4
  69. package/dist/labs/SideNav/SideNav.js.map +1 -1
  70. package/dist/labs/TopNav/UserProfile.js +16 -3
  71. package/dist/labs/TopNav/UserProfile.js.map +1 -1
  72. package/dist/labs/TopNav/UserProfileMenuButton.js +41 -0
  73. package/dist/labs/TopNav/UserProfileMenuButton.js.map +1 -0
  74. package/dist/labs/TopNav/index.js +1 -0
  75. package/dist/labs/TopNav/index.js.map +1 -1
  76. package/dist/labs/UiShell/UiShell.js +6 -5
  77. package/dist/labs/UiShell/UiShell.js.map +1 -1
  78. package/dist/labs/UiShell/UiShellContent.js +53 -13
  79. package/dist/labs/UiShell/UiShellContent.js.map +1 -1
  80. package/dist/labs/UiShell/renderUiShell.js +4 -0
  81. package/dist/labs/UiShell/renderUiShell.js.map +1 -1
  82. package/dist/labs/index.js +1 -0
  83. package/dist/labs/index.js.map +1 -1
  84. package/dist/src/{Button.d.ts → Buttons/BaseButton.d.ts} +12 -34
  85. package/dist/src/Buttons/BaseButton.d.ts.map +1 -0
  86. package/dist/src/{MenuButton.d.ts → Buttons/BaseMenuButton.d.ts} +37 -14
  87. package/dist/src/Buttons/BaseMenuButton.d.ts.map +1 -0
  88. package/dist/src/Buttons/Button.d.ts +16 -0
  89. package/dist/src/Buttons/Button.d.ts.map +1 -0
  90. package/dist/src/Buttons/ButtonContext.d.ts.map +1 -0
  91. package/dist/src/Buttons/MenuButton.d.ts +17 -0
  92. package/dist/src/Buttons/MenuButton.d.ts.map +1 -0
  93. package/dist/src/Buttons/MenuContext.d.ts.map +1 -0
  94. package/dist/src/{MenuItem.d.ts → Buttons/MenuItem.d.ts} +1 -1
  95. package/dist/src/Buttons/MenuItem.d.ts.map +1 -0
  96. package/dist/src/Buttons/index.d.ts +18 -0
  97. package/dist/src/Buttons/index.d.ts.map +1 -0
  98. package/dist/src/Card.d.ts +1 -2
  99. package/dist/src/Card.d.ts.map +1 -1
  100. package/dist/src/DataTable/DataTable.d.ts +1 -1
  101. package/dist/src/DataTable/DataTable.d.ts.map +1 -1
  102. package/dist/src/DataTable/DataTableRowActions.d.ts +1 -2
  103. package/dist/src/DataTable/DataTableRowActions.d.ts.map +1 -1
  104. package/dist/src/DataTable/DataTableSettings.d.ts.map +1 -1
  105. package/dist/src/Dialog.d.ts +1 -1
  106. package/dist/src/Dialog.d.ts.map +1 -1
  107. package/dist/src/Drawer.d.ts +1 -1
  108. package/dist/src/Drawer.d.ts.map +1 -1
  109. package/dist/src/Form.d.ts +1 -1
  110. package/dist/src/Form.d.ts.map +1 -1
  111. package/dist/src/index.d.ts +1 -3
  112. package/dist/src/index.d.ts.map +1 -1
  113. package/dist/src/labs/AppSwitcher/AppSwitcher.d.ts +20 -0
  114. package/dist/src/labs/AppSwitcher/AppSwitcher.d.ts.map +1 -0
  115. package/dist/src/labs/AppSwitcher/AppSwitcherApp.d.ts +24 -0
  116. package/dist/src/labs/AppSwitcher/AppSwitcherApp.d.ts.map +1 -0
  117. package/dist/src/labs/AppSwitcher/OktaAura.d.ts.map +1 -0
  118. package/dist/src/labs/AppSwitcher/index.d.ts +13 -0
  119. package/dist/src/labs/AppSwitcher/index.d.ts.map +1 -0
  120. package/dist/src/labs/AppTile.d.ts +6 -4
  121. package/dist/src/labs/AppTile.d.ts.map +1 -1
  122. package/dist/src/labs/DataTable.d.ts +1 -1
  123. package/dist/src/labs/DataTable.d.ts.map +1 -1
  124. package/dist/src/labs/DataView/BulkActionsMenu.d.ts.map +1 -1
  125. package/dist/src/labs/DataView/DataCard.d.ts +1 -2
  126. package/dist/src/labs/DataView/DataCard.d.ts.map +1 -1
  127. package/dist/src/labs/DataView/LayoutSwitcher.d.ts.map +1 -1
  128. package/dist/src/labs/DataView/RowActions.d.ts +1 -2
  129. package/dist/src/labs/DataView/RowActions.d.ts.map +1 -1
  130. package/dist/src/labs/DataView/TableLayoutContent.d.ts.map +1 -1
  131. package/dist/src/labs/DataView/TableSettings.d.ts.map +1 -1
  132. package/dist/src/labs/SideNav/SideNav.d.ts.map +1 -1
  133. package/dist/src/labs/TopNav/UserProfile.d.ts +5 -1
  134. package/dist/src/labs/TopNav/UserProfile.d.ts.map +1 -1
  135. package/dist/src/labs/TopNav/UserProfileMenuButton.d.ts +17 -0
  136. package/dist/src/labs/TopNav/UserProfileMenuButton.d.ts.map +1 -0
  137. package/dist/src/labs/TopNav/index.d.ts +1 -0
  138. package/dist/src/labs/TopNav/index.d.ts.map +1 -1
  139. package/dist/src/labs/UiShell/UiShell.d.ts +2 -2
  140. package/dist/src/labs/UiShell/UiShell.d.ts.map +1 -1
  141. package/dist/src/labs/UiShell/UiShellContent.d.ts +19 -2
  142. package/dist/src/labs/UiShell/UiShellContent.d.ts.map +1 -1
  143. package/dist/src/labs/UiShell/renderUiShell.d.ts +2 -2
  144. package/dist/src/labs/UiShell/renderUiShell.d.ts.map +1 -1
  145. package/dist/src/labs/index.d.ts +1 -0
  146. package/dist/src/labs/index.d.ts.map +1 -1
  147. package/dist/src/theme/components.d.ts.map +1 -1
  148. package/dist/src/web-component/renderReactInWebComponent.d.ts +2 -2
  149. package/dist/src/web-component/renderReactInWebComponent.d.ts.map +1 -1
  150. package/dist/theme/components.js +25 -27
  151. package/dist/theme/components.js.map +1 -1
  152. package/dist/tsconfig.production.tsbuildinfo +1 -1
  153. package/dist/web-component/renderReactInWebComponent.js +6 -7
  154. package/dist/web-component/renderReactInWebComponent.js.map +1 -1
  155. package/package.json +3 -3
  156. package/src/{Button.tsx → Buttons/BaseButton.tsx} +48 -68
  157. package/src/{MenuButton.tsx → Buttons/BaseMenuButton.tsx} +94 -32
  158. package/src/Buttons/Button.tsx +30 -0
  159. package/src/Buttons/MenuButton.tsx +35 -0
  160. package/src/{MenuItem.tsx → Buttons/MenuItem.tsx} +1 -1
  161. package/src/Buttons/index.ts +22 -0
  162. package/src/Card.tsx +1 -3
  163. package/src/DataTable/DataTable.tsx +1 -2
  164. package/src/DataTable/DataTableRowActions.tsx +1 -3
  165. package/src/DataTable/DataTableSettings.tsx +1 -2
  166. package/src/Dialog.tsx +1 -1
  167. package/src/Drawer.tsx +1 -1
  168. package/src/FileUploader/FileUploader.tsx +1 -1
  169. package/src/Form.tsx +1 -1
  170. package/src/Pagination/Pagination.test.tsx +58 -36
  171. package/src/Pagination/Pagination.tsx +1 -1
  172. package/src/Toast.tsx +1 -1
  173. package/src/index.ts +1 -3
  174. package/src/labs/AppSwitcher/AppSwitcher.tsx +94 -0
  175. package/src/labs/AppSwitcher/AppSwitcherApp.tsx +146 -0
  176. package/src/labs/{SideNav → AppSwitcher}/OktaAura.tsx +19 -4
  177. package/src/labs/AppSwitcher/index.ts +13 -0
  178. package/src/labs/AppTile.tsx +171 -85
  179. package/src/labs/DataFilters.tsx +1 -1
  180. package/src/labs/DataTable.tsx +1 -1
  181. package/src/labs/DataTablePagination.tsx +1 -1
  182. package/src/labs/DataView/BulkActionsMenu.tsx +1 -2
  183. package/src/labs/DataView/DataCard.tsx +56 -31
  184. package/src/labs/DataView/DataView.tsx +1 -1
  185. package/src/labs/DataView/LayoutSwitcher.tsx +1 -2
  186. package/src/labs/DataView/RowActions.tsx +1 -3
  187. package/src/labs/DataView/TableLayoutContent.tsx +1 -2
  188. package/src/labs/DataView/TableSettings.tsx +1 -2
  189. package/src/labs/DatePicker.tsx +1 -1
  190. package/src/labs/SideNav/SideNav.tsx +10 -4
  191. package/src/labs/TopNav/UserProfile.tsx +26 -2
  192. package/src/labs/TopNav/UserProfileMenuButton.tsx +57 -0
  193. package/src/labs/TopNav/index.ts +1 -0
  194. package/src/labs/UiShell/UiShell.test.tsx +23 -38
  195. package/src/labs/UiShell/UiShell.tsx +14 -6
  196. package/src/labs/UiShell/UiShellContent.tsx +85 -16
  197. package/src/labs/UiShell/renderUiShell.test.tsx +21 -15
  198. package/src/labs/UiShell/renderUiShell.tsx +8 -1
  199. package/src/labs/index.ts +1 -0
  200. package/src/theme/components.tsx +25 -28
  201. package/src/web-component/renderReactInWebComponent.ts +10 -5
  202. package/dist/Button.js.map +0 -1
  203. package/dist/ButtonContext.js.map +0 -1
  204. package/dist/MenuButton.js.map +0 -1
  205. package/dist/MenuContext.js.map +0 -1
  206. package/dist/MenuItem.js.map +0 -1
  207. package/dist/labs/SideNav/OktaAura.js.map +0 -1
  208. package/dist/src/Button.d.ts.map +0 -1
  209. package/dist/src/ButtonContext.d.ts.map +0 -1
  210. package/dist/src/MenuButton.d.ts.map +0 -1
  211. package/dist/src/MenuContext.d.ts.map +0 -1
  212. package/dist/src/MenuItem.d.ts.map +0 -1
  213. package/dist/src/labs/SideNav/OktaAura.d.ts.map +0 -1
  214. /package/dist/{ButtonContext.js → Buttons/ButtonContext.js} +0 -0
  215. /package/dist/{MenuContext.js → Buttons/MenuContext.js} +0 -0
  216. /package/dist/{MenuItem.js → Buttons/MenuItem.js} +0 -0
  217. /package/dist/src/{ButtonContext.d.ts → Buttons/ButtonContext.d.ts} +0 -0
  218. /package/dist/src/{MenuContext.d.ts → Buttons/MenuContext.d.ts} +0 -0
  219. /package/dist/src/labs/{SideNav → AppSwitcher}/OktaAura.d.ts +0 -0
  220. /package/src/{ButtonContext.tsx → Buttons/ButtonContext.tsx} +0 -0
  221. /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
- }, [sideNavItems]);
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 = sideNavItems.flatMap((sideNavItem) =>
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
- }, [sideNavItems]);
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, scrollIntoViewRef]);
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 = ({ profileIcon, userName, orgName }: UserProfileProps) => {
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
- <Subordinate color="textPrimary">{userName}</Subordinate>
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 };
@@ -12,3 +12,4 @@
12
12
 
13
13
  export * from "./TopNav";
14
14
  export * from "./UserProfile";
15
+ export * from "./UserProfileMenuButton";
@@ -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 `appRootElement`", async () => {
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)).toBeInTheDocument();
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
- optionalComponentTestIds.forEach((testId) => {
110
- expect(within(container).getByTestId(testId)).toBeInTheDocument();
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)).toBeInTheDocument();
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).toBeInTheDocument();
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
- appName: "",
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<UiShellContentProps, "appComponent" | "onError" | "optionalComponents">;
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) => prop !== "odysseyDesignTokens",
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: Omit<TopNavProps, "leftSideComponent" | "rightSideComponent">;
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
- <ErrorBoundary fallback={null} onError={onError}>
147
- <TopNav
148
- {...topNavProps}
149
- isScrolled={isContentScrolled}
150
- leftSideComponent={optionalComponents?.topNavLeftSide}
151
- rightSideComponent={optionalComponents?.topNavRightSide}
152
- />
153
- </ErrorBoundary>
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
- tabIndex={0}
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
- expect(
130
- rootElement.querySelector(reactWebComponentElementName)!.shadowRoot,
131
- ).toHaveTextContent(appName);
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
- expect(
157
- rootElement.querySelector(reactWebComponentElementName)!.shadowRoot,
158
- ).toHaveTextContent(appName);
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
- consoleErrorSpy.mockRestore();
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
- expect(onError).toHaveBeenCalledTimes(1);
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.
package/src/labs/index.ts CHANGED
@@ -40,6 +40,7 @@ export {
40
40
  type GroupPickerProps,
41
41
  } from "./GroupPicker";
42
42
 
43
+ export * from "./AppSwitcher";
43
44
  export * from "./SideNav/NavAccordion";
44
45
  export * from "./SideNav";
45
46
  export * from "./TopNav";