From 451388d18c636889a2ed5abee9d40418a980c175 Mon Sep 17 00:00:00 2001 From: trevoring-okta <106110543+trevoring-okta@users.noreply.github.com> Date: Mon, 22 Jul 2024 15:25:21 -0400 Subject: [PATCH] PageTemplate and Layout component (#2219) OKTA-721245 feat: start layout component feat: more work on layout component feat: documentation and syntax updates fix: update to storybook and component feat: add more to Storybook fix: remove unncessary MUI ScopedCssBaseline feat: add full-width story feat(odyssey-react-mui): create surface component feat(odyssey-react-mui): create stories for Grid feat(odyssey-react-mui): remove surface styling from Grid fix: small nit fixes feat(odyssey-react-mui): panes => regions fix(odyssey-react-mui): rename components and add disclaimer fix: standardize vertical layout distance refactor: update css refactor: alphabetize the imports feat: add rudimentary responsiveness to Layout refactor: improve nested selectors refactor: update based on code review Merge branch 'main' into ti-OKTA-721245-layout-component fix: update redundant type export --- packages/odyssey-react-mui/src/Surface.tsx | 48 ++ packages/odyssey-react-mui/src/index.ts | 1 + .../src/labs/DataComponents/DataView.tsx | 6 +- .../labs/DataComponents/LayoutSwitcher.tsx | 10 +- .../src/labs/DataComponents/componentTypes.ts | 6 +- .../odyssey-react-mui/src/labs/Layout.tsx | 85 +++ .../src/labs/PageTemplate.tsx | 225 ++++++ packages/odyssey-react-mui/src/labs/index.ts | 2 + .../components/MuiThemeDecorator.tsx | 5 +- .../odyssey-labs/Layout/Layout.stories.tsx | 326 ++++++++ .../PageTemplate/PageTemplate.mdx | 25 + .../PageTemplate/PageTemplate.stories.tsx | 721 ++++++++++++++++++ 12 files changed, 1445 insertions(+), 15 deletions(-) create mode 100644 packages/odyssey-react-mui/src/Surface.tsx create mode 100644 packages/odyssey-react-mui/src/labs/Layout.tsx create mode 100644 packages/odyssey-react-mui/src/labs/PageTemplate.tsx create mode 100644 packages/odyssey-storybook/src/components/odyssey-labs/Layout/Layout.stories.tsx create mode 100644 packages/odyssey-storybook/src/components/odyssey-labs/PageTemplate/PageTemplate.mdx create mode 100644 packages/odyssey-storybook/src/components/odyssey-labs/PageTemplate/PageTemplate.stories.tsx diff --git a/packages/odyssey-react-mui/src/Surface.tsx b/packages/odyssey-react-mui/src/Surface.tsx new file mode 100644 index 0000000000..d1f3c3e4dc --- /dev/null +++ b/packages/odyssey-react-mui/src/Surface.tsx @@ -0,0 +1,48 @@ +/*! + * Copyright (c) 2022-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, ReactNode } from "react"; +import styled from "@emotion/styled"; +import { Paper as MuiPaper } from "@mui/material"; + +import { + DesignTokens, + useOdysseyDesignTokens, +} from "./OdysseyDesignTokensContext"; + +const StyledContainer = styled(MuiPaper, { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})<{ + odysseyDesignTokens: DesignTokens; +}>(({ odysseyDesignTokens }) => ({ + borderRadius: odysseyDesignTokens.Spacing4, + padding: odysseyDesignTokens.Spacing4, +})); + +export type SurfaceProps = { + children: ReactNode; +}; + +const Surface = ({ children }: SurfaceProps) => { + const odysseyDesignTokens = useOdysseyDesignTokens(); + + return ( + + {children} + + ); +}; + +const MemoizedSurface = memo(Surface); +MemoizedSurface.displayName = "Surface"; + +export { MemoizedSurface as Surface }; diff --git a/packages/odyssey-react-mui/src/index.ts b/packages/odyssey-react-mui/src/index.ts index 305d1c9692..3cd876ceb2 100644 --- a/packages/odyssey-react-mui/src/index.ts +++ b/packages/odyssey-react-mui/src/index.ts @@ -96,6 +96,7 @@ export * from "./ScreenReaderText"; export * from "./SearchField"; export * from "./Select"; export * from "./Status"; +export * from "./Surface"; export * from "./Tabs"; export * from "./Tag"; export * from "./TagList"; diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/DataView.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/DataView.tsx index 8c5adc1c82..32b4a9389d 100644 --- a/packages/odyssey-react-mui/src/labs/DataComponents/DataView.tsx +++ b/packages/odyssey-react-mui/src/labs/DataComponents/DataView.tsx @@ -23,7 +23,7 @@ import { densityValues, } from "./constants"; import { - Layout, + DataLayout, UniversalProps, ViewProps, TableState, @@ -47,7 +47,7 @@ import { } from "../../OdysseyDesignTokensContext"; import styled from "@emotion/styled"; -export type DataViewProps = UniversalProps & ViewProps; +export type DataViewProps = UniversalProps & ViewProps; const DataViewContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", @@ -100,7 +100,7 @@ const DataView = ({ const odysseyDesignTokens = useOdysseyDesignTokens(); const { t } = useTranslation(); - const [currentLayout, setCurrentLayout] = useState( + const [currentLayout, setCurrentLayout] = useState( initialLayout ?? availableLayouts[0], ); diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/LayoutSwitcher.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/LayoutSwitcher.tsx index ab5c704387..c292a977fe 100644 --- a/packages/odyssey-react-mui/src/labs/DataComponents/LayoutSwitcher.tsx +++ b/packages/odyssey-react-mui/src/labs/DataComponents/LayoutSwitcher.tsx @@ -13,14 +13,14 @@ import { Dispatch, memo, useCallback, SetStateAction, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { AvailableLayouts, Layout } from "./componentTypes"; +import { AvailableLayouts, DataLayout } from "./componentTypes"; import { MenuButton } from "../../MenuButton"; import { MenuItem } from "../../MenuItem"; export type LayoutSwitcherProps = { availableLayouts: AvailableLayouts; - currentLayout: Layout; - setCurrentLayout: Dispatch>; + currentLayout: DataLayout; + setCurrentLayout: Dispatch>; }; const LayoutSwitcher = ({ @@ -31,7 +31,7 @@ const LayoutSwitcher = ({ const { t } = useTranslation(); const changeLayout = useCallback( - (value: Layout) => { + (value: DataLayout) => { setCurrentLayout(value); }, [setCurrentLayout], @@ -39,7 +39,7 @@ const LayoutSwitcher = ({ const memoizedMenuItems = useMemo( () => - availableLayouts.map((value: Layout) => ({ + availableLayouts.map((value: DataLayout) => ({ value, onClick: () => changeLayout(value), label: t(`dataview.layout.${value}`), diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/componentTypes.ts b/packages/odyssey-react-mui/src/labs/DataComponents/componentTypes.ts index 13ffde63bd..eb038a9a4c 100644 --- a/packages/odyssey-react-mui/src/labs/DataComponents/componentTypes.ts +++ b/packages/odyssey-react-mui/src/labs/DataComponents/componentTypes.ts @@ -37,10 +37,10 @@ import { paginationTypeValues } from "../DataTablePagination"; import { ReactNode } from "react"; import { StackCardProps } from "./StackCard"; -export type Layout = (typeof availableLayouts)[number]; +export type DataLayout = (typeof availableLayouts)[number]; export type StackLayout = (typeof availableStackLayouts)[number]; -export type AvailableLayouts = Layout[]; +export type AvailableLayouts = DataLayout[]; export type AvailableStackLayouts = StackLayout[]; export type UniversalProps = { @@ -97,7 +97,7 @@ export type StackProps = { rowActionMenuItems?: DataTableRowActionsProps["rowActionMenuItems"]; }; -export type ViewProps = { +export type ViewProps = { availableLayouts?: L[]; initialLayout?: L; stackOptions?: StackProps; diff --git a/packages/odyssey-react-mui/src/labs/Layout.tsx b/packages/odyssey-react-mui/src/labs/Layout.tsx new file mode 100644 index 0000000000..30ac1e1b6f --- /dev/null +++ b/packages/odyssey-react-mui/src/labs/Layout.tsx @@ -0,0 +1,85 @@ +/*! + * 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 { Children, ReactNode, memo } from "react"; +import styled from "@emotion/styled"; + +import { Box } from "../Box"; +import { + DesignTokens, + useOdysseyDesignTokens, +} from "../OdysseyDesignTokensContext"; + +type SupportedRegionRatios = + | [1] + | [1, 1] + | [1, 2] + | [2, 1] + | [1, 3] + | [3, 1] + | [1, 1, 1] + | [1, 1, 1, 1]; + +export type LayoutProps = { + /** + * The supported region 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, 1, 1] defines a 1/3, 1/3, 1/3 layout + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Basic_concepts_of_grid_layout#the_fr_unit + */ + regions: SupportedRegionRatios; + /** + * The content of the Grid. May be a `string` or any other `ReactNode` or array of `ReactNode`s. + */ + children?: ReactNode; +}; + +interface LayoutContentProps { + odysseyDesignTokens: DesignTokens; + regions: string; +} + +const LayoutContent = styled("div", { + shouldForwardProp: (prop) => + !["odysseyDesignTokens", "regions"].includes(prop), +})(({ odysseyDesignTokens, regions }) => ({ + display: "grid", + gridTemplateColumns: regions, + gridColumnGap: odysseyDesignTokens.Spacing4, + columnGap: odysseyDesignTokens.Spacing4, + + "& + &": { + marginBlockStart: odysseyDesignTokens.Spacing4, + }, +})); + +const Layout = ({ regions, children }: LayoutProps) => { + const odysseyDesignTokens = useOdysseyDesignTokens(); + const mappedRegions = regions + .map((region) => `minmax(0, ${region}fr)`) + .join(" "); + + return ( + + + {Children.toArray(children).map((child) => child)} + + + ); +}; + +const MemoizedLayout = memo(Layout); +MemoizedLayout.displayName = "Layout"; + +export { MemoizedLayout as Layout }; diff --git a/packages/odyssey-react-mui/src/labs/PageTemplate.tsx b/packages/odyssey-react-mui/src/labs/PageTemplate.tsx new file mode 100644 index 0000000000..2d3e345f77 --- /dev/null +++ b/packages/odyssey-react-mui/src/labs/PageTemplate.tsx @@ -0,0 +1,225 @@ +/*! + * 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 styled from "@emotion/styled"; + +import { + DesignTokens, + useOdysseyDesignTokens, +} from "../OdysseyDesignTokensContext"; +import { DocumentationIcon } from "../icons.generated"; +import { Heading4, Subordinate } from "../Typography"; +import { Link } from "../Link"; + +export type PageTemplateProps = { + /** + * 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; + /** + * 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`. + */ + 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. Will often be `Grid` objects. + */ + children?: ReactNode; + /** + * When set to `true`, the layout expands past its max width of 1440px and spans the entire available screen width. + */ + isFullWidth?: boolean; +}; + +type TemplateContentProps = { + odysseyDesignTokens: DesignTokens; + isDrawerOpen?: boolean; + drawerVariant?: string; +}; + +const TemplateContainer = styled("div", { + shouldForwardProp: (prop) => + prop !== "odysseyDesignTokens" && prop !== "isFullWidth", +})<{ + odysseyDesignTokens: DesignTokens; + isFullWidth: boolean; +}>(({ odysseyDesignTokens, isFullWidth }) => ({ + maxWidth: isFullWidth + ? "100%" + : `calc(1440px + ${odysseyDesignTokens.Spacing6} + ${odysseyDesignTokens.Spacing6})`, + marginInline: isFullWidth ? odysseyDesignTokens.Spacing6 : "auto", + padding: odysseyDesignTokens.Spacing6, +})); + +const TemplateHeader = styled("div")(() => ({ + display: "flex", + alignItems: "flex-end", + justifyContent: "space-between", +})); + +const TemplateHeaderPrimaryContent = styled("div")(() => ({ + [".MuiTypography-root:last-child"]: { + marginBlockEnd: "0", + }, +})); + +const TemplateHeaderSecondaryContent = styled("div", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})<{ + odysseyDesignTokens: DesignTokens; +}>(({ odysseyDesignTokens }) => ({ + alignItems: "flex-end", + display: "flex", + flexDirection: "column", + gap: odysseyDesignTokens.Spacing2, + minHeight: odysseyDesignTokens.Spacing7, + justifyContent: "center", +})); + +const TemplateHeaderButtons = styled("div", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})<{ + odysseyDesignTokens: DesignTokens; +}>(({ odysseyDesignTokens }) => ({ + display: "flex", + gap: odysseyDesignTokens.Spacing2, +})); + +const TemplateContent = styled("div", { + shouldForwardProp: (prop) => + !["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", + }, + "100%": { + gridTemplateColumns: "minmax(0, 1fr) 0", + }, + }, + display: "grid", + gridGap: + drawerVariant === "persistent" && !isDrawerOpen + ? 0 + : odysseyDesignTokens.Spacing4, + gap: + drawerVariant === "persistent" && !isDrawerOpen + ? 0 + : odysseyDesignTokens.Spacing4, + marginBlock: odysseyDesignTokens.Spacing4, + gridTemplateColumns: + drawerVariant === "persistent" + ? isDrawerOpen + ? "minmax(0, 1fr) 360px" + : "minmax(0, 1fr) 0" + : "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)", + }), +); + +const PageTemplate = ({ + title, + description, + documentationLink, + documentationText, + primaryCallToActionComponent, + secondaryCallToActionComponent, + tertiaryCallToActionComponent, + children, + drawer, + isFullWidth = false, +}: PageTemplateProps) => { + const odysseyDesignTokens = useOdysseyDesignTokens(); + const { isOpen: isDrawerOpen, variant: drawerVariant } = drawer?.props ?? {}; + + return ( + + + + {title && {title}} + {description && {description}} + + + + {documentationLink && ( + }> + {documentationText} + + )} + {(primaryCallToActionComponent || + secondaryCallToActionComponent || + tertiaryCallToActionComponent) && ( + + {tertiaryCallToActionComponent} + {secondaryCallToActionComponent} + {primaryCallToActionComponent} + + )} + + + + {children} + {drawer} + + + ); +}; + +const MemoizedPageTemplate = memo(PageTemplate); +MemoizedPageTemplate.displayName = "PageTemplate"; + +export { MemoizedPageTemplate as PageTemplate }; diff --git a/packages/odyssey-react-mui/src/labs/index.ts b/packages/odyssey-react-mui/src/labs/index.ts index c6b2208783..5101a6c3d7 100644 --- a/packages/odyssey-react-mui/src/labs/index.ts +++ b/packages/odyssey-react-mui/src/labs/index.ts @@ -23,9 +23,11 @@ export * from "./DataComponents"; export * from "./DataTablePagination"; export * from "./DataFilters"; export * from "./FileUpload"; +export * from "./Layout"; export * from "./materialReactTableTypes"; /** @deprecated Will be removed in a future Odyssey version in lieu of DataTable */ export * from "./StaticTable"; +export * from "./PageTemplate"; /** @deprecated Will be removed in a future Odyssey version in lieu of DataTable */ export * from "./PaginatedTable"; diff --git a/packages/odyssey-storybook/.storybook/components/MuiThemeDecorator.tsx b/packages/odyssey-storybook/.storybook/components/MuiThemeDecorator.tsx index 9877161ef1..0afdafb0db 100644 --- a/packages/odyssey-storybook/.storybook/components/MuiThemeDecorator.tsx +++ b/packages/odyssey-storybook/.storybook/components/MuiThemeDecorator.tsx @@ -3,7 +3,6 @@ import { CssBaseline, OdysseyProvider, } from "@okta/odyssey-react-mui"; -import { ScopedCssBaseline } from "@mui/material"; import { ThemeProvider as StorybookThemeProvider } from "@storybook/theming"; import type { Decorator } from "@storybook/react"; import * as odysseyTokens from "@okta/odyssey-design-tokens"; @@ -25,9 +24,7 @@ export const MuiThemeDecorator: Decorator = (Story, context) => {
- - - +
diff --git a/packages/odyssey-storybook/src/components/odyssey-labs/Layout/Layout.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-labs/Layout/Layout.stories.tsx new file mode 100644 index 0000000000..ca7a2e839b --- /dev/null +++ b/packages/odyssey-storybook/src/components/odyssey-labs/Layout/Layout.stories.tsx @@ -0,0 +1,326 @@ +/*! + * 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 styled from "@emotion/styled"; + +import { Layout, LayoutProps } from "@okta/odyssey-react-mui/labs"; +import { Subordinate } from "@okta/odyssey-react-mui"; +import { MuiThemeDecorator } from "../../../../.storybook/components"; +// import { Surface } from "@okta/odyssey-react-mui"; + +const VisibleRegion = styled.div({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "16px", + border: "1px dashed #cbcbcb", +}); + +const RegionLabel = styled.h3({ + margin: 0, +}); + +const DisclaimerContainer = styled.div({ + maxWidth: "55ch", + marginBlockEnd: "8px", +}); + +const RegionDisclaimer = () => { + return ( + + + NOTE: Dashed border and padding are applied to show + region boundries in Storybook only. They will not be present when you + are using Layout + + + ); +}; + +const storybookMeta: Meta = { + title: "Labs Components/Layout", + component: Layout, + argTypes: { + regions: { + control: "text", + description: + "The supported region ratios for the `Layout`. Each number is a fractional unit that is mapped to the 'fr' CSS unit. For example: [2, 1] defines a 2/3, 1/3 layout and [1, 1, 1] defines a 1/3, 1/3, 1/3 layout", + table: { + type: { + summary: "SupportedRegionRatios", + }, + }, + }, + children: { + control: null, + description: + "The content of the `Layout`. May be a `string` or any other `ReactNode` or array of `ReactNode`s. Will often be either a single `Surface` or multiple `Surface's", + table: { + type: { + summary: "ReactNode", + }, + }, + }, + }, + decorators: [ + (Story) => { + return ( + <> + + + + ); + }, + MuiThemeDecorator, + ], + parameters: { + backgrounds: { + default: "gray", + values: [ + { name: "gray", value: "#f4f4f4" }, + { name: "white", value: "#ffffff" }, + ], + }, + }, +}; + +export default storybookMeta; + +export const Single: StoryObj = { + args: { + regions: [1], + }, + render: function C(args) { + return ( + + + Region + + + ); + }, +}; + +export const Split: StoryObj = { + args: { + regions: [1, 1], + }, + render: function C(args) { + return ( + + + Region + + + Region + + + ); + }, +}; + +export const TwoThirdsStart: StoryObj = { + args: { + regions: [2, 1], + }, + render: function C(args) { + return ( + + + Region + + + Region + + + ); + }, +}; + +export const TwoThirdsEnd: StoryObj = { + args: { + regions: [1, 2], + }, + render: function C(args) { + return ( + + + Region + + + Region + + + ); + }, +}; + +export const ThreeFourthsStart: StoryObj = { + args: { + regions: [3, 1], + }, + render: function C(args) { + return ( + + + Region + + + Region + + + ); + }, +}; + +export const ThreeFourthsEnd: StoryObj = { + args: { + regions: [1, 3], + }, + render: function C(args) { + return ( + + + Region + + + Region + + + ); + }, +}; + +export const ThreeRegionSplit: StoryObj = { + args: { + regions: [1, 1, 1], + }, + render: function C(args) { + return ( + + + Region + + + Region + + + Region + + + ); + }, +}; + +export const FourRegionSplit: StoryObj = { + args: { + regions: [1, 1, 1, 1], + }, + render: function C(args) { + return ( + + + Region + + + Region + + + Region + + + Region + + + ); + }, +}; + +export const KitchenSink: StoryObj = { + render: function () { + return ( + <> + + + Region + + + + + Region + + + Region + + + + + Region + + + Region + + + + + Region + + + Region + + + + + Region + + + Region + + + + + Region + + + Region + + + + + Region + + + Region + + + Region + + + + + Region + + + Region + + + Region + + + Region + + + + ); + }, +}; diff --git a/packages/odyssey-storybook/src/components/odyssey-labs/PageTemplate/PageTemplate.mdx b/packages/odyssey-storybook/src/components/odyssey-labs/PageTemplate/PageTemplate.mdx new file mode 100644 index 0000000000..31b98445e2 --- /dev/null +++ b/packages/odyssey-storybook/src/components/odyssey-labs/PageTemplate/PageTemplate.mdx @@ -0,0 +1,25 @@ +import { + Canvas, + Meta, + Title, + Subtitle, + Description, + Primary, + Controls, + Stories, +} from "@storybook/addon-docs"; +import { Story } from "@storybook/blocks"; +import * as PageTemplateStories from "./PageTemplate.stories"; + + + + +<Subtitle of={PageTemplateStories} /> +<Description of={PageTemplateStories} /> + +`PageTemplate` is a multi-purpose container that enforces Odyssey layout conventions, ensuring that +your screens look consistent without you doing extra work. + +If you're an Okta employee, you can see the [Figma here](https://www.figma.com/design/jMsVlc6QrA5dQXqPEEVT7S/%F0%9F%93%A6-Odyssey-Templates?node-id=1862-40643). + +<Controls /> diff --git a/packages/odyssey-storybook/src/components/odyssey-labs/PageTemplate/PageTemplate.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-labs/PageTemplate/PageTemplate.stories.tsx new file mode 100644 index 0000000000..d05a52415a --- /dev/null +++ b/packages/odyssey-storybook/src/components/odyssey-labs/PageTemplate/PageTemplate.stories.tsx @@ -0,0 +1,721 @@ +/*! + * 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 { + Person, + columns as personColumns, + data as personData, +} from "../../odyssey-mui/DataTable/personData"; +import { + Layout, + PageTemplate, + PageTemplateProps, +} from "@okta/odyssey-react-mui/labs"; +import { + Box, + Button, + Checkbox, + DataTable, + Dialog, + Drawer, + Form as OdysseyForm, + Heading5, + MenuButton, + MenuItem, + Paragraph, + Support, + Surface, + TextField, + useOdysseyDesignTokens, +} from "@okta/odyssey-react-mui"; +import { useCallback, useState } from "react"; + +const drawerLongText = ( + <> + <div> + 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. + </div> + <div> + 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. + </div> + <div> + 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. + </div> + </> +); + +const drawerShortText = ( + <div> + 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. + </div> +); + +const storybookMeta: Meta<PageTemplateProps> = { + title: "Labs Components/PageTemplate", + component: PageTemplate, + argTypes: { + title: { + control: "text", + description: "The title to be situated in the `PageTemplate` header", + table: { + type: { + summary: "string", + }, + }, + }, + description: { + control: "text", + description: + "A supplementary description to be situated in the `PageTemplate` header", + table: { + type: { + summary: "string", + }, + }, + }, + documentationLink: { + control: "text", + description: + "The destination for a documentation `Link` to be situated in the `PageTemplate` header", + table: { + type: { + summary: "string", + }, + }, + }, + documentationText: { + control: "text", + description: + "The text for a documentation `Link` to be situated in the `PageTemplate` header", + table: { + type: { + summary: "string", + }, + }, + }, + drawer: { + control: null, + description: + "An optional `Drawer` object. Can be of variant 'temporary' or 'persistent'.", + table: { + type: { + summary: "ReactElement<typeof Drawer>", + }, + }, + }, + primaryCallToActionComponent: { + control: null, + description: + "An optional `Button` object to be situated in the `PageTemplate` header. Should almost always be of variant `primary`.", + table: { + type: { + summary: "ReactElement<typeof Button>", + }, + }, + }, + secondaryCallToActionComponent: { + control: null, + description: + "An optional `Button` object to be situated in the `PageTemplate` header, alongside the `callToActionPrimaryComponent`.", + table: { + type: { + summary: "ReactElement<typeof Button>", + }, + }, + }, + tertiaryCallToActionComponent: { + control: null, + description: + "An optional `Button` object to be situated in the `PageTemplate` header, alongside the other two `callToAction` components.", + table: { + type: { + summary: "ReactElement<typeof Button>", + }, + }, + }, + children: { + control: null, + description: + "The content of the `PageTemplate`. May be a `string` or any other `ReactNode` or array of `ReactNode`s. Will often be `Grid` objects.", + table: { + type: { + summary: "ReactNode", + }, + }, + }, + isFullWidth: { + control: "boolean", + description: + "When set to `true`, the `PageTemplate` expands past its max width of 1440px and spans the entire available screen width.", + table: { + type: { + summary: "boolean", + }, + defaultValue: { + summary: false, + }, + }, + }, + }, + decorators: [MuiThemeDecorator], + parameters: { + backgrounds: { + default: "gray", + values: [ + { name: "gray", value: "#f4f4f4" }, + { name: "white", value: "#ffffff" }, + ], + }, + }, +}; + +export default storybookMeta; + +export const KitchenSink: StoryObj<PageTemplateProps> = { + args: { + title: "Table title", + description: "Optional brief description about the page", + documentationLink: "https://www.okta.com", + documentationText: "Documentation", + isFullWidth: false, + }, + render: function C(args) { + const [data] = useState<Person[]>(personData.slice(0, 10)); + const [isOverlayDrawerVisible, setIsOverlayVisible] = useState(false); + const [isDialogVisible, setIsDialogVisible] = useState(false); + + const getData = useCallback(() => { + return data; + }, [data]); + + const onOpenOverlayDrawer = useCallback(() => { + setIsOverlayVisible(true); + }, []); + + const onCloseOverlayDrawer = useCallback(() => { + setIsOverlayVisible(false); + }, []); + + const onOpenDialog = useCallback(() => { + setIsDialogVisible(true); + }, []); + + const onCloseDialog = useCallback(() => { + setIsDialogVisible(false); + }, []); + + return ( + <PageTemplate + title={args.title} + description={args.description} + documentationLink={args.documentationLink} + documentationText={args.documentationText} + primaryCallToActionComponent={ + <Button + label={ + isOverlayDrawerVisible + ? "Close overlay drawer" + : "Open overlay drawer" + } + variant="primary" + onClick={ + isOverlayDrawerVisible + ? onCloseOverlayDrawer + : onOpenOverlayDrawer + } + /> + } + secondaryCallToActionComponent={ + <Button + label={isDialogVisible ? "Close dialog" : "Open dialog"} + onClick={isDialogVisible ? onCloseDialog : onOpenDialog} + variant="secondary" + /> + } + tertiaryCallToActionComponent={ + <MenuButton + buttonLabel="More actions" + children={[ + <MenuItem key="1">View details</MenuItem>, + <MenuItem key="2">Edit configuration</MenuItem>, + <MenuItem key="3">Launch</MenuItem>, + ]} + /> + } + drawer={ + <Drawer + variant="temporary" + title="Drawer title" + primaryCallToActionComponent={ + <Button + label="Primary" + onClick={onCloseOverlayDrawer} + variant="primary" + /> + } + onClose={onCloseOverlayDrawer} + isOpen={isOverlayDrawerVisible} + showDividers + > + {drawerLongText} + </Drawer> + } + isFullWidth={args.isFullWidth} + > + <Dialog + title="Dialog title" + children="Consistently named a Leader by major analyst firms. Trusted by 15,000+ customers to secure digital interactions and accelerate innovation." + primaryCallToActionComponent={ + <Button + label="Button label" + onClick={onCloseDialog} + variant="primary" + /> + } + secondaryCallToActionComponent={ + <Button + label="Cancel" + onClick={onCloseDialog} + variant="secondary" + /> + } + onClose={onCloseDialog} + isOpen={isDialogVisible} + /> + <Layout regions={[3, 1]}> + <Surface> + <DataTable + columns={personColumns} + getData={getData} + hasSearch + hasFilters + totalRows={10} + /> + </Surface> + <Surface> + <h1>Another thing</h1> + </Surface> + </Layout> + </PageTemplate> + ); + }, +}; + +export const EmbeddedDrawer: StoryObj<PageTemplateProps> = { + args: { + title: "Table title", + description: "Optional brief description about the page", + documentationLink: "https://www.okta.com", + documentationText: "Documentation", + isFullWidth: false, + }, + render: function C(args) { + const [data] = useState<Person[]>(personData.slice(0, 10)); + const [isEmbeddedDrawerVisible, setIsEmbeddedVisible] = useState(false); + + const getData = useCallback(() => { + return data; + }, [data]); + + const onOpenEmbeddedDrawer = useCallback(() => { + setIsEmbeddedVisible(true); + }, []); + + const onCloseEmbeddedDrawer = useCallback(() => { + setIsEmbeddedVisible(false); + }, []); + + return ( + <PageTemplate + title={args.title} + description={args.description} + documentationLink={args.documentationLink} + documentationText={args.documentationText} + primaryCallToActionComponent={ + <Button + label={ + isEmbeddedDrawerVisible + ? "Close embedded drawer" + : "Open embedded drawer" + } + variant="primary" + onClick={ + isEmbeddedDrawerVisible + ? onCloseEmbeddedDrawer + : onOpenEmbeddedDrawer + } + /> + } + drawer={ + <Drawer + variant="persistent" + title="Drawer title" + primaryCallToActionComponent={ + <Button + label="Primary" + onClick={onCloseEmbeddedDrawer} + variant="primary" + /> + } + onClose={onCloseEmbeddedDrawer} + isOpen={isEmbeddedDrawerVisible} + showDividers + > + {drawerShortText} + </Drawer> + } + isFullWidth={args.isFullWidth} + > + <Layout regions={[1]}> + <Surface> + <DataTable + columns={personColumns} + getData={getData} + hasSearch + hasFilters + totalRows={10} + /> + </Surface> + </Layout> + </PageTemplate> + ); + }, +}; + +export const Form: StoryObj<PageTemplateProps> = { + args: { + title: "People", + description: "Optional brief description about the page", + documentationLink: "https://www.okta.com", + documentationText: "Help", + isFullWidth: false, + }, + render: function C(args) { + return ( + <PageTemplate + title={args.title} + description={args.description} + documentationLink={args.documentationLink} + documentationText={args.documentationText} + isFullWidth={args.isFullWidth} + primaryCallToActionComponent={ + <Button label="Reset passwords" variant="primary" /> + } + > + <Layout regions={[1]}> + <Surface> + <OdysseyForm + title="Add Person" + name="Add Person" + formActions={ + <> + <Button label="Reset" variant="secondary" /> + <Button type="submit" label="Submit" variant="primary" /> + </> + } + > + <TextField label="First name" /> + <TextField label="Last name" /> + <TextField label="Email" /> + </OdysseyForm> + </Surface> + </Layout> + </PageTemplate> + ); + }, +}; + +export const Dashboard: StoryObj<PageTemplateProps> = { + args: { + title: "Account", + documentationLink: "https://www.okta.com", + documentationText: "Help", + isFullWidth: false, + }, + render: function C(args) { + const organizationData = [ + { field: "Company name", value: "ACME Corporation" }, + { field: "Telephone number", value: "012-345-6789" }, + { field: "Address 1", value: undefined }, + { field: "Address 2", value: undefined }, + ]; + const endUserData = [ + { + field: "Technical contact", + value: "Add-Min O'Cloudy (admin@okta.com)", + }, + { field: "Support phone", value: "012-345-6789" }, + { field: "Help link", value: undefined }, + { field: "End User Help Form", value: undefined }, + ]; + const odysseyDesignTokens = useOdysseyDesignTokens(); + + return ( + <PageTemplate + title={args.title} + description={args.description} + documentationLink={args.documentationLink} + documentationText={args.documentationText} + isFullWidth={args.isFullWidth} + > + <Layout regions={[2, 1]}> + <Surface> + <Box + sx={{ + display: "flex", + justifyContent: "space-between", + alignItems: "flex-end", + }} + > + <Heading5>Organization Contact</Heading5> + <Button label="Edit" variant="floating" /> + </Box> + <Box + sx={{ + display: "flex", + flexDirection: "column", + rowGap: odysseyDesignTokens.Spacing4, + marginTop: odysseyDesignTokens.Spacing4, + }} + > + {organizationData.map((data, index) => { + return ( + <Box + key={index} + sx={{ + display: "flex", + justifyContent: "space-between", + }} + > + <Paragraph>{data.field}</Paragraph> + <Paragraph>{data.value}</Paragraph> + </Box> + ); + })} + </Box> + </Surface> + <Surface> + <Box + sx={{ + display: "flex", + justifyContent: "space-between", + alignItems: "flex-end", + }} + > + <Heading5>Billing information</Heading5> + <Button label="Edit" variant="floating" /> + </Box> + <Box + sx={{ + display: "flex", + flexDirection: "column", + rowGap: odysseyDesignTokens.Spacing2, + marginTop: odysseyDesignTokens.Spacing4, + }} + > + <Support> + The billing contact can be contacted by Okta for the purposes of + billing inquiries. + </Support> + <Box + sx={{ + display: "flex", + justifyContent: "space-between", + }} + > + <Paragraph>Billing contact</Paragraph> + <Paragraph>Add-Min O'Cloudy (admin@okta.com)</Paragraph> + </Box> + </Box> + </Surface> + </Layout> + <Layout regions={[1, 1, 1]}> + <Surface> + <Box + sx={{ + display: "flex", + justifyContent: "space-between", + alignItems: "flex-end", + }} + > + <Heading5>Give Access to Okta Support</Heading5> + <Button label="Edit" variant="floating" /> + </Box> + <Box + sx={{ + display: "flex", + flexDirection: "column", + rowGap: odysseyDesignTokens.Spacing2, + marginTop: odysseyDesignTokens.Spacing4, + }} + > + <Support> + For troubleshooting purposes, you can let Okta Support login to + your account as an administrator. + </Support> + <Box + sx={{ + display: "flex", + justifyContent: "space-between", + }} + > + <Paragraph>Okta Support access</Paragraph> + <Paragraph>Disabled</Paragraph> + </Box> + </Box> + </Surface> + <Surface> + <Box + sx={{ + display: "flex", + justifyContent: "space-between", + alignItems: "flex-end", + }} + > + <Heading5>End User Support</Heading5> + <Button label="Edit" variant="floating" /> + </Box> + <Box + sx={{ + display: "flex", + flexDirection: "column", + rowGap: odysseyDesignTokens.Spacing2, + marginTop: odysseyDesignTokens.Spacing4, + }} + > + <Support> + For troubleshooting purposes, you can let Okta Support login to + your account as an administrator. + </Support> + {endUserData.map((data, index) => { + return ( + <Box + key={index} + sx={{ + display: "flex", + justifyContent: "space-between", + }} + > + <Paragraph>{data.field}</Paragraph> + <Paragraph>{data.value}</Paragraph> + </Box> + ); + })} + </Box> + </Surface> + <Surface> + <Box + sx={{ + display: "flex", + justifyContent: "space-between", + alignItems: "flex-end", + }} + > + <Heading5>Third-Party Admins</Heading5> + <Button label="Edit" variant="floating" /> + </Box> + <Box + sx={{ + display: "flex", + flexDirection: "column", + rowGap: odysseyDesignTokens.Spacing2, + marginTop: odysseyDesignTokens.Spacing4, + }} + > + <Box + sx={{ + display: "flex", + justifyContent: "space-between", + columnGap: odysseyDesignTokens.Spacing8, + }} + > + <Paragraph>Manage Third-Party Admins</Paragraph> + <Checkbox label="This org contains third-party admins that need to be fully excluded from all Okta communications, including admin-related system notifications. Once enabled, you can exclude individual admins from communications by editing their admin record." /> + </Box> + </Box> + </Surface> + </Layout> + </PageTemplate> + ); + }, +}; + +export const FullWidth: StoryObj<PageTemplateProps> = { + args: { + title: "Full-width Table", + documentationLink: "https://www.okta.com", + documentationText: "Help", + isFullWidth: true, + }, + render: function C(args) { + const [data] = useState<Person[]>(personData.slice(0, 10)); + + const getData = useCallback(() => { + return data; + }, [data]); + + return ( + <PageTemplate + title={args.title} + documentationLink={args.documentationLink} + documentationText={args.documentationText} + isFullWidth={args.isFullWidth} + > + <Layout regions={[1]}> + <Surface> + <DataTable + columns={personColumns} + getData={getData} + hasSearch + hasFilters + totalRows={10} + /> + </Surface> + </Layout> + </PageTemplate> + ); + }, +};