Repository: Opentrons/opentrons
Pull request #18102: feat(app, components) update select liquid class and update RadioButton components
Author: login: koji
Created: 2025-04-16T21:39:30Z | Merged: 2025-04-21T20:55:38Z

<!--
Thanks for taking the time to open a Pull Request (PR)! Please make sure you've read the "Opening Pull Requests" section of our Contributing Guide:

https://github.com/Opentrons/opentrons/blob/edge/CONTRIBUTING.md#opening-pull-requests

GitHub provides robust markdown to format your PR. Links, diagrams, pictures, and videos along with text formatting make it possible to create a rich and informative PR. For more information on GitHub markdown, see:

https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax

To ensure your code is reviewed quickly and thoroughly, please fill out the sections below to the best of your ability!
-->

# Overview
update select liquid class  
- add compatibility check function to align with PD
update RadioButton components to introduce `aria-disabled` to RadioButton for the new UI behavior

I will add more test cases to select liquid class screen and its util function.

the setup flow will be updated by a following pr

close AUTH-1006
<!--
Describe your PR at a high level. State acceptance criteria and how this PR fits into other work. Link issues, PRs, and other relevant resources.
-->

## Test Plan and Hands on Testing
ODD mode
1. run odd-mode and flex robot server
2. turn on liquid classes ff
3. create a new quick transfer
4. displays all setup flow's data in overview
<!--
Describe your testing of the PR. Emphasize testing not reflected in the code. Attach protocols, logs, screenshots and any other assets that support your testing.
-->

## Changelog

<!--
List changes introduced by this PR considering future developers and the end user. Give careful thought and clear documentation to breaking changes.
-->

## Review requests

<!--
- What do you need from reviewers to feel confident this PR is ready to merge?
- Ask questions.
-->

## Risk assessment
low
<!--
- Indicate the level of attention this PR needs.
- Provide context to guide reviewers.
- Discuss trade-offs, coupling, and side effects.
- Look for the possibility, even if you think it's small, that your change may affect some other part of the system.
  - For instance, changing return tip behavior may also change the behavior of labware calibration.
- How do your unit tests and on hands on testing mitigate this PR's risks and the risk of future regressions?
- Especially in high risk PRs, explain how you know your testing is enough.
-->



Diff:
diff --git a/app/src/assets/localization/en/quick_transfer.json b/app/src/assets/localization/en/quick_transfer.json
index 731a6df85c68..9346455d95b1 100644
--- a/app/src/assets/localization/en/quick_transfer.json
+++ b/app/src/assets/localization/en/quick_transfer.json
@@ -36,11 +36,13 @@
   "character_limit_error": "Character limit exceeded",
   "column": "column",
   "columns": "columns",
+  "compatibility_error": "The {{pipetteOrLabware}} is incompatible with this liquid class",
   "consolidate_volume_error": "The selected destination well is too small to consolidate into. Try consolidating from fewer wells.",
   "create_new_to_edit": "Create a new quick transfer to edit",
   "create_new_transfer": "Create new quick transfer",
   "create_to_get_started": "Create a new quick transfer to get started.",
   "create_transfer": "Create transfer",
+  "default": "Default",
   "delay_after_aspirating": "Delay after aspirating",
   "delay_before_dispensing": "Delay before dispensing",
   "delay_duration_s": "Delay duration (seconds)",
@@ -63,6 +65,7 @@
   "disposal_volume_µL": "Disposal volume (µL)",
   "distance_bottom_of_well_mm": "Distance from bottom of well (mm)",
   "distribute_volume_error": "The selected source well is too small to distribute from. Try distributing to fewer wells.",
+  "do_not_use_liquid_class": "Don't use liquid class settings",
   "enter_characters": "Enter up to 60 characters",
   "error_analyzing": "<block>An error occurred while attempting to analyze <bold>{{transferName}}.</bold></block>",
   "exit_quick_transfer": "Exit quick transfer?",
@@ -151,6 +154,8 @@
   "touch_tip": "Touch tip",
   "transfer_analysis_failed": "quick transfer analysis failed",
   "transfer_name": "Transfer Name",
+  "transfer_pipette_path_incompatible": "The selected pipette path is incompatible with this liquid class",
+  "transfer_volumes_incompatible": "Transfer volumes of 10 µL or less are incompatible with liquid classes",
   "trashBin_location": "Trash bin in {{slotName}}",
   "trashBin": "Trash bin",
   "tubeRack": "Tube racks",
