diff --git a/packages/odyssey-react-mui/src/Checkbox.tsx b/packages/odyssey-react-mui/src/Checkbox.tsx index 2693430b97..3931721b4f 100644 --- a/packages/odyssey-react-mui/src/Checkbox.tsx +++ b/packages/odyssey-react-mui/src/Checkbox.tsx @@ -61,7 +61,10 @@ export type CheckboxProps = { * Callback fired when the blur event happens. Provides event value. */ onBlur?: MuiFormControlLabelProps["onBlur"]; -} & Pick & +} & Pick< + FieldComponentProps, + "hint" | "id" | "isDisabled" | "isReadOnly" | "name" +> & CheckedFieldProps & Pick; @@ -74,6 +77,7 @@ const Checkbox = ({ isDefaultChecked, isDisabled, isIndeterminate, + isReadOnly = false, isRequired, label: labelProp, hint, @@ -102,13 +106,11 @@ const Checkbox = ({ const localInputRef = useRef(null); useImperativeHandle( inputRef, - () => { - return { - focus: () => { - localInputRef.current?.focus(); - }, - }; - }, + () => ({ + focus: () => { + localInputRef.current?.focus(); + }, + }), [], ); @@ -136,6 +138,16 @@ const Checkbox = ({ [onChangeProp], ); + const onClick = useCallback>( + (event) => { + if (isReadOnly) { + event.stopPropagation(); + event.preventDefault(); + } + }, + [isReadOnly], + ); + const onBlur = useCallback>( (event) => { onBlurProp?.(event); @@ -143,9 +155,22 @@ const Checkbox = ({ [onBlurProp], ); + const checkboxStyles = useMemo( + () => ({ + alignItems: "flex-start", + ...(isReadOnly && { + cursor: "default", + "& .MuiTypography-root": { + cursor: "default", + }, + }), + }), + [isReadOnly], + ); + return ( + } required={isRequired} inputProps={{ "data-se": testId, + "aria-readonly": isReadOnly, + readOnly: isReadOnly, }} disabled={isDisabled} inputRef={localInputRef} diff --git a/packages/odyssey-react-mui/src/CheckboxGroup.tsx b/packages/odyssey-react-mui/src/CheckboxGroup.tsx index 85efc049c6..dd1c42bf7e 100644 --- a/packages/odyssey-react-mui/src/CheckboxGroup.tsx +++ b/packages/odyssey-react-mui/src/CheckboxGroup.tsx @@ -10,9 +10,9 @@ * See the License for the specific language governing permissions and limitations under the License. */ +import React, { memo, ReactNode, useCallback, useMemo } from "react"; import { FormGroup as MuiFormGroup } from "@mui/material"; -import { memo, ReactNode, useCallback } from "react"; - +import { CheckboxProps } from "./Checkbox"; import { Field } from "./Field"; import { FieldComponentProps, @@ -41,6 +41,7 @@ export type CheckboxGroupProps = { | "HintLinkComponent" | "id" | "isDisabled" + | "isReadOnly" > & Pick; @@ -61,11 +62,26 @@ const CheckboxGroup = ({ HintLinkComponent, id: idOverride, isDisabled, + isReadOnly = false, isRequired = false, label, testId, translate, }: CheckboxGroupProps) => { + const memoizedChildren = useMemo( + () => + React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + return React.cloneElement(child, { + isReadOnly, + isDisabled: isDisabled || child.props.isDisabled, + }); + } + return child; + }), + [children, isReadOnly, isDisabled], + ); + const renderFieldComponent = useCallback( ({ ariaDescribedBy, @@ -81,10 +97,10 @@ const CheckboxGroup = ({ id={id} translate={translate} > - {children} + {memoizedChildren} ), - [children, testId, translate], + [memoizedChildren, testId, translate], ); return ( diff --git a/packages/odyssey-react-mui/src/Field.tsx b/packages/odyssey-react-mui/src/Field.tsx index c93d4e74b2..293ad560d5 100644 --- a/packages/odyssey-react-mui/src/Field.tsx +++ b/packages/odyssey-react-mui/src/Field.tsx @@ -11,12 +11,10 @@ */ import { memo, ReactElement, useMemo } from "react"; - import { FormControl as MuiFormControl, FormLabel as MuiFormLabel, } from "@mui/material"; - import { FieldComponentProps } from "./FieldComponentProps"; import { FieldError } from "./FieldError"; import { FieldHint } from "./FieldHint"; @@ -35,6 +33,7 @@ export type RenderFieldComponentProps = { errorMessageElementId?: string; id: string; labelElementId: string; + isReadOnly?: boolean; }; export type FieldProps = { @@ -75,6 +74,7 @@ export type FieldProps = { errorMessageElementId, id, labelElementId, + isReadOnly, }: RenderFieldComponentProps) => ReactElement; }; @@ -91,6 +91,7 @@ const Field = ({ isFullWidth = false, isRadioGroup = false, isOptional = false, + isReadOnly = false, label, renderFieldComponent, }: FieldProps & @@ -104,6 +105,7 @@ const Field = ({ | "isDisabled" | "isFullWidth" | "isOptional" + | "isReadOnly" > & Pick) => { const { t } = useTranslation(); @@ -167,6 +169,7 @@ const Field = ({ errorMessageElementId, id, labelElementId, + isReadOnly, })} {(errorMessage || errorMessageList) && ( diff --git a/packages/odyssey-react-mui/src/Radio.tsx b/packages/odyssey-react-mui/src/Radio.tsx index 8fa4dc9040..74f0d60532 100644 --- a/packages/odyssey-react-mui/src/Radio.tsx +++ b/packages/odyssey-react-mui/src/Radio.tsx @@ -17,9 +17,7 @@ import { Radio as MuiRadio, RadioProps as MuiRadioProps, } from "@mui/material"; - import { memo, useCallback, useMemo, useRef, useImperativeHandle } from "react"; - import { FieldComponentProps } from "./FieldComponentProps"; import type { HtmlProps } from "./HtmlProps"; import { FocusHandle } from "./inputUtils"; @@ -31,11 +29,11 @@ export type RadioProps = { */ inputRef?: React.RefObject; /** - * If `true`, the Radio is selected + * Determines whether the Radio button is checked */ isChecked?: boolean; /** - * If `true`, the Radio has an invalid value + * If `true`, the radio button has an invalid value */ isInvalid?: boolean; /** @@ -46,30 +44,33 @@ export type RadioProps = { * The value attribute of the Radio */ value: string; - /** - * Callback fired when the state is changed. Provides event and checked value. - */ - onChange?: MuiRadioProps["onChange"]; /** * Callback fired when the blur event happens. Provides event value. */ + onChange?: MuiRadioProps["onChange"]; onBlur?: MuiFormControlLabelProps["onBlur"]; -} & Pick & + onClick?: React.MouseEventHandler; +} & Pick< + FieldComponentProps, + "hint" | "id" | "isDisabled" | "isReadOnly" | "name" +> & Pick; const Radio = ({ hint, inputRef, isChecked, - isDisabled, + isDisabled = false, isInvalid, label: labelProp, name, testId, translate, value, + isReadOnly = false, onChange: onChangeProp, onBlur: onBlurProp, + onClick, }: RadioProps) => { const localInputRef = useRef(null); useImperativeHandle( @@ -95,10 +96,25 @@ const Radio = ({ ); const onChange = useCallback>( - (event, checked) => onChangeProp?.(event, checked), - [onChangeProp], + (event, checked) => { + if (isReadOnly) { + event.preventDefault(); + } else { + onChangeProp?.(event, checked); + } + }, + [onChangeProp, isReadOnly], ); + const handleClick = useCallback>( + (event) => { + if (isReadOnly) { + event.stopPropagation(); + event.preventDefault(); + } + }, + [isReadOnly], + ); const onBlur = useCallback>( (event) => { onBlurProp?.(event); @@ -114,17 +130,30 @@ const Radio = ({ } - disabled={isDisabled} label={label} name={name} translate={translate} value={value} onBlur={onBlur} + disabled={isDisabled} + sx={{ + ...(isReadOnly && { + cursor: "default", + "& .MuiTypography-root": { + cursor: "default", + }, + }), + }} /> ); }; diff --git a/packages/odyssey-react-mui/src/RadioGroup.tsx b/packages/odyssey-react-mui/src/RadioGroup.tsx index 9fa85e1edb..6c2e87092c 100644 --- a/packages/odyssey-react-mui/src/RadioGroup.tsx +++ b/packages/odyssey-react-mui/src/RadioGroup.tsx @@ -14,9 +14,8 @@ import { RadioGroup as MuiRadioGroup, type RadioGroupProps as MuiRadioGroupProps, } from "@mui/material"; -import { memo, ReactNode, useCallback, useRef } from "react"; +import React, { memo, ReactNode, useCallback, useRef, useMemo } from "react"; -import { RadioProps } from "./Radio"; import { Field } from "./Field"; import { FieldComponentProps, @@ -24,6 +23,7 @@ import { } from "./FieldComponentProps"; import type { HtmlProps } from "./HtmlProps"; import { getControlState, useInputValues } from "./inputUtils"; +import { Radio, RadioProps } from "./Radio"; export type RadioGroupProps = { /** @@ -45,7 +45,7 @@ export type RadioGroupProps = { /** * The `value` on the selected Radio */ - value?: RadioProps["value"]; + value?: string; } & Pick< FieldComponentProps, | "errorMessage" @@ -54,6 +54,7 @@ export type RadioGroupProps = { | "HintLinkComponent" | "id" | "isDisabled" + | "isReadOnly" | "name" > & Pick; @@ -73,6 +74,7 @@ const RadioGroup = ({ HintLinkComponent, id: idOverride, isDisabled, + isReadOnly = false, label, name: nameOverride, onChange: onChangeProp, @@ -86,6 +88,7 @@ const RadioGroup = ({ uncontrolledValue: defaultValue, }), ); + const inputValues = useInputValues({ defaultValue, value, @@ -93,11 +96,26 @@ const RadioGroup = ({ }); const onChange = useCallback>( - (event, value) => { - onChangeProp?.(event, value); + (event, newValue) => { + onChangeProp?.(event, newValue); }, [onChangeProp], ); + + const memoizedChildren = useMemo( + () => + React.Children.map(children, (child) => { + if (React.isValidElement(child) && child.type === Radio) { + return React.cloneElement(child, { + isDisabled: isDisabled, + isReadOnly: isReadOnly, + }); + } + return child; + }), + [children, isDisabled, isReadOnly], + ); + const renderFieldComponent = useCallback( ({ ariaDescribedBy, @@ -116,10 +134,10 @@ const RadioGroup = ({ onChange={onChange} translate={translate} > - {children} + {memoizedChildren} ), - [children, inputValues, nameOverride, onChange, testId, translate], + [inputValues, nameOverride, onChange, memoizedChildren, testId, translate], ); return ( @@ -133,6 +151,7 @@ const RadioGroup = ({ HintLinkComponent={HintLinkComponent} id={idOverride} isDisabled={isDisabled} + isReadOnly={isReadOnly} label={label} renderFieldComponent={renderFieldComponent} /> diff --git a/packages/odyssey-react-mui/src/Select.tsx b/packages/odyssey-react-mui/src/Select.tsx index 1a49423030..616fdff3dc 100644 --- a/packages/odyssey-react-mui/src/Select.tsx +++ b/packages/odyssey-react-mui/src/Select.tsx @@ -18,7 +18,6 @@ import { useRef, useState, useImperativeHandle, - MouseEvent, } from "react"; import { Box as MuiBox, @@ -31,7 +30,6 @@ import { SelectChangeEvent, } from "@mui/material"; import { SelectProps as MuiSelectProps } from "@mui/material"; - import { Field } from "./Field"; import { FieldComponentProps, @@ -47,7 +45,6 @@ import { } from "./inputUtils"; import { normalizedKey } from "./useNormalizedKey"; import styled from "@emotion/styled"; - import { useOdysseyDesignTokens, DesignTokens, @@ -97,16 +94,20 @@ const NonInteractiveIcon = styled(CloseCircleFilledIcon, { const ChipsInnerContainer = styled(MuiBox, { shouldForwardProp: (prop) => - prop !== "odysseyDesignTokens" && prop !== "isInteractive", + prop !== "odysseyDesignTokens" && + prop !== "isInteractive" && + prop !== "isReadOnly", })<{ isInteractive?: boolean; + isReadOnly?: boolean; odysseyDesignTokens: DesignTokens; }>` display: flex; flex-wrap: wrap; gap: ${({ odysseyDesignTokens }) => odysseyDesignTokens.Spacing1}; pointer-events: none; - opacity: ${({ isInteractive }) => (isInteractive ? 1 : 0)}; + opacity: ${({ isInteractive, isReadOnly }) => + isInteractive || isReadOnly ? 1 : 0}; min-height: ${({ odysseyDesignTokens }) => odysseyDesignTokens.Spacing6}; `; @@ -168,6 +169,7 @@ export type SelectProps< | "isDisabled" | "isFullWidth" | "isOptional" + | "isReadOnly" | "name" > & Pick; @@ -210,6 +212,7 @@ const Select = < isFullWidth = false, isMultiSelect, isOptional = false, + isReadOnly = false, label, name: nameOverride, onBlur, @@ -220,6 +223,8 @@ const Select = < translate, value, }: SelectProps) => { + const selectRef = useRef(null); + const hasMultipleChoices = useMemo( () => hasMultipleChoicesProp === undefined @@ -227,6 +232,7 @@ const Select = < : hasMultipleChoicesProp, [hasMultipleChoicesProp, isMultiSelect], ); + const controlledStateRef = useRef( getControlState({ controlledValue: value, @@ -266,21 +272,24 @@ const Select = < const onChange = useCallback["onChange"]>>( (event, child) => { - const { - target: { value }, - } = event; - if (controlledStateRef.current !== CONTROLLED) { - setInternalSelectedValues( - (typeof value === "string" && hasMultipleChoices - ? value.split(",") - : value) as Value, - ); + if (isReadOnly) { + event.preventDefault(); + } else { + const { + target: { value }, + } = event; + if (controlledStateRef.current !== CONTROLLED) { + setInternalSelectedValues( + (typeof value === "string" && hasMultipleChoices + ? value.split(",") + : value) as Value, + ); + } + onChangeProp?.(event, child); } - onChangeProp?.(event, child); }, - [hasMultipleChoices, onChangeProp], + [hasMultipleChoices, onChangeProp, isReadOnly], ); - // Normalize the options array to accommodate the various // data types that might be passed const normalizedOptions = useMemo( @@ -323,8 +332,14 @@ const Select = < ); const Chips = useCallback( - ({ isInteractive }: { isInteractive: boolean }) => { - const stopPropagation = (event: MouseEvent) => + ({ + isInteractive, + isReadOnly, + }: { + isInteractive: boolean; + isReadOnly?: boolean; + }) => { + const stopPropagation = (event: React.MouseEvent) => event.stopPropagation(); const hasNonInteractiveIcon = @@ -336,6 +351,7 @@ const Select = < Array.isArray(internalSelectedValues) && ( {internalSelectedValues.map( @@ -417,6 +433,7 @@ const Select = < }), [hasMultipleChoices, normalizedOptions, internalSelectedValues], ); + const renderValue = useCallback( (value: Value) => Array.isArray(value) && , [Chips], @@ -436,8 +453,17 @@ const Select = < aria-errormessage={errorMessageElementId} displayEmpty id={id} - inputProps={{ "data-se": testId }} - inputRef={localInputRef} + inputProps={{ + "data-se": testId, + "aria-disabled": isDisabled || isReadOnly, + readOnly: isReadOnly, + }} + inputRef={(el: HTMLInputElement | HTMLTextAreaElement | null) => { + if (localInputRef.current !== el) { + (localInputRef as React.MutableRefObject).current = el; + } + selectRef.current = el; + }} labelId={labelElementId} multiple={hasMultipleChoices} name={nameOverride ?? id} @@ -454,7 +480,7 @@ const Select = < - + )} @@ -464,6 +490,8 @@ const Select = < Chips, inputValues, hasMultipleChoices, + isDisabled, + isReadOnly, nameOverride, odysseyDesignTokens, onBlur, diff --git a/packages/odyssey-react-mui/src/labs/Switch.tsx b/packages/odyssey-react-mui/src/labs/Switch.tsx index bbb5cf6997..49fe304520 100644 --- a/packages/odyssey-react-mui/src/labs/Switch.tsx +++ b/packages/odyssey-react-mui/src/labs/Switch.tsx @@ -43,13 +43,14 @@ const nonForwardedProps = [ "isChecked", "isDisabled", "isFullWidth", + "isReadOnly", "odysseyDesignTokens", ]; const SwitchAndLabelContainer = styled("div", { shouldForwardProp: (prop) => !nonForwardedProps.includes(prop), })< - Pick & { + Pick & { odysseyDesignTokens: DesignTokens; } >(({ isFullWidth, odysseyDesignTokens }) => ({ @@ -66,17 +67,16 @@ const SwitchContainer = styled.div({ const StyledSwitchLabel = styled(FormLabel, { shouldForwardProp: (prop) => !nonForwardedProps.includes(prop), })< - Pick & { + Pick & { odysseyDesignTokens: DesignTokens; } ->(({ isDisabled, odysseyDesignTokens }) => ({ +>(({ isDisabled, isReadOnly, odysseyDesignTokens }) => ({ display: "block", margin: 0, - color: isDisabled - ? odysseyDesignTokens.TypographyColorDisabled - : odysseyDesignTokens.PaletteNeutralDark, + color: odysseyDesignTokens.PaletteNeutralDark, ...(isDisabled && { + color: odysseyDesignTokens.TypographyColorDisabled, p: { color: odysseyDesignTokens.TypographyColorDisabled, }, @@ -84,38 +84,57 @@ const StyledSwitchLabel = styled(FormLabel, { color: `${odysseyDesignTokens.TypographyColorDisabled} !important`, }, }), + + ...(isReadOnly && { + color: odysseyDesignTokens.HueNeutral700, + p: { + color: odysseyDesignTokens.HueNeutral700, + }, + a: { + color: `${odysseyDesignTokens.HueNeutral700} !important`, + }, + }), })); const SwitchTrack = styled("div", { shouldForwardProp: (prop) => !nonForwardedProps.includes(prop), })< - Pick & { + Pick & { odysseyDesignTokens: DesignTokens; } ->(({ isChecked, isDisabled, odysseyDesignTokens }) => ({ +>(({ isChecked, isDisabled, isReadOnly, odysseyDesignTokens }) => ({ position: "relative", width: odysseyDesignTokens.Spacing7, height: `calc(${odysseyDesignTokens.Spacing4} + ${odysseyDesignTokens.Spacing1})`, borderRadius: odysseyDesignTokens.BorderRadiusOuter, - backgroundColor: isDisabled - ? odysseyDesignTokens.HueNeutral200 - : isChecked - ? odysseyDesignTokens.PaletteSuccessLight - : odysseyDesignTokens.HueNeutral300, + backgroundColor: odysseyDesignTokens.HueNeutral300, transition: `background-color ${odysseyDesignTokens.TransitionDurationMain}`, + + ...(isDisabled && { + backgroundColor: odysseyDesignTokens.HueNeutral200, + }), + + ...(isReadOnly && { + backgroundColor: odysseyDesignTokens.HueNeutral600, + }), + + ...(isChecked && + !isDisabled && + !isReadOnly && { + backgroundColor: odysseyDesignTokens.PaletteSuccessLight, + }), })); const SwitchThumb = styled("span", { shouldForwardProp: (prop) => !nonForwardedProps.includes(prop), })< - Pick & { + Pick & { odysseyDesignTokens: DesignTokens; } ->(({ isChecked, isDisabled, odysseyDesignTokens }) => { +>(({ isChecked, isDisabled, isReadOnly, odysseyDesignTokens }) => { const thumbOffset = toRem(3); const trackWidth = stripRem(odysseyDesignTokens.Spacing7); const thumbWidth = stripRem(odysseyDesignTokens.Spacing4) - toRem(2); - const transformDistance = trackWidth - thumbWidth - thumbOffset * 2; return { @@ -125,40 +144,57 @@ const SwitchThumb = styled("span", { width: `calc(${odysseyDesignTokens.Spacing4} - ${toRem(2)}rem)`, height: `calc(${odysseyDesignTokens.Spacing4} - ${toRem(2)}rem)`, borderRadius: odysseyDesignTokens.BorderRadiusRound, - backgroundColor: isDisabled - ? odysseyDesignTokens.HueNeutral50 - : odysseyDesignTokens.HueNeutralWhite, - transform: isChecked - ? `translate3d(${transformDistance}rem, -50%, 0)` - : "translate3d(0, -50%, 0)", + backgroundColor: odysseyDesignTokens.HueNeutralWhite, + transform: "translate3d(0, -50%, 0)", transition: `transform ${odysseyDesignTokens.TransitionDurationMain}`, + + ...(isDisabled && { + backgroundColor: odysseyDesignTokens.HueNeutral50, + }), + + ...(isReadOnly && { + backgroundColor: odysseyDesignTokens.HueNeutral400, + }), + + ...(isChecked && { + transform: `translate3d(${transformDistance}rem, -50%, 0)`, + }), }; }); const SwitchCheckMark = styled(CheckIcon, { shouldForwardProp: (prop) => !nonForwardedProps.includes(prop), })< - Pick & { + Pick & { odysseyDesignTokens: DesignTokens; } ->(({ isChecked, isDisabled, odysseyDesignTokens }) => ({ +>(({ isChecked, isDisabled, isReadOnly, odysseyDesignTokens }) => ({ position: "absolute", top: "50%", left: 3, width: odysseyDesignTokens.Spacing4, transform: "translateY(-50%)", transition: `opacity ${odysseyDesignTokens.TransitionDurationMain}`, - opacity: isChecked ? 1 : 0, + opacity: 0, path: { - fill: isDisabled - ? odysseyDesignTokens.HueNeutral50 - : odysseyDesignTokens.HueNeutralWhite, + fill: odysseyDesignTokens.HueNeutralWhite, }, + + ...(isChecked && { + opacity: 1, + }), + + ...((isDisabled || isReadOnly) && { + path: { + fill: odysseyDesignTokens.HueNeutral50, + }, + }), })); const HiddenCheckbox = styled.input<{ odysseyDesignTokens: DesignTokens; -}>(({ odysseyDesignTokens }) => ({ + isReadOnly?: boolean; +}>(({ odysseyDesignTokens, isReadOnly }) => ({ position: "absolute", top: 0, left: 0, @@ -166,7 +202,7 @@ const HiddenCheckbox = styled.input<{ height: "100%", margin: 0, opacity: 0, - cursor: "pointer", + cursor: isReadOnly ? "default" : "pointer", zIndex: 2, "&:focus-visible": { @@ -194,6 +230,10 @@ export type SwitchProps = { * The value attribute of the Switch */ value: string; + /** + * Determines whether the Switch is read-only + */ + isReadOnly?: boolean; } & Pick< FieldComponentProps, "hint" | "HintLinkComponent" | "id" | "isFullWidth" | "isDisabled" | "name" @@ -209,6 +249,7 @@ type SwitchLabelComponentProps = { isDisabled: SwitchProps["isDisabled"]; isFullWidth: SwitchProps["isFullWidth"]; label: SwitchProps["label"]; + isReadOnly: SwitchProps["isReadOnly"]; }; const SwitchLabel = ({ @@ -218,6 +259,7 @@ const SwitchLabel = ({ inputId, isDisabled, label, + isReadOnly, }: SwitchLabelComponentProps) => { const odysseyDesignTokens = useOdysseyDesignTokens(); @@ -226,6 +268,7 @@ const SwitchLabel = ({ {label} @@ -252,6 +295,7 @@ const Switch = ({ isDefaultChecked, isDisabled, isFullWidth = false, + isReadOnly = false, label, name, onChange, @@ -292,12 +336,16 @@ const Switch = ({ const handleOnChange = useCallback>( (event) => { + if (isReadOnly) { + event.preventDefault(); + return; + } const target = event.target; const { checked, value } = target; setInternalSwitchChecked(checked); onChange?.({ checked, value }); }, - [onChange, setInternalSwitchChecked], + [onChange, setInternalSwitchChecked, isReadOnly], ); return ( @@ -321,6 +369,7 @@ const Switch = ({ isDisabled={isDisabled} isFullWidth={isFullWidth} label={label} + isReadOnly={isReadOnly} /> diff --git a/packages/odyssey-react-mui/src/theme/components.tsx b/packages/odyssey-react-mui/src/theme/components.tsx index d0d1ef650b..9e8cfd3ac1 100644 --- a/packages/odyssey-react-mui/src/theme/components.tsx +++ b/packages/odyssey-react-mui/src/theme/components.tsx @@ -873,86 +873,117 @@ export const components = ({ indeterminateIcon: , }, styleOverrides: { - root: ({ theme }) => ({ - width: `${odysseyTokens.TypographyLineHeightUi}em`, - minWidth: `${odysseyTokens.TypographyLineHeightUi}em`, - height: `${odysseyTokens.TypographyLineHeightUi}em`, - borderRadius: odysseyTokens.BorderRadiusTight, - borderWidth: odysseyTokens.BorderWidthMain, - borderStyle: odysseyTokens.BorderStyleMain, - borderColor: odysseyTokens.HueNeutral500, - padding: 0, - boxShadow: `0 0 0 0 transparent`, - transition: theme.transitions.create( - ["border-color", "background-color", "box-shadow"], - { - duration: odysseyTokens.TransitionDurationMain, + root: ({ ownerState, theme }) => { + const isReadOnly = ownerState?.inputProps?.readOnly; + + return { + width: `${odysseyTokens.TypographyLineHeightUi}em`, + minWidth: `${odysseyTokens.TypographyLineHeightUi}em`, + height: `${odysseyTokens.TypographyLineHeightUi}em`, + borderRadius: odysseyTokens.BorderRadiusTight, + border: `1px solid ${odysseyTokens.HueNeutral500}`, + padding: 0, + boxShadow: `0 0 0 0 transparent`, + transition: theme.transitions.create( + ["border-color", "background-color", "box-shadow"], + { + duration: odysseyTokens.TransitionDurationMain, + }, + ), + + [`.${svgIconClasses.root}`]: { + color: odysseyTokens.HueNeutralWhite, + transition: theme.transitions.create(["color"], { + duration: odysseyTokens.TransitionDurationMain, + }), }, - ), - [`.${svgIconClasses.root}`]: { - color: odysseyTokens.HueNeutralWhite, - transition: theme.transitions.create(["color"], { - duration: odysseyTokens.TransitionDurationMain, - }), - }, + "&.Mui-checked, &.MuiCheckbox-indeterminate": { + backgroundColor: odysseyTokens.PalettePrimaryMain, + borderColor: odysseyTokens.PalettePrimaryMain, - "&.Mui-checked, &.MuiCheckbox-indeterminate": { - backgroundColor: odysseyTokens.PalettePrimaryMain, - borderColor: odysseyTokens.PalettePrimaryMain, + [`.${formControlLabelClasses.root}:hover > &`]: { + backgroundColor: odysseyTokens.PalettePrimaryDark, + borderColor: odysseyTokens.PalettePrimaryDark, + }, + }, [`.${formControlLabelClasses.root}:hover > &`]: { - backgroundColor: odysseyTokens.PalettePrimaryDark, - borderColor: odysseyTokens.PalettePrimaryDark, + backgroundColor: "transparent", + borderColor: odysseyTokens.HueNeutral900, }, - }, - - [`.${formControlLabelClasses.root}:hover > &`]: { - backgroundColor: "transparent", - borderColor: odysseyTokens.HueNeutral900, - }, - ".Mui-error:not(.Mui-valid):hover > &": { - borderColor: odysseyTokens.BorderColorDangerDark, - "&.Mui-checked": { - backgroundColor: odysseyTokens.PaletteDangerDark, + ".Mui-error:not(.Mui-valid):hover > &": { borderColor: odysseyTokens.BorderColorDangerDark, - }, - }, - ".Mui-error:not(.Mui-valid) > &": { - borderColor: odysseyTokens.BorderColorDangerControl, - "&.Mui-checked": { - backgroundColor: odysseyTokens.PaletteDangerMain, - borderColor: odysseyTokens.BorderColorDangerControl, + "&.Mui-checked": { + backgroundColor: odysseyTokens.PaletteDangerDark, + borderColor: odysseyTokens.BorderColorDangerDark, + }, }, + ".Mui-error:not(.Mui-valid) > &": { + borderColor: odysseyTokens.BorderColorDangerControl, + + "&.Mui-checked": { + backgroundColor: odysseyTokens.PaletteDangerMain, + borderColor: odysseyTokens.BorderColorDangerControl, + }, + "&.Mui-focusVisible": { + boxShadow: `0 0 0 2px ${odysseyTokens.HueNeutralWhite}, 0 0 0 4px ${odysseyTokens.PaletteDangerMain}`, + }, + }, "&.Mui-focusVisible": { - boxShadow: `0 0 0 2px ${odysseyTokens.HueNeutralWhite}, 0 0 0 4px ${odysseyTokens.PaletteDangerMain}`, + borderColor: odysseyTokens.HueNeutral900, + boxShadow: `0 0 0 2px ${odysseyTokens.HueNeutralWhite}, 0 0 0 4px ${odysseyTokens.PalettePrimaryMain}`, + outline: "2px solid transparent", + outlineOffset: "1px", }, - }, - "&.Mui-focusVisible": { - borderColor: odysseyTokens.HueNeutral900, - boxShadow: `0 0 0 2px ${odysseyTokens.HueNeutralWhite}, 0 0 0 4px ${odysseyTokens.PalettePrimaryMain}`, - outline: "2px solid transparent", - outlineOffset: "1px", - }, - "&.Mui-disabled": { - backgroundColor: odysseyTokens.HueNeutral50, - borderColor: odysseyTokens.HueNeutral300, - - ".Mui-error:not(.Mui-valid) > &": { + "&.Mui-disabled": { backgroundColor: odysseyTokens.HueNeutral50, borderColor: odysseyTokens.HueNeutral300, - }, - [`.${svgIconClasses.root}`]: { - color: odysseyTokens.HueNeutral300, + ".Mui-error:not(.Mui-valid) > &": { + backgroundColor: odysseyTokens.HueNeutral50, + borderColor: odysseyTokens.HueNeutral300, + }, + + [`.${svgIconClasses.root}`]: { + color: odysseyTokens.HueNeutral300, + }, }, - }, - }), + + ...(isReadOnly && { + // Override default styles + backgroundColor: odysseyTokens.HueNeutral100, + border: `1px solid ${odysseyTokens.HueNeutral300}`, + cursor: "default", + + // Override checked/indeterminate styles + "&.Mui-checked, &.MuiCheckbox-indeterminate": { + backgroundColor: odysseyTokens.HueNeutral100, + borderColor: odysseyTokens.HueNeutral300, + + //Override hover styles + [`.${formControlLabelClasses.root}:hover > &`]: { + backgroundColor: odysseyTokens.HueNeutral100, + borderColor: odysseyTokens.HueNeutral300, + }, + }, + [`.${formControlLabelClasses.root}:hover > &`]: { + backgroundColor: odysseyTokens.HueNeutral100, + borderColor: odysseyTokens.HueNeutral300, + }, + // ReadOnly styles for SVG check icon + [`.${svgIconClasses.root}`]: { + color: odysseyTokens.HueNeutral700, + }, + }), + }; + }, }, }, + MuiChip: { defaultProps: { deleteIcon: , @@ -2244,83 +2275,105 @@ export const components = ({ checkedIcon: <>, }, styleOverrides: { - root: ({ theme }) => ({ - position: "relative", - // to visually align input with label - insetBlockStart: `${2 / theme.typography.fontSize}rem`, - width: `${odysseyTokens.TypographyLineHeightUi}em`, - minWidth: `${odysseyTokens.TypographyLineHeightUi}em`, - height: `${odysseyTokens.TypographyLineHeightUi}em`, - borderRadius: `${odysseyTokens.TypographyLineHeightUi}em`, - borderWidth: odysseyTokens.BorderWidthMain, - borderStyle: odysseyTokens.BorderStyleMain, - borderColor: odysseyTokens.HueNeutral500, - padding: 0, - boxShadow: `0 0 0 0 transparent`, - transition: theme.transitions.create( - ["border-color", "background-color", "box-shadow"], - { - duration: odysseyTokens.TransitionDurationMain, - }, - ), + root: ({ ownerState, theme }) => { + const isReadOnly = ownerState?.inputProps?.readOnly; - "&::before": { - content: "''", - position: "absolute", - width: odysseyTokens.Spacing2, - height: odysseyTokens.Spacing2, - borderRadius: "50%", - backgroundColor: "transparent", - transition: theme.transitions.create(["background-color"], { - duration: odysseyTokens.TransitionDurationMain, - }), - }, - - [`.${formControlLabelClasses.root}:hover > &`]: { - backgroundColor: "transparent", - borderColor: odysseyTokens.HueNeutral900, - }, - ".Mui-error:hover > &": { - backgroundColor: "transparent", - borderColor: odysseyTokens.BorderColorDangerDark, + return { + position: "relative", + insetBlockStart: `${2 / theme.typography.fontSize}rem`, + width: `${odysseyTokens.TypographyLineHeightUi}em`, + minWidth: `${odysseyTokens.TypographyLineHeightUi}em`, + height: `${odysseyTokens.TypographyLineHeightUi}em`, + borderRadius: `${odysseyTokens.TypographyLineHeightUi}em`, + borderWidth: odysseyTokens.BorderWidthMain, + borderStyle: odysseyTokens.BorderStyleMain, + borderColor: odysseyTokens.HueNeutral500, + padding: 0, + boxShadow: `0 0 0 0 transparent`, + transition: theme.transitions.create( + ["border-color", "background-color", "box-shadow"], + { + duration: odysseyTokens.TransitionDurationMain, + }, + ), "&::before": { - backgroundColor: odysseyTokens.PaletteDangerDark, + content: "''", + position: "absolute", + width: odysseyTokens.Spacing2, + height: odysseyTokens.Spacing2, + borderRadius: "50%", + backgroundColor: "transparent", + transition: theme.transitions.create(["background-color"], { + duration: odysseyTokens.TransitionDurationMain, + }), + }, + [`.${formControlLabelClasses.root}:hover > &`]: { + backgroundColor: "transparent", + borderColor: odysseyTokens.HueNeutral900, + }, + ".Mui-error:hover > &": { + backgroundColor: "transparent", + borderColor: odysseyTokens.BorderColorDangerDark, + "&::before": { + backgroundColor: odysseyTokens.PaletteDangerDark, + }, + }, + ".Mui-error > &": { + borderColor: odysseyTokens.BorderColorDangerControl, + "&.Mui-focusVisible": { + boxShadow: `0 0 0 2px ${odysseyTokens.HueNeutralWhite}, 0 0 0 4px ${odysseyTokens.PaletteDangerMain}`, + }, }, - }, - ".Mui-error > &": { - borderColor: odysseyTokens.BorderColorDangerControl, - "&.Mui-focusVisible": { - boxShadow: `0 0 0 2px ${odysseyTokens.HueNeutralWhite}, 0 0 0 4px ${odysseyTokens.PaletteDangerMain}`, + borderColor: odysseyTokens.HueNeutral900, + boxShadow: `0 0 0 2px ${odysseyTokens.HueNeutralWhite}, 0 0 0 4px ${odysseyTokens.PalettePrimaryMain}`, + outline: "2px solid transparent", + outlineOffset: "1px", }, - }, - "&.Mui-focusVisible": { - borderColor: odysseyTokens.HueNeutral900, - boxShadow: `0 0 0 2px ${odysseyTokens.HueNeutralWhite}, 0 0 0 4px ${odysseyTokens.PalettePrimaryMain}`, - outline: "2px solid transparent", - outlineOffset: "1px", - }, - "&.Mui-checked": { - position: "relative", - - "&::before": { - backgroundColor: odysseyTokens.PalettePrimaryMain, + "&.Mui-checked": { + position: "relative", + "&::before": { + backgroundColor: odysseyTokens.PalettePrimaryMain, + }, }, - }, - ".Mui-error > &.Mui-checked::before": { - backgroundColor: odysseyTokens.PaletteDangerMain, - }, - "&.Mui-disabled": { - backgroundColor: odysseyTokens.HueNeutral50, - borderColor: odysseyTokens.BorderColorDisabled, - - "&.Mui-checked::before": { - backgroundColor: odysseyTokens.BorderColorDisabled, + ".Mui-error > &.Mui-checked::before": { + backgroundColor: odysseyTokens.PaletteDangerMain, }, - }, - }), + "&.Mui-disabled": { + backgroundColor: odysseyTokens.HueNeutral50, + borderColor: odysseyTokens.BorderColorDisabled, + "&.Mui-checked::before": { + backgroundColor: odysseyTokens.BorderColorDisabled, + }, + }, + ...(isReadOnly && { + backgroundColor: odysseyTokens.HueNeutral100, + borderColor: odysseyTokens.HueNeutral300, + cursor: "default", + "&::before": { + content: "''", + position: "absolute", + width: odysseyTokens.Spacing2, + height: odysseyTokens.Spacing2, + borderRadius: "50%", + backgroundColor: "transparent", + transition: theme.transitions.create(["background-color"], { + duration: odysseyTokens.TransitionDurationMain, + }), + }, + "&.Mui-checked::before": { + backgroundColor: odysseyTokens.HueNeutral700, + }, + [`.${formControlLabelClasses.root}:hover > &`]: { + backgroundColor: odysseyTokens.HueNeutral100, + borderColor: odysseyTokens.HueNeutral300, + }, + }), + }; + }, }, }, + MuiSnackbar: { defaultProps: { anchorOrigin: { @@ -2347,33 +2400,58 @@ export const components = ({ }, }, styleOverrides: { - select: { - height: "auto", - // We're subtracting a pixel so the total height, including borders, is 40px - paddingBlock: `calc(${odysseyTokens.Spacing3} - ${odysseyTokens.BorderWidthMain})`, - paddingInline: odysseyTokens.Spacing3, - minHeight: 0, + root: ({ ownerState }) => { + const isReadOnly = ownerState?.inputProps?.readOnly; + return { + ...(isReadOnly && { + "&.MuiInputBase-root": { + backgroundColor: odysseyTokens.HueNeutral50, + borderColor: odysseyTokens.HueNeutral200, + "&:hover": { + backgroundColor: odysseyTokens.HueNeutral50, + }, + "&.Mui-focused": { + borderColor: odysseyTokens.PalettePrimaryMain, + }, + }, + }), + "& .MuiSelect-select": { + height: "auto", + paddingBlock: `calc(${odysseyTokens.Spacing3} - ${odysseyTokens.BorderWidthMain})`, + paddingInline: odysseyTokens.Spacing3, + minHeight: 0, + + "&:focus": { + backgroundColor: "transparent", + }, - "&:focus": { - backgroundColor: "transparent", - }, + "& .MuiBox-root": { + display: "flex", + flexWrap: "wrap", + gap: odysseyTokens.Spacing1, + marginBlock: `-${odysseyTokens.Spacing2}`, + marginInline: `-${odysseyTokens.Spacing2}`, + }, - "& .MuiBox-root": { - display: "flex", - flexWrap: "wrap", - gap: odysseyTokens.Spacing1, - marginBlock: `-${odysseyTokens.Spacing2}`, - marginInline: `-${odysseyTokens.Spacing2}`, - }, + ["& .MuiListItemSecondaryAction-root"]: { + display: "none", + }, - ["& .MuiListItemSecondaryAction-root"]: { - display: "none", - }, - }, - icon: { - right: "unset", - insetInlineEnd: odysseyTokens.Spacing3, - color: odysseyTokens.TypographyColorSubordinate, + ...(isReadOnly && { + color: odysseyTokens.HueNeutral700, + cursor: "default", + "&:focus": { + backgroundColor: "transparent", + borderColor: odysseyTokens.PalettePrimaryMain, + }, + }), + }, + "& .MuiSelect-icon": { + right: "unset", + insetInlineEnd: odysseyTokens.Spacing3, + color: odysseyTokens.TypographyColorSubordinate, + }, + }; }, }, }, diff --git a/packages/odyssey-storybook/src/components/odyssey-labs/Switch/Switch.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-labs/Switch/Switch.stories.tsx index 2877f53474..170a51c7c0 100644 --- a/packages/odyssey-storybook/src/components/odyssey-labs/Switch/Switch.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-labs/Switch/Switch.stories.tsx @@ -46,6 +46,18 @@ const storybookMeta: Meta = { HintLinkComponent: fieldComponentPropsMetaData.HintLinkComponent, id: fieldComponentPropsMetaData.id, isDisabled: fieldComponentPropsMetaData.isDisabled, + isReadOnly: { + control: "boolean", + description: "The value attribute of the Switch", + table: { + type: { + summary: "boolean", + }, + defaultValue: { + summary: false, + }, + }, + }, label: { control: "text", description: "The label text for the Switch button", @@ -113,7 +125,12 @@ export const CheckedDisabled: StoryObj = { isDefaultChecked: true, }, }; - +export const CheckedReadOnly: StoryObj = { + args: { + isReadOnly: true, + isDefaultChecked: true, + }, +}; export const Controlled: StoryObj = { render: function C({ ...props }) { const [checked, setChecked] = useState(true); diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Checkbox/Checkbox.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Checkbox/Checkbox.stories.tsx index 02488e0ded..24534fd826 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/Checkbox/Checkbox.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/Checkbox/Checkbox.stories.tsx @@ -76,6 +76,18 @@ const storybookMeta: Meta = { }, }, }, + isReadOnly: { + control: "boolean", + description: "If `true`, the checkbox is read-only", + table: { + type: { + summary: "boolean", + }, + defaultValue: { + summary: false, + }, + }, + }, isRequired: { control: "boolean", description: "If `true`, the checkbox is required", @@ -199,7 +211,7 @@ export const Required: StoryObj = { export const Checked: StoryObj = { args: { - label: "Pre-flight systems check complete", + label: "Automatically assign Okta Admin Console", isDefaultChecked: true, }, }; @@ -214,7 +226,7 @@ export const Disabled: StoryObj = { }, }, args: { - label: "Pre-flight systems check complete", + label: "Automatically assign Okta Admin Console", isDisabled: true, isDefaultChecked: false, }, @@ -230,7 +242,7 @@ export const Indeterminate: StoryObj = { }, }, args: { - label: "Pre-flight systems check complete", + label: "Automatically assign Okta Admin Console", isIndeterminate: true, isDefaultChecked: true, }, @@ -238,7 +250,7 @@ export const Indeterminate: StoryObj = { export const Invalid: StoryObj = { args: { - label: "Pre-flight systems check complete", + label: "Automatically assign Okta Admin Console", validity: "invalid", isDefaultChecked: false, }, @@ -247,6 +259,17 @@ export const Invalid: StoryObj = { }, }; +export const ReadOnly: StoryObj = { + args: { + label: "Automatically assign Okta Admin Console", + isReadOnly: true, + isDefaultChecked: true, + }, + play: async ({ canvasElement, step }) => { + await checkTheBox({ canvasElement, step })("ReadOnly Checkbox"); + }, +}; + export const Hint: StoryObj = { parameters: { docs: { @@ -274,7 +297,7 @@ export const Controlled: StoryObj = { }, }, args: { - label: "Pre-flight systems check complete", + label: "Automatically assign Okta Admin Console", isChecked: true, onChange: () => {}, }, diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/CheckboxGroup/CheckboxGroup.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/CheckboxGroup/CheckboxGroup.stories.tsx index d878e85340..f64297bf50 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/CheckboxGroup/CheckboxGroup.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/CheckboxGroup/CheckboxGroup.stories.tsx @@ -51,6 +51,18 @@ const storybookMeta: Meta = { hint: fieldComponentPropsMetaData.hint, HintLinkComponent: fieldComponentPropsMetaData.HintLinkComponent, isDisabled: fieldComponentPropsMetaData.isDisabled, + isReadOnly: { + control: "boolean", + description: "If `true`, the checkbox group is read-only", + table: { + type: { + summary: "boolean", + }, + defaultValue: { + summary: false, + }, + }, + }, isRequired: { control: "boolean", description: "If `true`, the checkbox group is required", @@ -91,6 +103,7 @@ const GroupTemplate: StoryObj = { hint={args.hint} HintLinkComponent={args.HintLinkComponent} isDisabled={args.isDisabled} + isReadOnly={args.isReadOnly} label="Systems check" isRequired={args.isRequired} > @@ -126,6 +139,18 @@ export const Disabled: StoryObj = { }, }; +export const ReadOnly: StoryObj = { + ...GroupTemplate, + parameters: { + controls: { + exclude: ["isDefaultChecked", "isIndeterminate"], + }, + }, + args: { + isReadOnly: true, + }, +}; + export const Error: StoryObj = { ...GroupTemplate, parameters: { diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Radio/Radio.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Radio/Radio.stories.tsx index c6a67a06e7..a4a703bd78 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/Radio/Radio.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/Radio/Radio.stories.tsx @@ -32,6 +32,15 @@ const storybookMeta: Meta = { }, }, }, + hint: { + control: "text", + description: "The helper text content", + table: { + type: { + summary: "string", + }, + }, + }, isDisabled: fieldComponentPropsMetaData.isDisabled, isInvalid: { control: "boolean", @@ -42,12 +51,15 @@ const storybookMeta: Meta = { }, }, }, - hint: { - control: "text", - description: "The helper text content", + isReadOnly: { + control: "boolean", + description: "If `true`, the radio button is read-only", table: { type: { - summary: "string", + summary: "boolean", + }, + defaultValue: { + summary: false, }, }, }, @@ -123,7 +135,12 @@ export const Default: StoryObj = { }); }, }; - +export const Checked: StoryObj = { + args: { + label: "Automatically assign Okta Admin Console", + isChecked: true, + }, +}; export const Disabled: StoryObj = { args: { isDisabled: true, @@ -142,6 +159,14 @@ export const Hint: StoryObj = { hint: "All admin roles get access when the role is assigned.", }, }; +export const ReadOnly: StoryObj = { + args: { + label: "Automatically assign Okta Admin Console", + isReadOnly: true, + isChecked: true, + }, +}; + export const Invalid: StoryObj = { args: { isChecked: true, diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/RadioGroup/RadioGroup.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/RadioGroup/RadioGroup.stories.tsx index f3cca940e8..9443b1477f 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/RadioGroup/RadioGroup.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/RadioGroup/RadioGroup.stories.tsx @@ -19,7 +19,6 @@ import { fieldComponentPropsMetaData } from "../../../fieldComponentPropsMetaDat import { MuiThemeDecorator } from "../../../../.storybook/components"; import { userEvent, within } from "@storybook/testing-library"; import { axeRun } from "../../../axe-util"; - const storybookMeta: Meta = { title: "MUI Components/Forms/RadioGroup", component: RadioGroup, @@ -54,6 +53,7 @@ const storybookMeta: Meta = { HintLinkComponent: fieldComponentPropsMetaData.HintLinkComponent, id: fieldComponentPropsMetaData.id, isDisabled: fieldComponentPropsMetaData.isDisabled, + isReadOnly: fieldComponentPropsMetaData.isReadOnly, label: { control: "text", description: "The text label for the radio group", @@ -137,7 +137,13 @@ export const Disabled: StoryObj = { defaultValue: "", }, }; - +export const ReadOnly: StoryObj = { + ...Template, + args: { + isReadOnly: true, + defaultValue: "Warp Speed", + }, +}; export const Error: StoryObj = { ...Template, parameters: { @@ -168,20 +174,6 @@ export const UncontrolledRadioGroup: StoryObj = { args: { defaultValue: "Warp Speed", }, - play: async ({ canvasElement, step }) => { - await step("select controlled radio button", async () => { - const canvas = within(canvasElement); - const radiogroup = canvas.getByRole("radiogroup") as HTMLInputElement; - const radio = canvas.getByLabelText( - "Ludicrous Speed", - ) as HTMLInputElement; - if (radiogroup && radio) { - await userEvent.click(radio); - } - await expect(radio).toBeChecked(); - await axeRun("select controlled radio button"); - }); - }, }; export const ControlledRadioGroup: StoryObj = { @@ -236,7 +228,7 @@ export const ControlledRadioGroupWithRadioHints: StoryObj = { value: "Ludicrous Speed", }, render: function C(props) { - const [value, setValue] = useState("Ludicrous Speed"); + const [value, setValue] = useState("Turtle Speed"); const onChange = useCallback( (_event: ChangeEvent, value: string) => setValue(value), [], diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Select/Select.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Select/Select.stories.tsx index 6eeb7cec63..fc35038a1d 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/Select/Select.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/Select/Select.stories.tsx @@ -22,88 +22,88 @@ import { fieldComponentPropsMetaData } from "../../../fieldComponentPropsMetaDat import { SelectChangeEvent } from "@mui/material"; const optionsArray: SelectProps["options"] = [ - "Earth", - "Mars", - "Ceres", - "Eros", - "Tycho Station", - "Phoebe", - "Ganymede", + "Roles and permissions", + "Okta Privileged Access components", + "Users and Groups administration", + "Resource administration", + "Security administration", + "Deploy and manage servers", + "Okta Privileged Access clients", ]; const optionsObject: SelectProps["options"] = [ { - text: "Earth", - value: "earth", + text: "Roles and permissions", + value: "roles-and-permissions", }, { - text: "Mars", - value: "mars", + text: "Okta Privileged Access gateways", + value: "okta-privileged-access-gateways", }, { - text: "Ceres", - value: "ceres", + text: "Users and Groups administration", + value: "users-and-groups-administration", }, { - text: "Eros", - value: "eros", + text: "Resource administration", + value: "resource-administration", }, { - text: "Tycho Station", - value: "tycho-station", + text: "Security administration", + value: "security-administrator", }, { - text: "Phoebe", - value: "phoebe", + text: "Deploy and manage servers", + value: "deploy-and-manage-servers", }, { - text: "Ganymede", - value: "ganymede", + text: "Okta Privileged Access clients", + value: "okta-privileged-access-clients", }, ]; const optionsGrouped: SelectProps["options"] = [ { - text: "Sol System", + text: "Okta Privileged Access", type: "heading", }, { - text: "Earth", - value: "earth", + text: "Roles and permissions", + value: "roles-and-permissions", }, { - text: "Mars", - value: "mars", + text: "Okta Privileged Access gateways", + value: "okta-privileged-access-gateways", }, { - text: "Ceres", - value: "ceres", + text: "Users and Groups administration", + value: "users-and-groups-administration", }, { - text: "Eros", - value: "eros", + text: "Resource administration", + value: "resource-administration", }, { - text: "Tycho Station", - value: "tycho-station", + text: "Security administration", + value: "security-administrator", }, { - text: "Phoebe", - value: "phoebe", + text: "Deploy and manage servers", + value: "deploy-and-manage-servers", }, { - text: "Ganymede", - value: "ganymede", + text: "Okta Privileged Access clients", + value: "okta-privileged-access-clients", }, { - text: "Extrasolar", + text: "Audit events", type: "heading", }, - "Auberon", - "Al-Halub", - "Freehold", - "Laconia", - "New Terra", + "Resource", + "Action", + "Related Info", + "Actor", + "Date", ]; const storybookMeta: Meta> = { @@ -143,6 +143,7 @@ const storybookMeta: Meta> = { isDisabled: fieldComponentPropsMetaData.isFullWidth, isFullWidth: fieldComponentPropsMetaData.isFullWidth, isOptional: fieldComponentPropsMetaData.isOptional, + isReadOnly: fieldComponentPropsMetaData.isReadOnly, label: { control: "text", description: "The label text for the select component", @@ -211,8 +212,8 @@ const storybookMeta: Meta> = { }, }, args: { - hint: "Select your destination in the Sol system.", - label: "Destination", + hint: "Select a topic to learn more", + label: "Okta documentation", options: optionsArray, }, decorators: [MuiThemeDecorator], @@ -223,7 +224,7 @@ export default storybookMeta; export const Default: StoryObj = { play: async ({ canvasElement, step }) => { - await step("Select Earth from the listbox", async () => { + await step("Select Roles and permissions from the listbox", async () => { const comboBoxElement = canvasElement.querySelector( '[aria-haspopup="listbox"]', ); @@ -236,7 +237,7 @@ export const Default: StoryObj = { await userEvent.tab(); await waitFor(() => expect(listboxElement).not.toBeInTheDocument()); const inputElement = canvasElement.querySelector("input"); - await expect(inputElement?.value).toBe("Earth"); + await expect(inputElement?.value).toBe("Roles and permissions"); await waitFor(() => axeRun("Select Default")); } }); @@ -246,7 +247,7 @@ Default.args = { defaultValue: "" }; export const DefaultValue: StoryObj = { args: { - defaultValue: "Mars", + defaultValue: "Roles and permissions", }, }; @@ -256,10 +257,9 @@ export const Disabled: StoryObj = { defaultValue: "", }, }; - export const Error: StoryObj = { args: { - errorMessage: "Select your destination.", + errorMessage: "Select a topic.", defaultValue: "", }, play: async ({ step }) => { @@ -272,7 +272,7 @@ export const Error: StoryObj = { export const ErrorsList: StoryObj = { args: { isMultiSelect: true, - errorMessage: "Select your destination.", + errorMessage: "Select a topic.", errorMessageList: [ "Select at least one item", "Select no more than 3 items", @@ -297,7 +297,6 @@ export const HintLink: StoryObj = { HintLinkComponent: Learn more, }, }; - export const OptionsObject: StoryObj = { args: { options: optionsObject, @@ -349,14 +348,32 @@ export const MultiSelect: StoryObj = { await waitFor(() => expect(listboxElement).not.toBeInTheDocument()); const inputElement = canvasElement.querySelector("input"); - await expect(inputElement?.value).toBe("Earth,Mars"); + await expect(inputElement?.value).toBe( + "Roles and permissions,Okta Privileged Access components", + ); await userEvent.click(canvasElement); await waitFor(() => axeRun("Select Multiple")); } }); }, }; - +export const ReadOnly: StoryObj = { + args: { + isReadOnly: true, + defaultValue: "Security administration", + }, +}; +export const ReadOnlyMultiSelect: StoryObj = { + args: { + isMultiSelect: true, + isReadOnly: true, + defaultValue: [ + "Roles and permissions", + "Security administration", + "Deploy and manage servers", + ], + }, +}; export const ControlledSelect: StoryObj = { parameters: { docs: { @@ -418,7 +435,10 @@ export const ControlledPreselectedMultipleSelect: StoryObj = { hasMultipleChoices: true, }, render: function C(props) { - const [localValue, setLocalValue] = useState(["Earth", "Mars"]); + const [localValue, setLocalValue] = useState([ + "Roles and permissions", + "Resource administration", + ]); const onChange = useCallback( (event: SelectChangeEvent) => setLocalValue(event.target.value as string[]),