Skip to content

Commit

Permalink
refactor(protocol-designer): fix TextArea key issue and replace style…
Browse files Browse the repository at this point in the history
…d textareas (#17502)

* refactor(protocol-designer): fix TextArea key issue and replace styled textareas
  • Loading branch information
koji authored Feb 14, 2025
1 parent c87e3e0 commit 5287385
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 158 deletions.
3 changes: 2 additions & 1 deletion protocol-designer/cypress/support/MixSteps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ enum MixLocators {
PosFromBottom = '[id="TipPositionField_mix_touchTip_mmFromBottom"]',
RenameBtn = 'button:contains("Rename")',
StepNameInput = '[class="InputField__StyledInput-sc-1gyyvht-0 cLVzBl"]',
StepNotesInput = '[class="RenameStepModal__DescriptionField-sc-1k5vjxe-0 lkzOSf"]',
StepNotesInput = '[class="TextAreaField__StyledTextArea-sc-ug50vm-0 fSXuLe"]',
// StepNotesInput = '[data-testid="TextAreaField_step_notes"]',
PosFromTop = '[data-testid="TipPositionField_mix_touchTip_mmFromTop"]',
}

Expand Down
6 changes: 4 additions & 2 deletions protocol-designer/src/molecules/TextAreaField/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ const useFocusVisible = (): boolean => {
const [isKeyboardFocus, setIsKeyboardFocus] = useState(false)

useEffect(() => {
const handleKeyDown = (): void => {
setIsKeyboardFocus(true)
const handleKeyDown = (event: KeyboardEvent): void => {
if (event.key === 'Tab') {
setIsKeyboardFocus(true)
}
}
const handleMouseDown = (): void => {
setIsKeyboardFocus(false)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useTranslation } from 'react-i18next'
import { Controller } from 'react-hook-form'

import {
COLORS,
DIRECTION_COLUMN,
DropdownMenu,
Flex,
} from '@opentrons/components'

import type { Control, UseFormSetValue } from 'react-hook-form'
import type { Ingredient } from '@opentrons/step-generation'

interface LiquidClassDropdownProps {
control: Control<Ingredient, any>
setValue: UseFormSetValue<Ingredient>
liquidClassOptions: Array<{ name: string; value: string }>
liquidClass?: string
}

export function LiquidClassDropdown({
control,
liquidClassOptions,
liquidClass,
setValue,
}: LiquidClassDropdownProps): JSX.Element {
const { t } = useTranslation('liquids')

return (
<Flex flexDirection={DIRECTION_COLUMN} color={COLORS.grey60}>
<Controller
control={control}
name="liquidClass"
render={({ field }) => (
<DropdownMenu
title={t('liquid_class.title')}
tooltipText={t('liquid_class.tooltip')}
dropdownType="neutral"
width="100%"
filterOptions={liquidClassOptions}
currentOption={
liquidClassOptions.find(({ value }) => value === liquidClass) ??
liquidClassOptions[0]
}
onClick={value => {
field.onChange(value)
setValue('liquidClass', value)
}}
/>
)}
/>
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { SketchPicker } from 'react-color'
import { Controller } from 'react-hook-form'

import { Flex, POSITION_ABSOLUTE } from '@opentrons/components'
import { DEFAULT_LIQUID_COLORS } from '@opentrons/shared-data'
import { rgbaToHex } from './util'

import type { RefObject } from 'react'
import type { Control, UseFormSetValue } from 'react-hook-form'
import type { ColorResult } from 'react-color'
import type { Ingredient } from '@opentrons/step-generation'

interface LiquidColorPickerProps {
chooseColorWrapperRef: RefObject<HTMLDivElement>
control: Control<Ingredient, any>
color: string
setValue: UseFormSetValue<Ingredient>
}

export function LiquidColorPicker({
chooseColorWrapperRef,
control,
color,
setValue,
}: LiquidColorPickerProps): JSX.Element {
return (
<Flex
position={POSITION_ABSOLUTE}
left="4.375rem"
top="4.6875rem"
ref={chooseColorWrapperRef}
zIndex={2}
>
<Controller
name="displayColor"
control={control}
render={({ field }) => (
<SketchPicker
presetColors={DEFAULT_LIQUID_COLORS}
color={color}
onChange={(color: ColorResult) => {
const hex = rgbaToHex(color.rgb)
setValue('displayColor', hex)
field.onChange(hex)
}}
/>
)}
/>
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, it, expect, vi } from 'vitest'
import { AIR } from '@opentrons/step-generation'
import { DEFAULT_LIQUID_COLORS } from '@opentrons/shared-data'
import { swatchColors } from '../swatchColors'

vi.spyOn(console, 'warn').mockImplementation(() => {})

describe('swatchColors', () => {
it('returns the correct color for an integer ingredient group ID', () => {
const color = swatchColors('2') // Assuming index 2 exists
expect(color).toBe(DEFAULT_LIQUID_COLORS[2 % DEFAULT_LIQUID_COLORS.length])
})

it('returns "transparent" for AIR', () => {
expect(swatchColors(AIR)).toBe('transparent')
})

it('logs a warning and returns "transparent" for a non-integer string', () => {
const invalidInput = 'invalidString'
const result = swatchColors(invalidInput)

expect(console.warn).toHaveBeenCalledWith(
`swatchColors expected an integer or ${AIR}, got ${invalidInput}`
)
expect(result).toBe('transparent')
})

it('correctly wraps around DEFAULT_LIQUID_COLORS using modulo', () => {
const indexBeyondRange = DEFAULT_LIQUID_COLORS.length + 5
const expectedColor =
DEFAULT_LIQUID_COLORS[indexBeyondRange % DEFAULT_LIQUID_COLORS.length]

expect(swatchColors(String(indexBeyondRange))).toBe(expectedColor)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, it, expect } from 'vitest'
import { rgbaToHex } from '../util'

describe('rgbaToHex', () => {
it('should convert rgba values to hex format', () => {
expect(rgbaToHex({ r: 255, g: 99, b: 71, a: 1 })).toBe('#ff6347ff') // Tomata Red
expect(rgbaToHex({ r: 0, g: 0, b: 0, a: 1 })).toBe('#000000ff') // Black
expect(rgbaToHex({ r: 255, g: 255, b: 255, a: 0.5 })).toBe('#ffffff80') // Semi-transparent White
})

it('should handle the absence of alpha value', () => {
expect(rgbaToHex({ r: 255, g: 165, b: 0 })).toBe('#ffa500ff') // Orange
})
})
103 changes: 24 additions & 79 deletions protocol-designer/src/organisms/DefineLiquidsModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
import { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import { SketchPicker } from 'react-color'
import { yupResolver } from '@hookform/resolvers/yup'
import * as Yup from 'yup'
import { Controller, useForm } from 'react-hook-form'
import styled from 'styled-components'
import {
DEFAULT_LIQUID_COLORS,
getAllLiquidClassDefs,
} from '@opentrons/shared-data'

import { getAllLiquidClassDefs } from '@opentrons/shared-data'
import {
BORDERS,
Btn,
COLORS,
DIRECTION_COLUMN,
DropdownMenu,
Flex,
InputField,
JUSTIFY_END,
JUSTIFY_SPACE_BETWEEN,
LiquidIcon,
Modal,
POSITION_ABSOLUTE,
PrimaryButton,
SecondaryButton,
SPACING,
Expand All @@ -34,13 +27,15 @@ import * as labwareIngredActions from '../../labware-ingred/actions'
import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors'
import { HandleEnter } from '../../atoms/HandleEnter'
import { LINE_CLAMP_TEXT_STYLE } from '../../atoms'
import { TextAreaField } from '../../molecules'
import { getEnableLiquidClasses } from '../../feature-flags/selectors'
import { swatchColors } from './swatchColors'
import { LiquidColorPicker } from './LiquidColorPicker'
import { LiquidClassDropdown } from './LiquidClassDropdown'

import type { ColorResult, RGBColor } from 'react-color'
import type { Ingredient } from '@opentrons/step-generation'
import type { ThunkDispatch } from 'redux-thunk'
import type { BaseState } from '../../types'
import type { Ingredient } from '@opentrons/step-generation'

const liquidEditFormSchema: any = Yup.object().shape({
displayName: Yup.string().required('liquid name is required'),
Expand Down Expand Up @@ -139,13 +134,6 @@ export function DefineLiquidsModal(
})
}

const rgbaToHex = (rgba: RGBColor): string => {
const { r, g, b, a } = rgba
const toHex = (n: number): string => n.toString(16).padStart(2, '0')
const alpha = a != null ? Math.round(a * 255) : 255
return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(alpha)}`
}

const liquidClassOptions = [
{ name: 'Choose an option', value: '' },
...Object.entries(liquidClassDefs).map(
Expand Down Expand Up @@ -190,29 +178,12 @@ export function DefineLiquidsModal(
>
<>
{showColorPicker ? (
<Flex
position={POSITION_ABSOLUTE}
left="4.375rem"
top="4.6875rem"
ref={chooseColorWrapperRef}
zIndex={2}
>
<Controller
name="displayColor"
control={control}
render={({ field }) => (
<SketchPicker
presetColors={DEFAULT_LIQUID_COLORS}
color={color}
onChange={(color: ColorResult) => {
const hex = rgbaToHex(color.rgb)
setValue('displayColor', hex)
field.onChange(hex)
}}
/>
)}
/>
</Flex>
<LiquidColorPicker
chooseColorWrapperRef={chooseColorWrapperRef}
control={control}
color={color}
setValue={setValue}
/>
) : null}

<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing32}>
Expand Down Expand Up @@ -251,36 +222,20 @@ export function DefineLiquidsModal(
color={COLORS.grey60}
gridGap={SPACING.spacing4}
>
<StyledText desktopStyle="bodyDefaultRegular">
{t('description')}
</StyledText>
<DescriptionField {...register('description')} />
<TextAreaField
title={t('description')}
{...register('description')}
value={watch('description')}
height="4.75rem"
/>
</Flex>
{enableLiquidClasses ? (
<Flex flexDirection={DIRECTION_COLUMN} color={COLORS.grey60}>
<Controller
control={control}
name="liquidClass"
render={({ field }) => (
<DropdownMenu
title={t('liquid_class.title')}
tooltipText={t('liquid_class.tooltip')}
dropdownType="neutral"
width="100%"
filterOptions={liquidClassOptions}
currentOption={
liquidClassOptions.find(
({ value }) => value === liquidClass
) ?? liquidClassOptions[0]
}
onClick={value => {
field.onChange(value)
setValue('liquidClass', value)
}}
/>
)}
/>
</Flex>
<LiquidClassDropdown
control={control}
setValue={setValue}
liquidClassOptions={liquidClassOptions}
liquidClass={liquidClass}
/>
) : null}
<Flex
flexDirection={DIRECTION_COLUMN}
Expand Down Expand Up @@ -340,13 +295,3 @@ export function DefineLiquidsModal(
</HandleEnter>
)
}

export const DescriptionField = styled.textarea`
min-height: 5rem;
width: 100%;
border: 1px ${BORDERS.styleSolid} ${COLORS.grey50};
border-radius: ${BORDERS.borderRadius4};
padding: ${SPACING.spacing8};
font-size: ${TYPOGRAPHY.fontSizeP};
resize: none;
`
18 changes: 18 additions & 0 deletions protocol-designer/src/organisms/DefineLiquidsModal/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { RGBColor } from 'react-color'

/**
* Converts an RGBA color object to a hexadecimal string representation.
*
* @param {RGBColor} rgba - The RGBA color object.
* @returns {string} - The hexadecimal string representation of the color, including the alpha component.
*
* @example
* // Returns "#ffa500ff" (alpha defaults to 1)
* rgbaToHex({ r: 255, g: 165, b: 0 });
*/
export const rgbaToHex = (rgba: RGBColor): string => {
const { r, g, b, a } = rgba
const toHex = (n: number): string => n.toString(16).padStart(2, '0')
const alpha = a != null ? Math.round(a * 255) : 255
return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(alpha)}`
}
Loading

0 comments on commit 5287385

Please sign in to comment.