diff --git a/app/src/organisms/ODD/QuickTransferFlow/Overview.tsx b/app/src/organisms/ODD/QuickTransferFlow/Overview.tsx
index 46d3262b731b..44fe6d6237dd 100644
--- a/app/src/organisms/ODD/QuickTransferFlow/Overview.tsx
+++ b/app/src/organisms/ODD/QuickTransferFlow/Overview.tsx
@@ -94,7 +94,7 @@ export function Overview(props: OverviewProps): JSX.Element | null {
         },
         {
           option: t('liquid_class'),
-          value: 'Dummy liquid class',
+          value: state.liquidClass.displayName,
         },
       ]
     : [
diff --git a/app/src/organisms/ODD/QuickTransferFlow/README.md b/app/src/organisms/ODD/QuickTransferFlow/README.md
index fdf7fd983d2f..5d1b524978f1 100644
--- a/app/src/organisms/ODD/QuickTransferFlow/README.md
+++ b/app/src/organisms/ODD/QuickTransferFlow/README.md
@@ -75,5 +75,53 @@ export interface QuickTransferWizardState {
   path?: PathOption // this has been added
   changeTip?: ChangeTipOptions // this has been added
   dropTipLocation?: CutoutConfig // this has been added
+  liquidClass?: LiquidClass // this has been added
+}
+```
+
+```ts
+export interface QuickTransferSummaryState {
+  pipette: PipetteV2Specs
+  mount: Mount
+  tipRack: LabwareDefinition2
+  source: LabwareDefinition2
+  sourceWells: string[]
+  destination: LabwareDefinition2 | 'source'
+  destinationWells: string[]
+  transferType: TransferType
+  volume: number
+  aspirateFlowRate: number
+  dispenseFlowRate: number
+  path: PathOption
+  tipPositionAspirate: number
+  preWetTip: boolean
+  mixOnAspirate?: {
+    mixVolume: number
+    repititions: number
+  }
+  delayAspirate?: {
+    delayDuration: number
+    positionFromBottom: number
+  }
+  touchTipAspirate?: number
+  touchTipAspirateSpeed?: number
+  airGapAspirate?: number
+  tipPositionDispense: number
+  mixOnDispense?: {
+    mixVolume: number
+    repititions: number
+  }
+  delayDispense?: {
+    delayDuration: number
+    positionFromBottom: number
+  }
+  touchTipDispense?: number
+  touchTipDispenseSpeed?: number
+  disposalVolume?: number
+  blowOut?: BlowOutLocation
+  airGapDispense?: number
+  changeTip: ChangeTipOptions
+  dropTipLocation: CutoutConfig
+  liquidClass: LiquidClass // this has been added
 }
 ```
diff --git a/app/src/organisms/ODD/QuickTransferFlow/SelectLiquidClass.tsx b/app/src/organisms/ODD/QuickTransferFlow/SelectLiquidClass.tsx
index a618abbcaaa6..89d6fa0a54ed 100644
--- a/app/src/organisms/ODD/QuickTransferFlow/SelectLiquidClass.tsx
+++ b/app/src/organisms/ODD/QuickTransferFlow/SelectLiquidClass.tsx
@@ -1,17 +1,20 @@
+import { useState } from 'react'
 import { useTranslation } from 'react-i18next'
 import type { ComponentProps, Dispatch } from 'react'
 
 import {
   DIRECTION_COLUMN,
   Flex,
-  //   RadioButton,
+  RadioButton,
   SPACING,
   StyledText,
-  //   StyledText,
-  //   TYPOGRAPHY,
 } from '@opentrons/components'
+import { getAllLiquidClassDefs } from '@opentrons/shared-data'
+import { useToaster } from '/app/organisms/ToasterOven'
 import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation'
+import { checkLiquidClassCompatibility } from './utils'
 
+import type { LiquidClass } from '@opentrons/shared-data'
 import type { SmallButton } from '/app/atoms/buttons'
 import type {
   QuickTransferWizardState,
@@ -33,12 +36,52 @@ export function SelectLiquidClass({
   dispatch,
 }: SelectLiquidClassProps): JSX.Element {
   const { i18n, t } = useTranslation(['quick_transfer', 'shared'])
+  const [selectedLiquidClass, setSelectedLiquidClass] = useState<LiquidClass>()
+  const { makeSnackbar } = useToaster()
 
+  const liquidClasses = getAllLiquidClassDefs()
+
+  const noLiquidClass: LiquidClass = {
+    byPipette: [],
+    description: t('default'),
+    displayName: t('do_not_use_liquid_class'),
+    liquidClassName: 'none',
+    namespace: 'opentrons',
+    schemaVersion: 1,
+  }
+
+  const liquidClassOptions = [noLiquidClass, ...Object.values(liquidClasses)]
   const handleClickNext = (): void => {
-    // dispatch to set pipette path
+    dispatch({
+      type: 'SET_LIQUID_CLASS',
+      liquidClass: selectedLiquidClass ?? noLiquidClass,
+    })
     onNext()
   }
 
+  const handleClick = (option: LiquidClass): void => {
+    const {
+      incompatible,
+      pipetteIncompatible,
+      tipRackIncompatible,
+      pipettePathIncompatible,
+      volumeIncompatible,
+    } = checkLiquidClassCompatibility(option, state)
+    if (incompatible) {
+      if (volumeIncompatible === true) {
+        makeSnackbar(t('transfer_volumes_incompatible') as string)
+      } else if (pipettePathIncompatible === true) {
+        makeSnackbar(t('transfer_pipette_path_incompatible') as string)
+      } else if (pipetteIncompatible === true || tipRackIncompatible === true) {
+        makeSnackbar(
+          t('compatibility_error', {
+            pipetteOrLabware: state.pipette?.displayName,
+          }) as string
+        )
+      }
+    }
+  }
+
   return (
     <Flex>
       <ChildNavigation
@@ -48,19 +91,39 @@ export function SelectLiquidClass({
         onClickButton={handleClickNext}
         secondaryButtonProps={exitButtonProps}
         top={SPACING.spacing8}
-        // if selected liquid class is null
-        // buttonIsDisabled={selectedPipette == null}
+        buttonIsDisabled={selectedLiquidClass == null}
       />
       <Flex
         marginTop={SPACING.spacing120}
         flexDirection={DIRECTION_COLUMN}
         padding={`${SPACING.spacing16} ${SPACING.spacing60} ${SPACING.spacing40} ${SPACING.spacing60}`}
         gridGap={SPACING.spacing4}
+        width="100%"
       >
         <StyledText oddStyle="level4HeaderRegular">
           {t('apply_predefined_settings')}
         </StyledText>
         {/* radio buttons */}
+        {liquidClassOptions.map(option => (
+          <RadioButton
+            key={option.liquidClassName}
+            isSelected={
+              selectedLiquidClass?.liquidClassName === option.liquidClassName
+            }
+            buttonLabel={option.displayName}
+            buttonValue={option.liquidClassName}
+            buttonSubLabel={{ label: option.description, align: 'vertical' }}
+            onChange={() => {
+              setSelectedLiquidClass(option)
+            }}
+            onClick={() => {
+              handleClick(option)
+            }}
+            ariaDisabled={
+              checkLiquidClassCompatibility(option, state).incompatible
+            }
+          />
+        ))}
       </Flex>
     </Flex>
   )
diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/Overview.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/Overview.test.tsx
index 3fc81d826236..d81ac0d6f2ad 100644
--- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/Overview.test.tsx
+++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/Overview.test.tsx
@@ -43,6 +43,14 @@ describe('Overview', () => {
         } as any,
         transferType: 'transfer',
         volume: 25,
+        liquidClass: {
+          liquidClassName: 'dummyLiquidClass',
+          displayName: 'Dummy liquid class',
+          description: 'Dummy liquid class description',
+          schemaVersion: 0,
+          namespace: '',
+          byPipette: [],
+        },
       } as any,
     }
     when(vi.mocked(useFeatureFlag))
diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectLiquidClass.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectLiquidClass.test.tsx
index 1eb0e5c34310..1b6752e445d9 100644
--- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectLiquidClass.test.tsx
+++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectLiquidClass.test.tsx
@@ -1,13 +1,150 @@
 import { describe, it, beforeEach, vi, expect } from 'vitest'
 import { fireEvent, screen } from '@testing-library/react'
 
+import { getAllLiquidClassDefs } from '@opentrons/shared-data'
+
 import { renderWithProviders } from '/app/__testing-utils__'
 import { i18n } from '/app/i18n'
-
+import { checkLiquidClassCompatibility } from '../utils'
+import { useToaster } from '/app/organisms/ToasterOven'
 import { SelectLiquidClass } from '../SelectLiquidClass'
 
 import type { ComponentProps } from 'react'
 
+vi.mock('../utils')
+vi.mock('/app/organisms/ToasterOven')
+vi.mock('@opentrons/shared-data', async importOriginal => {
+  const actual = await importOriginal<typeof getAllLiquidClassDefs>()
+  return {
+    ...actual,
+    getAllLiquidClassDefs: vi.fn(() => mockLiquidClasses),
+  }
+})
+const mockMakeSnackbar = vi.fn()
+
+const mockNoLiquidClass = {
+  byPipette: [],
+  description: 'Default',
+  displayName: "Don't use liquid class settings",
+  liquidClassName: 'none',
+  namespace: 'opentrons',
+  schemaVersion: 1,
+}
+
+const mockByPipette = [
+  {
+    tiprack: 'opentrons/opentrons_flex_96_tiprack_50ul/1',
+    aspirate: {},
+    singleDispense: {
+      submerge: {
+        positionReference: 'well-top',
+        offset: {
+          x: 0,
+          y: 0,
+          z: 2,
+        },
+        speed: 100,
+        delay: {
+          enable: false,
+          params: {
+            duration: 0,
+          },
+        },
+      },
+    },
+    multiDispense: {},
+  },
+]
+
+const mockLiquidClasses = {
+  ethanol: {
+    liquidClassName: 'mock ethanol',
+    displayName: 'Volatile',
+    description: '80% ethanol',
+    schemaVersion: 0,
+    namespace: '',
+    byPipette: mockByPipette,
+  },
+  glyeral: {
+    liquidClassName: 'mock glyeral',
+    displayName: 'Viscous',
+    description: '50% glycerol',
+    schemaVersion: 0,
+    namespace: '',
+    byPipette: mockByPipette,
+  },
+  water: {
+    displayName: 'Aqueous',
+    liquidClassName: 'mock water',
+    description: 'Deionized water',
+    schemaVersion: 0,
+    namespace: '',
+    byPipette: mockByPipette,
+  },
+} as any
+
+const mockState = {
+  pipette: {
+    displayName: 'Flex 1-Channel 50 µL',
+    model: 'p50',
+    displayCategory: 'FLEX',
+    channels: 1,
+    liquids: {
+      default: {
+        $otSharedSchema:
+          '#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json',
+        supportedTips: {},
+        maxVolume: 50,
+        minVolume: 5,
+        defaultTipracks: [
+          'opentrons/opentrons_flex_96_tiprack_50ul/1',
+          'opentrons/opentrons_flex_96_filtertiprack_50ul/1',
+        ],
+      },
+      lowVolumeDefault: {
+        $otSharedSchema:
+          '#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json',
+        supportedTips: {},
+        maxVolume: 30,
+        minVolume: 1,
+        defaultTipracks: [
+          'opentrons/opentrons_flex_96_tiprack_50ul/1',
+          'opentrons/opentrons_flex_96_filtertiprack_50ul/1',
+        ],
+      },
+    },
+  } as any,
+  mount: 'left',
+  tipRack: {
+    wells: {
+      A1: {
+        totalLiquidVolume: 200,
+      },
+    },
+    parameters: {
+      format: '96Standard',
+      quirks: [],
+      isTiprack: true,
+      tipLength: 57.9,
+      tipOverlap: 10.5,
+      isMagneticModuleCompatible: false,
+      loadName: 'opentrons_flex_96_tiprack_50ul',
+    },
+  } as any,
+  source: {},
+  sourceWells: ['A1'],
+  destination: 'source',
+  destinationWells: ['A1'],
+  transferType: 'transfer',
+  volume: 15,
+  path: 'single',
+  changeTip: 'once',
+  dropTipLocation: {
+    cutoutFixtureId: 'trashBinAdapter',
+    cutoutId: 'cutoutA3',
+  },
+} as any
+
 const render = (props: ComponentProps<typeof SelectLiquidClass>) => {
   return renderWithProviders(<SelectLiquidClass {...props} />, {
     i18nInstance: i18n,
@@ -26,9 +163,20 @@ describe('SelectLiquidClass', () => {
         buttonText: 'Exit',
         onClick: vi.fn(),
       },
-      state: {},
+      state: mockState,
       dispatch: vi.fn(),
     }
+    vi.mocked(checkLiquidClassCompatibility).mockReturnValue({
+      incompatible: false,
+      pipetteIncompatible: false,
+      tipRackIncompatible: false,
+      pipettePathIncompatible: false,
+    })
+    vi.mocked(useToaster).mockReturnValue({
+      makeSnackbar: mockMakeSnackbar,
+      makeToast: vi.fn(),
+      eatToast: vi.fn(),
+    })
   })
 
   it('renders text, exit button and continue button', () => {
@@ -39,6 +187,14 @@ describe('SelectLiquidClass', () => {
     )
     screen.getByText('Exit')
     screen.getByText('Continue')
+    screen.getByText("Don't use liquid class settings")
+    screen.getByText('Default')
+    screen.getByText('Aqueous')
+    screen.getByText('Deionized water')
+    screen.getByText('Viscous')
+    screen.getByText('50% glycerol')
+    screen.getByText('Volatile')
+    screen.getByText('80% ethanol')
   })
 
   it('should call mock function when tappin exit button', () => {
@@ -47,9 +203,70 @@ describe('SelectLiquidClass', () => {
     expect(props.exitButtonProps.onClick).toHaveBeenCalled()
   })
 
-  it('should call mock function when tappin continue button', () => {
+  it('should call mock function when tapping continue button - not using liquid class', () => {
     render(props)
+    fireEvent.click(screen.getByText("Don't use liquid class settings"))
     fireEvent.click(screen.getByText('Continue'))
     expect(props.onNext).toHaveBeenCalled()
+    expect(props.dispatch).toHaveBeenCalledWith({
+      type: 'SET_LIQUID_CLASS',
+      liquidClass: mockNoLiquidClass,
+    })
+  })
+
+  it('should call mock function when tapping continue button - water', () => {
+    vi.mocked(getAllLiquidClassDefs).mockReturnValue(mockLiquidClasses)
+    render(props)
+    fireEvent.click(screen.getByText('Aqueous'))
+    fireEvent.click(screen.getByText('Continue'))
+    expect(props.onNext).toHaveBeenCalled()
+    expect(props.dispatch).toHaveBeenCalledWith({
+      type: 'SET_LIQUID_CLASS',
+      liquidClass: mockLiquidClasses.water,
+    })
+  })
+
+  it('should call mock snackbar function when tapping liquid class button - pipette incompatible', () => {
+    props.state.pipette = {
+      displayName: 'mock pipette',
+    } as any
+    vi.mocked(checkLiquidClassCompatibility).mockReturnValue({
+      incompatible: true,
+      pipetteIncompatible: true,
+      tipRackIncompatible: true,
+      pipettePathIncompatible: false,
+    })
+    render(props)
+    fireEvent.click(screen.getByText('Aqueous'))
+    expect(mockMakeSnackbar).toHaveBeenCalledWith(
+      'The mock pipette is incompatible with this liquid class'
+    )
+  })
+
+  it('should call mock snackbar function when tapping liquid class button - pipette path incompatible', () => {
+    vi.mocked(checkLiquidClassCompatibility).mockReturnValue({
+      incompatible: true,
+      pipetteIncompatible: false,
+      tipRackIncompatible: false,
+      pipettePathIncompatible: true,
+    })
+    render(props)
+    fireEvent.click(screen.getByText('Aqueous'))
+    expect(mockMakeSnackbar).toHaveBeenCalledWith(
+      'The selected pipette path is incompatible with this liquid class'
+    )
+  })
+
+  it('should call mock function when tapping liquid class button - volume incompatible', () => {
+    vi.mocked(checkLiquidClassCompatibility).mockReturnValue({
+      incompatible: true,
+      volumeIncompatible: true,
+    })
+    render(props)
+    fireEvent.click(screen.getByText('Aqueous'))
+    fireEvent.click(screen.getByText('Continue'))
+    expect(mockMakeSnackbar).toHaveBeenCalledWith(
+      'Transfer volumes of 10 µL or less are incompatible with liquid classes'
+    )
   })
 })
diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/utils/checkLiquidClassCompatibility.test.ts b/app/src/organisms/ODD/QuickTransferFlow/__tests__/utils/checkLiquidClassCompatibility.test.ts
new file mode 100644
index 000000000000..a31bcdb30b23
--- /dev/null
+++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/utils/checkLiquidClassCompatibility.test.ts
@@ -0,0 +1,170 @@
+import { describe, it, expect } from 'vitest'
+
+import { checkLiquidClassCompatibility } from '../../utils'
+
+const mockNotUsedLiquidClass = {
+  byPipette: [],
+  description: 'Default',
+  displayName: "Don't use liquid class settings",
+  liquidClassName: 'none',
+  namespace: 'opentrons',
+  schemaVersion: 1,
+}
+
+const mockLiquid = {
+  displayName: 'A',
+  liquidClassName: '',
+  description: '',
+  schemaVersion: 0,
+  namespace: '',
+  byPipette: [
+    {
+      pipetteModel: 'flex_1channel_50',
+      byTipType: [
+        {
+          tiprack: 'opentrons/opentrons_flex_96_tiprack_50ul/1',
+          aspirate: {},
+          singleDispense: {
+            submerge: {
+              positionReference: 'well-top',
+              offset: {
+                x: 0,
+                y: 0,
+                z: 2,
+              },
+              speed: 100,
+              delay: {
+                enable: false,
+                params: {
+                  duration: 0,
+                },
+              },
+            },
+          },
+          multiDispense: {},
+        },
+      ],
+    },
+  ],
+} as any
+
+const mockState = {
+  pipette: {
+    displayName: 'Flex 1-Channel 50 µL',
+    model: 'p50',
+    displayCategory: 'FLEX',
+    channels: 1,
+    liquids: {
+      default: {
+        $otSharedSchema:
+          '#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json',
+        supportedTips: {},
+        maxVolume: 50,
+        minVolume: 5,
+        defaultTipracks: [
+          'opentrons/opentrons_flex_96_tiprack_50ul/1',
+          'opentrons/opentrons_flex_96_filtertiprack_50ul/1',
+        ],
+      },
+      lowVolumeDefault: {
+        $otSharedSchema:
+          '#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json',
+        supportedTips: {},
+        maxVolume: 30,
+        minVolume: 1,
+        defaultTipracks: [
+          'opentrons/opentrons_flex_96_tiprack_50ul/1',
+          'opentrons/opentrons_flex_96_filtertiprack_50ul/1',
+        ],
+      },
+    },
+  } as any,
+  mount: 'left',
+  tipRack: {
+    wells: {
+      A1: {
+        totalLiquidVolume: 200,
+      },
+    },
+    parameters: {
+      format: '96Standard',
+      quirks: [],
+      isTiprack: true,
+      tipLength: 57.9,
+      tipOverlap: 10.5,
+      isMagneticModuleCompatible: false,
+      loadName: 'opentrons_flex_96_tiprack_50ul',
+    },
+  } as any,
+  source: {},
+  sourceWells: ['A1'],
+  destination: 'source',
+  destinationWells: ['A1'],
+  transferType: 'transfer',
+  volume: 15,
+  path: 'single',
+  changeTip: 'once',
+  dropTipLocation: {
+    cutoutFixtureId: 'trashBinAdapter',
+    cutoutId: 'cutoutA3',
+  },
+} as any
+
+describe('checkLiquidClassCompatibility', () => {
+  it('no liquid class should return inCompatible false', () => {
+    const result = checkLiquidClassCompatibility(
+      mockNotUsedLiquidClass,
+      mockState
+    )
+    expect(result.incompatible).toBe(false)
+  })
+
+  it('liquid volume should be less than 10 incompatible', () => {
+    const invalidState = { ...mockState, volume: 10 }
+    console.log('invalidState', invalidState)
+    const result = checkLiquidClassCompatibility(mockLiquid, invalidState)
+    expect(result.incompatible).toBe(true)
+    expect(result.volumeIncompatible).toBe(true)
+  })
+
+  it('mock liquid should return incompatible true pipette is undefined', () => {
+    const invalidState = { ...mockState, pipette: undefined }
+    const result = checkLiquidClassCompatibility(mockLiquid, invalidState)
+    expect(result.incompatible).toBe(true)
+  })
+
+  it('mock liquid should return incompatible true tipRack is undefined', () => {
+    const invalidState = { ...mockState, tipRack: undefined }
+    const result = checkLiquidClassCompatibility(mockLiquid, invalidState)
+    expect(result.incompatible).toBe(true)
+  })
+
+  it('mock liquid should return incompatible true path is undefined', () => {
+    const invalidState = { ...mockState, path: undefined }
+    const result = checkLiquidClassCompatibility(mockLiquid, invalidState)
+    expect(result.incompatible).toBe(true)
+  })
+
+  it('mock liquid should return incompatible true volume is undefined', () => {
+    const invalidState = { ...mockState, volume: undefined }
+    const result = checkLiquidClassCompatibility(mockLiquid, invalidState)
+    expect(result.incompatible).toBe(true)
+  })
+
+  it('mock liquid should return incompatible false', () => {
+    const result = checkLiquidClassCompatibility(mockLiquid, mockState)
+    expect(result.incompatible).toBe(false)
+    expect(result.pipetteIncompatible).toBe(false)
+    expect(result.tipRackIncompatible).toBe(false)
+    expect(result.pipettePathIncompatible).toBe(false)
+  })
+
+  it('mock water should return incompatible true singleDispense is undefined', () => {
+    const invalidLiquid = { ...mockLiquid, byPipette: [] }
+    const result = checkLiquidClassCompatibility(invalidLiquid, mockState)
+    expect(result.incompatible).toBe(true)
+    expect(result.pipettePathIncompatible).toBe(true)
+  })
+
+  // TODO (kk:04/18/2025) add more test cases
+})
diff --git a/app/src/organisms/ODD/QuickTransferFlow/constants.ts b/app/src/organisms/ODD/QuickTransferFlow/constants.ts
index d21a4d5bb341..680447ca4ea4 100644
--- a/app/src/organisms/ODD/QuickTransferFlow/constants.ts
+++ b/app/src/organisms/ODD/QuickTransferFlow/constants.ts
@@ -24,6 +24,7 @@ export const ACTIONS = {
   SET_CHANGE_TIP: 'SET_CHANGE_TIP',
   SET_DROP_TIP_LOCATION: 'SET_DROP_TIP_LOCATION',
   SET_FLOW_RATE: 'SET_FLOW_RATE',
+  SET_LIQUID_CLASS: 'SET_LIQUID_CLASS',
 } as const
 
 export const DISTRIBUTE = 'distribute'
diff --git a/app/src/organisms/ODD/QuickTransferFlow/reducers.ts b/app/src/organisms/ODD/QuickTransferFlow/reducers.ts
index e4771b62e1f0..8d7bff875967 100644
--- a/app/src/organisms/ODD/QuickTransferFlow/reducers.ts
+++ b/app/src/organisms/ODD/QuickTransferFlow/reducers.ts
@@ -107,6 +107,12 @@ export function quickTransferWizardReducer(
         dropTipLocation: action.location,
       }
     }
+    case 'SET_LIQUID_CLASS': {
+      return {
+        ...state,
+        liquidClass: action.liquidClass,
+      }
+    }
   }
 }
 
diff --git a/app/src/organisms/ODD/QuickTransferFlow/types.ts b/app/src/organisms/ODD/QuickTransferFlow/types.ts
index 61b1d5b49635..052c5147506e 100644
--- a/app/src/organisms/ODD/QuickTransferFlow/types.ts
+++ b/app/src/organisms/ODD/QuickTransferFlow/types.ts
@@ -2,6 +2,7 @@ import type { Mount } from '@opentrons/api-client'
 import type {
   CutoutConfig,
   LabwareDefinition2,
+  LiquidClass,
   PipetteV2Specs,
 } from '@opentrons/shared-data'
 import type {
@@ -27,6 +28,7 @@ export interface QuickTransferWizardState {
   path?: PathOption
   changeTip?: ChangeTipOptions
   dropTipLocation?: CutoutConfig
+  liquidClass?: LiquidClass
 }
 export type PathOption = 'single' | 'multiAspirate' | 'multiDispense'
 export type ChangeTipOptions =
@@ -89,6 +91,7 @@ export interface QuickTransferSummaryState {
   airGapDispense?: number
   changeTip: ChangeTipOptions
   dropTipLocation: CutoutConfig
+  liquidClass: LiquidClass
 }
 
 export type TransferType =
@@ -107,6 +110,7 @@ export type QuickTransferWizardAction =
   | SetPipettePath
   | SetChangeTip
   | SetDropTipLocation
+  | SetLiquidClassAction
 
 export type QuickTransferSummaryAction =
   | SetAspirateFlowRateAction
@@ -203,6 +207,12 @@ interface SetDropTipLocation {
   type: typeof ACTIONS.SET_DROP_TIP_LOCATION
   location: CutoutConfig
 }
+
+interface SetLiquidClassAction {
+  type: typeof ACTIONS.SET_LIQUID_CLASS
+  liquidClass: LiquidClass
+}
+
 interface SelectPipetteAction {
   type: typeof ACTIONS.SELECT_PIPETTE
   mount: Mount
diff --git a/app/src/organisms/ODD/QuickTransferFlow/utils/checkLiquidClassCompatibility.ts b/app/src/organisms/ODD/QuickTransferFlow/utils/checkLiquidClassCompatibility.ts
new file mode 100644
index 000000000000..889829ac87a2
--- /dev/null
+++ b/app/src/organisms/ODD/QuickTransferFlow/utils/checkLiquidClassCompatibility.ts
@@ -0,0 +1,85 @@
+import { getFlexNameConversion } from '@opentrons/shared-data'
+
+import type { LiquidClass } from '@opentrons/shared-data'
+import type { QuickTransferWizardState } from '../types'
+
+const MINIMUM_LIQUID_CLASS_VOLUME = 10
+export interface Compatibility {
+  pipetteIncompatible?: boolean
+  tipRackIncompatible?: boolean
+  pipettePathIncompatible?: boolean
+  volumeIncompatible?: boolean
+  incompatible: boolean
+}
+
+const checkTipRackExist = (tipTypes: string[], target: string): boolean => {
+  return tipTypes.some(item => {
+    const parts = item.split('/')
+    return parts.length === 3 && parts[1] === target
+  })
+}
+
+export const checkLiquidClassCompatibility = (
+  liquid: LiquidClass,
+  state: QuickTransferWizardState
+): Compatibility => {
+  const { liquidClassName, byPipette } = liquid
+  if (liquidClassName === 'none') {
+    return { incompatible: false }
+  }
+
+  if (
+    state?.pipette === undefined ||
+    state?.tipRack === undefined ||
+    state.path === undefined ||
+    state.volume === undefined
+  ) {
+    return { incompatible: true }
+  }
+
+  if (state.volume <= MINIMUM_LIQUID_CLASS_VOLUME) {
+    return { incompatible: true, volumeIncompatible: true }
+  }
+
+  const pipetteModels = byPipette.map(pipette => pipette.pipetteModel)
+  const tipTypes = byPipette.flatMap(pipette =>
+    pipette.byTipType.map(tipType => tipType.tiprack)
+  )
+
+  const attachedPipetteModel: string = getFlexNameConversion(state?.pipette)
+  const isPipetteCompatible = pipetteModels.includes(attachedPipetteModel)
+  const isTipRackCompatible = checkTipRackExist(
+    tipTypes,
+    state.tipRack.parameters.loadName
+  )
+  let isPathCompatible = false
+  isPathCompatible = byPipette.some(pipette =>
+    pipette.byTipType.some(tipType => {
+      if (isTipRackCompatible) {
+        switch (state.path) {
+          case 'single':
+            // For 'single' path, check if 'singleDispense' property is defined
+            return tipType.singleDispense !== undefined
+          case 'multiDispense':
+            // For 'multiDispense' path, check if 'multiDispense' property is defined
+            return tipType.multiDispense !== undefined
+          default:
+            return true
+        }
+      }
+    })
+  )
+
+  const pipetteIncompatible = !isPipetteCompatible
+  const tipRackIncompatible = !isTipRackCompatible
+  const pipettePathIncompatible = !isPathCompatible
+  const incompatible =
+    pipetteIncompatible || tipRackIncompatible || pipettePathIncompatible
+
+  return {
+    pipetteIncompatible,
+    tipRackIncompatible,
+    pipettePathIncompatible,
+    incompatible,
+  }
+}
diff --git a/app/src/organisms/ODD/QuickTransferFlow/utils/getInitialSummaryState.ts b/app/src/organisms/ODD/QuickTransferFlow/utils/getInitialSummaryState.ts
index 4d22b0371b46..0365a2a06502 100644
--- a/app/src/organisms/ODD/QuickTransferFlow/utils/getInitialSummaryState.ts
+++ b/app/src/organisms/ODD/QuickTransferFlow/utils/getInitialSummaryState.ts
@@ -8,6 +8,7 @@ import type {
   LabwareDefinition2,
   PipetteV2Specs,
   DeckConfiguration,
+  LiquidClass,
 } from '@opentrons/shared-data'
 import type { Mount } from '@opentrons/api-client'
 import type {
@@ -29,6 +30,7 @@ interface InitialSummaryStateProps {
     transferType: TransferType
     volume: number
     path: PathOption
+    liquidClass: LiquidClass
   }
   deckConfig: DeckConfiguration
 }
@@ -100,5 +102,6 @@ export function getInitialSummaryState(
     tipPositionDispense: 1,
     changeTip,
     dropTipLocation: trashConfigCutout,
+    liquidClass: state.liquidClass,
   }
 }
diff --git a/app/src/organisms/ODD/QuickTransferFlow/utils/index.ts b/app/src/organisms/ODD/QuickTransferFlow/utils/index.ts
index a054d6c0398d..cf6a824b9d54 100644
--- a/app/src/organisms/ODD/QuickTransferFlow/utils/index.ts
+++ b/app/src/organisms/ODD/QuickTransferFlow/utils/index.ts
@@ -1,7 +1,8 @@
+export { checkLiquidClassCompatibility } from './checkLiquidClassCompatibility'
+export { createQuickTransferFile } from './createQuickTransferFile'
+export { generateCompatibleLabwareForPipette } from './generateCompatibleLabwareForPipette'
+export { generateQuickTransferArgs } from './generateQuickTransferArgs'
 export { getCompatibleLabwareByCategory } from './getCompatibleLabwareByCategory'
+export { getInitialSummaryState } from './getInitialSummaryState'
 export { getSelectedWellCount } from './getSelectedWellCount'
 export { getVolumeRange } from './getVolumeRange'
-export { generateCompatibleLabwareForPipette } from './generateCompatibleLabwareForPipette'
-export { getInitialSummaryState } from './getInitialSummaryState'
-export { generateQuickTransferArgs } from './generateQuickTransferArgs'
-export { createQuickTransferFile } from './createQuickTransferFile'
diff --git a/components/src/atoms/buttons/RadioButton.tsx b/components/src/atoms/buttons/RadioButton.tsx
index 7c4f1b7ea295..cfb2751afc8d 100644
--- a/components/src/atoms/buttons/RadioButton.tsx
+++ b/components/src/atoms/buttons/RadioButton.tsx
@@ -11,12 +11,12 @@ import {
   DIRECTION_COLUMN,
   DIRECTION_ROW,
   Icon,
-  Tag,
   JUSTIFY_SPACE_BETWEEN,
   StyledText,
+  Tag,
 } from '../..'
 
-import type { ChangeEventHandler, ReactNode } from 'react'
+import type { ChangeEventHandler, ReactNode, MouseEventHandler } from 'react'
 import type { FlattenSimpleInterpolation } from 'styled-components'
 import type { IconName } from '../../icons'
 import type { StyleProps } from '../../primitives'
@@ -29,24 +29,50 @@ export interface RadioButtonSubLabel {
 }
 
 interface RadioButtonProps extends StyleProps {
+  /** Radio button label */
   buttonLabel: string | ReactNode
+  /** Radio button value */
   buttonValue: string | number
+  /** Radio button onChange handler */
   onChange: ChangeEventHandler<HTMLInputElement>
+  /** Radio button disabled state. This will be replaced with aria-disabled in the future. */
   disabled?: boolean
+  /** Radio button icon name */
   iconName?: IconName
+  /** Radio button tag text */
   tagText?: string
+  /** Radio button isSelected state */
   isSelected?: boolean
+  /** Radio button largeDesktopBorderRadius state */
   largeDesktopBorderRadius?: boolean
+  /** Radio button radioButtonType state */
   radioButtonType?: 'large' | 'small'
+  /**
+   *  id is used for the special case in ODD where the screen has two radio buttons.
+   *  The screen that uses this prop is CSV runtime parameter CSV file selection screen.
+   *  Basically, this would not be used in desktop app/web app.
+   */
   id?: string
+  /** Radio button maxLines for label */
   maxLines?: number
-  //  used for mouseEnter and mouseLeave
+  /** used for mouseEnter and mouseLeave */
   setNoHover?: () => void
   setHovered?: () => void
   // TODO wire up the error state for the radio button
   error?: string | null
+  /** Radio button buttonSubLabel */
   buttonSubLabel?: RadioButtonSubLabel
+  /** testid is used for testing */
   testid?: string
+  /**
+   * with the changes in UX and accessibility support, 'disabled' will eventually be replaced with 'aria-disabled' in the future.
+   */
+  ariaDisabled?: boolean
+  /**
+   *  onClick is needed for RadioButton since onChange requires actual selected value change.
+   *  For this case, selected value change shouldn't be happened.
+   */
+  onClick?: MouseEventHandler
 }
 
 // used for ODD and helix
@@ -69,59 +95,30 @@ export function RadioButton(props: RadioButtonProps): JSX.Element {
     setHovered,
     setNoHover,
     testid,
+    ariaDisabled = false,
+    onClick,
   } = props
   const isLarge = radioButtonType === 'large'
 
-  const AVAILABLE_BUTTON_STYLE = css`
-    background: ${COLORS.blue35};
-
-    &:hover,
-    &:active {
-      background-color: ${disabled ? COLORS.grey35 : COLORS.blue40};
-    }
-  `
-
-  const SELECTED_BUTTON_STYLE = css`
-    background: ${COLORS.blue50};
-    color: ${COLORS.white};
-
-    &:active {
-      background-color: ${disabled ? COLORS.grey35 : COLORS.blue60};
-    }
-  `
-  //  TODO: the max line to clamp for subtext
-  const SUBBUTTON_LABEL_STYLE = css`
-    color: ${disabled
-      ? COLORS.grey50
-      : isSelected
-      ? COLORS.white
-      : COLORS.grey60};
-    display: -webkit-box;
-    -webkit-box-orient: vertical;
-    -webkit-line-clamp: ${buttonSubLabel?.align === 'vertical' ? 2 : 1};
-    overflow: hidden;
-    word-break: break-all;
-    text-overflow: ellipsis;
-  `
-
   const getButtonStyle = (
     isSelected: boolean,
     disabled: boolean
   ): FlattenSimpleInterpolation => {
     if (disabled) return DISABLED_BUTTON_STYLE
-    if (isSelected) return SELECTED_BUTTON_STYLE
-    return AVAILABLE_BUTTON_STYLE
+    if (isSelected) return SELECTED_BUTTON_STYLE(disabled)
+    return AVAILABLE_BUTTON_STYLE(disabled)
   }
 
   return (
     <RadioButtonWrapper>
       <SettingButton
         checked={isSelected}
-        disabled={disabled}
+        disabled={ariaDisabled ? false : disabled}
         id={id}
-        onChange={onChange}
+        onChange={ariaDisabled ? () => {} : onChange}
         type="radio"
         value={buttonValue}
+        onClick={onClick}
       />
       <SettingButtonLabel
         data-testid={testid}
@@ -137,6 +134,7 @@ export function RadioButton(props: RadioButtonProps): JSX.Element {
         onMouseLeave={setNoHover}
         css={getButtonStyle(isSelected, disabled)}
         aria-selected={isSelected}
+        aria-disabled={ariaDisabled}
       >
         <Flex
           flexDirection={DIRECTION_ROW}
@@ -167,7 +165,13 @@ export function RadioButton(props: RadioButtonProps): JSX.Element {
               buttonLabel
             )}
             {buttonSubLabel && (
-              <Flex css={SUBBUTTON_LABEL_STYLE}>
+              <Flex
+                css={SUBBUTTON_LABEL_STYLE(
+                  disabled,
+                  isSelected,
+                  buttonSubLabel
+                )}
+              >
                 <StyledText
                   color={isSelected ? COLORS.white : COLORS.grey60}
                   oddStyle="bodyTextRegular"
@@ -198,6 +202,47 @@ const copyContainerStyle = (
   word-break: break-word;
 `
 
+const AVAILABLE_BUTTON_STYLE = (
+  disabled: boolean
+): FlattenSimpleInterpolation => css`
+  background: ${COLORS.blue35};
+
+  &:hover,
+  &:active {
+    background-color: ${disabled ? COLORS.grey35 : COLORS.blue40};
+  }
+`
+
+const SELECTED_BUTTON_STYLE = (
+  disabled: boolean
+): FlattenSimpleInterpolation => css`
+  background: ${COLORS.blue50};
+  color: ${COLORS.white};
+
+  &:active {
+    background-color: ${disabled ? COLORS.grey35 : COLORS.blue60};
+  }
+`
+
+//  TODO: the max line to clamp for subtext
+const SUBBUTTON_LABEL_STYLE = (
+  disabled: boolean,
+  isSelected: boolean,
+  buttonSubLabel: RadioButtonSubLabel
+): FlattenSimpleInterpolation => css`
+  color: ${disabled
+    ? COLORS.grey50
+    : isSelected
+    ? COLORS.white
+    : COLORS.grey60};
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: ${buttonSubLabel?.align === 'vertical' ? 2 : 1};
+  overflow: hidden;
+  word-break: break-all;
+  text-overflow: ellipsis;
+`
+
 const DISABLED_BUTTON_STYLE = css`
   background-color: ${COLORS.grey35};
   color: ${COLORS.grey50};
@@ -256,6 +301,16 @@ const SettingButtonLabel = styled.label<SettingsButtonLabelProps>`
     outline-offset: 0.12rem;
   }
 
