UNPKG

@okta/okta-signin-widget

Version:
357 lines (344 loc) 11.9 kB
/* * 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. */ // Leonardo doesn't have TS support in stable version alpha.13 so types are defined in leonardo.d.ts import { BackgroundColor, Color, CssColor, Theme, } from '@adobe/leonardo-contrast-colors'; import * as Tokens from '@okta/odyssey-design-tokens'; import { createOdysseyMuiTheme, DesignTokensOverride, ThemeOptions } from '@okta/odyssey-react-mui'; import Logger from '../../../util/Logger'; import { BrandColors } from '../types'; import { isLtrField } from '.'; import { mergeThemes } from './mergeThemes'; const WHITE_HEX = '#ffffff'; // Odyssey-defined contrast ratios for their WCAG-friendly palettes const ODYSSEY_RATIOS = [1.1, 1.31, 1.61, 2.22, 3.32, 4.52, 4.93, 8.72, 11.73, 14.94]; interface OdysseyPalette { CustomHue50: string, CustomHue100: string, CustomHue200: string, CustomHue300: string, CustomHue400: string, CustomHue500: string, CustomHue600: string, CustomHue700: string, CustomHue800: string, CustomHue900: string } /** * Generates a palette using @adobe/leonardo-contrast-colors based on the "main" * (f.k.a. "base") value. Leonardo generates 10 different hues that adhere to Odyssey's * contrast ratios * * Example: * PalettePrimaryMain -> CustomHue500 * PalettePrimaryLighter -> CustomHue50 * PalettePrimaryLight -> CustomHue300 * PalettePrimaryDark -> CustomHue700 * PalettePrimaryDarker -> CustomHue900 * * @param main Main color used to generate a palette */ export const generatePalette = (main: string): OdysseyPalette | null => { try { const primaryColor = new Color({ name: 'Custom Hue', // Leonardo will throw "Invalid Color Key" error if string is not of type CssColor colorKeys: [main as CssColor], ratios: ODYSSEY_RATIOS, }); // SIW always has a white background color const backgroundColor = new BackgroundColor({ name: 'Background Color', colorKeys: [WHITE_HEX], ratios: [], }); const leonardoTheme = new Theme({ colors: [primaryColor], backgroundColor, lightness: 100, }).contrastColorPairs; // Remap Leonardo theme to use Odyssey color stops from 50-900 const theme = { CustomHue50: leonardoTheme.CustomHue100, CustomHue100: leonardoTheme.CustomHue200, CustomHue200: leonardoTheme.CustomHue300, CustomHue300: leonardoTheme.CustomHue400, CustomHue400: leonardoTheme.CustomHue500, CustomHue500: leonardoTheme.CustomHue600, CustomHue600: leonardoTheme.CustomHue700, CustomHue700: leonardoTheme.CustomHue800, CustomHue800: leonardoTheme.CustomHue900, CustomHue900: leonardoTheme.CustomHue1000, }; return theme; } catch (err) { Logger.warn(err); return null; } }; export const createThemeAndTokens = ( brandColors?: BrandColors, customTokens?: Partial<DesignTokensOverride>, ): { themeOverride: ThemeOptions, tokensOverride: DesignTokensOverride } => { const tokensOverride = { ...customTokens }; if (brandColors?.primaryColor) { const p = generatePalette(brandColors.primaryColor); tokensOverride.PalettePrimaryLighter ??= p?.CustomHue50; tokensOverride.PalettePrimaryHighlight ??= p?.CustomHue100; tokensOverride.PalettePrimaryLight ??= p?.CustomHue300; tokensOverride.PalettePrimaryMain ??= p?.CustomHue500; tokensOverride.FocusOutlineColorPrimary ??= p?.CustomHue500; tokensOverride.BorderColorPrimaryControl ??= p?.CustomHue500; tokensOverride.TypographyColorAction ??= p?.CustomHue600; tokensOverride.PalettePrimaryText ??= p?.CustomHue600; tokensOverride.BorderColorPrimaryDark ??= p?.CustomHue700; tokensOverride.PalettePrimaryDark ??= p?.CustomHue700; tokensOverride.PalettePrimaryDarker ??= p?.CustomHue800; tokensOverride.PalettePrimaryHeading ??= p?.CustomHue900; } const mergedTokens = { ...Tokens, ...tokensOverride }; // Odyssey 1.x does not export their theme, but we can recreate it const baseOdysseyTheme = createOdysseyMuiTheme({ odysseyTokens: mergedTokens, }); // Merge default Odyssey 1.x theme with component overrides const themeOverride = mergeThemes(baseOdysseyTheme, { components: { MuiAccordion: { styleOverrides: { root: { border: 0, }, }, }, MuiAccordionDetails: { styleOverrides: { root: { paddingInline: mergedTokens.Spacing0, paddingBlock: mergedTokens.Spacing0, paddingBlockStart: mergedTokens.Spacing4, }, }, }, MuiAccordionSummary: { styleOverrides: { root: { display: 'inline-flex', minHeight: 0, paddingInline: mergedTokens.Spacing0, paddingBlock: mergedTokens.Spacing0, backgroundColor: 'transparent', '&:hover': { backgroundColor: 'transparent', }, '&:focus': { backgroundColor: 'transparent', }, '& .MuiAccordionSummary-content': { margin: mergedTokens.Spacing0, '& .MuiTypography-root': { textDecoration: 'underline', fontWeight: mergedTokens.TypographyWeightBody, color: mergedTokens.TypographyColorAction, '&:hover': { color: mergedTokens.BorderColorPrimaryDark, }, }, }, '& .MuiAccordionSummary-expandIconWrapper': { display: 'none', }, }, }, }, MuiAlert: { styleOverrides: { root: { gap: mergedTokens.Spacing0, }, icon: { paddingInlineEnd: mergedTokens.Spacing5, flexShrink: 0, }, }, }, MuiButton: { styleOverrides: { root: ({ ownerState }) => ({ display: 'flex', justifyContent: 'center', alignItems: 'center', whiteSpace: 'normal', // Odyssey CircularProgress does not allow color change but SIW needs a white spinner // when rendering it as the startIcon for primary buttons ...(ownerState.variant === 'primary' && { '&:disabled': { color: mergedTokens.PalettePrimaryLight, backgroundColor: mergedTokens.PalettePrimaryHighlight, '& .MuiCircularProgress-root': { color: mergedTokens.PalettePrimaryLight, }, }, }), // Fix for IE11 - don't shrink icon if the button text is multiline ...(ownerState.startIcon && { '& .MuiButton-startIcon': { flexShrink: 0, }, }), }), }, }, MuiCheckbox: { styleOverrides: { root: { // Odyssey uses gap for spacing between Checkbox and label which is not IE11-friendly marginInlineEnd: mergedTokens.Spacing2, }, }, }, MuiChip: { styleOverrides: { root: { lineHeight: 'normal', }, }, }, MuiFormControlLabel: { styleOverrides: { root: { // Odyssey uses gap for spacing between Checkbox and label which is not IE11-friendly gap: mergedTokens.Spacing0, }, label: { // Fixes text overflow in IE11 width: '100%', // Forces Odyssey Checkbox hint text to be grey even during error state '& .MuiFormHelperText-root.Mui-error': { color: mergedTokens.TypographyColorSubordinate, }, }, }, }, MuiFormHelperText: { styleOverrides: { root: { display: 'block', }, }, }, MuiInputBase: { styleOverrides: { root: ({ ownerState }) => ({ width: '100%', // Odyssey sets flex: "1" but that results in the following IE11 flexbug // https://github.com/philipwalton/flexbugs?tab=readme-ov-file#flexbug-7 flex: 'auto', ...(ownerState.name && isLtrField(ownerState.name) && { direction: 'ltr', }), }), input: { '::-ms-reveal': { display: 'none', }, }, adornedEnd: ({ ownerState }) => ({ // Odyssey does not support a focus indicator around toggle icon button '& .MuiIconButton-root:focus-visible': { borderRadius: mergedTokens.BorderRadiusMain, outlineColor: mergedTokens.FocusOutlineColorPrimary, outlineStyle: mergedTokens.FocusOutlineStyle, outlineWidth: mergedTokens.FocusOutlineWidthTight, }, // Explicitly switch to physical properties for password toggle icon since // IE11 stylis plugin cannot handle nested logical properties ...(ownerState.name && isLtrField(ownerState.name) && { '& .MuiInputAdornment-root': { marginRight: mergedTokens.Spacing2, }, }), }), adornedStart: ({ ownerState }) => ({ // Explicitly switch to physical properties for telephone code ...(ownerState.type === 'tel' && { '& .MuiInputAdornment-root': { marginInlineStart: mergedTokens.Spacing0, marginLeft: mergedTokens.Spacing2, }, }), }), }, }, MuiInputLabel: { styleOverrides: { root: ({ ownerState }) => ({ wordBreak: 'break-word', whiteSpace: 'normal', ...(ownerState.formControl && { // Odyssey sets position: "initial" which is not supported in IE11 // "initial" uses browser default which is "static" position: 'static', }), }), }, }, MuiLink: { styleOverrides: { button: { verticalAlign: 'baseline', }, }, }, MuiNativeSelect: { styleOverrides: { select: { paddingRight: `${mergedTokens.Spacing7} !important`, }, }, }, MuiRadio: { styleOverrides: { root: { // Odyssey uses gap for spacing between Checkbox/Radio and label marginInlineEnd: mergedTokens.Spacing2, '&.Mui-checked': { // Odyssey position: absolute breaks radio checked circle alignment '&::before': { position: 'relative', backgroundColor: mergedTokens.PalettePrimaryMain, }, }, }, }, }, MuiScopedCssBaseline: { styleOverrides: { root: { figure: {}, }, }, }, MuiTypography: { styleOverrides: { root: { '&[data-se="o-form-head"]': { outline: 'none', }, }, }, }, }, }); return { themeOverride, tokensOverride }; };