diff --git a/packages/odyssey-react-mui/src/Autocomplete.tsx b/packages/odyssey-react-mui/src/Autocomplete.tsx index f740721b6c..4b5c972a94 100644 --- a/packages/odyssey-react-mui/src/Autocomplete.tsx +++ b/packages/odyssey-react-mui/src/Autocomplete.tsx @@ -92,6 +92,17 @@ export type AutocompleteProps< undefined, IsCustomValueAllowed >["defaultValue"]; + /** + * Used to determine the string value for a given option. It's used to fill the input (and the list box options if renderOption is not provided). If used in free solo mode, it must accept both the type of the options and a string. + * + * `function(option: Value) => string` + */ + getOptionLabel?: UseAutocompleteProps< + OptionType, + HasMultipleChoices, + undefined, + IsCustomValueAllowed + >["getOptionLabel"]; /** * Enables multiple choice selection */ @@ -249,6 +260,7 @@ const Autocomplete = < defaultValue, errorMessage, errorMessageList, + getOptionLabel, hasMultipleChoices, id: idOverride, inputValue, @@ -571,6 +583,7 @@ const Autocomplete = < disabled={isDisabled} freeSolo={isCustomValueAllowed} filterSelectedOptions={true} + getOptionLabel={getOptionLabel} id={idOverride} fullWidth={isFullWidth} loading={isLoading} diff --git a/packages/odyssey-react-mui/src/Banner.tsx b/packages/odyssey-react-mui/src/Banner.tsx index 3c87dc8e12..cdefa89287 100644 --- a/packages/odyssey-react-mui/src/Banner.tsx +++ b/packages/odyssey-react-mui/src/Banner.tsx @@ -15,7 +15,7 @@ import { useTranslation } from "react-i18next"; import { Alert, AlertColor, AlertTitle, AlertProps } from "@mui/material"; import type { HtmlProps } from "./HtmlProps"; -import { Link } from "./Link"; +import { Link, LinkProps } from "./Link"; import { ScreenReaderText } from "./ScreenReaderText"; export const bannerRoleValues = ["status", "alert"] as const; @@ -27,16 +27,6 @@ export const bannerSeverityValues: AlertColor[] = [ ]; export type BannerProps = { - /** - * If linkUrl is not undefined, this is the text of the link. - * If left blank, it defaults to "Learn more". - * Note that linkText does nothing if linkUrl is not defined - */ - linkText?: string; - /** - * If defined, the alert will include a link to the URL - */ - linkUrl?: string; /** * The function that's fired when the user clicks the close button. If undefined, * the close button will not be shown. @@ -56,11 +46,30 @@ export type BannerProps = { * The text content of the alert */ text: string; -} & Pick; +} & Pick & + ( + | { + linkRel?: LinkProps["rel"]; + linkTarget?: LinkProps["target"]; + linkText: string; + /** + * If defined, the Banner will include a link to the URL + */ + linkUrl: LinkProps["href"]; + } + | { + linkRel?: never; + linkTarget?: never; + linkText?: never; + linkUrl?: never; + } + ); const Banner = ({ - linkUrl, + linkRel, + linkTarget, linkText, + linkUrl, onClose, role, severity, @@ -83,7 +92,13 @@ const Banner = ({ {text} {linkUrl && ( - + {linkText} )} diff --git a/packages/odyssey-react-mui/src/Callout.tsx b/packages/odyssey-react-mui/src/Callout.tsx index 8b22564533..f4539e355e 100644 --- a/packages/odyssey-react-mui/src/Callout.tsx +++ b/packages/odyssey-react-mui/src/Callout.tsx @@ -11,11 +11,12 @@ */ import styled from "@emotion/styled"; -import { Alert, AlertTitle, Box, Link as MuiLink } from "@mui/material"; +import { Alert, AlertTitle, Box } from "@mui/material"; import { memo, ReactNode } from "react"; import { useTranslation } from "react-i18next"; import type { HtmlProps } from "./HtmlProps"; +import { Link, LinkProps } from "./Link"; import { DesignTokens, useOdysseyDesignTokens, @@ -92,18 +93,17 @@ export type CalloutProps = { ) & ( | { + linkRel?: LinkProps["rel"]; + linkTarget?: LinkProps["target"]; + linkText: string; /** - * If linkUrl is not undefined, this is the text of the link. - * If left blank, it defaults to "Learn more". - * Note that linkText does nothing if linkUrl is not defined - */ - linkUrl: string; - /** - * If defined, the Toast will include a link to the URL + * If defined, the Callout will include a link to the URL */ - linkText: string; + linkUrl: LinkProps["href"]; } | { + linkRel?: never; + linkTarget?: never; linkUrl?: never; linkText?: never; } @@ -120,6 +120,8 @@ const ContentContainer = styled("div", { const Callout = ({ children, + linkRel, + linkTarget, linkText, linkUrl, role, @@ -153,9 +155,14 @@ const Callout = ({ {text && {text}} {linkUrl && ( - + {linkText} - + )} diff --git a/packages/odyssey-react-mui/src/Link.tsx b/packages/odyssey-react-mui/src/Link.tsx index 48ac5739b3..0fd5823b33 100644 --- a/packages/odyssey-react-mui/src/Link.tsx +++ b/packages/odyssey-react-mui/src/Link.tsx @@ -41,7 +41,9 @@ export type LinkProps = { */ onClick?: MuiLinkProps["onClick"]; /** - * The HTML `rel` attribute for the Link + * The rel attribute defines the relationship between a linked resource and the current document + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel */ rel?: string; /** diff --git a/packages/odyssey-react-mui/src/Stack.tsx b/packages/odyssey-react-mui/src/Stack.tsx new file mode 100644 index 0000000000..d02a85a493 --- /dev/null +++ b/packages/odyssey-react-mui/src/Stack.tsx @@ -0,0 +1,56 @@ +/*! + * 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 { memo } from "react"; +import { Stack as MuiStack, StackProps as MuiStackProps } from "@mui/material"; + +export const stackSpacingValues = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] as const; +export const stackDirectionValues = [ + "row", + "row-reverse", + "column", + "column-reverse", +] as const; + +export type OdysseyStackProps = { + children?: MuiStackProps["children"]; + /** + * The component used for the root node. Either a string to use a HTML element or a component. + */ + component?: MuiStackProps["component"]; + /** + * Defines the flex-direction style property. It is applied for all screen sizes. + */ + direction?: (typeof stackDirectionValues)[number]; + /** + * Defines the space between immediate children. + */ + spacing?: (typeof stackSpacingValues)[number]; + sx?: MuiStackProps["sx"]; +}; + +const Stack = ({ + children, + direction = "column", + spacing = 2, +}: OdysseyStackProps) => { + return ( + + {children} + + ); +}; + +const MemoizedStack = memo(Stack); +MemoizedStack.displayName = "Stack"; + +export { MemoizedStack as Stack }; diff --git a/packages/odyssey-react-mui/src/index.ts b/packages/odyssey-react-mui/src/index.ts index c73bbae4f2..2614bbd237 100644 --- a/packages/odyssey-react-mui/src/index.ts +++ b/packages/odyssey-react-mui/src/index.ts @@ -34,6 +34,7 @@ export { Paper, /** @deprecated Will be removed in a future Odyssey version in lieu of a wrapped version. */ ScopedCssBaseline, + /** @deprecated Will be removed in a future Odyssey version in lieu of a wrapped version. */ ThemeProvider, } from "@mui/material"; @@ -49,6 +50,7 @@ export type { MenuListProps, PaperProps, ScopedCssBaselineProps, + StackProps, ThemeOptions, } from "@mui/material"; @@ -100,6 +102,7 @@ export * from "./RadioGroup"; export * from "./ScreenReaderText"; export * from "./SearchField"; export * from "./Select"; +export * from "./Stack"; export * from "./Status"; export * from "./Surface"; export * from "./Tabs"; diff --git a/packages/odyssey-react-mui/src/labs/datePickerTheme.tsx b/packages/odyssey-react-mui/src/labs/datePickerTheme.tsx index e439131f70..1336cb796b 100644 --- a/packages/odyssey-react-mui/src/labs/datePickerTheme.tsx +++ b/packages/odyssey-react-mui/src/labs/datePickerTheme.tsx @@ -47,10 +47,6 @@ const dateStyles: StateStyles = { hoverSelected: ({ theme }) => ({ backgroundColor: theme.palette.primary.dark, color: theme.palette.primary.contrastText, - - "@media (pointer: fine)": { - backgroundColor: theme.palette.primary.main, - }, }), outsideOfMonth: ({ theme }) => ({ backgroundColor: "transparent", @@ -158,8 +154,8 @@ export const datePickerTheme: ThemeOptions = { borderStyle: theme.mixins.borderStyle, borderWidth: theme.mixins.borderWidth, borderRadius: theme.mixins.borderRadius, - paddingBlock: theme.spacing(3), - paddingInline: theme.spacing(3), + paddingBlock: theme.spacing(6), + paddingInline: theme.spacing(6), }, }), }, diff --git a/packages/odyssey-react-mui/src/theme/palette.ts b/packages/odyssey-react-mui/src/theme/palette.ts index 0c4c88fe02..7cb057b777 100644 --- a/packages/odyssey-react-mui/src/theme/palette.ts +++ b/packages/odyssey-react-mui/src/theme/palette.ts @@ -29,7 +29,8 @@ export const palette = ({ lighter: odysseyTokens.HueBlue50, light: odysseyTokens.HueBlue300, main: odysseyTokens.HueBlue500, - dark: odysseyTokens.HueBlue900, + dark: odysseyTokens.HueBlue700, + darker: odysseyTokens.HueBlue800, contrastText: odysseyTokens.TypographyColorInverse, }, secondary: { diff --git a/packages/odyssey-react-mui/src/theme/palette.types.ts b/packages/odyssey-react-mui/src/theme/palette.types.ts index 2cfb2b39fa..bda658880d 100644 --- a/packages/odyssey-react-mui/src/theme/palette.types.ts +++ b/packages/odyssey-react-mui/src/theme/palette.types.ts @@ -12,9 +12,11 @@ declare module "@mui/material/styles" { interface PaletteColor { + darker?: string; lighter?: string; } interface SimplePaletteColorOptions { + darker?: string; lighter?: string; } } diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Banner/Banner.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Banner/Banner.stories.tsx index 9e704b1ac0..799441efe7 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/Banner/Banner.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/Banner/Banner.stories.tsx @@ -22,11 +22,38 @@ import { MuiThemeDecorator } from "../../../../.storybook/components"; import { userEvent, within } from "@storybook/testing-library"; import { expect, jest } from "@storybook/jest"; import { axeRun } from "../../../axe-util"; +import type { PlaywrightProps } from "../storybookTypes"; -const storybookMeta: Meta = { +type PlayType = { + args: BannerProps; + canvasElement: HTMLElement; + step: PlaywrightProps["step"]; +}; + +const storybookMeta: Meta = { title: "MUI Components/Banner", component: Banner, argTypes: { + linkRel: { + control: "text", + description: + "The rel attribute defines the relationship between a linked resource and the current document.", + table: { + type: { + summary: "string", + }, + }, + }, + linkTarget: { + control: "text", + description: + "The target property of the `HTMLAnchorElement` interface is a string that indicates where to display the linked resource.", + table: { + type: { + summary: "string", + }, + }, + }, linkText: { control: "text", description: @@ -138,7 +165,7 @@ export const Linked: StoryObj = { severity: "error", text: "An unidentified flying object compromised Hangar 18.", }, - play: async ({ canvasElement, step }) => { + play: async ({ canvasElement, step }: PlayType) => { await step("check for the link text", async () => { const canvas = within(canvasElement); const link = canvas.getByText("View report") as HTMLAnchorElement; @@ -148,11 +175,22 @@ export const Linked: StoryObj = { }, }; +export const LinkWithTarget: StoryObj = { + args: { + linkTarget: "_blank", + linkText: "View report", + linkUrl: "#anchor", + role: "status", + severity: "error", + text: "An unidentified flying object compromised Hangar 18.", + }, +}; + export const Dismissible: StoryObj = { args: { onClose: jest.fn(), }, - play: async ({ args, canvasElement, step }) => { + play: async ({ args, canvasElement, step }: PlayType) => { await step("dismiss the banner on click", async () => { const canvas = within(canvasElement); const button = canvas.getByTitle("Close"); diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Callout/Callout.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Callout/Callout.stories.tsx index 02678bc596..a2c1b1c20f 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/Callout/Callout.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/Callout/Callout.stories.tsx @@ -40,6 +40,26 @@ const storybookMeta: Meta = { value: "ReactNode | Array", }, }, + linkRel: { + control: "text", + description: + "The rel attribute defines the relationship between a linked resource and the current document.", + table: { + type: { + summary: "string", + }, + }, + }, + linkTarget: { + control: "text", + description: + "The target property of the `HTMLAnchorElement` interface is a string that indicates where to display the linked resource.", + table: { + type: { + summary: "string", + }, + }, + }, linkText: { control: "text", description: @@ -163,6 +183,18 @@ export const WithLink: StoryObj = { }, }; +export const WithLinkAndTarget: StoryObj = { + args: { + role: "alert", + severity: "error", + title: "Safety checks failed", + text: "There is an issue with the fuel mixture ratios. Reconfigure the fuel mixture and perform the safety checks again.", + linkTarget: "_blank", + linkText: "Visit fueling console", + linkUrl: "#", + }, +}; + export const ChildrenWithList: StoryObj = { args: { role: "status", diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Stack/Stack.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Stack/Stack.stories.tsx new file mode 100644 index 0000000000..910af1f638 --- /dev/null +++ b/packages/odyssey-storybook/src/components/odyssey-mui/Stack/Stack.stories.tsx @@ -0,0 +1,167 @@ +/*! + * Copyright (c) 2021-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 { + DesignTokens, + OdysseyStackProps, + Stack, + stackDirectionValues, + stackSpacingValues, + useOdysseyDesignTokens, +} from "@okta/odyssey-react-mui"; +import { MuiThemeDecorator } from "../../../../.storybook/components"; + +const ContentBox = styled.div<{ odysseyDesignTokens: DesignTokens }>( + ({ odysseyDesignTokens }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: odysseyDesignTokens.Spacing8, + height: odysseyDesignTokens.Spacing8, + backgroundColor: odysseyDesignTokens.HueNeutral100, + border: `1px dashed ${odysseyDesignTokens.PalettePrimaryDarker}`, + borderRadius: odysseyDesignTokens.BorderRadiusMain, + }), +); + +const Content = () => { + const odysseyDesignTokens = useOdysseyDesignTokens(); + + return ( + <> + 1 + 2 + 3 + 4 + + ); +}; + +const storybookMeta: Meta = { + title: "MUI Components/Stack", + component: Stack, + argTypes: { + children: { + control: <>, + description: "The content of the component", + table: { + type: { + summary: "ReactNode", + }, + }, + }, + direction: { + options: stackDirectionValues, + control: { type: "radio" }, + description: "The flex-direction applied to the Stack", + table: { + type: { + summary: stackDirectionValues.join(" | "), + }, + }, + type: { + name: "other", + value: "radio", + }, + }, + spacing: { + control: "select", + options: stackSpacingValues.map((value) => value), + }, + sx: { + control: "object", + description: + "The system prop that allows defining system overrides as well as additional CSS styles. See the [MUI `sx` page](https://mui.com/system/getting-started/the-sx-prop/) for more details.", + table: { + type: { + summary: "object", + }, + }, + }, + }, + args: { + children: , + spacing: 2, + }, + decorators: [MuiThemeDecorator], + tags: ["autodocs"], +}; + +export default storybookMeta; + +export const Default: StoryObj = {}; +// export const DefaultPill: StoryObj = { +// args: { +// label: "Warp drive in standby", +// }, +// }; + +// export const ErrorPill: StoryObj = { +// args: { +// label: "Warp drive unstable", +// severity: "error", +// }, +// }; + +// export const Info: StoryObj = { +// args: { +// label: "Warp drive unstable", +// severity: "info", +// }, +// }; + +// export const Success: StoryObj = { +// args: { +// label: "Warp drive online", +// severity: "success", +// }, +// }; + +// export const WarningPill: StoryObj = { +// args: { +// label: "Warp fuel low", +// severity: "warning", +// }, +// }; + +// export const DefaultLamp: StoryObj = { +// args: { +// label: "Warp drive in standby", +// variant: "lamp", +// }, +// }; + +// export const ErrorLamp: StoryObj = { +// args: { +// label: "Warp drive unstable", +// severity: "error", +// variant: "lamp", +// }, +// }; + +// export const SuccessLamp: StoryObj = { +// args: { +// label: "Warp drive online", +// severity: "success", +// variant: "lamp", +// }, +// }; + +// export const WarningLamp: StoryObj = { +// args: { +// label: "Warp fuel low", +// severity: "warning", +// variant: "lamp", +// }, +// }; diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.stories.tsx index e9497602e5..5ccf4a2ef4 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.stories.tsx @@ -317,7 +317,7 @@ export const Multiline: StoryObj = { isMultiline: true, defaultValue: "", }, - storyName: "Multiline (Textarea)", + name: "Multiline (Textarea)", }; export const Placeholder: StoryObj = { diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Tooltip/Tooltip.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Tooltip/Tooltip.stories.tsx index d258ec9a6a..eb5ed5056b 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/Tooltip/Tooltip.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/Tooltip/Tooltip.stories.tsx @@ -175,18 +175,6 @@ export const StatusWrapper: StoryObj = { }, }; -export const Disabled: StoryObj = { - ...Template, - args: { - children: ( -