From 3ece4155793533c98c80769be739944d238eaed7 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Tue, 18 Oct 2022 13:46:39 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20New=20internal=20component:=20`Inpu?= =?UTF-8?q?tWrapper`=20(#2395)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Adornments support in `Input` component (#2354) * 🚧 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 * 📸 Updated snapshots * Working example with colored helpertext * 🚧 WIP * 🚧 WIP * tweaks * Added description * ♻️ Textfield using `InputWrapper` (#2416) * ✨ Adornments support in `Input` component (#2354) * 🚧 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 * ♻️ Working with just input * 🚧 WIP * ♻️ Fixed helper text & colors * ♻️ changed to usecallback * ⚰️ Removed unused files * ♻️ improvements to input * ♻️ Simplified adornment width * 🐛 Fixed new variant in Textarea * ♻️ Textfield with discriminating union * ♻️ re-introduced old proxy component * Fiex typing * 🗑️ Cleaning up files * Added overridablecomponent to input * 🏷️ Made as optional in overridableComponent * 🐛 Fix aria-describedby when no helpertext is provided * ♻️ Updated Textarea with input * Cleaning up helpertext * ♻️ Tweak adornments * 📸 Update snapshots * ♻️ Working textarea within Textfield * ⏪ Reverted adornment styling changes * Cleanup * ♻️ Update Textfield rstories * 📸 Update snapshots * ♻️ Re-added support for manual width * ♻️ Refactor `Search` to use new `InputWrapper` (#2481) * ♻️ Initial rewrite of Search * ♻️ Re-introduce height on input due to compact * 📝 Updated search docs * ♻️ Introduces Inputwrapper * ✅ Updated test * 🐛 Fixed bugs * 🗑️ Clean code * Updated snapshots * 🔥 Removed unused files * 🐛 Fixed height for when multilines * 📸 Updated snapshots * 🚨 Cleaned up some linting warnings * 📝 Wording --- .vscode/settings.json | 2 +- .../Autocomplete/Autocomplete.tokens.ts | 1 + .../components/Autocomplete/Autocomplete.tsx | 72 ++-- .../src/components/Input/Input.docs.mdx | 12 +- .../src/components/Input/Input.stories.tsx | 168 +++++--- .../src/components/Input/Input.test.tsx | 51 +-- .../src/components/Input/Input.tokens.ts | 138 ++++--- .../src/components/Input/Input.tsx | 289 ++++++++++---- .../Input/__snapshots__/Input.test.tsx.snap | 92 +++-- .../HelperText/HelperText.token.ts | 39 ++ .../InputWrapper/HelperText/HelperText.tsx | 52 +++ .../InputWrapper/HelperText/index.ts | 1 + .../InputWrapper/InputWrapper.stories.tsx | 26 ++ .../InputWrapper/InputWrapper.tokens.ts | 174 ++++++++ .../components/InputWrapper/InputWrapper.tsx | 85 ++++ .../src/components/InputWrapper/index.tsx | 2 + .../src/components/Search/Search.docs.mdx | 29 +- .../src/components/Search/Search.stories.tsx | 137 +------ .../src/components/Search/Search.test.tsx | 49 ++- .../src/components/Search/Search.tokens.ts | 93 ----- .../src/components/Search/Search.tsx | 323 +++------------ .../Search/__snapshots__/Search.test.tsx.snap | 370 ++++-------------- .../__snapshots__/SingleSelect.test.tsx.snap | 137 +++---- .../src/components/TextField/Field.tsx | 247 ------------ .../TextField/HelperText/HelperText.token.ts | 61 --- .../TextField/HelperText/HelperText.tsx | 103 ----- .../components/TextField/HelperText/index.ts | 1 - .../components/TextField/Icon/Icon.tokens.ts | 56 --- .../src/components/TextField/Icon/Icon.tsx | 81 ---- .../src/components/TextField/Icon/index.ts | 1 - .../TextField/TextField.context.tsx | 46 --- .../components/TextField/TextField.docs.mdx | 6 +- .../TextField/TextField.stories.tsx | 62 ++- .../components/TextField/TextField.test.tsx | 8 +- .../components/TextField/TextField.tokens.ts | 128 ------ .../src/components/TextField/TextField.tsx | 141 ++++--- .../__snapshots__/TextField.test.tsx.snap | 157 ++++---- .../src/components/TextField/types.ts | 7 - .../src/components/Textarea/Textarea.tsx | 115 ++---- .../eds-core-react/src/components/types.ts | 1 + 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 ++ .../src/utils/overridableComponent.ts | 2 +- 46 files changed, 1426 insertions(+), 2177 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 delete mode 100644 packages/eds-core-react/src/components/Search/Search.tokens.ts delete mode 100644 packages/eds-core-react/src/components/TextField/Field.tsx delete mode 100644 packages/eds-core-react/src/components/TextField/HelperText/HelperText.token.ts delete mode 100644 packages/eds-core-react/src/components/TextField/HelperText/HelperText.tsx delete mode 100644 packages/eds-core-react/src/components/TextField/HelperText/index.ts delete mode 100644 packages/eds-core-react/src/components/TextField/Icon/Icon.tokens.ts delete mode 100644 packages/eds-core-react/src/components/TextField/Icon/Icon.tsx delete mode 100644 packages/eds-core-react/src/components/TextField/Icon/index.ts delete mode 100644 packages/eds-core-react/src/components/TextField/TextField.context.tsx delete mode 100644 packages/eds-core-react/src/components/TextField/TextField.tokens.ts delete mode 100644 packages/eds-core-react/src/components/TextField/types.ts create mode 100644 packages/eds-core-react/src/components/types.ts create mode 100644 packages/eds-utils/src/mixins/index.ts create mode 100644 packages/eds-utils/src/mixins/typography.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index fbb30eceed..5a301a5365 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,5 +15,5 @@ "editor.formatOnSave": true, "prettier.prettierPath": "./node_modules/prettier", "typescript.tsdk": "node_modules/typescript/lib", - "task.allowAutomaticTasks": "off", + "task.allowAutomaticTasks": "off" } 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..db48ceaebe 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,14 @@ It's important to link the `Input` to the corresponding label. Compact `Input` using `EdsProvider`. - \ No newline at end of file + + +### With Adornments + + + + +### Casted as textarea + + + 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..d45b4cc6c5 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', @@ -19,7 +22,7 @@ export default { decorators: [ (Story) => { return ( - + ) @@ -30,53 +33,20 @@ export default { export const Introduction: Story = (args) => { return } -Introduction.decorators = [ - (Story) => { - return ( - - - - ) - }, -] export const Types: Story = () => ( <>
-
-
-
-
-
) @@ -144,15 +114,6 @@ export const ReadOnly: Story = () => ( ) ReadOnly.storyName = 'Read only' -ReadOnly.decorators = [ - (Story) => { - return ( - - - - ) - }, -] export const Accessiblity: Story = () => { // To wrap the input component inside the label element is not yet supported @@ -163,15 +124,6 @@ export const Accessiblity: Story = () => { ) } -Accessiblity.decorators = [ - (Story) => { - return ( - - - - ) - }, -] export const Compact: Story = () => { // To wrap the input component inside the label element is not yet supported @@ -198,3 +150,103 @@ Compact.decorators = [ ) }, ] + +const SmallButton = styled(Button)` + height: 24px; + width: 24px; +` + +export const WithAdornments: Story = () => { + return ( + + + ) +} + +export const casted: Story = (args) => { + 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..42ae4f9de3 100644 --- a/packages/eds-core-react/src/components/Input/Input.tsx +++ b/packages/eds-core-react/src/components/Input/Input.tsx @@ -1,72 +1,126 @@ -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 { - typographyTemplate, + forwardRef, + ReactNode, + useState, + useCallback, + CSSProperties, + ElementType, + ComponentPropsWithoutRef, +} from 'react' +import styled, { css } from 'styled-components' +import { ComponentToken } from '@equinor/eds-tokens' +import { + typographyMixin, spacingsTemplate, outlineTemplate, useToken, + OverridableComponent, } from '@equinor/eds-utils' -import type { Variants } from '../TextField/types' +import { inputToken as tokens } from './Input.tokens' +import type { InputToken } from './Input.tokens' +import type { Variants } from '../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}; - border: none; - height: ${theme.minHeight}; - box-shadow: ${boxShadow}; + --eds-input-adornment-color: ${entities.adornment.typography.color}; + --eds-input-color: ${token.typography.color}; - ${outlineTemplate(activeOutline)} - ${typographyTemplate(theme.typography)} - ${spacingsTemplate(theme.spacings)}; + position: relative; + height: ${token.height}; + width: ${token.width}; + display: flex; + flex-direction: row; + border: none; + box-sizing: border-box; + box-shadow: ${token.boxShadow}; + background: ${token.background}; + ${outlineTemplate(token.outline)} - &::placeholder { - color: ${theme.entities.placeholder.typography.color}; - } + &:focus-within { + --eds-input-adornment-color: ${entities.adornment?.states.focus?.outline + .color}; - &:active, - &:focus { - outline-offset: 0; box-shadow: none; - ${outlineTemplate(focusOutline)} + ${outlineTemplate(states.focus.outline)} } - &:disabled { - color: ${disabled.typography.color}; + ${disabled && + css` + --eds-input-adornment-color: ${states.disabled.typography.color}; + --eds-input-color: ${states.disabled.typography.color}; cursor: not-allowed; box-shadow: none; + `} + ${readOnly && + css({ + background: states.readOnly.background, + boxShadow: states.readOnly.boxShadow, + })} + ` +}) + +const StyledInput = styled.input( + ({ token, paddingLeft, paddingRight }: StyledProps) => { + return css` + width: 100%; + border: none; + background: transparent; + ${spacingsTemplate(token.spacings)} + ${typographyMixin(token.typography)} outline: none; - &:focus, - &:active { - outline: none; + + padding-left: ${paddingLeft}; + padding-right: ${paddingRight}; + + &::placeholder { + color: ${token.entities.placeholder.typography.color}; } - } - &[readOnly] { - background: ${readOnly.background}; - box-shadow: ${readOnly.boxShadow}; - } + + &:disabled { + color: var(--eds-input-color); + cursor: not-allowed; + } + ` + }, +) + +type AdornmentProps = { + token: InputToken +} + +const Adornments = styled.div(({ token }) => { + return css` + position: absolute; + top: ${token.spacings.top}; + bottom: ${token.spacings.bottom}; + 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 = { - theme: InputToken -} + token: InputToken + paddingLeft?: string + paddingRight?: string +} & Required> export type InputProps = { /** Placeholder */ @@ -79,27 +133,124 @@ export type InputProps = { type?: string /** Read Only */ readOnly?: boolean -} & InputHTMLAttributes - -export const Input = forwardRef(function Input( - { variant = 'default', disabled = false, type = 'text', ...other }, - ref, -) { - const actualVariant = variant === 'default' ? 'input' : variant - const inputVariant = tokens[actualVariant] - const { density } = useEds() - const token = useToken({ density }, inputVariant) - - const inputProps = { + /** Left adornments */ + leftAdornments?: ReactNode + /** Right adornments */ + rightAdornments?: ReactNode + /** Left adornments props */ + leftAdornmentsProps?: ComponentPropsWithoutRef<'div'> + /** Right adornments props */ + rightAdornmentsProps?: ComponentPropsWithoutRef<'div'> + /** Manually specify left adornments width. The width will be the dom element width if not defined */ + leftAdornmentsWidth?: number + /** Manually specify right adornments width. The width will be the dom element width if not defined */ + rightAdornmentsWidth?: number + /** Cast the input to another element */ + as?: ElementType + /** */ + className?: string + style?: CSSProperties +} + +export const Input: OverridableComponent = + forwardRef(function Input( + { + variant, + disabled = false, + type = 'text', + leftAdornments, + rightAdornments, + readOnly, + className, + style, + leftAdornmentsProps, + rightAdornmentsProps, + leftAdornmentsWidth, + rightAdornmentsWidth, + ...other + }, ref, - type, - disabled, - ...other, - } - - return ( - - - - ) -}) + ) { + const inputVariant = tokens[variant] ? tokens[variant] : tokens.input + const { density } = useEds() + const _token = useToken({ density }, inputVariant)() + + const [rightAdornmentsRef, setRightAdornmentsRef] = + useState() + const [leftAdornmentsRef, setLeftAdornmentsRef] = useState() + + const token = useCallback((): ComponentToken => { + const _leftAdornmentsWidth = + leftAdornmentsWidth || + (leftAdornmentsRef ? leftAdornmentsRef.clientWidth : 0) + const _rightAdornmentsWidth = + rightAdornmentsWidth || + (rightAdornmentsRef ? rightAdornmentsRef.clientWidth : 0) + return { + ..._token, + spacings: { + ..._token.spacings, + left: `${_leftAdornmentsWidth + parseInt(_token.spacings.left)}px`, + right: `${_rightAdornmentsWidth + parseInt(_token.spacings.right)}px`, + }, + } + }, [ + leftAdornmentsWidth, + leftAdornmentsRef, + rightAdornmentsWidth, + rightAdornmentsRef, + _token, + ])() + + const inputProps = { + ref, + type, + disabled, + readOnly, + token, + style: { + resize: 'none', + }, + ...other, + } + + const containerProps = { + disabled, + readOnly, + className, + style, + token, + } + + const _leftAdornmentProps = { + ...leftAdornmentsProps, + ref: setLeftAdornmentsRef, + token, + } + const _rightAdornmentProps = { + ...rightAdornmentsProps, + ref: setRightAdornmentsRef, + token, + } + + 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..2befe8540d 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,81 +3,87 @@ 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; + height: 36px; 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; - height: 36px; + box-sizing: border-box; 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; - margin: 0; - color: var(--eds_text__static_icons__default,rgba(61,61,61,1)); + 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; font-family: Equinor; font-size: 1.000rem; font-weight: 400; - line-height: 1.500em; -webkit-letter-spacing: 0.025em; -moz-letter-spacing: 0.025em; -ms-letter-spacing: 0.025em; letter-spacing: 0.025em; - text-align: left; + line-height: 1.500em; + outline: none; padding-left: 8px; - padding-top: 6px; padding-right: 8px; - padding-bottom: 6px; } -.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..541e839cc2 --- /dev/null +++ b/packages/eds-core-react/src/components/InputWrapper/HelperText/HelperText.token.ts @@ -0,0 +1,39 @@ +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', + }, + }, +} 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..24f6d20591 --- /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)}; +` + +export 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..a5e6cf6966 --- /dev/null +++ b/packages/eds-core-react/src/components/InputWrapper/HelperText/index.ts @@ -0,0 +1 @@ +export * 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..095f90ee93 --- /dev/null +++ b/packages/eds-core-react/src/components/InputWrapper/InputWrapper.stories.tsx @@ -0,0 +1,26 @@ +import { Story, ComponentMeta } from '@storybook/react' +import { InputWrapper, InputWrapperProps, Input } from '../..' + +export default { + title: 'Inputs/InputWrapper', + component: InputWrapper, +} as ComponentMeta + +export const Introduction: Story = (args) => { + const helperProps = { + text: 'helperText', + } + return ( + + + + ) +} + +Introduction.args = {} 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..e0b864eb3f --- /dev/null +++ b/packages/eds-core-react/src/components/InputWrapper/InputWrapper.tokens.ts @@ -0,0 +1,174 @@ +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, + }, + }, + + helperText: { + 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: 'none', + outline: { + color: danger__resting.rgba, + }, + states: { + focus: { + outline: { + color: danger__hover.rgba, + }, + }, + }, + entities: { + helperText: { + typography: { + ...typography.input.label, + color: danger__hover.rgba, + }, + }, + }, +}) + +export const warning: InputToken = mergeDeepRight(input, { + boxShadow: 'none', + outline: { + color: warning__resting.rgba, + }, + states: { + focus: { + outline: { + color: warning__hover.rgba, + }, + }, + }, + entities: { + helperText: { + typography: { + ...typography.input.label, + color: warning__hover.rgba, + }, + }, + }, +}) + +export const success: InputToken = mergeDeepRight(input, { + boxShadow: 'none', + outline: { + color: success__resting.rgba, + }, + states: { + focus: { + outline: { + color: success__hover.rgba, + }, + }, + }, + entities: { + helperText: { + typography: { + ...typography.input.label, + color: success__hover.rgba, + }, + }, + }, +}) + +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..c101bf9db9 --- /dev/null +++ b/packages/eds-core-react/src/components/InputWrapper/InputWrapper.tsx @@ -0,0 +1,85 @@ +import { HTMLAttributes, forwardRef, ReactNode, useCallback } from 'react' +import styled, { ThemeProvider } from 'styled-components' +import { useToken } from '@equinor/eds-utils' +import { Label as _Label, LabelProps } from '../Label' +import { HelperText as _HelperText, HelperTextProps } from './HelperText' +import { useEds } from './../EdsProvider' +import { inputToken as tokens } from './InputWrapper.tokens' +import { Variants } from '../types' + +const Container = styled.div`` + +const HelperText = styled(_HelperText)` + margin-top: 8px; + margin-left: 8px; +` + +const Label = styled(_Label)` + margin-left: 8px; + margin-right: 8px; +` + +export type InputWrapperProps = { + /** Label */ + label?: string + /** Disabled state */ + disabled?: boolean + /** Read Only */ + readOnly?: boolean + /** Highlight color */ + color?: Variants + /** Label props */ + labelProps?: LabelProps + /** Helpertext props */ + helperProps?: HelperTextProps + /** Helper Icon */ + helperIcon?: ReactNode + /** Input or Textarea element */ + children: ReactNode +} & HTMLAttributes + +/** InputWrapper is a internal skeleton component for structuring input elements */ +export const InputWrapper = forwardRef( + function InputWrapper( + { + children, + color, + label, + labelProps = {}, + helperProps = {}, + helperIcon, + ...other + }, + ref, + ) { + const { density } = useEds() + const actualVariant = color || 'input' + const inputToken = tokens[actualVariant] + const token = useToken({ density }, inputToken) + + const helperTextColor = useCallback(() => { + const _token = token() + return other.disabled + ? _token.entities.helperText.states.disabled.typography.color + : _token.entities.helperText.typography.color + }, [token, other.disabled])() + + const hasHelperText = Boolean(helperProps?.text) + const hasLabel = Boolean(label || labelProps?.label) + + return ( + + + {hasLabel && + + ) + }, +) 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/Search.docs.mdx b/packages/eds-core-react/src/components/Search/Search.docs.mdx index 4d090268a7..b0db598ce9 100644 --- a/packages/eds-core-react/src/components/Search/Search.docs.mdx +++ b/packages/eds-core-react/src/components/Search/Search.docs.mdx @@ -20,35 +20,22 @@ Allows users to locate or refine content based on simple words or phrases. `Search` can be used as a primary way of discovering content. -
    -
  • Search placeholder text should always say "Search".
  • -
  • Search should replace the center custom content, placeholder of the top bar.
  • -
  • Search can be a button icon that is expanded into the search field if search is less common in the application.
  • -
- ```tsx import { Search } from '@equinor/eds-core-react' - +
+ + ``` -## Examples - -### With `onFocus` and `onBlur` - - +## Accessibility -### With predefined value +* Use either a `label` or `aria-label` for description of whats being searched +* `Search` should be used inside a `form` - + -### Centered and styled - - - -### Inside a form element - - +## Examples ### Disabled diff --git a/packages/eds-core-react/src/components/Search/Search.stories.tsx b/packages/eds-core-react/src/components/Search/Search.stories.tsx index 2efe58a0fa..a6d5117644 100644 --- a/packages/eds-core-react/src/components/Search/Search.stories.tsx +++ b/packages/eds-core-react/src/components/Search/Search.stories.tsx @@ -1,40 +1,9 @@ import { useState, useEffect } from 'react' import { action } from '@storybook/addon-actions' -import styled from 'styled-components' -import { - Search, - Typography, - Button, - SearchProps, - EdsProvider, - Density, -} from '../..' +import { Search, Button, SearchProps, EdsProvider, Density } from '../..' import { Story, ComponentMeta } from '@storybook/react' import page from './Search.docs.mdx' -const Columns = styled.div` - display: grid; - width: 100%; - grid-gap: 16px; - grid-auto-flow: column; - grid-auto-columns: max-content; -` - -const Wrapper = styled.div` - background: lightblue; - padding: 8px; - width: 50%; - height: 100px; - display: flex; - align-items: center; - justify-content: center; -` - -const StyledSearch = styled(Search)` - width: 50%; - margin-left: 32px; -` - export default { title: 'Inputs/Search', component: Search, @@ -46,8 +15,6 @@ export default { } as ComponentMeta const handleOnChange = action('onChange') -const handleOnBlur = action('onBlur') -const handleOnFocus = action('onFocus') export const Introduction: Story = () => { // This story is not interactive, because Search has no props beyond the default HTML ones. @@ -61,80 +28,21 @@ export const Introduction: Story = () => { ) } -export const WithOnFocusAndBlur: Story = () => { - const [isFocused, setIsFocused] = useState(false) - - const handleFocus = () => { - setIsFocused(true) - handleOnFocus() - } - - const handleBlur = () => { - setIsFocused(false) - handleOnBlur() - } - - return ( -
- - I am connected to the search input - - -
- ) -} -WithOnFocusAndBlur.storyName = 'With on focus and blur' - -export const WithPredefinedValue: Story = () => ( - -) -WithPredefinedValue.storyName = 'With predefined value' - -export const CenteredAndStyled: Story = () => ( - - 50% width - = () => ( +
+ - -) -CenteredAndStyled.storyName = 'Centered and styled' - -export const InsideAForm: Story = () => ( - - ) -InsideAForm.storyName = 'Inside a form element' export const Disabled: Story = () => ( ) @@ -152,24 +60,21 @@ export const Controlled: Story = () => { return ( <> - Value: {searchValue} - - - - + + ) } @@ -185,7 +90,7 @@ export const Compact: Story = () => { return ( { const { getComputedStyle } = window window.getComputedStyle = (elt) => getComputedStyle(elt) - const { container } = render() + const { container } = render( + , + ) await act(async () => { const result = await axe(container) expect(result).toHaveNoViolations() @@ -37,11 +39,11 @@ describe('Search', () => { it('Has rendered provided value in input field', () => { const value = 'provided value' - render() + render() - const searchBox = screen.queryByRole('searchbox') + const searchInput = screen.getByTestId('search') - expect(searchBox).toHaveValue(value) + expect(searchInput).toHaveValue(value) }) it('Has called onChange once with event & new value, when value is changed', () => { const searchId = 'search-id-when-testing' @@ -54,11 +56,16 @@ describe('Search', () => { }) render( - , + , ) - const searchBox = screen.queryByRole('searchbox') + const searchInput = screen.queryByTestId('search') - fireEvent.change(searchBox, { + fireEvent.change(searchInput, { target: { value: newValue }, }) @@ -81,15 +88,15 @@ describe('Search', () => { id={searchId} defaultValue="initial value" onChange={handleOnChange} + data-testid="search" />, ) const clearButton = screen.queryByRole('button') - const searchBox = screen.queryByRole('searchbox') - + const searchInput = screen.queryByTestId('search') fireEvent.click(clearButton) expect(handleOnChange).toHaveBeenCalled() - expect(searchBox).toHaveValue('') + expect(searchInput).toHaveValue('') expect(callbackValue).toEqual('') expect(callbackId).toEqual(searchId) }) @@ -101,10 +108,12 @@ describe('Search', () => { callbackId = id as string }) - render() - const searchBox = screen.queryByRole('searchbox') + render( + , + ) + const searchInput = screen.queryByTestId('search') - fireEvent.focus(searchBox) + fireEvent.focus(searchInput) expect(handleOnFocus).toHaveBeenCalled() expect(callbackId).toEqual(searchId) @@ -116,22 +125,22 @@ describe('Search', () => { callbackId = id as string }) - render() - const searchBox = screen.queryByRole('searchbox') + render() + const searchInput = screen.queryByTestId('search') - fireEvent.blur(searchBox) + fireEvent.blur(searchInput) expect(handleOnBlur).toHaveBeenCalled() expect(callbackId).toEqual(searchId) }) it('Has new value, when value property is changed after first render', () => { - const { rerender } = render() + const { rerender } = render() - rerender() + rerender() - const searchBox: HTMLInputElement = screen.queryByRole('searchbox') + const searchInput: HTMLInputElement = screen.queryByTestId('search-new') - expect(searchBox.value).toEqual('new') + expect(searchInput.value).toEqual('new') }) }) diff --git a/packages/eds-core-react/src/components/Search/Search.tokens.ts b/packages/eds-core-react/src/components/Search/Search.tokens.ts deleted file mode 100644 index 5e7c414b0b..0000000000 --- a/packages/eds-core-react/src/components/Search/Search.tokens.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { tokens } from '@equinor/eds-tokens' -import type { ComponentToken } from '@equinor/eds-tokens' - -const { - colors: { - ui: { - background__light: { rgba: background }, - }, - interactive: { - primary__hover_alt: { rgba: primaryHoverAlt }, - primary__resting: { rgba: primaryColor }, - }, - text: { - static_icons__tertiary: { rgba: placeholderColor }, - }, - }, - spacings: { - comfortable: { small }, - }, - typography: { - input: { text: typography }, - }, - shape, -} = tokens - -type SearchToken = ComponentToken - -export const search: SearchToken = { - background, - typography, - height: '36px', - clickbound: { - offset: { top: '6px' }, - height: '48px', - }, - spacings: { - left: small, - right: small, - }, - border: { - type: 'border', - width: '1px', - style: 'solid', - color: 'transparent', - }, - states: { - focus: { - border: { - type: 'border', - width: '1px', - style: 'solid', - color: primaryColor, - }, - }, - }, - entities: { - placeholder: { - typography: { - color: placeholderColor, - }, - }, - icon: { - typography: { - color: placeholderColor, - }, - border: { - type: 'border', - radius: '50%', - }, - states: { - hover: { - background: primaryHoverAlt, - }, - }, - clickbound: { - offset: { top: '6px' }, - height: '36px', - }, - }, - button: { - height: '24px', - width: '24px', - spacings: { - right: small, - }, - }, - }, - modes: { - compact: { - height: shape._modes.compact.straight.minHeight, - }, - }, -} diff --git a/packages/eds-core-react/src/components/Search/Search.tsx b/packages/eds-core-react/src/components/Search/Search.tsx index 0678a6d128..247b45a0e2 100644 --- a/packages/eds-core-react/src/components/Search/Search.tsx +++ b/packages/eds-core-react/src/components/Search/Search.tsx @@ -1,313 +1,98 @@ import { useState, useRef, - useEffect, InputHTMLAttributes, - RefAttributes, forwardRef, useMemo, + ChangeEvent, + useEffect, } from 'react' -import styled, { css, ThemeProvider } from 'styled-components' +import styled from 'styled-components' import { search, close } from '@equinor/eds-icons' -import { search as searchToken } from './Search.tokens' -import { useEds } from '../EdsProvider' import { Button } from '../Button' import { Icon } from '../Icon' import { Input } from '../Input' -import { - spacingsTemplate, - typographyTemplate, - setReactInputValue, - bordersTemplate, - mergeRefs, - useToken, -} from '@equinor/eds-utils' - -type ContainerProps = { - isFocused: boolean - disabled: boolean -} - -const Container = styled.span( - ({ disabled, isFocused, theme }) => { - const { - height, - spacings, - background, - border, - clickbound, - entities: { icon, placeholder }, - states, - } = theme - - return css` - position: relative; - background: ${background}; - width: 100%; - height: ${height}; - display: grid; - grid-gap: 8px; - grid-auto-flow: column; - grid-auto-columns: max-content auto max-content; - align-items: center; - box-sizing: border-box; - ${bordersTemplate(border)} - z-index: 0; - - svg { - fill: ${icon.typography.color}; - } - - ${spacingsTemplate(spacings)} - - ${isFocused && bordersTemplate(states.focus.border)} - - - &::placeholder { - color: ${placeholder.typography.color}; - } - ${disabled - ? css` - cursor: not-allowed; - ` - : css` - @media (hover: hover) and (pointer: fine) { - &:hover { - ${bordersTemplate(states.focus.border)} - cursor: text; - } - } - `} +import { InputWrapper } from '../InputWrapper' +import { setReactInputValue, mergeRefs } from '@equinor/eds-utils' - &::after { - z-index: -1; - position: absolute; - top: -${clickbound.offset}; - left: 0; - width: 100%; - height: ${clickbound.height}; - content: ''; - } - - &::before { - position: absolute; - top: 0; - left: 0; - width: auto; - min-height: auto; - content: ''; - } - ` - }, -) - -const SearchInput = styled(Input)(({ theme, disabled }) => { - return css` - height: calc(${theme.height} - 2px); - align-self: start; - box-shadow: unset; +const SearchInput = styled(Input)` + input { &[type='search']::-webkit-search-decoration, &[type='search']::-webkit-search-cancel-button, &[type='search']::-webkit-search-results-button, &[type='search']::-webkit-search-results-decoration { -webkit-appearance: none; } + } +` - ${typographyTemplate(theme.typography)} - - &:focus { - outline: none; - } - &:-webkit-autofill { - box-shadow: 0 0 0px 1000px ${theme.background} inset; - } - &:autofill { - box-shadow: 0 0 0px 1000px ${theme.background} inset; - } - ${disabled && - css` - cursor: not-allowed; - `} - ` -}) - -type InsideButtonProps = { - isActive: boolean -} - -const InsideButton = styled(Button)( - ({ theme, isActive }) => { - const { - entities: { button }, - } = theme - - return css` - visibility: hidden; - position: absolute; - right: ${button.spacings.right}; - height: ${button.height}; - width: ${button.width}; - - ${isActive && - css` - visibility: visible; - `} - ` - }, -) - -type ControlledSearch = ( - props: SearchProps & RefAttributes, - value: SearchProps['value'], - defaultValue: SearchProps['defaultValue'], -) => SearchProps & RefAttributes +const InsideButton = styled(Button)` + height: 24px; + width: 24px; +` export type SearchProps = InputHTMLAttributes export const Search = forwardRef(function Search( - { - onChange, - defaultValue = '', - value, - className = '', - style, - disabled = false, - onBlur, - onFocus, - ...rest - }, + { onChange, style, className, ...rest }, ref, ) { - const { density } = useEds() - const token = useToken({ density }, searchToken) - - const isControlled = typeof value !== 'undefined' - const isActive = (isControlled && value !== '') || defaultValue !== '' const inputRef = useRef(null) - - const [state, setState] = useState({ - isActive, - isFocused: false, - }) + const [showClear, setShowClear] = useState(Boolean(rest.defaultValue)) useEffect(() => { - setState((prevState) => ({ ...prevState, isActive })) - }, [value, defaultValue, isActive]) - - const handleOnClick = () => { - const inputEl = inputRef.current - inputEl.focus() - } - const handleFocus = () => - setState((prevState) => ({ ...prevState, isFocused: true })) - const handleBlur = () => - setState((prevState) => ({ ...prevState, isFocused: false })) - const handleOnChange = (event: React.ChangeEvent) => { - setIsActive((event.target as HTMLInputElement).value) - } + if (rest.value) { + setShowClear(Boolean(rest.value)) + } + }, [rest.value]) - const handleOnDelete = () => { + const clearInputValue = () => { const input = inputRef.current const clearedValue = '' setReactInputValue(input, clearedValue) - setState((prevState) => ({ ...prevState, isActive: false })) } - const setIsActive = (newValue: string) => - setState((prevState) => ({ ...prevState, isActive: newValue !== '' })) - /** Applying props for controlled vs. uncontrolled scnarios */ - const applyControllingProps: ControlledSearch = ( - props, - value, - defaultValue, - ): SearchProps => { - if (isControlled) { - return { - ...props, - value, - } - } - - return { - ...props, - defaultValue, - } + const handleOnChange = (e: ChangeEvent) => { + setShowClear(Boolean(e.currentTarget.value)) } - const { isFocused } = state - const size = 24 - - const containerProps = { - isFocused, - className, - style, - disabled, - role: 'search', - 'aria-label': rest['aria-label'], - onClick: handleOnClick, - } const combinedRef = useMemo( () => mergeRefs(inputRef, ref), [inputRef, ref], ) - const inputProps = applyControllingProps( - { - ...rest, - disabled, - ref: combinedRef, - type: 'search', - role: 'searchbox', - 'aria-label': 'search input', - onBlur: (e) => { - handleBlur() - if (onBlur) { - onBlur(e) - } - }, - onFocus: (e) => { - handleFocus() - if (onFocus) { - onFocus(e) - } - }, - onChange: (e) => { - handleOnChange(e) - if (onChange) { - onChange(e) - } - }, - }, - value, - defaultValue, - ) - - const clearButtonProps = { - isActive: state.isActive, - onClick: (e: React.MouseEvent) => { - e.stopPropagation() - if (state.isActive) { - handleOnDelete() - } - }, - } - return ( - - - - - - - - - + + ) => { + handleOnChange(e) + if (onChange) { + onChange(e) + } + }} + ref={combinedRef} + leftAdornmentsWidth={24 + 8} + leftAdornments={} + rightAdornmentsWidth={24 + 8} + rightAdornments={ + <> + {showClear && ( + ) => { + e.stopPropagation() + clearInputValue() + }} + > + + + )} + + } + {...rest} + /> + ) }) - -// Search.displayName = 'eds-search' 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..c8ce6411f5 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,360 +2,146 @@ exports[`Search Matches snapshot 1`] = ` - .c5 { - display: grid; - grid-gap: var(--eds_button__gap,8px); - grid-auto-flow: column; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - height: 100%; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; -} - -.c5 > :is(svg,img) { - margin-top: var(--eds_button__icon__margin_y,0); - margin-bottom: var(--eds_button__icon__margin_y,0); -} - -.c3 { - box-sizing: border-box; - margin: 0; - padding: 0; - -webkit-text-decoration: none; - text-decoration: none; + .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; - cursor: pointer; - display: inline-block; - background: transparent; - height: 40px; - width: 40px; - padding-left: 0; - padding-top: var(--eds_button__padding_y,0); - padding-right: 0; - padding-bottom: var(--eds_button__padding_y,0); - border: 0px solid transparent; - border-radius: 50%; - margin: 0; - color: var(--eds_interactive_primary__resting,rgba(0,112,121,1)); - font-family: Equinor; - font-size: var(--eds_button__font_size,0.875rem); - font-weight: 500; - line-height: 1.143em; - text-align: center; -} - -.c3 svg { - justify-self: center; -} - -.c3::before { - position: absolute; - top: 0; - left: 0; - width: auto; - min-height: auto; - content: ''; -} - -.c3::after { - position: absolute; - top: -0; - left: -4px; - width: 48px; - height: 48px; - content: ''; -} - -.c3:focus { - outline: none; -} - -.c3[data-focus-visible-added]:focus { - outline: 2px dashed rgba(0,112,121,1); - outline-offset: -2px; -} - -.c3:focus-visible { - outline: 2px dashed rgba(0,112,121,1); - outline-offset: -2px; -} - -.c3::-moz-focus-inner { - border: 0; + height: 36px; + width: 100%; + 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; + 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; } -.c3:disabled { - cursor: not-allowed; - background: transparent; - border: 1px solid transparent; - margin: 0; - color: var(--eds_interactive__disabled__text,rgba(190,190,190,1)); - font-family: Equinor; - font-size: 0.875rem; - font-weight: 500; - line-height: 1.143em; - text-align: center; +.c0:focus-within { + box-shadow: none; + outline: 2px solid var(--eds_interactive_primary__resting,rgba(0,112,121,1)); + outline-offset: 0px; } -.c1 { +.c4 { 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)); border: none; - height: 36px; - box-shadow: inset 0px -1px 0px 0px var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); - outline: 1px solid transparent; - outline-offset: 0px; - margin: 0; - color: var(--eds_text__static_icons__default,rgba(61,61,61,1)); + background: transparent; + padding-left: 40px; + padding-top: 6px; + padding-right: 40px; + padding-bottom: 6px; font-family: Equinor; font-size: 1.000rem; font-weight: 400; - line-height: 1.500em; -webkit-letter-spacing: 0.025em; -moz-letter-spacing: 0.025em; -ms-letter-spacing: 0.025em; letter-spacing: 0.025em; - text-align: left; - padding-left: 8px; - padding-top: 6px; - padding-right: 8px; - padding-bottom: 6px; + line-height: 1.500em; + outline: none; + padding-left: 40px; + padding-right: 40px; } -.c1::-webkit-input-placeholder { +.c4::-webkit-input-placeholder { color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); } -.c1::-moz-placeholder { +.c4::-moz-placeholder { color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); } -.c1:-ms-input-placeholder { +.c4:-ms-input-placeholder { color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); } -.c1::placeholder { +.c4::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)); +.c4: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 { - position: relative; - background: var(--eds_ui_background__light,rgba(247,247,247,1)); - width: 100%; - height: 36px; - display: grid; - grid-gap: 8px; - grid-auto-flow: column; - grid-auto-columns: max-content auto max-content; +.c2 { + position: absolute; + top: 6px; + bottom: 6px; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; align-items: center; - box-sizing: border-box; - border: 1px solid transparent; - z-index: 0; - padding-left: 8px; - padding-right: 8px; -} - -.c0 svg { - fill: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); -} - -.c0::-webkit-input-placeholder { - color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); -} - -.c0::-moz-placeholder { - color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); -} - -.c0:-ms-input-placeholder { - color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); -} - -.c0::placeholder { - color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); -} - -.c0::after { - z-index: -1; - position: absolute; - top: -top:6px; - left: 0; - width: 100%; - height: 48px; - content: ''; + font-family: Equinor; + font-size: 0.750rem; + font-weight: 500; + line-height: 1.333em; + color: var(--eds-input-adornment-color); } -.c0::before { - position: absolute; - top: 0; +.c3 { left: 0; - width: auto; - min-height: auto; - content: ''; + padding-left: 8px; } -.c2 { - height: calc(36px - 2px); - -webkit-align-self: start; - -ms-flex-item-align: start; - align-self: start; - box-shadow: unset; - margin: 0; - color: rgba(0,0,0,1); - font-family: Equinor; - font-size: 1.000rem; - font-weight: 400; - line-height: 1.500em; - -webkit-letter-spacing: 0.025em; - -moz-letter-spacing: 0.025em; - -ms-letter-spacing: 0.025em; - letter-spacing: 0.025em; - text-align: left; +.c5 { + right: 0; + padding-right: 8px; } -.c2[type='search']::-webkit-search-decoration,.c2[type='search']::-webkit-search-cancel-button,.c2[type='search']::-webkit-search-results-button,.c2[type='search']::-webkit-search-results-decoration { +.c1 input[type='search']::-webkit-search-decoration,.c1 input[type='search']::-webkit-search-cancel-button,.c1 input[type='search']::-webkit-search-results-button,.c1 input[type='search']::-webkit-search-results-decoration { -webkit-appearance: none; } -.c2:focus { - outline: none; -} - -.c2:-webkit-autofill { - box-shadow: 0 0 0px 1000px var(--eds_ui_background__light,rgba(247,247,247,1)) inset; -} - -.c2:autofill { - box-shadow: 0 0 0px 1000px var(--eds_ui_background__light,rgba(247,247,247,1)) inset; -} - -.c4 { - visibility: hidden; - position: absolute; - right: 8px; - height: 24px; - width: 24px; -} - -@media (hover:hover) and (pointer:fine) { - .c3:hover { - background: var(--eds_interactive_primary__hover_alt,rgba(222,237,238,1)); - border: 0px solid transparent; - border-radius: 50%; - } -} - -@media (hover:hover) and (pointer:fine) { - .c3:disabled:hover { - background: transparent; - } -} - -@media (hover:hover) and (pointer:fine) { - .c0:hover { - border: 1px solid var(--eds_interactive_primary__resting,rgba(0,112,121,1)); - cursor: text; - } -} - - - - - - + + +
+
+
`; diff --git a/packages/eds-core-react/src/components/Select/SingleSelect/__snapshots__/SingleSelect.test.tsx.snap b/packages/eds-core-react/src/components/Select/SingleSelect/__snapshots__/SingleSelect.test.tsx.snap index b39446680f..3a382bba76 100644 --- a/packages/eds-core-react/src/components/Select/SingleSelect/__snapshots__/SingleSelect.test.tsx.snap +++ b/packages/eds-core-react/src/components/Select/SingleSelect/__snapshots__/SingleSelect.test.tsx.snap @@ -29,77 +29,75 @@ exports[`SingleSelect Matches snapshot 1`] = ` } .c4 { + --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; + height: 36px; 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; - height: 36px; + box-sizing: border-box; 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; - margin: 0; - color: var(--eds_text__static_icons__default,rgba(61,61,61,1)); +} + +.c4:focus-within { + box-shadow: none; + outline: 2px solid var(--eds_interactive_primary__resting,rgba(0,112,121,1)); + outline-offset: 0px; +} + +.c6 { + width: 100%; + border: none; + background: transparent; + padding-left: 8px; + padding-top: 6px; + padding-right: 8px; + padding-bottom: 6px; font-family: Equinor; font-size: 1.000rem; font-weight: 400; - line-height: 1.500em; -webkit-letter-spacing: 0.025em; -moz-letter-spacing: 0.025em; -ms-letter-spacing: 0.025em; letter-spacing: 0.025em; - text-align: left; + line-height: 1.500em; + outline: none; padding-left: 8px; - padding-top: 6px; padding-right: 8px; - padding-bottom: 6px; } -.c4::-webkit-input-placeholder { +.c6::-webkit-input-placeholder { color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); } -.c4::-moz-placeholder { +.c6::-moz-placeholder { color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); } -.c4:-ms-input-placeholder { +.c6:-ms-input-placeholder { color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); } -.c4::placeholder { +.c6::placeholder { color: var(--eds_text__static_icons__tertiary,rgba(111,111,111,1)); } -.c4:active, -.c4:focus { - outline-offset: 0; - box-shadow: none; - outline: 2px solid var(--eds_interactive_primary__resting,rgba(0,112,121,1)); - outline-offset: 0px; -} - -.c4:disabled { - color: var(--eds_interactive__disabled__text,rgba(190,190,190,1)); +.c6:disabled { + color: var(--eds-input-color); cursor: not-allowed; - box-shadow: none; - outline: none; -} - -.c4:disabled:focus, -.c4:disabled:active { - outline: none; } -.c4[readOnly] { - background: transparent; - box-shadow: none; -} - -.c9 { +.c10 { margin: 0; color: rgba(61,61,61,1); font-family: Equinor; @@ -109,7 +107,7 @@ exports[`SingleSelect Matches snapshot 1`] = ` text-align: left; } -.c8 { +.c9 { display: grid; grid-gap: var(--eds_button__gap,8px); grid-auto-flow: column; @@ -124,12 +122,12 @@ exports[`SingleSelect Matches snapshot 1`] = ` justify-content: center; } -.c8 > :is(svg,img) { +.c9 > :is(svg,img) { margin-top: var(--eds_button__icon__margin_y,0); margin-bottom: var(--eds_button__icon__margin_y,0); } -.c6 { +.c7 { box-sizing: border-box; margin: 0; padding: 0; @@ -156,11 +154,11 @@ exports[`SingleSelect Matches snapshot 1`] = ` text-align: center; } -.c6 svg { +.c7 svg { justify-self: center; } -.c6::before { +.c7::before { position: absolute; top: 0; left: 0; @@ -169,7 +167,7 @@ exports[`SingleSelect Matches snapshot 1`] = ` content: ''; } -.c6::after { +.c7::after { position: absolute; top: -0; left: -4px; @@ -178,25 +176,25 @@ exports[`SingleSelect Matches snapshot 1`] = ` content: ''; } -.c6:focus { +.c7:focus { outline: none; } -.c6[data-focus-visible-added]:focus { +.c7[data-focus-visible-added]:focus { outline: 2px dashed rgba(0,112,121,1); outline-offset: -2px; } -.c6:focus-visible { +.c7:focus-visible { outline: 2px dashed rgba(0,112,121,1); outline-offset: -2px; } -.c6::-moz-focus-inner { +.c7::-moz-focus-inner { border: 0; } -.c6:disabled { +.c7:disabled { cursor: not-allowed; background: transparent; border: 1px solid transparent; @@ -217,7 +215,7 @@ exports[`SingleSelect Matches snapshot 1`] = ` padding-right: calc( 24px + 8px + 8px + 0px ); } -.c10 { +.c11 { background-color: var(--eds_ui_background__default,rgba(255,255,255,1)); box-shadow: 0 7px 8px rgba(0,0,0,0.2),0 5px 22px rgba(0,0,0,0.12),0 12px 17px rgba(0,0,0,0.14); overflow-y: scroll; @@ -231,7 +229,7 @@ exports[`SingleSelect Matches snapshot 1`] = ` z-index: 50; } -.c7 { +.c8 { position: absolute; right: 8px; height: 24px; @@ -245,7 +243,7 @@ exports[`SingleSelect Matches snapshot 1`] = ` } @media (hover:hover) and (pointer:fine) { - .c6:hover { + .c7:hover { background: var(--eds_interactive_primary__hover_alt,rgba(222,237,238,1)); border: 0px solid transparent; border-radius: 50%; @@ -253,7 +251,7 @@ exports[`SingleSelect Matches snapshot 1`] = ` } @media (hover:hover) and (pointer:fine) { - .c6:disabled:hover { + .c7:disabled:hover { background: transparent; } } @@ -279,26 +277,31 @@ exports[`SingleSelect Matches snapshot 1`] = ` class="c3" role="combobox" > - + > + +