+  &[aria-disabled='true'] {
+    background-color: ${COLORS.grey35};
+    color: ${COLORS.grey50};
+
+    &:hover,
+    &:active {
+      background-color: ${COLORS.grey35};
+    }
+  }
+
   @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
     cursor: ${CURSOR_DEFAULT};
     padding: ${({ largeDesktopBorderRadius }) =>
diff --git a/components/src/atoms/buttons/__tests__/RadioButton.test.tsx b/components/src/atoms/buttons/__tests__/RadioButton.test.tsx
index fbbf0415ecea..5e22e409abf7 100644
--- a/components/src/atoms/buttons/__tests__/RadioButton.test.tsx
+++ b/components/src/atoms/buttons/__tests__/RadioButton.test.tsx
@@ -102,4 +102,16 @@ describe('RadioButton', () => {
 
     render(props)
   })
+
+  it('onClick is called when ariaDisabled is true', () => {
+    props = {
+      ...props,
+      ariaDisabled: true,
+      onClick: vi.fn(),
+    }
+    render(props)
+    const label = screen.getByRole('label')
+    label.click()
+    expect(props.onClick).toHaveBeenCalled()
+  })
 })


Reviews:
user: login: ncdiehl11
body: looks good but left some comments 
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T13:33:47Z
user: login: koji
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T14:27:06Z
user: login: koji
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T18:33:49Z
user: login: ncdiehl11
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T19:00:25Z
user: login: ncdiehl11
body: thank you for the fixes. tested on dev ODD and looks good
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-21T19:02:23Z

