diff --git a/protocol-designer/src/assets/images/tip_side_bottom_layer.svg b/protocol-designer/src/assets/images/tip_side_bottom_layer.svg
new file mode 100644
index 000000000000..717282ed9514
--- /dev/null
+++ b/protocol-designer/src/assets/images/tip_side_bottom_layer.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/protocol-designer/src/assets/images/tip_side_mid_layer.svg b/protocol-designer/src/assets/images/tip_side_mid_layer.svg
new file mode 100644
index 000000000000..097f163b705f
--- /dev/null
+++ b/protocol-designer/src/assets/images/tip_side_mid_layer.svg
@@ -0,0 +1,45 @@
+
+
\ No newline at end of file
diff --git a/protocol-designer/src/assets/images/tip_side_top_layer.svg b/protocol-designer/src/assets/images/tip_side_top_layer.svg
new file mode 100644
index 000000000000..adf16b116917
--- /dev/null
+++ b/protocol-designer/src/assets/images/tip_side_top_layer.svg
@@ -0,0 +1,49 @@
+
+
\ No newline at end of file
diff --git a/protocol-designer/src/assets/images/tip_top_bottom_layer.svg b/protocol-designer/src/assets/images/tip_top_bottom_layer.svg
new file mode 100644
index 000000000000..9def3aa184df
--- /dev/null
+++ b/protocol-designer/src/assets/images/tip_top_bottom_layer.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/protocol-designer/src/assets/images/tip_top_mid_layer.svg b/protocol-designer/src/assets/images/tip_top_mid_layer.svg
new file mode 100644
index 000000000000..7e97da22d380
--- /dev/null
+++ b/protocol-designer/src/assets/images/tip_top_mid_layer.svg
@@ -0,0 +1,37 @@
+
+
\ No newline at end of file
diff --git a/protocol-designer/src/assets/images/tip_top_top_layer.svg b/protocol-designer/src/assets/images/tip_top_top_layer.svg
new file mode 100644
index 000000000000..d5c7edbbc9fc
--- /dev/null
+++ b/protocol-designer/src/assets/images/tip_top_top_layer.svg
@@ -0,0 +1,40 @@
+
+
\ No newline at end of file
diff --git a/protocol-designer/src/assets/localization/en/modal.json b/protocol-designer/src/assets/localization/en/modal.json
index ebf4e0d9b804..c7d749f57537 100644
--- a/protocol-designer/src/assets/localization/en/modal.json
+++ b/protocol-designer/src/assets/localization/en/modal.json
@@ -86,7 +86,7 @@
},
"body": {
"blowout_z_offset": "Change from where in the well the robot emits blowout",
- "aspirate_mmFromBottom": "Change from where in the well the robot aspirates",
+ "aspirate_mmFromBottom": "Change where in the well the robot aspirates from.",
"dispense_mmFromBottom": "Change from where in the well the robot dispenses",
"mix_mmFromBottom": "Change from where in the well the robot aspirates and dispenses during the mix",
"aspirate_touchTip_mmFromBottom": "Change from where in the well the robot performs touch tip",
diff --git a/protocol-designer/src/assets/localization/en/shared.json b/protocol-designer/src/assets/localization/en/shared.json
index 8151c1e32704..4b8c40c6e5b7 100644
--- a/protocol-designer/src/assets/localization/en/shared.json
+++ b/protocol-designer/src/assets/localization/en/shared.json
@@ -107,6 +107,7 @@
"reject": "Reject",
"reset_hints_and_tips": "Reset all hints and tips notifications",
"reset_hints": "Reset hints",
+ "reset_to_default": "Reset to default",
"review_our_privacy_policy": "You can adjust this setting at any time by clicking on the settings icon. Find detailed information in our privacy policy.",
"right": "Right",
"save": "Save",
@@ -121,6 +122,7 @@
"step_count": "Step {{current}}",
"step": "Step {{current}} / {{max}}",
"consent_to_eula": "By using Protocol Designer, you consent to the Opentrons EULA.",
+ "swap_view": "Swap view",
"temperaturemoduletype": "Temperature Module",
"thermocyclermoduletype": "Thermocycler Module",
"trashBin": "Trash Bin",
diff --git a/protocol-designer/src/organisms/TipPositionModal/TipPositionSideView.tsx b/protocol-designer/src/organisms/TipPositionModal/TipPositionSideView.tsx
new file mode 100644
index 000000000000..9be4fc6e2719
--- /dev/null
+++ b/protocol-designer/src/organisms/TipPositionModal/TipPositionSideView.tsx
@@ -0,0 +1,77 @@
+import round from 'lodash/round'
+import { useTranslation } from 'react-i18next'
+import {
+ Box,
+ COLORS,
+ OVERFLOW_HIDDEN,
+ POSITION_ABSOLUTE,
+ POSITION_RELATIVE,
+ StyledText,
+} from '@opentrons/components'
+import BOTTOM_LAYER from '../../assets/images/tip_side_bottom_layer.svg'
+import MID_LAYER from '../../assets/images/tip_side_mid_layer.svg'
+import TOP_LAYER from '../../assets/images/tip_side_top_layer.svg'
+
+const WELL_HEIGHT_PIXELS = 71
+const WELL_WIDTH_PIXELS = 70
+const PIXEL_DECIMALS = 2
+
+interface TipPositionAllVizProps {
+ mmFromBottom: number
+ xPosition: number
+ wellDepthMm: number
+ xWidthMm: number
+}
+
+export function TipPositionSideView(
+ props: TipPositionAllVizProps
+): JSX.Element {
+ const { mmFromBottom, xPosition, wellDepthMm, xWidthMm } = props
+ const { t } = useTranslation('application')
+ const fractionOfWellHeight = mmFromBottom / wellDepthMm
+ const pixelsFromBottom =
+ Number(fractionOfWellHeight) * WELL_HEIGHT_PIXELS - WELL_HEIGHT_PIXELS
+ const roundedPixelsFromBottom = round(pixelsFromBottom, PIXEL_DECIMALS)
+ const bottomPx = wellDepthMm
+ ? roundedPixelsFromBottom * 2
+ : mmFromBottom - WELL_HEIGHT_PIXELS
+
+ const xPx = (WELL_WIDTH_PIXELS / xWidthMm) * xPosition
+ const roundedXPx = round(xPx, PIXEL_DECIMALS)
+
+ return (
+
+
+
+
+ {wellDepthMm !== null && (
+
+
+ {round(wellDepthMm, 0)}
+ {t('units.millimeter')}
+
+
+ )}
+ {xWidthMm !== null && (
+
+
+ {xWidthMm}
+ {t('units.millimeter')}
+
+
+ )}
+
+ )
+}
diff --git a/protocol-designer/src/organisms/TipPositionModal/TipPositionTopView.tsx b/protocol-designer/src/organisms/TipPositionModal/TipPositionTopView.tsx
new file mode 100644
index 000000000000..4235aedcce42
--- /dev/null
+++ b/protocol-designer/src/organisms/TipPositionModal/TipPositionTopView.tsx
@@ -0,0 +1,61 @@
+import round from 'lodash/round'
+import { useTranslation } from 'react-i18next'
+import {
+ Box,
+ COLORS,
+ OVERFLOW_HIDDEN,
+ POSITION_ABSOLUTE,
+ POSITION_RELATIVE,
+ StyledText,
+} from '@opentrons/components'
+import BOTTOM_LAYER from '../../assets/images/tip_top_bottom_layer.svg'
+import MID_LAYER from '../../assets/images/tip_top_mid_layer.svg'
+import TOP_LAYER from '../../assets/images/tip_top_top_layer.svg'
+
+const WELL_WIDTH_PIXELS = 70
+const PIXEL_DECIMALS = 2
+
+interface TipPositionAllVizProps {
+ xPosition: number
+ xWidthMm: number
+ yPosition: number
+ yWidthMm: number
+}
+
+export function TipPositionTopView(props: TipPositionAllVizProps): JSX.Element {
+ const { yPosition, xPosition, yWidthMm, xWidthMm } = props
+ const { t } = useTranslation('application')
+
+ const xPx = (WELL_WIDTH_PIXELS / xWidthMm) * xPosition
+ const yPx = (WELL_WIDTH_PIXELS / yWidthMm) * yPosition
+
+ const roundedXPx = round(xPx, PIXEL_DECIMALS)
+ const roundedYPx = round(yPx, PIXEL_DECIMALS)
+ const translateY = roundedYPx < 0 ? Math.abs(roundedYPx) : -roundedYPx
+ return (
+
+
+
+
+ {xWidthMm !== null && (
+
+
+ {xWidthMm}
+ {t('units.millimeter')}
+
+
+ )}
+
+ )
+}
diff --git a/protocol-designer/src/organisms/TipPositionModal/constants.ts b/protocol-designer/src/organisms/TipPositionModal/constants.ts
new file mode 100644
index 000000000000..528d9a0262e8
--- /dev/null
+++ b/protocol-designer/src/organisms/TipPositionModal/constants.ts
@@ -0,0 +1,5 @@
+export const DECIMALS_ALLOWED = 1
+export const SMALL_STEP_MM = 1
+export const LARGE_STEP_MM = 10
+export const TOO_MANY_DECIMALS: 'TOO_MANY_DECIMALS' = 'TOO_MANY_DECIMALS'
+export const PERCENT_RANGE_TO_SHOW_WARNING = 0.9
diff --git a/protocol-designer/src/organisms/TipPositionModal/index.tsx b/protocol-designer/src/organisms/TipPositionModal/index.tsx
new file mode 100644
index 000000000000..16656190e6f6
--- /dev/null
+++ b/protocol-designer/src/organisms/TipPositionModal/index.tsx
@@ -0,0 +1,360 @@
+import * as React from 'react'
+import { createPortal } from 'react-dom'
+import { useTranslation } from 'react-i18next'
+import {
+ DIRECTION_COLUMN,
+ Flex,
+ SPACING,
+ Modal,
+ JUSTIFY_SPACE_BETWEEN,
+ ALIGN_CENTER,
+ Btn,
+ JUSTIFY_END,
+ SecondaryButton,
+ PrimaryButton,
+ StyledText,
+ Banner,
+ InputField,
+ TYPOGRAPHY,
+} from '@opentrons/components'
+import { getMainPagePortalEl } from '../../components/portals/MainPageModalPortal'
+import { getIsTouchTipField } from '../../form-types'
+import { BUTTON_LINK_STYLE } from '../../atoms'
+import { TOO_MANY_DECIMALS, PERCENT_RANGE_TO_SHOW_WARNING } from './constants'
+import * as utils from './utils'
+import { TipPositionTopView } from './TipPositionTopView'
+import { TipPositionSideView } from './TipPositionSideView'
+
+import type { StepFieldName } from '../../form-types'
+
+type Offset = 'x' | 'y' | 'z'
+interface PositionSpec {
+ name: StepFieldName
+ value: number | null
+ updateValue: (val?: number | null) => void
+}
+export type PositionSpecs = Record
+
+interface TipPositionModalProps {
+ closeModal: () => void
+ specs: PositionSpecs
+ wellDepthMm: number
+ wellXWidthMm: number
+ wellYWidthMm: number
+ isIndeterminate?: boolean
+}
+
+export const TipPositionModal = (
+ props: TipPositionModalProps
+): JSX.Element | null => {
+ const {
+ isIndeterminate,
+ specs,
+ wellDepthMm,
+ wellXWidthMm,
+ wellYWidthMm,
+ closeModal,
+ } = props
+ const { t } = useTranslation([
+ 'modal',
+ 'button',
+ 'tooltip',
+ 'shared',
+ 'application',
+ ])
+ const [view, setView] = React.useState<'top' | 'side'>('side')
+ const zSpec = specs.z
+ const ySpec = specs.y
+ const xSpec = specs.x
+
+ if (zSpec == null || xSpec == null || ySpec == null) {
+ console.error(
+ 'expected to find specs for one of the positions but could not'
+ )
+ }
+
+ const defaultMmFromBottom = utils.getDefaultMmFromBottom({
+ name: zSpec.name,
+ wellDepthMm,
+ })
+
+ const [zValue, setZValue] = React.useState(
+ zSpec?.value == null ? String(defaultMmFromBottom) : String(zSpec?.value)
+ )
+ const [yValue, setYValue] = React.useState(
+ ySpec?.value == null ? null : String(ySpec?.value)
+ )
+ const [xValue, setXValue] = React.useState(
+ xSpec?.value == null ? null : String(xSpec?.value)
+ )
+
+ // in this modal, pristinity hides the OUT_OF_BOUNDS error only.
+ const [isPristine, setPristine] = React.useState(true)
+ const getMinMaxMmFromBottom = (): {
+ maxMmFromBottom: number
+ minMmFromBottom: number
+ } => {
+ if (getIsTouchTipField(zSpec?.name ?? '')) {
+ return {
+ maxMmFromBottom: utils.roundValue(wellDepthMm, 'up'),
+ minMmFromBottom: utils.roundValue(wellDepthMm / 2, 'up'),
+ }
+ }
+ return {
+ maxMmFromBottom: utils.roundValue(wellDepthMm, 'up'),
+ minMmFromBottom: 0,
+ }
+ }
+
+ const { maxMmFromBottom, minMmFromBottom } = getMinMaxMmFromBottom()
+ const { minValue: yMinWidth, maxValue: yMaxWidth } = utils.getMinMaxWidth(
+ wellYWidthMm
+ )
+ const { minValue: xMinWidth, maxValue: xMaxWidth } = utils.getMinMaxWidth(
+ wellXWidthMm
+ )
+
+ const createErrors = (
+ value: string | null,
+ min: number,
+ max: number
+ ): utils.Error[] => {
+ return utils.getErrors({ minMm: min, maxMm: max, value })
+ }
+ const zErrors = createErrors(zValue, minMmFromBottom, maxMmFromBottom)
+ const xErrors = createErrors(xValue, xMinWidth, xMaxWidth)
+ const yErrors = createErrors(yValue, yMinWidth, yMaxWidth)
+
+ const hasErrors =
+ zErrors.length > 0 || xErrors.length > 0 || yErrors.length > 0
+ const hasVisibleErrors = isPristine
+ ? zErrors.includes(TOO_MANY_DECIMALS) ||
+ xErrors.includes(TOO_MANY_DECIMALS) ||
+ yErrors.includes(TOO_MANY_DECIMALS)
+ : hasErrors
+
+ const createErrorText = (
+ errors: utils.Error[],
+ min: number,
+ max: number
+ ): string | null => {
+ return utils.getErrorText({ errors, minMm: min, maxMm: max, isPristine, t })
+ }
+
+ const roundedXMin = utils.roundValue(xMinWidth, 'up')
+ const roundedYMin = utils.roundValue(yMinWidth, 'up')
+ const roundedXMax = utils.roundValue(xMaxWidth, 'down')
+ const roundedYMax = utils.roundValue(yMaxWidth, 'down')
+
+ const zErrorText = createErrorText(zErrors, minMmFromBottom, maxMmFromBottom)
+ const xErrorText = createErrorText(xErrors, roundedXMin, roundedXMax)
+ const yErrorText = createErrorText(yErrors, roundedYMin, roundedYMax)
+
+ const handleDone = (): void => {
+ if (!hasErrors) {
+ zSpec?.updateValue(zValue === null ? null : Number(zValue))
+ xSpec?.updateValue(xValue === null ? null : Number(xValue))
+ ySpec?.updateValue(yValue === null ? null : Number(yValue))
+ closeModal()
+ }
+ }
+
+ const handleCancel = (): void => {
+ closeModal()
+ }
+
+ const handleZChange = (newValueRaw: string | number): void => {
+ // if string, strip non-number characters from string and cast to number
+ const newValue =
+ typeof newValueRaw === 'string'
+ ? newValueRaw.replace(/[^.0-9]/, '')
+ : String(newValueRaw)
+
+ if (newValue === '.') {
+ setZValue('0.')
+ } else {
+ setZValue(Number(newValue) >= 0 ? newValue : '0')
+ }
+ }
+
+ const handleZInputFieldChange = (
+ e: React.ChangeEvent
+ ): void => {
+ handleZChange(e.currentTarget.value)
+ setPristine(false)
+ }
+
+ const handleXChange = (newValueRaw: string | number): void => {
+ // if string, strip non-number characters from string and cast to number
+ const newValue =
+ typeof newValueRaw === 'string'
+ ? newValueRaw.replace(/[^-.0-9]/g, '')
+ : String(newValueRaw)
+
+ if (newValue === '.') {
+ setXValue('0.')
+ } else {
+ setXValue(newValue)
+ }
+ }
+
+ const handleXInputFieldChange = (
+ e: React.ChangeEvent
+ ): void => {
+ handleXChange(e.currentTarget.value)
+ setPristine(false)
+ }
+
+ const handleYChange = (newValueRaw: string | number): void => {
+ // if string, strip non-number characters from string and cast to number
+ const newValue =
+ typeof newValueRaw === 'string'
+ ? newValueRaw.replace(/[^-.0-9]/g, '')
+ : String(newValueRaw)
+
+ if (newValue === '.') {
+ setYValue('0.')
+ } else {
+ setYValue(newValue)
+ }
+ }
+
+ const handleYInputFieldChange = (
+ e: React.ChangeEvent
+ ): void => {
+ handleYChange(e.currentTarget.value)
+ setPristine(false)
+ }
+ const isXValueNearEdge =
+ xValue != null &&
+ (parseInt(xValue) > PERCENT_RANGE_TO_SHOW_WARNING * xMaxWidth ||
+ parseInt(xValue) < PERCENT_RANGE_TO_SHOW_WARNING * xMinWidth)
+ const isYValueNearEdge =
+ yValue != null &&
+ (parseInt(yValue) > PERCENT_RANGE_TO_SHOW_WARNING * yMaxWidth ||
+ parseInt(yValue) < PERCENT_RANGE_TO_SHOW_WARNING * yMinWidth)
+ const isZValueAtBottom = zValue != null && zValue === '0'
+
+ return createPortal(
+
+ {
+ setXValue('0')
+ setYValue('0')
+ setZValue('1')
+ }}
+ css={BUTTON_LINK_STYLE}
+ >
+ {t('shared:reset_to_default')}
+
+
+
+ {t('shared:cancel')}
+
+
+ {t('shared:save')}
+
+
+
+ }
+ >
+ {isXValueNearEdge || isYValueNearEdge || isZValueAtBottom ? (
+
+
+ {t('tip_position.warning')}
+
+
+ ) : null}
+
+
+
+ {t(`tip_position.body.${zSpec?.name}`)}
+
+
+
+
+
+
+
+
+ {view === 'side' ? 'Side view' : 'Top view'}
+
+ {
+ setView(view === 'side' ? 'top' : 'side')
+ }}
+ >
+ {t('shared:swap_view')}
+
+
+ {view === 'side' ? (
+
+ ) : (
+
+ )}
+
+
+ ,
+ getMainPagePortalEl()
+ )
+}
diff --git a/protocol-designer/src/organisms/TipPositionModal/utils.tsx b/protocol-designer/src/organisms/TipPositionModal/utils.tsx
new file mode 100644
index 000000000000..b5fbb377eea1
--- /dev/null
+++ b/protocol-designer/src/organisms/TipPositionModal/utils.tsx
@@ -0,0 +1,125 @@
+import floor from 'lodash/floor'
+import round from 'lodash/round'
+import { getIsTouchTipField } from '../../form-types'
+import {
+ DEFAULT_MM_FROM_BOTTOM_ASPIRATE,
+ DEFAULT_MM_FROM_BOTTOM_DISPENSE,
+ DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP,
+} from '../../constants'
+import { DECIMALS_ALLOWED, TOO_MANY_DECIMALS } from './constants'
+import type { StepFieldName } from '../../form-types'
+
+// TODO: Ian + Brian 2019-02-13 this should switch on stepType, not use field
+// name to infer step type!
+//
+// TODO(IL, 2021-03-10): after resolving #7470, use this util instead
+// of directly using these constants, wherever these constants are used. See also #7469
+export function getDefaultMmFromBottom(args: {
+ name: StepFieldName
+ wellDepthMm: number
+}): number {
+ const { name, wellDepthMm } = args
+
+ switch (name) {
+ case 'aspirate_mmFromBottom':
+ return DEFAULT_MM_FROM_BOTTOM_ASPIRATE
+
+ case 'aspirate_delay_mmFromBottom':
+ return DEFAULT_MM_FROM_BOTTOM_ASPIRATE
+
+ case 'dispense_mmFromBottom':
+ return DEFAULT_MM_FROM_BOTTOM_DISPENSE
+
+ case 'dispense_delay_mmFromBottom':
+ return DEFAULT_MM_FROM_BOTTOM_DISPENSE
+
+ case 'mix_mmFromBottom':
+ // TODO: Ian 2018-11-131 figure out what offset makes most sense for mix
+ return DEFAULT_MM_FROM_BOTTOM_DISPENSE
+
+ default:
+ // touch tip fields
+ console.assert(
+ getIsTouchTipField(name),
+ `getDefaultMmFromBottom fn does not know what to do with field ${name}`
+ )
+ return DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP + wellDepthMm
+ }
+}
+
+export const roundValue = (
+ value: number | string | null,
+ direction: 'up' | 'down'
+): number => {
+ if (value === null) return 0
+
+ switch (direction) {
+ case 'up': {
+ return round(Number(value), DECIMALS_ALLOWED)
+ }
+ case 'down': {
+ return floor(Number(value), DECIMALS_ALLOWED)
+ }
+ }
+}
+
+const OUT_OF_BOUNDS: 'OUT_OF_BOUNDS' = 'OUT_OF_BOUNDS'
+export type Error = typeof TOO_MANY_DECIMALS | typeof OUT_OF_BOUNDS
+
+export const getErrorText = (args: {
+ errors: Error[]
+ maxMm: number
+ minMm: number
+ isPristine: boolean
+ t: any
+}): string | null => {
+ const { errors, minMm, maxMm, isPristine, t } = args
+
+ if (errors.includes(TOO_MANY_DECIMALS)) {
+ return t('tip_position.errors.TOO_MANY_DECIMALS')
+ } else if (!isPristine && errors.includes(OUT_OF_BOUNDS)) {
+ return t('tip_position.errors.OUT_OF_BOUNDS', {
+ minMm,
+ maxMm,
+ })
+ } else {
+ return null
+ }
+}
+
+export const getErrors = (args: {
+ value: string | null
+ maxMm: number
+ minMm: number
+}): Error[] => {
+ const { value: rawValue, maxMm, minMm } = args
+ const errors: Error[] = []
+
+ const value = Number(rawValue)
+ if (rawValue === null || Number.isNaN(value)) {
+ // blank or otherwise invalid should show this error as a fallback
+ return [OUT_OF_BOUNDS]
+ }
+ const incorrectDecimals = round(value, DECIMALS_ALLOWED) !== value
+ const outOfBounds = value > maxMm || value < minMm
+console.log(outOfBounds)
+ if (incorrectDecimals) {
+ errors.push(TOO_MANY_DECIMALS)
+ }
+ if (outOfBounds) {
+ errors.push(OUT_OF_BOUNDS)
+ }
+ return errors
+}
+
+interface MinMaxValues {
+ minValue: number
+ maxValue: number
+}
+
+export const getMinMaxWidth = (width: number): MinMaxValues => {
+ return {
+ minValue: -width * 0.5,
+ maxValue: width * 0.5,
+ }
+}
diff --git a/protocol-designer/src/organisms/index.ts b/protocol-designer/src/organisms/index.ts
index cda71137c57e..f01b0f43cfe5 100644
--- a/protocol-designer/src/organisms/index.ts
+++ b/protocol-designer/src/organisms/index.ts
@@ -15,3 +15,4 @@ export * from './ProtocolMetadataNav'
export * from './SelectWellsModal'
export * from './SlotDetailsContainer'
export * from './SlotInformation'
+export * from './TipPositionModal'
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PositionField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PositionField.tsx
index 5023dca0a3f6..0708edb2170e 100644
--- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PositionField.tsx
+++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PositionField.tsx
@@ -13,18 +13,19 @@ import {
useHoverTooltip,
} from '@opentrons/components'
import { getWellsDepth, getWellDimension } from '@opentrons/shared-data'
+import { TipPositionModal } from '../../../../../organisms'
import { getIsDelayPositionField } from '../../../../../form-types'
+import { getDefaultMmFromBottom } from '../../../../../organisms/TipPositionModal/utils'
import { selectors as stepFormSelectors } from '../../../../../step-forms'
-import { TipPositionModal } from '../../../../../components/StepEditForm/fields/TipPositionField/TipPositionModal'
-import { getDefaultMmFromBottom } from '../../../../../components/StepEditForm/fields/TipPositionField/utils'
import { ZTipPositionModal } from '../../../../../components/StepEditForm/fields/TipPositionField/ZTipPositionModal'
import type {
TipXOffsetFields,
TipYOffsetFields,
TipZOffsetFields,
} from '../../../../../form-types'
+import type { PositionSpecs } from '../../../../../organisms'
+
import type { FieldPropsByName } from '../types'
-import type { PositionSpecs } from '../../../../../components/StepEditForm/fields/TipPositionField/TipPositionModal'
interface PositionFieldProps {
prefix: 'aspirate' | 'dispense' | 'mix'
propsForFields: FieldPropsByName
@@ -91,6 +92,7 @@ export function PositionField(props: PositionFieldProps): JSX.Element {
}
const isDelayPositionField = getIsDelayPositionField(zName)
let zValue: string | number = '0'
+
const mmFromBottom = typeof rawZValue === 'number' ? rawZValue : null
if (wellDepthMm !== null) {
// show default value for field in parens if no mmFromBottom value is selected