diff --git a/protocol-designer/src/molecules/TextAreaField/TextAreaField.stories.tsx b/protocol-designer/src/molecules/TextAreaField/TextAreaField.stories.tsx new file mode 100644 index 00000000000..ec0cf083d27 --- /dev/null +++ b/protocol-designer/src/molecules/TextAreaField/TextAreaField.stories.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' +import { + DIRECTION_COLUMN, + Flex, + SPACING, + VIEWPORT, +} from '@opentrons/components' +import { TextAreaField as TextAreaFieldComponent } from '.' + +import type { ComponentProps } from 'react' +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { + // ToDo (kk05/02/2024) this should be in Library but at this moment there is the same name component in components + // The unification for this component will be done when the old component is retired completely. + title: 'Protocol-Designer/Molecules/TextAreaField', + component: TextAreaFieldComponent, + parameters: VIEWPORT.touchScreenViewport, + argTypes: {}, +} + +export default meta +type Story = StoryObj + +export const TextAreaField: Story = ( + args: ComponentProps +) => { + const [value, setValue] = React.useState(args.value) + return ( + + { + setValue(e.currentTarget.value) + }} + /> + + ) +} + +TextAreaField.args = { + title: 'TextAreaField', + height: '6.8125rem', + placeholder: 'Placeholder Text', +} diff --git a/protocol-designer/src/molecules/TextAreaField/__tests__/TextAreaField.test.tsx b/protocol-designer/src/molecules/TextAreaField/__tests__/TextAreaField.test.tsx new file mode 100644 index 00000000000..ce7a90feef4 --- /dev/null +++ b/protocol-designer/src/molecules/TextAreaField/__tests__/TextAreaField.test.tsx @@ -0,0 +1,69 @@ +import { describe, it, beforeEach, vi, expect } from 'vitest' +import { screen, fireEvent } from '@testing-library/react' + +import { renderWithProviders } from '../../../__testing-utils__' +import { TextAreaField } from '../' + +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { + return renderWithProviders() +} + +describe('TextAreaField', () => { + let props: ComponentProps + + beforeEach(() => { + props = { + title: 'TextAreaField', + placeholder: 'Enter text...', + value: '', + onChange: vi.fn(), + } + }) + + it('renders the TextAreaField component', () => { + render(props) + screen.getByText('TextAreaField') + expect(screen.getByTestId('TextAreaField')).toBeInTheDocument() + }) + + it('displays the correct placeholder text', () => { + render(props) + expect(screen.getByPlaceholderText('Enter text...')).toBeInTheDocument() + }) + + it('updates value when user types', () => { + render(props) + const textarea = screen.getByTestId('TextAreaField') + + fireEvent.change(textarea, { target: { value: 'Hello, world!' } }) + + expect(props.onChange).toHaveBeenCalledTimes(1) + }) + + it('disables the textarea when disabled prop is true', () => { + props.disabled = true + render(props) + expect(screen.getByTestId('TextAreaField')).toBeDisabled() + }) + + it('displays an error message when error prop is provided', () => { + props.error = 'Error: Invalid input' + render(props) + + expect(screen.getByText('Error: Invalid input')).toBeInTheDocument() + }) + + it('display an icon when tooltip prop is provided', () => { + props.tooltipText = 'ot-icon-check' + render(props) + screen.getByTestId('tooltip-icon') + }) + + it('display left icon when leftIcon prop is provided', () => { + props.leftIcon = 'information' + render(props) + screen.getByTestId('left-icon') + }) +}) diff --git a/protocol-designer/src/molecules/TextAreaField/index.tsx b/protocol-designer/src/molecules/TextAreaField/index.tsx new file mode 100644 index 00000000000..7bc91ea616a --- /dev/null +++ b/protocol-designer/src/molecules/TextAreaField/index.tsx @@ -0,0 +1,314 @@ +import { useEffect, useState, forwardRef } from 'react' +import styled, { css } from 'styled-components' +import { + ALIGN_CENTER, + BORDERS, + COLORS, + DIRECTION_COLUMN, + DIRECTION_ROW, + Flex, + Icon, + PRODUCT, + SPACING, + StyledText, + TEXT_ALIGN_RIGHT, + Tooltip, + TYPOGRAPHY, + useHoverTooltip, +} from '@opentrons/components' + +import type { + ChangeEventHandler, + FocusEvent, + MouseEvent, + MutableRefObject, +} from 'react' +import type { FlattenSimpleInterpolation } from 'styled-components' +import type { IconName } from '@opentrons/components' + +const COLOR_WARNING_DARK = '#9e5e00' // ToDo (kk:08/13/2024) replace this with COLORS + +// hook to detect tab focus vs mouse focus +const useFocusVisible = (): boolean => { + const [isKeyboardFocus, setIsKeyboardFocus] = useState(false) + + useEffect(() => { + const handleKeyDown = (): void => { + setIsKeyboardFocus(true) + } + const handleMouseDown = (): void => { + setIsKeyboardFocus(false) + } + + document.addEventListener('keydown', handleKeyDown) + document.addEventListener('mousedown', handleMouseDown) + + return () => { + document.removeEventListener('keydown', handleKeyDown) + document.removeEventListener('mousedown', handleMouseDown) + } + }, []) + + return isKeyboardFocus +} + +export interface TextAreaFieldProps { + /** field is disabled if value is true */ + disabled?: boolean + /** change handler */ + onChange?: ChangeEventHandler + /** name of field in form */ + name?: string + /** optional ID of