/* * The MIT License (MIT) * * Copyright (c) 2015 - present Instructure, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ import type { NewComponentTypes, SharedTokens } from '@instructure/ui-themes' import { TextInputProps, TextInputStyle, TextInputStyleProps } from './props.js' import { calcFocusOutlineStyles } from '@instructure/emotion' /** * --- * private: true * --- * Generates the style object from the theme and provided additional information * @param componentTheme The theme variable object. * @param props the props of the component, the style is applied to * @param sharedTokens Shared token object that stores common values for the theme. * @param state the state of the component, the style is applied to * @return The final style object, which will be used in the component */ const generateStyle = ( componentTheme: ReturnType, props: TextInputProps, sharedTokens: SharedTokens, state: TextInputStyleProps ): TextInputStyle => { const { size, textAlign, shouldNotWrap, renderBeforeInputElementGap } = props const { interaction, success, invalid, beforeElementExists } = state const isEvenSpacing = renderBeforeInputElementGap === 'even' const contentGap = { small: sharedTokens.spacing.general.spaceXs, medium: sharedTokens.spacing.general.spaceXs, large: sharedTokens.spacing.general.spaceSm }[size!] const contentPaddingBlock = { small: sharedTokens.spacing.general.space2xs, medium: sharedTokens.spacing.general.space2xs, large: sharedTokens.spacing.general.spaceXs }[size!] const heightTokens = { small: componentTheme.heightSm, medium: componentTheme.heightMd, large: componentTheme.heightLg } const inputHeightStyle = (sizeHeight: string) => isEvenSpacing ? { height: 'auto' } : { height: `calc(${sizeHeight} - (2 * ${componentTheme.borderWidth}))`, lineHeight: `calc(${sizeHeight} - (2 * ${componentTheme.borderWidth}))` } const sizeVariants = { small: { fontSize: componentTheme.fontSizeSm, ...inputHeightStyle(componentTheme.heightSm) }, medium: { fontSize: componentTheme.fontSizeMd, ...inputHeightStyle(componentTheme.heightMd) }, large: { fontSize: componentTheme.fontSizeLg, ...inputHeightStyle(componentTheme.heightLg) } } const paddingHorizontalVariants = { small: componentTheme.paddingHorizontalSm, medium: componentTheme.paddingHorizontalMd, large: componentTheme.paddingHorizontalLg } const inputInteractionStates = { ...(interaction === 'enabled' && { color: componentTheme.textColor, '&::placeholder': { color: componentTheme.placeholderColor }, '&:hover::placeholder': { color: componentTheme.placeholderHoverColor } // placeholder is not rendered in the `readOnly` and `disabled` state }), ...(interaction === 'readonly' && { color: componentTheme.textReadonlyColor }), ...(interaction === 'disabled' && { color: componentTheme.textDisabledColor }) } const inputStyle = { all: 'initial', width: '100%', WebkitFontSmoothing: 'antialiased', MozOsxFontSmoothing: 'grayscale', appearance: 'none', margin: 0, display: 'block', boxSizing: 'border-box', outline: 'none', fontFamily: componentTheme.fontFamily, fontWeight: componentTheme.fontWeight, // padding of the text in the input padding: `0 ${componentTheme.gapContent} 0 ${componentTheme.gapContent}`, background: 'transparent', border: 'none', verticalAlign: 'baseline', '&[autocomplete="off"]::-webkit-contacts-auto-fill-button': { display: 'none !important' }, '&:focus': { boxShadow: 'initial' }, ...sizeVariants[size!], textAlign: textAlign, ...inputInteractionStates } const viewBase = { boxSizing: 'border-box', fontFamily: componentTheme.fontFamily, maxWidth: '100%', overflow: 'visible', unicodeBidi: 'isolate' } const containerInteractionStates = { ...(interaction === 'enabled' && { backgroundColor: componentTheme.backgroundColor, borderColor: componentTheme.borderColor, ...(success && { borderColor: componentTheme.successBorderColor }), ...(invalid && { borderColor: componentTheme.errorBorderColor }), '&:hover': { backgroundColor: componentTheme.backgroundHoverColor, borderColor: componentTheme.borderHoverColor, ...(success && { borderColor: componentTheme.successBorderColor }), ...(invalid && { borderColor: componentTheme.errorBorderColor }) } }), ...(interaction === 'readonly' && { backgroundColor: componentTheme.backgroundReadonlyColor, borderColor: componentTheme.borderReadonlyColor }), ...(interaction === 'disabled' && { cursor: 'not-allowed', pointerEvents: 'none', backgroundColor: componentTheme.backgroundDisabledColor, borderColor: componentTheme.borderDisabledColor }) } const focusOutline = calcFocusOutlineStyles(sharedTokens.focusOutline, { focusColor: invalid ? 'danger' : success ? 'success' : undefined, // Only display the focus outline when the input is focused. // This is to prevent double focus ring when e.g., a Button is rendered in // `renderBeforeInput` customCSSSelector: '&:has(input:focus)' }) return { textInput: { label: 'textInput', ...inputStyle, '&:is(input)[type]': inputStyle, '&:-webkit-any(input)[type]': inputStyle }, facade: { label: 'textInput__facade', position: 'relative', display: 'block', boxSizing: 'border-box', border: `${componentTheme.borderWidth} solid`, borderRadius: componentTheme.borderRadius, color: componentTheme.textColor, ...containerInteractionStates, ...focusOutline }, layout: { label: 'textInput__layout', ...viewBase, display: 'flex', alignItems: 'center', justifyContent: 'flex-start', flexDirection: 'row', ...(!shouldNotWrap && { flexWrap: 'wrap' }), // left padding of the `renderBeforeInput` element ...(beforeElementExists && { paddingInlineStart: paddingHorizontalVariants[size!] }), ...(isEvenSpacing && { minHeight: heightTokens[size!], paddingBlock: contentPaddingBlock, gap: contentGap }) }, inputLayout: { label: 'textInput__inputLayout', flexGrow: 1, ...viewBase, display: 'flex', alignItems: 'center', justifyContent: 'flex-start', flexDirection: 'row' }, beforeElement: { label: 'textInput__beforeElement', ...(interaction === 'disabled' && !isEvenSpacing ? { opacity: 0.5 } : { display: 'contents' }), ...(interaction === 'disabled' && isEvenSpacing && { '& > *': { opacity: 0.5 } }) }, afterElement: { ...(interaction === 'disabled' && { opacity: 0.5 }), display: 'flex', alignItems: 'center', paddingInlineEnd: paddingHorizontalVariants[size!], label: 'textInput__afterElement', ...viewBase, borderRadius: componentTheme.borderRadius, flexShrink: 0 } } } export default generateStyle