Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PageTemplate and Layout component #2219

Merged
merged 21 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fac7853
feat: start layout component
trevoring-okta Apr 24, 2024
0a1cd83
feat: more work on layout component
trevoring-okta Apr 25, 2024
2f7ca88
feat: documentation and syntax updates
trevoring-okta Apr 27, 2024
baded28
fix: update to storybook and component
trevoring-okta Apr 30, 2024
cba53dc
feat: add more to Storybook
trevoring-okta May 2, 2024
2a8f9a2
fix: remove unncessary MUI ScopedCssBaseline
trevoring-okta May 2, 2024
5c3c1b5
feat: add full-width story
trevoring-okta May 3, 2024
3da76c1
feat(odyssey-react-mui): create surface component
bryancunningham-okta May 6, 2024
762febb
feat(odyssey-react-mui): create stories for Grid
bryancunningham-okta May 6, 2024
ed431aa
feat(odyssey-react-mui): remove surface styling from Grid
bryancunningham-okta May 6, 2024
24d9734
fix: small nit fixes
trevoring-okta May 8, 2024
5341d2b
feat(odyssey-react-mui): panes => regions
bryancunningham-okta May 22, 2024
28662cb
fix(odyssey-react-mui): rename components and add disclaimer
bryancunningham-okta Jun 26, 2024
9192e28
fix: standardize vertical layout distance
jordankoschei-okta Jul 15, 2024
4337437
refactor: update css
jordankoschei-okta Jul 16, 2024
db40151
refactor: alphabetize the imports
jordankoschei-okta Jul 16, 2024
5ee9de1
feat: add rudimentary responsiveness to Layout
jordankoschei-okta Jul 16, 2024
286a346
refactor: improve nested selectors
jordankoschei-okta Jul 17, 2024
7b046a4
refactor: update based on code review
jordankoschei-okta Jul 22, 2024
af78af3
Merge branch 'main' into ti-OKTA-721245-layout-component
jordankoschei-okta Jul 22, 2024
f7b20d1
fix: update redundant type export
jordankoschei-okta Jul 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions packages/odyssey-react-mui/src/Surface.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*!
* 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 }) => ({
// backgroundColor: odysseyDesignTokens.HueNeutralWhite,
jordankoschei-okta marked this conversation as resolved.
Show resolved Hide resolved
borderRadius: odysseyDesignTokens.Spacing4,
padding: odysseyDesignTokens.Spacing4,
}));

export type SurfaceProps = {
children: ReactNode;
};

const Surface = ({ children }: SurfaceProps) => {
const odysseyDesignTokens = useOdysseyDesignTokens();

return (
<StyledContainer odysseyDesignTokens={odysseyDesignTokens}>
{children}
</StyledContainer>
);
};

const MemoizedSurface = memo(Surface);
MemoizedSurface.displayName = "Surface";

export { MemoizedSurface as Surface };
1 change: 1 addition & 0 deletions packages/odyssey-react-mui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export * from "./ScreenReaderText";
export * from "./SearchField";
export * from "./Select";
export * from "./Status";
export * from "./Surface";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this to Labs.

export * from "./Tabs";
export * from "./Tag";
export * from "./TagList";
Expand Down
90 changes: 90 additions & 0 deletions packages/odyssey-react-mui/src/labs/Layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*!
* 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),
})<LayoutContentProps>(({ odysseyDesignTokens, regions }) => ({
display: "grid",
gridTemplateColumns: regions,
gridColumnGap: odysseyDesignTokens.Spacing4,
columnGap: odysseyDesignTokens.Spacing4,

"& + &": {
marginBlockStart: odysseyDesignTokens.Spacing4,
},

["@media (max-width: 800px)"]: {
jordankoschei-okta marked this conversation as resolved.
Show resolved Hide resolved
gridTemplateColumns: "1fr !important",
gap: odysseyDesignTokens.Spacing4,
},
}));

const Layout = ({ regions, children }: LayoutProps) => {
const odysseyDesignTokens = useOdysseyDesignTokens();
const mappedRegions = regions
.map((region) => `minmax(0, ${region}fr)`)
.join(" ");

return (
<Box>
jordankoschei-okta marked this conversation as resolved.
Show resolved Hide resolved
<LayoutContent
odysseyDesignTokens={odysseyDesignTokens}
regions={mappedRegions}
>
{Children.toArray(children).map((child) => child)}
</LayoutContent>
</Box>
);
};

const MemoizedLayout = memo(Layout);
MemoizedLayout.displayName = "Layout";

export { MemoizedLayout as Layout };
249 changes: 249 additions & 0 deletions packages/odyssey-react-mui/src/labs/PageTemplate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
/*!
* 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", {
shouldForwardProp: (prop) => prop !== "odysseyDesignTokens",
})<{
odysseyDesignTokens: DesignTokens;
}>(({ odysseyDesignTokens }) => ({
display: "flex",
alignItems: "flex-end",
justifyContent: "space-between",

["@media (max-width: 800px)"]: {
alignItems: "flex-start",
flexDirection: "column",
gap: odysseyDesignTokens.Spacing2,
},
}));

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",

["@media (max-width: 800px)"]: {
alignItems: "flex-start",
},
}));

const TemplateHeaderButtons = styled("div", {
shouldForwardProp: (prop) => prop !== "odysseyDesignTokens",
})<{
odysseyDesignTokens: DesignTokens;
}>(({ odysseyDesignTokens }) => ({
display: "flex",

["& > div, & > button"]: {
jordankoschei-okta marked this conversation as resolved.
Show resolved Hide resolved
marginInlineStart: odysseyDesignTokens.Spacing2,
},

["@media (max-width: 800px)"]: {
["& > div, & > button"]: {
marginInlineEnd: odysseyDesignTokens.Spacing2,
marginInlineStart: odysseyDesignTokens.Spacing0,
},
},
}));

const TemplateContent = styled("div", {
shouldForwardProp: (prop) =>
!["odysseyDesignTokens", "isDrawerOpen", "drawerVariant"].includes(prop),
})<TemplateContentProps>(
({ 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 (
<TemplateContainer
odysseyDesignTokens={odysseyDesignTokens}
isFullWidth={isFullWidth}
>
<TemplateHeader odysseyDesignTokens={odysseyDesignTokens}>
<TemplateHeaderPrimaryContent>
{title && <Heading4>{title}</Heading4>}
{description && <Subordinate>{description}</Subordinate>}
</TemplateHeaderPrimaryContent>

<TemplateHeaderSecondaryContent
odysseyDesignTokens={odysseyDesignTokens}
>
{documentationLink && (
<Link href={documentationLink} icon={<DocumentationIcon />}>
{documentationText}
</Link>
)}
{(primaryCallToActionComponent ||
secondaryCallToActionComponent ||
tertiaryCallToActionComponent) && (
<TemplateHeaderButtons odysseyDesignTokens={odysseyDesignTokens}>
{tertiaryCallToActionComponent}
{secondaryCallToActionComponent}
{primaryCallToActionComponent}
</TemplateHeaderButtons>
)}
</TemplateHeaderSecondaryContent>
</TemplateHeader>
<TemplateContent
odysseyDesignTokens={odysseyDesignTokens}
isDrawerOpen={isDrawerOpen}
drawerVariant={drawerVariant}
>
{children}
{drawer}
</TemplateContent>
</TemplateContainer>
);
};

const MemoizedPageTemplate = memo(PageTemplate);
MemoizedPageTemplate.displayName = "PageTemplate";

export { MemoizedPageTemplate as PageTemplate };
Loading
Loading