@okta/okta-signin-widget
Version:
The Okta Sign-In Widget
357 lines (344 loc) • 11.9 kB
text/typescript
/*
* 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 };
};