From fc9b133cb62ac48f27ae85aea28f08043ebbbca9 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 4 Aug 2022 09:57:53 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Adornments=20support=20in=20`Input`?= =?UTF-8?q?=20component=20(#2354)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚧 WIP * 📝 Added story * wip * 🚧 WIP * 🚧 WIP * 🚧 wip * 🚚 Moved over `Input` adornment changes * ♻️ Added `OldInput` for interim development * 🐛 Fixed textarea outline * ♻️ back to new input * 🔥 removed OldInput * Testing something old * updated snapshots * its something * ♻️ working version before testing styles * ♻️ Further testing and tweaking * 📸 ✅ Updated test & snapshots * ♻️ working example * 🚚 Moved typographymixin * ♻️ Started cleaning up helpertext --- .../Autocomplete/Autocomplete.tokens.ts | 1 + .../components/Autocomplete/Autocomplete.tsx | 72 +++---- .../src/components/Input/Input.docs.mdx | 7 +- .../src/components/Input/Input.stories.tsx | 131 +++++++++++- .../src/components/Input/Input.test.tsx | 51 ++--- .../src/components/Input/Input.tokens.ts | 138 +++++++----- .../src/components/Input/Input.tsx | 200 +++++++++++++----- .../Input/__snapshots__/Input.test.tsx.snap | 86 ++++---- .../HelperText/HelperText.token.ts | 62 ++++++ .../InputWrapper/HelperText/HelperText.tsx | 52 +++++ .../InputWrapper/HelperText/index.ts | 1 + .../InputWrapper/InputWrapper.stories.tsx | 29 +++ .../InputWrapper/InputWrapper.tokens.ts | 180 ++++++++++++++++ .../components/InputWrapper/InputWrapper.tsx | 52 +++++ .../src/components/InputWrapper/index.tsx | 2 + .../Search/__snapshots__/Search.test.tsx.snap | 119 ++++++----- .../__snapshots__/SingleSelect.test.tsx.snap | 131 ++++++------ .../components/TextField/TextField.test.tsx | 8 +- .../src/components/Textarea/Textarea.tsx | 4 +- packages/eds-core-react/src/index.ts | 1 + packages/eds-tokens/src/types/component.ts | 7 +- packages/eds-utils/src/index.ts | 1 + packages/eds-utils/src/mixins/index.ts | 1 + packages/eds-utils/src/mixins/typography.ts | 28 +++ 24 files changed, 1019 insertions(+), 345 deletions(-) create mode 100644 packages/eds-core-react/src/components/InputWrapper/HelperText/HelperText.token.ts create mode 100644 packages/eds-core-react/src/components/InputWrapper/HelperText/HelperText.tsx create mode 100644 packages/eds-core-react/src/components/InputWrapper/HelperText/index.ts create mode 100644 packages/eds-core-react/src/components/InputWrapper/InputWrapper.stories.tsx create mode 100644 packages/eds-core-react/src/components/InputWrapper/InputWrapper.tokens.ts create mode 100644 packages/eds-core-react/src/components/InputWrapper/InputWrapper.tsx create mode 100644 packages/eds-core-react/src/components/InputWrapper/index.tsx create mode 100644 packages/eds-utils/src/mixins/index.ts create mode 100644 packages/eds-utils/src/mixins/typography.ts diff --git a/packages/eds-core-react/src/components/Autocomplete/Autocomplete.tokens.ts b/packages/eds-core-react/src/components/Autocomplete/Autocomplete.tokens.ts index aa5ddb0950..9d15ee7550 100644 --- a/packages/eds-core-react/src/components/Autocomplete/Autocomplete.tokens.ts +++ b/packages/eds-core-react/src/components/Autocomplete/Autocomplete.tokens.ts @@ -55,6 +55,7 @@ export const selectTokens: ComponentToken = { entities: { button: { height: '24px', + width: '24px', spacings: { left: spacingSmall, right: spacingSmall, diff --git a/packages/eds-core-react/src/components/Autocomplete/Autocomplete.tsx b/packages/eds-core-react/src/components/Autocomplete/Autocomplete.tsx index 24fdc70bdb..3e254e24e7 100644 --- a/packages/eds-core-react/src/components/Autocomplete/Autocomplete.tsx +++ b/packages/eds-core-react/src/components/Autocomplete/Autocomplete.tsx @@ -44,21 +44,6 @@ const Container = styled.div` position: relative; ` -const StyledInput = styled(Input)( - ({ - theme: { - entities: { button }, - }, - }) => { - return css` - padding-right: calc( - ${button.spacings.left} + ${button.spacings.right} + - (${button.height} * 2) - ); - ` - }, -) - const StyledList = styled(List)( ({ theme }) => css` background-color: ${theme.background}; @@ -76,11 +61,8 @@ const StyledButton = styled(Button)( entities: { button }, }, }) => css` - position: absolute; height: ${button.height}; width: ${button.height}; - right: ${button.spacings.right}; - top: ${button.spacings.top}; `, ) @@ -600,7 +582,7 @@ function AutocompleteInner( /> - ( readOnly={readOnly} onFocus={openSelect} onClick={openSelect} + rightAdornmentsWidth={24 * 2 + 8 + 8} + rightAdornments={ + <> + {showClearButton && ( + + + + )} + {!readOnly && ( + + + + )} + + } {...other} /> - {showClearButton && ( - - - - )} - - {!readOnly && ( - - )} - {disablePortal ? ( optionsList diff --git a/packages/eds-core-react/src/components/Input/Input.docs.mdx b/packages/eds-core-react/src/components/Input/Input.docs.mdx index 268f96ba62..0267ac8c02 100644 --- a/packages/eds-core-react/src/components/Input/Input.docs.mdx +++ b/packages/eds-core-react/src/components/Input/Input.docs.mdx @@ -52,4 +52,9 @@ It's important to link the `Input` to the corresponding label. Compact `Input` using `EdsProvider`. - \ No newline at end of file + + +### With Adornments + + + diff --git a/packages/eds-core-react/src/components/Input/Input.stories.tsx b/packages/eds-core-react/src/components/Input/Input.stories.tsx index 9bd943488a..9504bcf265 100644 --- a/packages/eds-core-react/src/components/Input/Input.stories.tsx +++ b/packages/eds-core-react/src/components/Input/Input.stories.tsx @@ -1,9 +1,12 @@ import { useState, useEffect } from 'react' +import { Story, ComponentMeta } from '@storybook/react' +import { anchor } from '@equinor/eds-icons' import { Input, InputProps, Label, EdsProvider, Density } from '../..' -import { Story } from '@storybook/react/types-6-0' -import { ComponentMeta } from '@storybook/react' +import styled from 'styled-components' import { Stack } from './../../../.storybook/components' import page from './Input.docs.mdx' +import { Button } from '../Button' +import { Icon } from '../Icon' export default { title: 'Inputs/Input', @@ -198,3 +201,127 @@ Compact.decorators = [ ) }, ] + +const SmallButton = styled(Button)` + height: 24px; + width: 24px; +` + +export const WithAdornments: Story = () => { + return ( + + + ) +} + +WithAdornments.decorators = [ + (Story) => { + return ( + + + + ) + }, +] diff --git a/packages/eds-core-react/src/components/Input/Input.test.tsx b/packages/eds-core-react/src/components/Input/Input.test.tsx index fdb986e2e6..e2690e96fe 100644 --- a/packages/eds-core-react/src/components/Input/Input.test.tsx +++ b/packages/eds-core-react/src/components/Input/Input.test.tsx @@ -8,18 +8,6 @@ import { Input } from './Input' import * as tokens from './Input.tokens' import { trimSpaces } from '@equinor/eds-utils' -const { - error: { - states: { active: activeError }, - }, - success: { - states: { active: activeSuccess }, - }, - warning: { - states: { active: activeWarning }, - }, -} = tokens - afterEach(cleanup) describe('Input', () => { @@ -57,12 +45,13 @@ describe('Input', () => { , ) - const inputNode = screen.getByLabelText(label) + // eslint-disable-next-line testing-library/no-node-access + const inputWrapper = screen.getByLabelText(label).parentElement - expect(inputNode).toHaveStyleRule( + expect(inputWrapper).toHaveStyleRule( 'outline', - `${activeSuccess.outline.width} solid ${trimSpaces( - activeSuccess.outline.color, + `${tokens.input.outline.width} solid ${trimSpaces( + tokens.success.outline.color, )}`, ) }) @@ -75,12 +64,13 @@ describe('Input', () => { , ) - const inputNode = screen.getByLabelText(label) + // eslint-disable-next-line testing-library/no-node-access + const inputWrapper = screen.getByLabelText(label).parentElement - expect(inputNode).toHaveStyleRule( + expect(inputWrapper).toHaveStyleRule( 'outline', - `${activeWarning.outline.width} solid ${trimSpaces( - activeWarning.outline.color, + `${tokens.input.outline.width} solid ${trimSpaces( + tokens.warning.outline.color, )}`, ) }) @@ -93,22 +83,23 @@ describe('Input', () => { , ) - const inputNode = screen.getByLabelText(label) + // eslint-disable-next-line testing-library/no-node-access + const inputWrapper = screen.getByLabelText(label).parentElement - expect(inputNode).toHaveStyleRule( + expect(inputWrapper).toHaveStyleRule( 'outline', - `${activeError.outline.width} solid ${trimSpaces( - activeError.outline.color, + `${tokens.input.outline.width} solid ${trimSpaces( + tokens.error.outline.color, )}`, ) }) - const StyledTextField = styled(Input)` + const StyledInput = styled(Input)` margin-top: 48px; ` it('Can extend the css of the component', () => { render( - { />, ) - expect(screen.getByDisplayValue('textfield')).toHaveStyleRule( - 'margin-top', - '48px', - ) + // eslint-disable-next-line testing-library/no-node-access + const inputWrapper = screen.getByDisplayValue('textfield').parentElement + + expect(inputWrapper).toHaveStyleRule('margin-top', '48px') }) }) diff --git a/packages/eds-core-react/src/components/Input/Input.tokens.ts b/packages/eds-core-react/src/components/Input/Input.tokens.ts index f4958a82cf..859316bf05 100644 --- a/packages/eds-core-react/src/components/Input/Input.tokens.ts +++ b/packages/eds-core-react/src/components/Input/Input.tokens.ts @@ -29,7 +29,8 @@ const { export type InputToken = ComponentToken export const input: InputToken = { - minHeight: shape.straight.minHeight, + height: shape.straight.minHeight, + width: '100%', background, spacings: { left: small, @@ -41,12 +42,36 @@ export const input: InputToken = { ...typography.input.text, color: static_icons__default.rgba, }, + outline: { + type: 'outline', + color: 'transparent', + width: '1px', + style: 'solid', + offset: '0px', + }, entities: { placeholder: { typography: { color: static_icons__tertiary.rgba, }, }, + adornment: { + typography: { + ...typography.input.label, + color: static_icons__tertiary.rgba, + }, + spacings: { + left: small, + right: small, + }, + states: { + disabled: { + typography: { + color: disabled__text.rgba, + }, + }, + }, + }, }, states: { disabled: { @@ -58,15 +83,7 @@ export const input: InputToken = { background: 'transparent', boxShadow: 'none', }, - active: { - outline: { - type: 'outline', - color: 'transparent', - width: '1px', - style: 'solid', - offset: '0px', - }, - }, + active: {}, focus: { outline: { type: 'outline', @@ -80,7 +97,7 @@ export const input: InputToken = { boxShadow: 'inset 0px -1px 0px 0px ' + static_icons__tertiary.rgba, modes: { compact: { - minHeight: shape._modes.compact.straight.minHeight, + height: shape._modes.compact.straight.minHeight, spacings: { left: x_small, right: x_small, @@ -92,72 +109,87 @@ export const input: InputToken = { } export const error: InputToken = mergeDeepRight(input, { - boxShadow: 'inset 0px -1px 0px 0px transparent', + boxShadow: 'none', + outline: { + color: danger__resting.rgba, + }, states: { - active: { - outline: { - type: 'outline', - color: danger__resting.rgba, - width: '1px', - style: 'solid', - offset: '0px', - }, - }, focus: { outline: { - type: 'outline', - width: '2px', color: danger__hover.rgba, - style: 'solid', - offset: '0px', + }, + }, + }, + entities: { + adornment: { + typography: { + ...typography.input.label, + color: danger__resting.rgba, + }, + states: { + focus: { + outline: { + color: danger__hover.rgba, + }, + }, }, }, }, }) export const warning: InputToken = mergeDeepRight(input, { - boxShadow: 'inset 0px -1px 0px 0px transparent', + boxShadow: 'none', + outline: { + color: warning__resting.rgba, + }, states: { - active: { - outline: { - type: 'outline', - color: warning__resting.rgba, - width: '1px', - style: 'solid', - offset: '0px', - }, - }, focus: { outline: { - type: 'outline', - width: '2px', color: warning__hover.rgba, - style: 'solid', - offset: '0px', + }, + }, + }, + entities: { + adornment: { + typography: { + ...typography.input.label, + color: warning__resting.rgba, + }, + states: { + focus: { + outline: { + color: warning__hover.rgba, + }, + }, }, }, }, }) export const success: InputToken = mergeDeepRight(input, { - boxShadow: 'inset 0px -1px 0px 0px transparent', + boxShadow: 'none', + outline: { + color: success__resting.rgba, + }, states: { - active: { - outline: { - type: 'outline', - color: success__resting.rgba, - width: '1px', - style: 'solid', - offset: '0px', - }, - }, focus: { outline: { - type: 'outline', - width: '2px', color: success__hover.rgba, - style: 'solid', - offset: '0px', + }, + }, + }, + entities: { + adornment: { + typography: { + ...typography.input.label, + color: success__resting.rgba, + }, + states: { + focus: { + outline: { + color: success__hover.rgba, + }, + }, }, }, }, diff --git a/packages/eds-core-react/src/components/Input/Input.tsx b/packages/eds-core-react/src/components/Input/Input.tsx index 848e329597..9eb15c8b45 100644 --- a/packages/eds-core-react/src/components/Input/Input.tsx +++ b/packages/eds-core-react/src/components/Input/Input.tsx @@ -1,73 +1,112 @@ -import { InputHTMLAttributes, forwardRef } from 'react' -import styled, { css, ThemeProvider } from 'styled-components' -import { inputToken as tokens } from './Input.tokens' -import type { InputToken } from './Input.tokens' +import { InputHTMLAttributes, forwardRef, ReactNode, useMemo } from 'react' +import styled, { css } from 'styled-components' +import { ComponentToken } from '@equinor/eds-tokens' import { - typographyTemplate, + typographyMixin, spacingsTemplate, outlineTemplate, useToken, } from '@equinor/eds-utils' +import { inputToken as tokens } from './Input.tokens' +import type { InputToken } from './Input.tokens' import type { Variants } from '../TextField/types' import { useEds } from '../EdsProvider' -const StyledInput = styled.input(({ theme }: StyledProps) => { - const { - states: { - focus: { outline: focusOutline }, - active: { outline: activeOutline }, - disabled, - readOnly, - }, - boxShadow, - } = theme +const Container = styled.div(({ token, disabled, readOnly }: StyledProps) => { + const { states, entities } = token return css` - width: 100%; - box-sizing: border-box; - margin: 0; - appearance: none; - background: ${theme.background}; + --eds-input-adornment-color: ${entities.adornment.typography.color}; + --eds-input-color: ${token.typography.color}; + + position: relative; + width: ${token.width}; + display: flex; + flex-direction: row; border: none; - height: ${theme.minHeight}; - box-shadow: ${boxShadow}; + box-sizing: border-box; + height: ${token.height}; + box-shadow: ${token.boxShadow}; + background: ${token.background}; + ${outlineTemplate(token.outline)} - ${outlineTemplate(activeOutline)} - ${typographyTemplate(theme.typography)} - ${spacingsTemplate(theme.spacings)}; + &:focus-within { + --eds-input-adornment-color: ${entities.adornment?.states.focus?.outline + .color}; - &::placeholder { - color: ${theme.entities.placeholder.typography.color}; + box-shadow: none; + ${outlineTemplate(states.focus.outline)} } - &:active, - &:focus { - outline-offset: 0; + ${disabled && + css` + --eds-input-adornment-color: ${states.disabled.typography.color}; + --eds-input-color: ${states.disabled.typography.color}; + cursor: not-allowed; box-shadow: none; - ${outlineTemplate(focusOutline)} + `} + ${readOnly && + css({ + background: states.readOnly.background, + boxShadow: states.readOnly.boxShadow, + })} + ` +}) + +const StyledInput = styled.input(({ token }: StyledProps) => { + return css` + width: 100%; + border: none; + background: transparent; + ${spacingsTemplate(token.spacings)} + ${typographyMixin(token.typography)} + outline: none; + + &::placeholder { + color: ${token.entities.placeholder.typography.color}; } &:disabled { - color: ${disabled.typography.color}; + color: var(--eds-input-color); cursor: not-allowed; - box-shadow: none; - outline: none; - &:focus, - &:active { - outline: none; - } - } - &[readOnly] { - background: ${readOnly.background}; - box-shadow: ${readOnly.boxShadow}; } ` }) -type StyledProps = { - theme: InputToken +type AdornmentProps = { + token: InputToken } +const Adornments = styled.div(({ token }) => { + return css` + position: absolute; + top: 0; + bottom: 0; + display: flex; + align-items: center; + ${typographyMixin(token.entities.adornment.typography)} + color: var(--eds-input-adornment-color); + ` +}) + +const LeftAdornments = styled(Adornments)( + ({ token }) => css` + left: 0; + padding-left: ${token.entities.adornment.spacings.left}; + `, +) + +const RightAdornments = styled(Adornments)( + ({ token }) => css` + right: 0; + padding-right: ${token.entities.adornment.spacings.right}; + `, +) + +type StyledProps = { + token: InputToken +} & Required> + export type InputProps = { /** Placeholder */ placeholder?: string @@ -79,27 +118,92 @@ export type InputProps = { type?: string /** Read Only */ readOnly?: boolean + /** Left adornments */ + leftAdornments?: ReactNode + /** Left adornments width */ + leftAdornmentsWidth?: number + /** Right adornments */ + rightAdornments?: ReactNode + /** Right adornments width */ + rightAdornmentsWidth?: number } & InputHTMLAttributes export const Input = forwardRef(function Input( - { variant = 'default', disabled = false, type = 'text', ...other }, + { + variant = 'default', + disabled = false, + type = 'text', + leftAdornments, + rightAdornments, + leftAdornmentsWidth, + rightAdornmentsWidth, + readOnly, + className, + style, + ...other + }, ref, ) { const actualVariant = variant === 'default' ? 'input' : variant const inputVariant = tokens[actualVariant] const { density } = useEds() - const token = useToken({ density }, inputVariant) + const token = useToken({ density }, inputVariant)() + const updatedToken = useMemo( + (): ComponentToken => ({ + ...token, + spacings: { + ...token.spacings, + left: + typeof leftAdornmentsWidth !== 'undefined' + ? `${leftAdornmentsWidth}px` + : token.spacings.left, + right: + typeof rightAdornmentsWidth !== 'undefined' + ? `${rightAdornmentsWidth}px` + : token.spacings.right, + }, + }), + [leftAdornmentsWidth, rightAdornmentsWidth, token], + ) const inputProps = { ref, type, disabled, + readOnly, + token: updatedToken, ...other, } + const containerProps = { + disabled, + readOnly, + className, + style, + token: updatedToken, + } + + const leftAdornmentProps = { + token: updatedToken, + } + const rightAdornmentProps = { + token: updatedToken, + } + return ( - + // Not using because of cascading styling messing with adornments + + {leftAdornments ? ( + + {leftAdornments} + + ) : null} - + {rightAdornments ? ( + + {rightAdornments} + + ) : null} + ) }) diff --git a/packages/eds-core-react/src/components/Input/__snapshots__/Input.test.tsx.snap b/packages/eds-core-react/src/components/Input/__snapshots__/Input.test.tsx.snap index 323a8392fd..780d6214a9 100644 --- a/packages/eds-core-react/src/components/Input/__snapshots__/Input.test.tsx.snap +++ b/packages/eds-core-react/src/components/Input/__snapshots__/Input.test.tsx.snap @@ -3,18 +3,42 @@ exports[`Input Matches snapshot 1`] = ` .c0 { + --eds-input-adornment-color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); + --eds-input-color: var(--eds_text__static_icons__default,rgba(61,61,61,1)); + position: relative; width: 100%; - box-sizing: border-box; - margin: 0; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background: var(--eds_ui_background__light,rgba(247,247,247,1)); + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; border: none; + box-sizing: border-box; height: 36px; box-shadow: inset 0px -1px 0px 0px var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); + background: var(--eds_ui_background__light,rgba(247,247,247,1)); outline: 1px solid transparent; outline-offset: 0px; + background: transparent; + box-shadow: none; +} + +.c0:focus-within { + box-shadow: none; + outline: 2px solid var(--eds_interactive_primary__resting,rgba(0,112,121,1)); + outline-offset: 0px; +} + +.c1 { + width: 100%; + border: none; + background: transparent; + padding-left: 8px; + padding-top: 6px; + padding-right: 8px; + padding-bottom: 6px; margin: 0; color: var(--eds_text__static_icons__default,rgba(61,61,61,1)); font-family: Equinor; @@ -26,58 +50,40 @@ exports[`Input Matches snapshot 1`] = ` -ms-letter-spacing: 0.025em; letter-spacing: 0.025em; text-align: left; - padding-left: 8px; - padding-top: 6px; - padding-right: 8px; - padding-bottom: 6px; + outline: none; } -.c0::-webkit-input-placeholder { +.c1::-webkit-input-placeholder { color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); } -.c0::-moz-placeholder { +.c1::-moz-placeholder { color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); } -.c0:-ms-input-placeholder { +.c1:-ms-input-placeholder { color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); } -.c0::placeholder { +.c1::placeholder { color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); } -.c0:active, -.c0:focus { - outline-offset: 0; - box-shadow: none; - outline: 2px solid var(--eds_interactive_primary__resting,rgba(0,112,121,1)); - outline-offset: 0px; -} - -.c0:disabled { - color: var(--eds_interactive__disabled__text,rgba(190,190,190,1)); +.c1:disabled { + color: var(--eds-input-color); cursor: not-allowed; - box-shadow: none; - outline: none; -} - -.c0:disabled:focus, -.c0:disabled:active { - outline: none; -} - -.c0[readOnly] { - background: transparent; - box-shadow: none; } - + > + + `; diff --git a/packages/eds-core-react/src/components/InputWrapper/HelperText/HelperText.token.ts b/packages/eds-core-react/src/components/InputWrapper/HelperText/HelperText.token.ts new file mode 100644 index 0000000000..485c079ae4 --- /dev/null +++ b/packages/eds-core-react/src/components/InputWrapper/HelperText/HelperText.token.ts @@ -0,0 +1,62 @@ +import { tokens } from '@equinor/eds-tokens' +import type { Spacing, Typography } from '@equinor/eds-tokens' + +const { + colors, + spacings: { comfortable }, + typography, +} = tokens + +export type HelperTextProps = { + background: string + typography: Typography + spacings: { + comfortable: Spacing + compact: Spacing + } +} + +export const helperText: HelperTextProps = { + background: colors.ui.background__light.hex, + typography: { + ...typography.input.helper, + color: colors.text.static_icons__tertiary.rgba, + }, + spacings: { + comfortable: { + left: comfortable.small, + right: comfortable.small, + top: comfortable.small, + bottom: '6px', + }, + compact: { + left: comfortable.small, + right: comfortable.small, + top: comfortable.xx_small, + bottom: '6px', + }, + }, +} + +const colorVariants = { + default: { + color: colors.text.static_icons__tertiary.hex, + disabledColor: colors.interactive.disabled__text.hex, + focusColor: colors.text.static_icons__tertiary.hex, + }, + error: { + color: colors.interactive.danger__text.hex, + disabledColor: colors.interactive.disabled__text.hex, + focusColor: colors.interactive.danger__hover.hex, + }, + warning: { + color: colors.interactive.warning__text.hex, + disabledColor: colors.interactive.disabled__text.hex, + focusColor: colors.interactive.warning__hover.hex, + }, + success: { + color: colors.interactive.success__text.hex, + disabledColor: colors.interactive.disabled__text.hex, + focusColor: colors.interactive.success__hover.hex, + }, +} diff --git a/packages/eds-core-react/src/components/InputWrapper/HelperText/HelperText.tsx b/packages/eds-core-react/src/components/InputWrapper/HelperText/HelperText.tsx new file mode 100644 index 0000000000..6825f626d9 --- /dev/null +++ b/packages/eds-core-react/src/components/InputWrapper/HelperText/HelperText.tsx @@ -0,0 +1,52 @@ +import { forwardRef, ReactNode, HTMLAttributes } from 'react' +import styled, { css } from 'styled-components' +import { typographyMixin } from '@equinor/eds-utils' +import { helperText as tokens } from './HelperText.token' + +type ContainerProps = { + color?: string +} + +const Container = styled.div(({ color }) => + css({ + display: 'grid', + gap: '8px', + gridAutoFlow: 'column', + alignItems: 'start', + justifyContent: 'start', + color, + }), +) +const Text = styled.p` + margin: 0; + ${typographyMixin(tokens.typography)}; +` + +type HelperTextProps = { + /** Helper text */ + text?: string + /** Icon */ + icon?: ReactNode + /** Color */ + color?: string +} & HTMLAttributes + +const TextfieldHelperText = forwardRef( + function TextfieldHelperText( + { text, icon, color = tokens.typography.color, ...rest }, + ref, + ) { + if (!text) { + return null + } + + return ( + + {icon} + {text} + + ) + }, +) + +export { TextfieldHelperText as HelperText } diff --git a/packages/eds-core-react/src/components/InputWrapper/HelperText/index.ts b/packages/eds-core-react/src/components/InputWrapper/HelperText/index.ts new file mode 100644 index 0000000000..23c77d008a --- /dev/null +++ b/packages/eds-core-react/src/components/InputWrapper/HelperText/index.ts @@ -0,0 +1 @@ +export { HelperText } from './HelperText' diff --git a/packages/eds-core-react/src/components/InputWrapper/InputWrapper.stories.tsx b/packages/eds-core-react/src/components/InputWrapper/InputWrapper.stories.tsx new file mode 100644 index 0000000000..7e6230b285 --- /dev/null +++ b/packages/eds-core-react/src/components/InputWrapper/InputWrapper.stories.tsx @@ -0,0 +1,29 @@ +import { Story, ComponentMeta } from '@storybook/react' +import { accessible, dropper, clear, search } from '@equinor/eds-icons' +import { InputWrapper, InputWrapperProps, Input, Icon, Button } from '../..' +import styled from 'styled-components' + +export default { + title: 'Inputs/InputWrapper', + component: InputWrapper, +} as ComponentMeta + +const SmallButton = styled(Button)` + height: 24px; + width: 24px; +` + +export const Introduction: Story = (args) => ( + } + {...args} + > + + +) + +Introduction.args = { + helperText: 'Helper text', +} diff --git a/packages/eds-core-react/src/components/InputWrapper/InputWrapper.tokens.ts b/packages/eds-core-react/src/components/InputWrapper/InputWrapper.tokens.ts new file mode 100644 index 0000000000..28213f7eb3 --- /dev/null +++ b/packages/eds-core-react/src/components/InputWrapper/InputWrapper.tokens.ts @@ -0,0 +1,180 @@ +import { tokens } from '@equinor/eds-tokens' +import type { ComponentToken } from '@equinor/eds-tokens' +import { mergeDeepRight } from 'ramda' + +const { + colors: { + ui: { + background__light: { rgba: background }, + }, + text: { static_icons__default, static_icons__tertiary }, + interactive: { + disabled__text, + primary__resting, + danger__resting, + danger__hover, + warning__resting, + warning__hover, + success__resting, + success__hover, + }, + }, + spacings: { + comfortable: { small, x_small }, + }, + typography, + shape, +} = tokens + +export type InputToken = ComponentToken + +export const input: InputToken = { + minHeight: shape.straight.minHeight, + background, + spacings: { + left: small, + right: small, + top: '6px', + bottom: '6px', + }, + typography: { + ...typography.input.text, + color: static_icons__default.rgba, + }, + entities: { + placeholder: { + typography: { + color: static_icons__tertiary.rgba, + }, + }, + + adornment: { + typography: { + ...typography.input.label, + color: static_icons__tertiary.rgba, + }, + states: { + disabled: { + typography: { + color: disabled__text.rgba, + }, + }, + }, + }, + }, + states: { + disabled: { + typography: { + color: disabled__text.rgba, + }, + }, + readOnly: { + background: 'transparent', + boxShadow: 'none', + }, + active: { + outline: { + type: 'outline', + color: 'transparent', + width: '1px', + style: 'solid', + offset: '0px', + }, + }, + focus: { + outline: { + type: 'outline', + width: '2px', + color: primary__resting.rgba, + style: 'solid', + offset: '0px', + }, + }, + }, + boxShadow: 'inset 0px -1px 0px 0px ' + static_icons__tertiary.rgba, + modes: { + compact: { + minHeight: shape._modes.compact.straight.minHeight, + spacings: { + left: x_small, + right: x_small, + top: x_small, + bottom: x_small, + }, + }, + }, +} + +export const error: InputToken = mergeDeepRight(input, { + boxShadow: 'inset 0px -1px 0px 0px transparent', + states: { + active: { + outline: { + type: 'outline', + color: danger__resting.rgba, + width: '1px', + style: 'solid', + offset: '0px', + }, + }, + focus: { + outline: { + type: 'outline', + width: '2px', + color: danger__hover.rgba, + style: 'solid', + offset: '0px', + }, + }, + }, +}) + +export const warning: InputToken = mergeDeepRight(input, { + boxShadow: 'inset 0px -1px 0px 0px transparent', + states: { + active: { + outline: { + type: 'outline', + color: warning__resting.rgba, + width: '1px', + style: 'solid', + offset: '0px', + }, + }, + focus: { + outline: { + type: 'outline', + width: '2px', + color: warning__hover.rgba, + style: 'solid', + offset: '0px', + }, + }, + }, +}) + +export const success: InputToken = mergeDeepRight(input, { + boxShadow: 'inset 0px -1px 0px 0px transparent', + states: { + active: { + outline: { + type: 'outline', + color: success__resting.rgba, + width: '1px', + style: 'solid', + offset: '0px', + }, + }, + focus: { + outline: { + type: 'outline', + width: '2px', + color: success__hover.rgba, + style: 'solid', + offset: '0px', + }, + }, + }, +}) + +export const inputToken = { input, error, warning, success } diff --git a/packages/eds-core-react/src/components/InputWrapper/InputWrapper.tsx b/packages/eds-core-react/src/components/InputWrapper/InputWrapper.tsx new file mode 100644 index 0000000000..cab39c3c2e --- /dev/null +++ b/packages/eds-core-react/src/components/InputWrapper/InputWrapper.tsx @@ -0,0 +1,52 @@ +import { HTMLAttributes, forwardRef, ReactElement, ReactNode } from 'react' +import styled, { css, ThemeProvider } from 'styled-components' +import { Label } from '../Label' +import { HelperText } from './HelperText' +import { useEds } from './../EdsProvider' +import { inputToken } from './InputWrapper.tokens' +import { useToken } from '@equinor/eds-utils' + +const Container = styled.div`` + +const WrapperHelperText = styled(HelperText)` + margin-top: 8px; + margin-left: 8px; +` + +export type InputWrapperProps = { + /** Label */ + label?: string + /** Meta */ + meta?: string + /** Disabled state */ + disabled?: boolean + /** Read Only */ + readOnly?: boolean + /** Helper text icon */ + helperIcon?: ReactNode + /** Helper text */ + helperText?: string +} & HTMLAttributes + +export const InputWrapper = forwardRef( + function InputWrapper( + { label, meta, children, helperIcon, helperText, ...other }, + ref, + ) { + const { density } = useEds() + const token = useToken({ density }, inputToken.input) + + return ( + + + + + ) + }, +) diff --git a/packages/eds-core-react/src/components/InputWrapper/index.tsx b/packages/eds-core-react/src/components/InputWrapper/index.tsx new file mode 100644 index 0000000000..5da7d3d650 --- /dev/null +++ b/packages/eds-core-react/src/components/InputWrapper/index.tsx @@ -0,0 +1,2 @@ +export { InputWrapper } from './InputWrapper' +export type { InputWrapperProps } from './InputWrapper' diff --git a/packages/eds-core-react/src/components/Search/__snapshots__/Search.test.tsx.snap b/packages/eds-core-react/src/components/Search/__snapshots__/Search.test.tsx.snap index a66c6e3e6e..d553390543 100644 --- a/packages/eds-core-react/src/components/Search/__snapshots__/Search.test.tsx.snap +++ b/packages/eds-core-react/src/components/Search/__snapshots__/Search.test.tsx.snap @@ -2,7 +2,7 @@ exports[`Search Matches snapshot 1`] = ` - .c5 { + .c6 { display: grid; grid-gap: var(--eds_button__gap,8px); grid-auto-flow: column; @@ -17,12 +17,12 @@ exports[`Search Matches snapshot 1`] = ` justify-content: center; } -.c5 > :is(svg,img) { +.c6 > :is(svg,img) { margin-top: var(--eds_button__icon__margin_y,0); margin-bottom: var(--eds_button__icon__margin_y,0); } -.c3 { +.c4 { box-sizing: border-box; margin: 0; padding: 0; @@ -49,11 +49,11 @@ exports[`Search Matches snapshot 1`] = ` text-align: center; } -.c3 svg { +.c4 svg { justify-self: center; } -.c3::before { +.c4::before { position: absolute; top: 0; left: 0; @@ -62,7 +62,7 @@ exports[`Search Matches snapshot 1`] = ` content: ''; } -.c3::after { +.c4::after { position: absolute; top: -0; left: -4px; @@ -71,25 +71,25 @@ exports[`Search Matches snapshot 1`] = ` content: ''; } -.c3:focus { +.c4:focus { outline: none; } -.c3[data-focus-visible-added]:focus { +.c4[data-focus-visible-added]:focus { outline: 2px dashed rgba(0,112,121,1); outline-offset: -2px; } -.c3:focus-visible { +.c4:focus-visible { outline: 2px dashed rgba(0,112,121,1); outline-offset: -2px; } -.c3::-moz-focus-inner { +.c4::-moz-focus-inner { border: 0; } -.c3:disabled { +.c4:disabled { cursor: not-allowed; background: transparent; border: 1px solid transparent; @@ -103,18 +103,40 @@ exports[`Search Matches snapshot 1`] = ` } .c1 { + --eds-input-adornment-color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); + --eds-input-color: var(--eds_text__static_icons__default,rgba(61,61,61,1)); + position: relative; width: 100%; - box-sizing: border-box; - margin: 0; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background: var(--eds_ui_background__light,rgba(247,247,247,1)); + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; border: none; + box-sizing: border-box; height: 36px; box-shadow: inset 0px -1px 0px 0px var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); + background: var(--eds_ui_background__light,rgba(247,247,247,1)); outline: 1px solid transparent; outline-offset: 0px; +} + +.c1:focus-within { + box-shadow: none; + outline: 2px solid var(--eds_interactive_primary__resting,rgba(0,112,121,1)); + outline-offset: 0px; +} + +.c3 { + width: 100%; + border: none; + background: transparent; + padding-left: 8px; + padding-top: 6px; + padding-right: 8px; + padding-bottom: 6px; margin: 0; color: var(--eds_text__static_icons__default,rgba(61,61,61,1)); font-family: Equinor; @@ -126,51 +148,28 @@ exports[`Search Matches snapshot 1`] = ` -ms-letter-spacing: 0.025em; letter-spacing: 0.025em; text-align: left; - padding-left: 8px; - padding-top: 6px; - padding-right: 8px; - padding-bottom: 6px; + outline: none; } -.c1::-webkit-input-placeholder { +.c3::-webkit-input-placeholder { color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); } -.c1::-moz-placeholder { +.c3::-moz-placeholder { color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); } -.c1:-ms-input-placeholder { +.c3:-ms-input-placeholder { color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); } -.c1::placeholder { +.c3::placeholder { color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); } -.c1:active, -.c1:focus { - outline-offset: 0; - box-shadow: none; - outline: 2px solid var(--eds_interactive_primary__resting,rgba(0,112,121,1)); - outline-offset: 0px; -} - -.c1:disabled { - color: var(--eds_interactive__disabled__text,rgba(190,190,190,1)); +.c3:disabled { + color: var(--eds-input-color); cursor: not-allowed; - box-shadow: none; - outline: none; -} - -.c1:disabled:focus, -.c1:disabled:active { - outline: none; -} - -.c1[readOnly] { - background: transparent; - box-shadow: none; } .c0 { @@ -267,7 +266,7 @@ exports[`Search Matches snapshot 1`] = ` box-shadow: 0 0 0px 1000px var(--eds_ui_background__light,rgba(247,247,247,1)) inset; } -.c4 { +.c5 { visibility: hidden; position: absolute; right: 8px; @@ -276,7 +275,7 @@ exports[`Search Matches snapshot 1`] = ` } @media (hover:hover) and (pointer:fine) { - .c3:hover { + .c4:hover { background: var(--eds_interactive_primary__hover_alt,rgba(222,237,238,1)); border: 0px solid transparent; border-radius: 50%; @@ -284,7 +283,7 @@ exports[`Search Matches snapshot 1`] = ` } @media (hover:hover) and (pointer:fine) { - .c3:disabled:hover { + .c4:disabled:hover { background: transparent; } } @@ -318,22 +317,26 @@ exports[`Search Matches snapshot 1`] = ` height="24" /> - + > + +