From fac7853039b1c6d0b72edf7b876ae2ce47f82f45 Mon Sep 17 00:00:00 2001 From: Trevor Ing Date: Wed, 24 Apr 2024 11:58:09 -0400 Subject: [PATCH 01/20] feat: start layout component --- .../src/labs/OdysseyLayout.tsx | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 packages/odyssey-react-mui/src/labs/OdysseyLayout.tsx diff --git a/packages/odyssey-react-mui/src/labs/OdysseyLayout.tsx b/packages/odyssey-react-mui/src/labs/OdysseyLayout.tsx new file mode 100644 index 0000000000..bf1d453bbc --- /dev/null +++ b/packages/odyssey-react-mui/src/labs/OdysseyLayout.tsx @@ -0,0 +1,51 @@ +/*! + * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +import { memo, ReactElement, ReactNode } from "react"; + +import { Button } from "../Button"; +import { Drawer } from "../Drawer"; + +export type OdysseyLayoutProps = { + title?: string; + description?: string; + documentation?: { + link: string; + text: string; + }; + drawer?: ReactElement; + /** + * An optional Button object to be situated in the layout header. Should almost always be of variant `primary`. + */ + primaryCallToActionComponent?: ReactElement; + /** + * An optional Button object to be situated in the layout header, alongside the `callToActionPrimaryComponent`. + */ + secondaryCallToActionComponent?: ReactElement; + /** + * An optional Button object to be situated in the layout header, alongside the other two `callToAction` components. + */ + tertiaryCallToActionComponent?: ReactElement; + /** + * The content of the layout. May be a `string` or any other `ReactNode` or array of `ReactNode`s. + */ + children?: ReactNode; +}; + +const OdysseyLayout = ({ title }: OdysseyLayoutProps) => { + return <>{title}; +}; + +const MemoizedOdysseyLayout = memo(OdysseyLayout); +MemoizedOdysseyLayout.displayName = "OdysseyLayout"; + +export { MemoizedOdysseyLayout as OdysseyLayout }; From 0a1cd83bf74ca124ccc80beb1e6b3c11b9894b29 Mon Sep 17 00:00:00 2001 From: Trevor Ing Date: Thu, 25 Apr 2024 17:33:33 -0400 Subject: [PATCH 02/20] feat: more work on layout component --- packages/odyssey-react-mui/src/labs/Grid.tsx | 100 +++++++++++++++ .../src/labs/OdysseyLayout.tsx | 120 ++++++++++++++++-- packages/odyssey-react-mui/src/labs/index.ts | 3 + 3 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 packages/odyssey-react-mui/src/labs/Grid.tsx diff --git a/packages/odyssey-react-mui/src/labs/Grid.tsx b/packages/odyssey-react-mui/src/labs/Grid.tsx new file mode 100644 index 0000000000..78b83525ec --- /dev/null +++ b/packages/odyssey-react-mui/src/labs/Grid.tsx @@ -0,0 +1,100 @@ +/*! + * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +import styled from "@emotion/styled"; +import { ReactNode, memo } from "react"; +import { + DesignTokens, + useOdysseyDesignTokens, +} from "../OdysseyDesignTokensContext"; +import { Box } from "../Box"; + +type SupportedColumnRatios = + | [1] + | [1, 1] + | [2, 1] + | [1, 2] + | [3, 1] + | [1, 3] + | [1, 1, 1] + | [1, 2, 1] + | [2, 1, 1] + | [1, 1, 2] + | [1, 1, 1, 1]; + +export type GridProps = { + columns: SupportedColumnRatios; + /** + * The content of the layout. May be a `string` or any other `ReactNode` or array of `ReactNode`s. + */ + children?: ReactNode; +}; + +interface GridContentProps { + odysseyDesignTokens: DesignTokens; + columns: string; +} + +const GridContent = styled("div", { + shouldForwardProp: (prop) => + !["odysseyDesignTokens", "columns"].includes(prop), +})(({ columns }) => ({ + maxWidth: "1440px", + display: "grid", + gridTemplateColumns: columns, + gridColumnGap: "16px", + columnGap: "16px", +})); + +const Grid = ({ columns, children }: GridProps) => { + const odysseyDesignTokens = useOdysseyDesignTokens(); + const mappedColumns = columns.map((col) => `minmax(0, ${col}fr)`).join(" "); + + return ( + + {Array.isArray(children) ? ( + children.map((child, idx) => { + return ( + + {child} + + ); + }) + ) : ( + + {children} + + )} + + ); +}; + +const MemoizedGrid = memo(Grid); +MemoizedGrid.displayName = "Grid"; + +export { MemoizedGrid as Grid }; diff --git a/packages/odyssey-react-mui/src/labs/OdysseyLayout.tsx b/packages/odyssey-react-mui/src/labs/OdysseyLayout.tsx index bf1d453bbc..31594ccbf8 100644 --- a/packages/odyssey-react-mui/src/labs/OdysseyLayout.tsx +++ b/packages/odyssey-react-mui/src/labs/OdysseyLayout.tsx @@ -11,9 +11,15 @@ */ import { memo, ReactElement, ReactNode } from "react"; - -import { Button } from "../Button"; -import { Drawer } from "../Drawer"; +import { Box } from "../Box"; +import styled from "@emotion/styled"; +import { + DesignTokens, + useOdysseyDesignTokens, +} from "../OdysseyDesignTokensContext"; +import { Heading4, Subordinate } from "../Typography"; +import { Link } from "../Link"; +import { DocumentationIcon } from "../icons.generated"; export type OdysseyLayoutProps = { title?: string; @@ -22,27 +28,123 @@ export type OdysseyLayoutProps = { link: string; text: string; }; - drawer?: ReactElement; + drawer?: ReactElement; /** * An optional Button object to be situated in the layout header. Should almost always be of variant `primary`. */ - primaryCallToActionComponent?: ReactElement; + primaryCallToActionComponent?: ReactElement; /** * An optional Button object to be situated in the layout header, alongside the `callToActionPrimaryComponent`. */ - secondaryCallToActionComponent?: ReactElement; + secondaryCallToActionComponent?: ReactElement; /** * An optional Button object to be situated in the layout header, alongside the other two `callToAction` components. */ - tertiaryCallToActionComponent?: ReactElement; + tertiaryCallToActionComponent?: ReactElement; /** * The content of the layout. May be a `string` or any other `ReactNode` or array of `ReactNode`s. */ children?: ReactNode; }; -const OdysseyLayout = ({ title }: OdysseyLayoutProps) => { - return <>{title}; +interface LayoutContentProps { + odysseyDesignTokens?: DesignTokens; + isDrawerVisible?: boolean; +} + +const LayoutHeader = styled("div", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})<{ + odysseyDesignTokens: DesignTokens; +}>(({}) => ({ + position: "sticky", + top: 0, + display: "flex", + justifyContent: "space-between", + alignItems: "center", + alignContent: "center", +})); + +const LayoutContent = styled("div", { + shouldForwardProp: (prop) => + !["odysseyDesignTokens", "isDrawerVisible"].includes(prop), +})(({ isDrawerVisible }) => ({ + "@keyframes animate-drawer-open": { + "0%": { + gridTemplateColumns: "minmax(0, 1fr) 0", + }, + "100%": { + gridTemplateColumns: "minmax(0, 1fr) 360px", + }, + }, + "@keyframes animate-drawer-close": { + "0%": { + gridTemplateColumns: "minmax(0, 1fr) 360px", + }, + "100%": { + gridTemplateColumns: "minmax(0, 1fr) 0", + }, + }, + display: "grid", + gridColumnGap: "16px", + columnGap: "16px", + gridTemplateColumns: isDrawerVisible + ? "minmax(0, 1fr) 360px" + : "minmax(0, 1fr)", + animation: isDrawerVisible + ? "animate-drawer-open 225ms cubic-bezier(0, 0, 0.2, 1)" + : "animate-drawer-close 225ms cubic-bezier(0, 0, 0.2, 1)", + marginBlock: "32px", +})); + +const OdysseyLayout = ({ + title, + description, + documentation, + primaryCallToActionComponent, + secondaryCallToActionComponent, + tertiaryCallToActionComponent, + children, + drawer, +}: OdysseyLayoutProps) => { + const odysseyDesignTokens = useOdysseyDesignTokens(); + + return ( + + + + {title} + {description} + + + {documentation && ( + }> + {documentation.text} + + )} + + {tertiaryCallToActionComponent} + {secondaryCallToActionComponent} + {primaryCallToActionComponent} + + + + + {children} + {drawer} + + + ); }; const MemoizedOdysseyLayout = memo(OdysseyLayout); diff --git a/packages/odyssey-react-mui/src/labs/index.ts b/packages/odyssey-react-mui/src/labs/index.ts index e6710d69b8..ad2463a15e 100644 --- a/packages/odyssey-react-mui/src/labs/index.ts +++ b/packages/odyssey-react-mui/src/labs/index.ts @@ -30,3 +30,6 @@ export * from "./PaginatedTable"; export * from "./GroupPicker"; export * from "./Switch"; + +export * from "./OdysseyLayout"; +export * from "./Grid"; From 2f7ca88e9e12101442f7bf88251a3b90873fc25d Mon Sep 17 00:00:00 2001 From: Trevor Ing Date: Sat, 27 Apr 2024 16:41:12 -0400 Subject: [PATCH 03/20] feat: documentation and syntax updates --- packages/odyssey-react-mui/src/labs/Grid.tsx | 63 +++-- .../src/labs/OdysseyLayout.tsx | 101 +++++--- .../OdysseyLayout/OdysseyLayout.stories.tsx | 225 ++++++++++++++++++ 3 files changed, 316 insertions(+), 73 deletions(-) create mode 100644 packages/odyssey-storybook/src/components/odyssey-labs/OdysseyLayout/OdysseyLayout.stories.tsx diff --git a/packages/odyssey-react-mui/src/labs/Grid.tsx b/packages/odyssey-react-mui/src/labs/Grid.tsx index 78b83525ec..8fbc87c739 100644 --- a/packages/odyssey-react-mui/src/labs/Grid.tsx +++ b/packages/odyssey-react-mui/src/labs/Grid.tsx @@ -11,20 +11,19 @@ */ import styled from "@emotion/styled"; -import { ReactNode, memo } from "react"; +import { Children, ReactNode, memo } from "react"; import { DesignTokens, useOdysseyDesignTokens, } from "../OdysseyDesignTokensContext"; -import { Box } from "../Box"; type SupportedColumnRatios = | [1] | [1, 1] - | [2, 1] | [1, 2] - | [3, 1] + | [2, 1] | [1, 3] + | [3, 1] | [1, 1, 1] | [1, 2, 1] | [2, 1, 1] @@ -32,9 +31,14 @@ type SupportedColumnRatios = | [1, 1, 1, 1]; export type GridProps = { + /** + * The supported column ratios for the Grid. Each number is a fractional unit that is mapped to the 'fr' CSS unit. + * e.g. [2, 1] defines a 2/3, 1/3 layout and [1, 2, 1] defines a 1/4, 1/2, 1/4 layout + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Basic_concepts_of_grid_layout#the_fr_unit + */ columns: SupportedColumnRatios; /** - * The content of the layout. May be a `string` or any other `ReactNode` or array of `ReactNode`s. + * The content of the Grid. May be a `string` or any other `ReactNode` or array of `ReactNode`s. */ children?: ReactNode; }; @@ -47,12 +51,22 @@ interface GridContentProps { const GridContent = styled("div", { shouldForwardProp: (prop) => !["odysseyDesignTokens", "columns"].includes(prop), -})(({ columns }) => ({ +})(({ odysseyDesignTokens, columns }) => ({ maxWidth: "1440px", display: "grid", gridTemplateColumns: columns, - gridColumnGap: "16px", - columnGap: "16px", + gridColumnGap: odysseyDesignTokens.Spacing4, + columnGap: odysseyDesignTokens.Spacing4, +})); + +const GridPane = styled("div", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})<{ + odysseyDesignTokens: DesignTokens; +}>(({ odysseyDesignTokens }) => ({ + backgroundColor: odysseyDesignTokens.HueNeutralWhite, + borderRadius: odysseyDesignTokens.Spacing4, + padding: odysseyDesignTokens.Spacing4, })); const Grid = ({ columns, children }: GridProps) => { @@ -64,32 +78,13 @@ const Grid = ({ columns, children }: GridProps) => { odysseyDesignTokens={odysseyDesignTokens} columns={mappedColumns} > - {Array.isArray(children) ? ( - children.map((child, idx) => { - return ( - - {child} - - ); - }) - ) : ( - - {children} - - )} + {Children.toArray(children).map((child, index) => { + return ( + + {child} + + ); + })} ); }; diff --git a/packages/odyssey-react-mui/src/labs/OdysseyLayout.tsx b/packages/odyssey-react-mui/src/labs/OdysseyLayout.tsx index 31594ccbf8..c4d196068b 100644 --- a/packages/odyssey-react-mui/src/labs/OdysseyLayout.tsx +++ b/packages/odyssey-react-mui/src/labs/OdysseyLayout.tsx @@ -22,12 +22,25 @@ import { Link } from "../Link"; import { DocumentationIcon } from "../icons.generated"; export type OdysseyLayoutProps = { + /** + * The title of the layout to be situated in the layout header + */ title?: string; + /** + * A supplementary description to be situated in the layout header + */ description?: string; - documentation?: { - link: string; - text: string; - }; + /** + * The destination for a documentation Link to be situated in the layout header + */ + documentationLink?: string; + /** + * The text for a documentation Link to be situated in the layout header + */ + documentationText?: string; + /** + * An optional Drawer object. Can be of variant 'temporary' or 'persistent'. + */ drawer?: ReactElement; /** * An optional Button object to be situated in the layout header. Should almost always be of variant `primary`. @@ -48,8 +61,9 @@ export type OdysseyLayoutProps = { }; interface LayoutContentProps { - odysseyDesignTokens?: DesignTokens; - isDrawerVisible?: boolean; + odysseyDesignTokens: DesignTokens; + isDrawerOpen?: boolean; + drawerVariant?: string; } const LayoutHeader = styled("div", { @@ -67,40 +81,45 @@ const LayoutHeader = styled("div", { const LayoutContent = styled("div", { shouldForwardProp: (prop) => - !["odysseyDesignTokens", "isDrawerVisible"].includes(prop), -})(({ isDrawerVisible }) => ({ - "@keyframes animate-drawer-open": { - "0%": { - gridTemplateColumns: "minmax(0, 1fr) 0", - }, - "100%": { - gridTemplateColumns: "minmax(0, 1fr) 360px", + !["odysseyDesignTokens", "isDrawerOpen", "drawerVariant"].includes(prop), +})( + ({ odysseyDesignTokens, isDrawerOpen, drawerVariant }) => ({ + "@keyframes animate-drawer-open": { + "0%": { + gridTemplateColumns: "minmax(0, 1fr) 0", + }, + "100%": { + gridTemplateColumns: "minmax(0, 1fr) 360px", + }, }, - }, - "@keyframes animate-drawer-close": { - "0%": { - gridTemplateColumns: "minmax(0, 1fr) 360px", + "@keyframes animate-drawer-close": { + "0%": { + gridTemplateColumns: "minmax(0, 1fr) 360px", + }, + "100%": { + gridTemplateColumns: "minmax(0, 1fr) 0", + }, }, - "100%": { - gridTemplateColumns: "minmax(0, 1fr) 0", - }, - }, - display: "grid", - gridColumnGap: "16px", - columnGap: "16px", - gridTemplateColumns: isDrawerVisible - ? "minmax(0, 1fr) 360px" - : "minmax(0, 1fr)", - animation: isDrawerVisible - ? "animate-drawer-open 225ms cubic-bezier(0, 0, 0.2, 1)" - : "animate-drawer-close 225ms cubic-bezier(0, 0, 0.2, 1)", - marginBlock: "32px", -})); + display: "grid", + gridGap: odysseyDesignTokens.Spacing4, + gap: odysseyDesignTokens.Spacing4, + gridTemplateColumns: + drawerVariant === "persistent" && isDrawerOpen + ? "minmax(0, 1fr) 360px" + : "minmax(0, 1fr)", + animation: + drawerVariant === "persistent" && isDrawerOpen + ? "animate-drawer-open 225ms cubic-bezier(0, 0, 0.2, 1)" + : "animate-drawer-close 225ms cubic-bezier(0, 0, 0.2, 1)", + marginBlock: odysseyDesignTokens.Spacing6, + }), +); const OdysseyLayout = ({ title, description, - documentation, + documentationLink, + documentationText, primaryCallToActionComponent, secondaryCallToActionComponent, tertiaryCallToActionComponent, @@ -108,6 +127,7 @@ const OdysseyLayout = ({ drawer, }: OdysseyLayoutProps) => { const odysseyDesignTokens = useOdysseyDesignTokens(); + const { isOpen: isDrawerOpen, variant: drawerVariant } = drawer?.props ?? {}; return ( @@ -124,9 +144,9 @@ const OdysseyLayout = ({ gap: "16px", }} > - {documentation && ( - }> - {documentation.text} + {documentationLink && ( + }> + {documentationText} )} @@ -138,9 +158,12 @@ const OdysseyLayout = ({ - {children} + + {children} + {drawer} diff --git a/packages/odyssey-storybook/src/components/odyssey-labs/OdysseyLayout/OdysseyLayout.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-labs/OdysseyLayout/OdysseyLayout.stories.tsx new file mode 100644 index 0000000000..ed5c5660a9 --- /dev/null +++ b/packages/odyssey-storybook/src/components/odyssey-labs/OdysseyLayout/OdysseyLayout.stories.tsx @@ -0,0 +1,225 @@ +/*! + * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +import { Meta, StoryObj } from "@storybook/react"; + +import { MuiThemeDecorator } from "../../../../.storybook/components"; +import { + Planet, + columns as planetColumns, + data as planetData, +} from "../../odyssey-mui/DataTable/planetData"; +import { + Grid, + OdysseyLayout, + OdysseyLayoutProps, +} from "@okta/odyssey-react-mui/labs"; +import { Box, Button, DataTable } from "@okta/odyssey-react-mui"; +import { useCallback, useState } from "react"; + +// const drawerLongText = ( +// <> +//
+// Okta Privileged Access is a Privileged Access Management (PAM) solution +// designed to help customers mitigate the risk of unauthorized access to +// resources, a critical area of security and risk management in any +// organization. Okta Privileged Access builds on the current server access +// control capabilities provided with Okta Advanced Server Access and +// delivers a unified approach to managing access to all your privileged +// accounts. It securely connects people, machines, and applications to +// privileged resources such as servers, containers, and enterprise apps. +//
+//
+// A critical capability that Okta Privileged Access offers is the separation +// of administrative roles and responsibilities. Management of users and +// groups, resources, and security are separated, with each administrative +// role designed to perform a specific function. For example, the management +// of security policies to access resources is separated and decoupled from +// the administration of the resources. To meet this requirement, the team +// that sets the policy is separated from the team that administers the +// resource. Likewise, the administrator managing users and groups can only +// perform user and group management tasks and isn't involved in +// administering resources or creating security policies. +//
+//
+// The level of access within a Okta Privileged Access team depends on the +// role that you're assigned and the permissions granted to that role. The +// table below discusses the types of roles, and each has a unique set of +// permissions and restrictions.To start using Okta Privileged Access, you +// need to add the Okta Privileged Access OIN application to your Okta org. +// You can then sync your users and groups from the Okta Universal Directory +// by configuring SCIM. End users must install the Okta Privileged Access +// client in their local machine, enroll the client, and then access their +// dashboard using the link provided by their team administrator. +//
+// +// ); + +const drawerShortText = ( +
+ Okta Privileged Access is a Privileged Access Management (PAM) solution + designed to help customers mitigate the risk of unauthorized access to + resources, a critical area of security and risk management in any + organization. Okta Privileged Access builds on the current server access + control capabilities provided with Okta Advanced Server Access and delivers + a unified approach to managing access to all your privileged accounts. It + securely connects people, machines, and applications to privileged resources + such as servers, containers, and enterprise apps. +
+); + +const storybookMeta: Meta = { + title: "Labs Components/OdysseyLayout", + component: OdysseyLayout, + argTypes: { + title: { + control: "text", + table: { + type: { + required: true, + summary: "string", + }, + }, + type: { + required: true, + name: "string", + }, + }, + }, + decorators: [MuiThemeDecorator], + parameters: { + backgrounds: { + default: "gray", + values: [ + { name: "gray", value: "#f4f4f4" }, + { name: "white", value: "#ffffff" }, + ], + }, + }, +}; + +export default storybookMeta; + +export const Basic: StoryObj = { + args: {}, + render: function C() { + const [data] = useState(planetData); + const [isOverlayDrawerVisible, setIsOverlayVisible] = useState(false); + const [isEmbeddedDrawerVisible, setIsEmbeddedVisible] = useState(false); + + const getData = useCallback(() => { + return data; + }, [data]); + + const onOpenOverlayDrawer = useCallback(() => { + setIsOverlayVisible(true); + }, []); + + const onCloseOverlayDrawer = useCallback(() => { + setIsOverlayVisible(false); + }, []); + + const onOpenEmbeddedDrawer = useCallback(() => { + setIsEmbeddedVisible(true); + }, []); + + const onCloseEmbeddedDrawer = useCallback(() => { + setIsEmbeddedVisible(false); + }, []); + + return ( + + } + secondaryCallToActionComponent={ +