diff --git a/libraries/core-react/src/components/Input/Input.tokens.ts b/libraries/core-react/src/components/Input/Input.tokens.ts index 3f25e29fab..9358ad57b7 100644 --- a/libraries/core-react/src/components/Input/Input.tokens.ts +++ b/libraries/core-react/src/components/Input/Input.tokens.ts @@ -42,6 +42,7 @@ type InputProps = { background: string typography: Typography placeholderColor: string + disabledColor: string spacings: { comfortable: Spacing compact: Spacing @@ -59,6 +60,7 @@ export const input: InputProps = { color: colors.text.static_icons__default.hex, }, placeholderColor: colors.text.static_icons__tertiary.hex, + disabledColor: colors.interactive.disabled__text.hex, spacings, default: { border: { diff --git a/libraries/core-react/src/components/Input/Input.tsx b/libraries/core-react/src/components/Input/Input.tsx index 502064ed45..f724e04a6c 100644 --- a/libraries/core-react/src/components/Input/Input.tsx +++ b/libraries/core-react/src/components/Input/Input.tsx @@ -6,7 +6,13 @@ import { typographyTemplate, spacingsTemplate } from '@utils' import type { Variants } from '../TextField/types' import type { Spacing } from '@equinor/eds-tokens' -const Variation = ({ variant }: { variant: InputVariantProps }) => { +const Variation = ({ + variant, + token, +}: { + variant: string + token: InputVariantProps +}) => { if (!variant) { return `` } @@ -14,12 +20,13 @@ const Variation = ({ variant }: { variant: InputVariantProps }) => { const { focus: { border: focusBorderOutline }, border: { outline: borderOutline, bottom: borderBottom }, - } = variant + } = token return css` border: none; outline: ${borderOutline.width} solid ${borderOutline.color}; box-shadow: inset 0 -${borderBottom.width} 0 0 ${borderBottom.color}; + &:active, &:focus { outline-offset: 0; @@ -31,7 +38,6 @@ const Variation = ({ variant }: { variant: InputVariantProps }) => { cursor: not-allowed; box-shadow: none; outline: none; - &:focus, &:active { outline: none; @@ -42,7 +48,8 @@ const Variation = ({ variant }: { variant: InputVariantProps }) => { type StyledProps = { spacings: Spacing - variant: InputVariantProps + token: InputVariantProps + variant: string } const StyledInput = styled.input` @@ -51,7 +58,6 @@ const StyledInput = styled.input` margin: 0; border: none; appearance: none; - background: ${tokens.background}; ${({ spacings }) => spacingsTemplate(spacings)} @@ -61,6 +67,9 @@ const StyledInput = styled.input` &::placeholder { color: ${tokens.placeholderColor}; } + &:disabled { + color: ${tokens.disabledColor}; + } ` export type InputProps = { @@ -98,7 +107,8 @@ export const Input = React.forwardRef( ref, type, disabled, - variant: inputVariant, + variant: variant, + token: inputVariant, spacings: spacings, ...other, } diff --git a/libraries/core-react/src/components/TextField/Icon/Icon.tsx b/libraries/core-react/src/components/TextField/Icon/Icon.tsx index 7268347606..bee2ae292b 100644 --- a/libraries/core-react/src/components/TextField/Icon/Icon.tsx +++ b/libraries/core-react/src/components/TextField/Icon/Icon.tsx @@ -49,13 +49,6 @@ const StyledIcon = styled.div` ${Variation} ` -const StyledIputIcon = styled(StyledIcon)` - position: absolute; - right: ${({ spacings }) => spacings.right}; - top: ${({ spacings }) => spacings.top}; - bottom: ${({ spacings }) => spacings.bottom}; -` - type TextfieldIconProps = { /** isDisabled */ isDisabled?: boolean @@ -74,7 +67,6 @@ const InputIcon = React.forwardRef( { variant = 'default', isDisabled = false, - isInputIcon = true, spacings = tokens.spacings.comfortable, colors = { color: tokens[variant].color, @@ -96,17 +88,9 @@ const InputIcon = React.forwardRef( } return ( - <> - {isInputIcon ? ( - - {children} - - ) : ( - - {children} - - )} - + + {children} + ) }, ) diff --git a/libraries/core-react/src/components/TextField/InputWrapper.tsx b/libraries/core-react/src/components/TextField/InputWrapper.tsx index 53ef06085d..4989d2407b 100644 --- a/libraries/core-react/src/components/TextField/InputWrapper.tsx +++ b/libraries/core-react/src/components/TextField/InputWrapper.tsx @@ -1,10 +1,111 @@ import * as React from 'react' -import { InputHTMLAttributes } from 'react' +import { InputHTMLAttributes, ReactNode } from 'react' import { useTextField } from './context' import { Input } from '../Input' +import { Icon } from './Icon' import type { Variants } from './types' +import type { TextFieldToken } from './TextField.tokens' +import styled, { css } from 'styled-components' +import { typographyTemplate, outlineTemplate } from '@utils' +import * as tokens from './TextField.tokens' -type TextfieldInputProps = { +const { textfield } = tokens + +const Variation = ({ + variant, + isFocused, + token, +}: { + variant: string + token: TextFieldToken + isFocused: boolean +}) => { + if (!variant) { + return `` + } + + return css` + box-shadow: ${isFocused + ? `none` + : variant === 'default' + ? `inset 0 -1px 0 0 ${ + token.border?.type === 'border' && token.border?.color + }` + : `0 0 0 1px ${token.border?.type === 'border' && token.border?.color}`}; + ${isFocused && outlineTemplate(token.states.focus.outline)} + ` +} + +const StyledInput = styled(Input)` + outline: none; + + &:active, + &:focus { + outline: none; + box-shadow: none; + } +` + +type InputWithAdornmentsType = { + isFocused: boolean + isDisabled: boolean + variant: string + token: TextFieldToken +} + +export const InputWithAdornments = styled.div` + display: flex; + align-items: center; + ${{ + background: textfield.background, + paddingRight: textfield.spacings.right, + }} + ${Variation} + ${({ isDisabled }) => + isDisabled && { + boxShadow: 'none', + cursor: 'not-allowed', + outline: 'none', + }} +` + +type UnitType = { + isDisabled: boolean +} + +const Unit = styled.span` + ${typographyTemplate(textfield.entities.unit.typography)}; + /* Yes, we don't like magic numbers, but if you have both unit and icon, + the unit is slightly off due to line-height and font */ + display: inline-block; + margin-top: 3px; + ${({ isDisabled }) => + isDisabled && { + color: textfield.entities.unit.states.disabled.typography.color, + }} +` + +type AdornmentsType = { + multiline: boolean +} + +const Adornments = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 100%; + margin-left: ${textfield.spacings.left}; + & div:nth-child(2) { + margin-left: ${textfield.spacings.left}; + } + ${({ multiline }) => + multiline && { + alignSelf: 'start', + marginTop: `${textfield.spacings.top}`, + }} +` + +type InputWrapperProps = { /** Specifies if text should be bold */ multiline?: boolean /** Placeholder */ @@ -17,14 +118,23 @@ type TextfieldInputProps = { type?: string /** Read Only */ readonly?: boolean + /** Unit text */ + unit?: string + /* Input icon */ + inputIcon?: ReactNode } & InputHTMLAttributes export const InputWrapper = React.forwardRef< HTMLInputElement, - TextfieldInputProps ->(function InputWrapper({ multiline, variant, disabled, type, ...other }, ref) { - const { handleFocus, handleBlur } = useTextField() + InputWrapperProps +>(function InputWrapper( + { multiline, variant, disabled, type, unit, inputIcon, ...other }, + ref, +) { + const { handleFocus, handleBlur, isFocused } = useTextField() + const actualVariant = variant === 'default' ? 'textfield' : variant + const inputVariant = tokens[actualVariant] const inputProps = { multiline, ref, @@ -34,5 +144,32 @@ export const InputWrapper = React.forwardRef< ...other, } - return + return ( + <> + {inputIcon || unit ? ( + + + + {unit && {unit}} + {inputIcon && ( + + {inputIcon} + + )} + + + ) : ( + + )} + + ) }) diff --git a/libraries/core-react/src/components/TextField/TextField.tokens.ts b/libraries/core-react/src/components/TextField/TextField.tokens.ts new file mode 100644 index 0000000000..e789ca2a14 --- /dev/null +++ b/libraries/core-react/src/components/TextField/TextField.tokens.ts @@ -0,0 +1,112 @@ +import { tokens } from '@equinor/eds-tokens' +import type { ComponentToken } from '@equinor/eds-tokens' + +const { + colors, + typography, + spacings: { comfortable }, +} = tokens + +export type TextFieldToken = ComponentToken & { + entities?: { + unit?: ComponentToken + } +} +export const textfield: TextFieldToken = { + background: colors.ui.background__light.hex, + border: { + type: 'border', + radius: 0, + width: '1px', + color: colors.interactive.primary__resting.hex, + }, + spacings: { + left: comfortable.small, + right: comfortable.small, + top: comfortable.small, + }, + states: { + focus: { + outline: { + width: '2px', + color: colors.interactive.primary__resting.hex, + style: 'solid', + type: 'outline', + offset: '0px', + }, + }, + }, + entities: { + unit: { + typography: { + ...typography.input.label, + color: colors.text.static_icons__tertiary.hex, + }, + states: { + disabled: { + typography: { + color: colors.interactive.disabled__text.hex, + }, + }, + }, + }, + }, +} + +export const error: TextFieldToken = { + border: { + type: 'border', + radius: 0, + width: '1px', + color: colors.interactive.danger__resting.hex, + }, + states: { + focus: { + outline: { + width: '2px', + color: colors.interactive.danger__hover.hex, + style: 'solid', + type: 'outline', + offset: '0px', + }, + }, + }, +} +export const warning: TextFieldToken = { + border: { + type: 'border', + radius: 0, + width: '1px', + color: colors.interactive.warning__resting.hex, + }, + states: { + focus: { + outline: { + width: '2px', + color: colors.interactive.warning__hover.hex, + style: 'solid', + type: 'outline', + offset: '0px', + }, + }, + }, +} +export const success: TextFieldToken = { + border: { + type: 'border', + radius: 0, + width: '1px', + color: colors.interactive.success__resting.hex, + }, + states: { + focus: { + outline: { + width: '2px', + color: colors.interactive.success__hover.hex, + style: 'solid', + type: 'outline', + offset: '0px', + }, + }, + }, +} diff --git a/libraries/core-react/src/components/TextField/TextField.tsx b/libraries/core-react/src/components/TextField/TextField.tsx index 500732abcd..6b18608742 100644 --- a/libraries/core-react/src/components/TextField/TextField.tsx +++ b/libraries/core-react/src/components/TextField/TextField.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import { ReactNode, InputHTMLAttributes, TextareaHTMLAttributes } from 'react' import styled from 'styled-components' -import { Icon } from './Icon' import { InputWrapper } from './InputWrapper' import { Label } from '../Label' import { HelperText } from './HelperText' @@ -13,14 +12,6 @@ const Container = styled.div` width: 100%; ` -const RelativeContainer = styled.div` - position: relative; -` - -const PaddedInputWrapper = styled(InputWrapper)` - padding-right: 32px; -` - export type TextFieldProps = { /** @ignore */ className?: string @@ -32,6 +23,8 @@ export type TextFieldProps = { label?: string /** Meta text */ meta?: string + /** Unit text */ + unit?: string /** Helper text */ helperText?: string /** Placeholder text */ @@ -59,6 +52,7 @@ export const TextField = React.forwardRef( id, label, meta, + unit, helperText, placeholder, disabled, @@ -80,6 +74,7 @@ export const TextField = React.forwardRef( variant, ref: inputRef, inputIcon, + unit, ...other, } @@ -108,18 +103,7 @@ export const TextField = React.forwardRef( {showLabel && diff --git a/libraries/core-react/src/components/TextField/context.tsx b/libraries/core-react/src/components/TextField/context.tsx index d24d6e39ca..462919dca1 100644 --- a/libraries/core-react/src/components/TextField/context.tsx +++ b/libraries/core-react/src/components/TextField/context.tsx @@ -26,7 +26,6 @@ export const TextFieldProvider = ({ children }: ProviderProps): JSX.Element => { const [state, setState] = useState(initalState) const handleFocus = () => { - console.log('handle focus', state.isFocused) setState((prevState) => ({ ...prevState, isFocused: true })) } const handleBlur = () => { diff --git a/libraries/core-react/stories/components/TextField.stories.tsx b/libraries/core-react/stories/components/TextField.stories.tsx index b67bbfcf28..f065040953 100644 --- a/libraries/core-react/stories/components/TextField.stories.tsx +++ b/libraries/core-react/stories/components/TextField.stories.tsx @@ -3,22 +3,42 @@ import { TextField, TextFieldProps, Icon } from '@components' import { Story, Meta } from '@storybook/react' import { thumbs_up, warning_filled, error_filled } from '@equinor/eds-icons' -Icon.add({ +const icons = { thumbs_up, warning_filled, error_filled, -}) +} +Icon.add(icons) import styled from 'styled-components' export default { title: 'Components/TextField', component: TextField, argTypes: { - rows: { - control: 'number', - description: 'Rows when "multiline" is true', - default: 1, + inputIcon: { + control: { + type: 'select', + options: { + error: [], + warning: [], + success: [], + }, + }, + description: + 'Please note that the option list of icons is not complete, this selection is only for demo purposes', + }, + helperIcon: { + control: { + type: 'select', + options: { + error: [], + warning: [], + success: [], + }, + }, + description: + 'Please note that the option list of icons is not complete, this selection is only for demo purposes', }, }, parameters: { @@ -39,7 +59,16 @@ const Wrapper = styled.div` grid-template-columns: repeat(2, fit-content(100%)); ` export const Default: Story = (args) => ( - + ) export const types: Story = () => ( @@ -90,37 +119,110 @@ export const types: Story = () => ( types.storyName = 'Types of input fields' export const Multiline: Story = () => ( - + <> + +
+ } + /> + ) +Multiline.parameters = { + docs: { + storyDescription: `With multiline we recommend to use rows in combination with a CSS rule of + resize: 'none'`, + }, +} export const Disabled: Story = () => ( - + + + } + /> + + } + /> + + } + /> + +) +export const WithUnit: Story = () => ( + + + + ) +WithUnit.storyName = 'With unit' + export const WithIcons: Story = () => ( } /> = () => ( /> = () => ( id="storybook-error-two" placeholder="Placeholder text " label="Error" - meta="Meta" + unit="Unit" helperText="Validation error" variant="error" inputIcon={} @@ -177,7 +279,6 @@ export const Variants: Story = () => ( variant="warning" inputIcon={} /> - = () => ( variant="success" inputIcon={} /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> ) Variants.parameters = { docs: { storyDescription: `Examples of validation states. You can add the icon in the helper - text or inside the text input, both not both places.`, + text or inside the text input, both not in both places.`, }, } diff --git a/libraries/tokens/src/types/focus.ts b/libraries/tokens/src/types/focus.ts index 95b7a8764e..f47a2f3122 100644 --- a/libraries/tokens/src/types/focus.ts +++ b/libraries/tokens/src/types/focus.ts @@ -2,6 +2,6 @@ export type Outline = { type: 'outline' color: string width: string - style: 'dashed' + style: 'dashed' | 'solid' offset?: string }