Review comments:
diff_hunk: @@ -33,12 +36,52 @@ export function SelectLiquidClass({
   dispatch,
 }: SelectLiquidClassProps): JSX.Element {
   const { i18n, t } = useTranslation(['quick_transfer', 'shared'])
+  const [selectedLiquidClass, setSelectedLiquidClass] = useState<LiquidClass>()
+  const { makeSnackbar } = useToaster()
 
+  const liquidClasses = getAllLiquidClassDefs()
+
+  const noLiquidClass: LiquidClass = {
+    byPipette: [],
+    description: t('default'),
+    displayName: t('do_not_use_liquid_class'),
+    liquidClassName: 'none',
+    namespace: 'opentrons',
+    schemaVersion: 1,
+  }
+
+  const liquidClassOptions = [noLiquidClass, ...Object.values(liquidClasses)]
   const handleClickNext = (): void => {
-    // dispatch to set pipette path
+    dispatch({
+      type: 'SET_LIQUID_CLASS',
+      liquidClass: selectedLiquidClass ?? noLiquidClass,
path: app/src/organisms/ODD/QuickTransferFlow/SelectLiquidClass.tsx
user: login: ncdiehl11
body: Do we need this null check? Shouldn't `selectedLiquidClass` always be of type `LiquidClass`?
created_at: 2025-04-21T13:15:04Z
author_association: COLLABORATOR
diff_hunk: @@ -78,6 +78,11 @@ export function SummaryAndSettings(
     initialSummaryState
   )
 
+  console.log('state', state)
path: app/src/organisms/ODD/QuickTransferFlow/SummaryAndSettings.tsx
user: login: ncdiehl11
body: ```suggestion
```
created_at: 2025-04-21T13:16:45Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,84 @@
+import { getFlexNameConversion } from '@opentrons/shared-data'
+
+import type { LiquidClass } from '@opentrons/shared-data'
+import type { QuickTransferWizardState } from '../types'
+
+export interface Compatibility {
+  pipetteInCompatible?: boolean
+  tipRackICompatible?: boolean
+  pipettePathInCompatible?: boolean
+  volumeInCompatible?: boolean
+  inCompatible: boolean
path: app/src/organisms/ODD/QuickTransferFlow/utils/checkLiquidClassCompatibility.ts
user: login: ncdiehl11
body: nit: use `incompatible` lowercase for all 
created_at: 2025-04-21T13:17:50Z
author_association: COLLABORATOR
diff_hunk: @@ -78,6 +78,11 @@ export function SummaryAndSettings(
     initialSummaryState
   )
 
+  console.log('state', state)
+  // console.log('wizardFlowState', wizardFlowState)
+  console.log('initialSummaryState', initialSummaryState)
+  console.log('liquidClass', state.liquidClass)
path: app/src/organisms/ODD/QuickTransferFlow/SummaryAndSettings.tsx
user: login: ncdiehl11
body: ```suggestion
```
created_at: 2025-04-21T13:18:25Z
author_association: COLLABORATOR
diff_hunk: @@ -107,6 +107,12 @@ export function quickTransferWizardReducer(
         dropTipLocation: action.location,
       }
     }
+    case 'SET_LIQUID_CLASS': {
+      return {
+        ...state,
+        liquidClass: action.liquidClass,
+      }
+    }
path: app/src/organisms/ODD/QuickTransferFlow/reducers.ts
user: login: ncdiehl11
body: 👍 

created_at: 2025-04-21T13:18:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,84 @@
+import { getFlexNameConversion } from '@opentrons/shared-data'
+
+import type { LiquidClass } from '@opentrons/shared-data'
+import type { QuickTransferWizardState } from '../types'
+
+export interface Compatibility {
+  pipetteInCompatible?: boolean
+  tipRackICompatible?: boolean
+  pipettePathInCompatible?: boolean
+  volumeInCompatible?: boolean
+  inCompatible: boolean
+}
+
+const checkTipRackExist = (tipTypes: string[], target: string): boolean => {
+  return tipTypes.some(item => {
path: app/src/organisms/ODD/QuickTransferFlow/utils/checkLiquidClassCompatibility.ts
user: login: ncdiehl11
body: might `getTiprackVolume` be useful here?
created_at: 2025-04-21T13:22:13Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,84 @@
+import { getFlexNameConversion } from '@opentrons/shared-data'
+
+import type { LiquidClass } from '@opentrons/shared-data'
+import type { QuickTransferWizardState } from '../types'
+
+export interface Compatibility {
+  pipetteInCompatible?: boolean
+  tipRackICompatible?: boolean
+  pipettePathInCompatible?: boolean
+  volumeInCompatible?: boolean
+  inCompatible: boolean
+}
+
+const checkTipRackExist = (tipTypes: string[], target: string): boolean => {
+  return tipTypes.some(item => {
+    const parts = item.split('/')
+    return parts.length === 3 && parts[1] === target
+  })
+}
+
+export const checkLiquidClassCompatibility = (
+  liquid: LiquidClass,
+  state: QuickTransferWizardState
+): Compatibility => {
+  const { liquidClassName, byPipette } = liquid
+  if (liquidClassName === 'none') {
+    return { inCompatible: false }
+  }
+
+  if (
+    state?.pipette === undefined ||
+    state?.tipRack === undefined ||
+    state.path === undefined ||
+    state.volume === undefined
+  ) {
+    return { inCompatible: true }
+  }
+
+  if (state.volume <= 10) {
path: app/src/organisms/ODD/QuickTransferFlow/utils/checkLiquidClassCompatibility.ts
user: login: ncdiehl11
body: maybe refactor `10` to a constant?
created_at: 2025-04-21T13:23:15Z
author_association: COLLABORATOR
path: app/src/organisms/ODD/QuickTransferFlow/utils/index.ts
user: login: ncdiehl11
body: 🙏 
created_at: 2025-04-21T13:31:57Z
author_association: COLLABORATOR
path: components/src/atoms/buttons/RadioButton.tsx
user: login: ncdiehl11
body: nice
created_at: 2025-04-21T13:33:15Z
author_association: COLLABORATOR
diff_hunk: @@ -33,12 +36,52 @@ export function SelectLiquidClass({
   dispatch,
 }: SelectLiquidClassProps): JSX.Element {
   const { i18n, t } = useTranslation(['quick_transfer', 'shared'])
+  const [selectedLiquidClass, setSelectedLiquidClass] = useState<LiquidClass>()
+  const { makeSnackbar } = useToaster()
 
+  const liquidClasses = getAllLiquidClassDefs()
+
+  const noLiquidClass: LiquidClass = {
+    byPipette: [],
+    description: t('default'),
+    displayName: t('do_not_use_liquid_class'),
+    liquidClassName: 'none',
+    namespace: 'opentrons',
+    schemaVersion: 1,
+  }
+
+  const liquidClassOptions = [noLiquidClass, ...Object.values(liquidClasses)]
   const handleClickNext = (): void => {
-    // dispatch to set pipette path
+    dispatch({
+      type: 'SET_LIQUID_CLASS',
+      liquidClass: selectedLiquidClass ?? noLiquidClass,
path: app/src/organisms/ODD/QuickTransferFlow/SelectLiquidClass.tsx
user: login: koji
body: unfortunately, yes
 liquidClass is optional and all items in QuickTransferWizard are optional.

created_at: 2025-04-21T14:27:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,84 @@
+import { getFlexNameConversion } from '@opentrons/shared-data'
+
+import type { LiquidClass } from '@opentrons/shared-data'
+import type { QuickTransferWizardState } from '../types'
+
+export interface Compatibility {
+  pipetteInCompatible?: boolean
+  tipRackICompatible?: boolean
+  pipettePathInCompatible?: boolean
+  volumeInCompatible?: boolean
+  inCompatible: boolean
+}
+
+const checkTipRackExist = (tipTypes: string[], target: string): boolean => {
+  return tipTypes.some(item => {
path: app/src/organisms/ODD/QuickTransferFlow/utils/checkLiquidClassCompatibility.ts
user: login: koji
body: you mean use `getTiprackVolume` instead of 10?
created_at: 2025-04-21T18:33:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,84 @@
+import { getFlexNameConversion } from '@opentrons/shared-data'
+
+import type { LiquidClass } from '@opentrons/shared-data'
+import type { QuickTransferWizardState } from '../types'
+
+export interface Compatibility {
+  pipetteInCompatible?: boolean
+  tipRackICompatible?: boolean
+  pipettePathInCompatible?: boolean
+  volumeInCompatible?: boolean
+  inCompatible: boolean
+}
+
+const checkTipRackExist = (tipTypes: string[], target: string): boolean => {
+  return tipTypes.some(item => {
path: app/src/organisms/ODD/QuickTransferFlow/utils/checkLiquidClassCompatibility.ts
user: login: ncdiehl11
body: Sorry, I misunderstood what you are checking here— I thought you were checking volumes, but you're checking the URI itself. I think this is good!
created_at: 2025-04-21T19:00:22Z
author_association: COLLABORATOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-16T21:41:51Z
body: ## [Codecov](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) Report
Attention: Patch coverage is `21.51163%` with `135 lines` in your changes missing coverage. Please review.
> Project coverage is 27.49%. Comparing base [(`21aa9b0`)](https://app.codecov.io/gh/Opentrons/opentrons/commit/21aa9b00c60b0aa891e586650b60cbbc1bdf92c2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) to head [(`56624f4`)](https://app.codecov.io/gh/Opentrons/opentrons/commit/56624f4063bc3660aafb3e59d15707e27a0dd942?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons).
> Report is 8 commits behind head on edge.

| [Files with missing lines](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) | Patch % | Lines |
|---|---|---|
| [...ransferFlow/utils/checkLiquidClassCompatibility.ts](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&filepath=app%2Fsrc%2Forganisms%2FODD%2FQuickTransferFlow%2Futils%2FcheckLiquidClassCompatibility.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#diff-YXBwL3NyYy9vcmdhbmlzbXMvT0REL1F1aWNrVHJhbnNmZXJGbG93L3V0aWxzL2NoZWNrTGlxdWlkQ2xhc3NDb21wYXRpYmlsaXR5LnRz) | 0.00% | [64 Missing :warning: ](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) |
| [...anisms/ODD/QuickTransferFlow/SelectLiquidClass.tsx](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&filepath=app%2Fsrc%2Forganisms%2FODD%2FQuickTransferFlow%2FSelectLiquidClass.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#diff-YXBwL3NyYy9vcmdhbmlzbXMvT0REL1F1aWNrVHJhbnNmZXJGbG93L1NlbGVjdExpcXVpZENsYXNzLnRzeA==) | 0.00% | [59 Missing :warning: ](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) |
| [...pp/src/organisms/ODD/QuickTransferFlow/reducers.ts](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&filepath=app%2Fsrc%2Forganisms%2FODD%2FQuickTransferFlow%2Freducers.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#diff-YXBwL3NyYy9vcmdhbmlzbXMvT0REL1F1aWNrVHJhbnNmZXJGbG93L3JlZHVjZXJzLnRz) | 0.00% | [6 Missing :warning: ](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) |
| [components/src/atoms/buttons/RadioButton.tsx](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&filepath=components%2Fsrc%2Fatoms%2Fbuttons%2FRadioButton.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#diff-Y29tcG9uZW50cy9zcmMvYXRvbXMvYnV0dG9ucy9SYWRpb0J1dHRvbi50c3g=) | 94.87% | [2 Missing :warning: ](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) |
| [...p/src/organisms/ODD/QuickTransferFlow/Overview.tsx](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&filepath=app%2Fsrc%2Forganisms%2FODD%2FQuickTransferFlow%2FOverview.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#diff-YXBwL3NyYy9vcmdhbmlzbXMvT0REL1F1aWNrVHJhbnNmZXJGbG93L092ZXJ2aWV3LnRzeA==) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) |
| [...p/src/organisms/ODD/QuickTransferFlow/constants.ts](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&filepath=app%2Fsrc%2Forganisms%2FODD%2FQuickTransferFlow%2Fconstants.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#diff-YXBwL3NyYy9vcmdhbmlzbXMvT0REL1F1aWNrVHJhbnNmZXJGbG93L2NvbnN0YW50cy50cw==) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) |
| [.../QuickTransferFlow/utils/getInitialSummaryState.ts](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&filepath=app%2Fsrc%2Forganisms%2FODD%2FQuickTransferFlow%2Futils%2FgetInitialSummaryState.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#diff-YXBwL3NyYy9vcmdhbmlzbXMvT0REL1F1aWNrVHJhbnNmZXJGbG93L3V0aWxzL2dldEluaXRpYWxTdW1tYXJ5U3RhdGUudHM=) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) |
| [...src/organisms/ODD/QuickTransferFlow/utils/index.ts](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&filepath=app%2Fsrc%2Forganisms%2FODD%2FQuickTransferFlow%2Futils%2Findex.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#diff-YXBwL3NyYy9vcmdhbmlzbXMvT0REL1F1aWNrVHJhbnNmZXJGbG93L3V0aWxzL2luZGV4LnRz) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) |

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102/graphs/tree.svg?width=650&height=150&src=pr&token=DIJPWI1JiU&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons)](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons)

```diff
@@            Coverage Diff             @@
##             edge   #18102      +/-   ##
==========================================
+ Coverage   25.61%   27.49%   +1.87%     
==========================================
  Files        3015     2977      -38     
  Lines      234489   230592    -3897     
  Branches    20151    20327     +176     
==========================================
+ Hits        60057    63394    +3337     
+ Misses     174417   167174    -7243     
- Partials       15       24       +9     
```

| [Flag](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) | Coverage Δ | |
|---|---|---|
| [protocol-designer](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) | `19.08% <21.51%> (+0.16%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files with missing lines](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) | Coverage Δ | |
|---|---|---|
| [app/src/organisms/ODD/QuickTransferFlow/types.ts](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&filepath=app%2Fsrc%2Forganisms%2FODD%2FQuickTransferFlow%2Ftypes.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#diff-YXBwL3NyYy9vcmdhbmlzbXMvT0REL1F1aWNrVHJhbnNmZXJGbG93L3R5cGVzLnRz) | `100.00% <ø> (ø)` | |
| [...p/src/organisms/ODD/QuickTransferFlow/Overview.tsx](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&filepath=app%2Fsrc%2Forganisms%2FODD%2FQuickTransferFlow%2FOverview.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#diff-YXBwL3NyYy9vcmdhbmlzbXMvT0REL1F1aWNrVHJhbnNmZXJGbG93L092ZXJ2aWV3LnRzeA==) | `0.00% <0.00%> (ø)` | |
| [...p/src/organisms/ODD/QuickTransferFlow/constants.ts](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&filepath=app%2Fsrc%2Forganisms%2FODD%2FQuickTransferFlow%2Fconstants.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#diff-YXBwL3NyYy9vcmdhbmlzbXMvT0REL1F1aWNrVHJhbnNmZXJGbG93L2NvbnN0YW50cy50cw==) | `0.00% <0.00%> (ø)` | |
| [.../QuickTransferFlow/utils/getInitialSummaryState.ts](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&filepath=app%2Fsrc%2Forganisms%2FODD%2FQuickTransferFlow%2Futils%2FgetInitialSummaryState.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#diff-YXBwL3NyYy9vcmdhbmlzbXMvT0REL1F1aWNrVHJhbnNmZXJGbG93L3V0aWxzL2dldEluaXRpYWxTdW1tYXJ5U3RhdGUudHM=) | `0.00% <0.00%> (ø)` | |
| [...src/organisms/ODD/QuickTransferFlow/utils/index.ts](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&filepath=app%2Fsrc%2Forganisms%2FODD%2FQuickTransferFlow%2Futils%2Findex.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#diff-YXBwL3NyYy9vcmdhbmlzbXMvT0REL1F1aWNrVHJhbnNmZXJGbG93L3V0aWxzL2luZGV4LnRz) | `0.00% <0.00%> (ø)` | |
| [components/src/atoms/buttons/RadioButton.tsx](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&filepath=components%2Fsrc%2Fatoms%2Fbuttons%2FRadioButton.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#diff-Y29tcG9uZW50cy9zcmMvYXRvbXMvYnV0dG9ucy9SYWRpb0J1dHRvbi50c3g=) | `98.17% <94.87%> (-0.45%)` | :arrow_down: |
| [...pp/src/organisms/ODD/QuickTransferFlow/reducers.ts](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&filepath=app%2Fsrc%2Forganisms%2FODD%2FQuickTransferFlow%2Freducers.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#diff-YXBwL3NyYy9vcmdhbmlzbXMvT0REL1F1aWNrVHJhbnNmZXJGbG93L3JlZHVjZXJzLnRz) | `0.00% <0.00%> (ø)` | |
| [...anisms/ODD/QuickTransferFlow/SelectLiquidClass.tsx](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&filepath=app%2Fsrc%2Forganisms%2FODD%2FQuickTransferFlow%2FSelectLiquidClass.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#diff-YXBwL3NyYy9vcmdhbmlzbXMvT0REL1F1aWNrVHJhbnNmZXJGbG93L1NlbGVjdExpcXVpZENsYXNzLnRzeA==) | `0.00% <0.00%> (ø)` | |
| [...ransferFlow/utils/checkLiquidClassCompatibility.ts](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102?src=pr&el=tree&filepath=app%2Fsrc%2Forganisms%2FODD%2FQuickTransferFlow%2Futils%2FcheckLiquidClassCompatibility.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#diff-YXBwL3NyYy9vcmdhbmlzbXMvT0REL1F1aWNrVHJhbnNmZXJGbG93L3V0aWxzL2NoZWNrTGlxdWlkQ2xhc3NDb21wYXRpYmlsaXR5LnRz) | `0.00% <0.00%> (ø)` | |

... and [140 files with indirect coverage changes](https://app.codecov.io/gh/Opentrons/opentrons/pull/18102/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons)

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: koji
created_at: 2025-04-17T04:37:32Z
body: - [x] move compatible check function to utils
- [x] add tests - utils
- [x] add test - SelectLiquidClass
author_association: CONTRIBUTOR