diff --git a/app/src/assets/localization/en/labware_position_check.json b/app/src/assets/localization/en/labware_position_check.json
index 85da36f38f8..b466fd7f344 100644
--- a/app/src/assets/localization/en/labware_position_check.json
+++ b/app/src/assets/localization/en/labware_position_check.json
@@ -47,6 +47,7 @@
"default_labware_offset": "Default Labware Offset",
"default_location_offset_added": "Default location offset added",
"default_location_offset_adjusted": "Default location offset adjusted",
+ "default_offset_description": "The default offset is used for all placements of the labware unless a manual adjustment is made to specific slot location.",
"detach_probe": "Remove calibration probe",
"ensure_nozzle_position_desktop": "Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, use the controls below or your keyboard to jog the pipette until it is properly aligned.",
"ensure_nozzle_position_odd": "Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, tap Move pipette and then jog the pipette until it is properly aligned.",
diff --git a/app/src/molecules/MultiDeckLabelTagBtns/index.tsx b/app/src/molecules/MultiDeckLabelTagBtns/index.tsx
index 46e04d22423..8f405f2a3e4 100644
--- a/app/src/molecules/MultiDeckLabelTagBtns/index.tsx
+++ b/app/src/molecules/MultiDeckLabelTagBtns/index.tsx
@@ -4,6 +4,8 @@ import {
ALIGN_CENTER,
BORDERS,
Btn,
+ COLORS,
+ CURSOR_DEFAULT,
DIRECTION_COLUMN,
DIRECTION_ROW,
DISPLAY_GRID,
@@ -69,7 +71,14 @@ export function MultiDeckLabelTagBtns({
{...colThreeSecondaryBtn}
/>
-
+
{colThreeSecondaryBtn.buttonText}
@@ -77,7 +86,11 @@ export function MultiDeckLabelTagBtns({
)}
<>
-
+
{colThreePrimaryBtn.buttonText}
@@ -182,3 +195,8 @@ const DESKTOP_ONLY_BUTTON = css`
display: none;
}
`
+
+const DESKTOP_SECONDARY_ARIA_DISABLED = css`
+ color: ${COLORS.grey40};
+ cursor: ${CURSOR_DEFAULT};
+`
diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/useLPCLabwareInfo/index.ts b/app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/useLPCLabwareInfo/index.ts
index 6271327b4a3..1e083129135 100644
--- a/app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/useLPCLabwareInfo/index.ts
+++ b/app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/useLPCLabwareInfo/index.ts
@@ -68,10 +68,7 @@ function useFlexLPCLabwareInfo({
const { data: lwOffsetsData } = useNotifySearchLabwareOffsets(
searchLwOffsetsParams,
{
- enabled:
- searchLwOffsetsParams.filters.length > 0 &&
- robotType === FLEX_ROBOT_TYPE &&
- runStatus === RUN_STATUS_IDLE,
+ enabled: runStatus === RUN_STATUS_IDLE && robotType === FLEX_ROBOT_TYPE,
refetchInterval: REFETCH_OFFSET_SEARCH_MS,
}
)
diff --git a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx
index 69f8a9f7e1e..f7e975b0dcf 100644
--- a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx
+++ b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx
@@ -12,7 +12,10 @@ import {
import { LPCRobotInMotion } from './LPCRobotInMotion'
import { LPCFatalError } from './LPCFatalError'
import { LPCProbeNotAttached } from './LPCProbeNotAttached'
-import { useLPCCommands } from '/app/organisms/LabwarePositionCheck/hooks'
+import {
+ useInfoBanners,
+ useLPCCommands,
+} from '/app/organisms/LabwarePositionCheck/hooks'
import {
closeLPC,
proceedStep as proceedStepDispatch,
@@ -40,6 +43,7 @@ export function LPCWizardFlex(props: LPCWizardFlexProps): JSX.Element {
const LPCHandlerUtils = useLPCCommands({
...props,
})
+ const bannerUtils = useInfoBanners()
// Clean up state on LPC close.
useEffect(() => {
@@ -53,6 +57,7 @@ export function LPCWizardFlex(props: LPCWizardFlexProps): JSX.Element {
LPCHandlerUtils,
proceedStep,
goBackLastStep,
+ bannerUtils,
})
return (
@@ -61,6 +66,7 @@ export function LPCWizardFlex(props: LPCWizardFlexProps): JSX.Element {
proceedStep={proceedStep}
goBackLastStep={goBackLastStep}
commandUtils={{ ...LPCHandlerUtils, headerCommands }}
+ bannerUtils={bannerUtils}
/>
)
}
diff --git a/app/src/organisms/LabwarePositionCheck/__fixtures__/mockLPCContentProps.ts b/app/src/organisms/LabwarePositionCheck/__fixtures__/mockLPCContentProps.ts
index 80c1d49a23a..cc0e8793630 100644
--- a/app/src/organisms/LabwarePositionCheck/__fixtures__/mockLPCContentProps.ts
+++ b/app/src/organisms/LabwarePositionCheck/__fixtures__/mockLPCContentProps.ts
@@ -7,4 +7,7 @@ export const mockLPCContentProps: LPCWizardContentProps = {
commandUtils: {} as any,
proceedStep: vi.fn(),
goBackLastStep: vi.fn(),
+ bannerUtils: {
+ defaultOffsetInfoBanner: { toggleBanner: vi.fn(), showBanner: false },
+ },
}
diff --git a/app/src/organisms/LabwarePositionCheck/hooks/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/index.ts
index 7ede1e117bb..ce3d57872e2 100644
--- a/app/src/organisms/LabwarePositionCheck/hooks/index.ts
+++ b/app/src/organisms/LabwarePositionCheck/hooks/index.ts
@@ -1,2 +1,3 @@
export * from './useLPCCommands'
export * from './useLPCSnackbars'
+export * from './useInfoBanners'
diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useInfoBanners.ts b/app/src/organisms/LabwarePositionCheck/hooks/useInfoBanners.ts
new file mode 100644
index 00000000000..b7ec938661f
--- /dev/null
+++ b/app/src/organisms/LabwarePositionCheck/hooks/useInfoBanners.ts
@@ -0,0 +1,31 @@
+import { useState } from 'react'
+
+interface BannerProps {
+ showBanner: boolean
+ toggleBanner: () => void
+}
+
+export interface UseInfoBannersResult {
+ defaultOffsetInfoBanner: BannerProps
+}
+
+// TODO(jh, 03-28-25): This could live in redux.
+
+// Holds state & functionality for managing banners that require persistent state for this LPC session only.
+export function useInfoBanners(): UseInfoBannersResult {
+ const [
+ showDefaultOffsetInfoBanner,
+ setShowDefaultOffsetInfoBanner,
+ ] = useState(true)
+
+ const toggleDefaultOffsetInfoBanner = (): void => {
+ setShowDefaultOffsetInfoBanner(!showDefaultOffsetInfoBanner)
+ }
+
+ return {
+ defaultOffsetInfoBanner: {
+ toggleBanner: toggleDefaultOffsetInfoBanner,
+ showBanner: showDefaultOffsetInfoBanner,
+ },
+ }
+}
diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/__tests__/useLPCHeaderCommands.test.tsx b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/__tests__/useLPCHeaderCommands.test.tsx
index d140554b53d..0b66fad058f 100644
--- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/__tests__/useLPCHeaderCommands.test.tsx
+++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/__tests__/useLPCHeaderCommands.test.tsx
@@ -62,6 +62,9 @@ describe('useLPCHeaderCommands', () => {
proceedStep: mockProceedStep,
goBackLastStep: vi.fn(),
runId: mockRunId,
+ bannerUtils: {
+ defaultOffsetInfoBanner: { showBanner: false, toggleBanner: vi.fn() },
+ },
}
store = createStore(vi.fn(), {})
diff --git a/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareDetails/__tests__/LPCLabwareDetails.test.tsx b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareDetails/__tests__/LPCLabwareDetails.test.tsx
index 8e891f6f4aa..da822e2c02e 100644
--- a/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareDetails/__tests__/LPCLabwareDetails.test.tsx
+++ b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareDetails/__tests__/LPCLabwareDetails.test.tsx
@@ -107,11 +107,13 @@ describe('LPCLabwareDetails', () => {
let props: ComponentProps
let mockDispatch: Mock
let mockSaveWorkingOffsets: Mock
+ let mockToggleInfoBanner: Mock
beforeEach(() => {
mockDispatch = vi.fn()
vi.mocked(useDispatch).mockReturnValue(mockDispatch)
mockSaveWorkingOffsets = vi.fn(() => Promise.resolve('mock-data'))
+ mockToggleInfoBanner = vi.fn()
props = {
...mockLPCContentProps,
@@ -119,6 +121,12 @@ describe('LPCLabwareDetails', () => {
saveWorkingOffsets: mockSaveWorkingOffsets,
isSavingWorkingOffsetsLoading: false,
} as any,
+ bannerUtils: {
+ defaultOffsetInfoBanner: {
+ toggleBanner: mockToggleInfoBanner,
+ showBanner: false,
+ },
+ },
}
vi.mocked(getIsOnDevice).mockReturnValue(false)
@@ -213,4 +221,24 @@ describe('LPCLabwareDetails', () => {
'Hardcoded offsets must be changed in your Python protocol'
)
})
+
+ it('should render the info banner when show banner is true and allow for the user to dismiss it', () => {
+ vi.mocked(getIsOnDevice).mockReturnValue(true)
+
+ render({
+ ...props,
+ bannerUtils: {
+ defaultOffsetInfoBanner: {
+ toggleBanner: mockToggleInfoBanner,
+ showBanner: true,
+ },
+ },
+ })
+
+ const notification = screen.getByTestId('inline-notification')
+ expect(notification).toBeInTheDocument()
+ expect(notification.getAttribute('data-heading')).toBe(
+ 'The default offset is used for all placements of the labware unless a manual adjustment is made to specific slot location.'
+ )
+ })
})
diff --git a/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareDetails/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareDetails/index.tsx
index 30f4b3037e2..a10b9988210 100644
--- a/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareDetails/index.tsx
+++ b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareDetails/index.tsx
@@ -101,7 +101,11 @@ export function LPCLabwareDetails(props: LPCWizardContentProps): JSX.Element {
function LPCLabwareDetailsContent(props: LPCWizardContentProps): JSX.Element {
const { t } = useTranslation('labware_position_check')
- const { runId } = props
+ const { runId, bannerUtils } = props
+ const {
+ showBanner: showInfoBanner,
+ toggleBanner: toggleInfoBanner,
+ } = bannerUtils.defaultOffsetInfoBanner
const selectedLwInfo = useSelector(selectSelectedLwOverview(runId))
const isOnDevice = useSelector(getIsOnDevice)
@@ -150,6 +154,13 @@ function LPCLabwareDetailsContent(props: LPCWizardContentProps): JSX.Element {
}
/>
)}
+ {showInfoBanner && (
+
+ )}
{/* Accommodate scrolling on the ODD. */}
diff --git a/app/src/organisms/LabwarePositionCheck/types/content.ts b/app/src/organisms/LabwarePositionCheck/types/content.ts
index 7dd7ec301dc..0918f8b21cc 100644
--- a/app/src/organisms/LabwarePositionCheck/types/content.ts
+++ b/app/src/organisms/LabwarePositionCheck/types/content.ts
@@ -1,4 +1,7 @@
-import type { UseLPCCommandsResult } from '/app/organisms/LabwarePositionCheck/hooks'
+import type {
+ UseInfoBannersResult,
+ UseLPCCommandsResult,
+} from '/app/organisms/LabwarePositionCheck/hooks'
import type { LPCWizardFlexProps } from '/app/organisms/LabwarePositionCheck/LPCWizardFlex'
import type { LPCStep } from '/app/redux/protocol-runs'
import type { UseLPCHeaderCommandsResult } from '/app/organisms/LabwarePositionCheck/hooks/useLPCCommands/useLPCHeaderCommands'
@@ -9,4 +12,5 @@ export type LPCWizardContentProps = Pick & {
commandUtils: UseLPCCommandsResult & {
headerCommands: UseLPCHeaderCommandsResult
}
+ bannerUtils: UseInfoBannersResult
}
diff --git a/components/src/atoms/buttons/SecondaryButton.tsx b/components/src/atoms/buttons/SecondaryButton.tsx
index f999866f9d3..2c2b70ccd1b 100644
--- a/components/src/atoms/buttons/SecondaryButton.tsx
+++ b/components/src/atoms/buttons/SecondaryButton.tsx
@@ -9,15 +9,24 @@ import type { StyleProps } from '../../index'
interface SecondaryButtonProps extends StyleProps {
/** button action is dangerous and may have non-reversible side-effects for user */
isDangerous?: boolean
+ 'aria-disabled'?: boolean
}
export const SecondaryButton = styled.button.withConfig({
- shouldForwardProp: p => isntStyleProp(p) && p !== 'isDangerous',
+ shouldForwardProp: p =>
+ isntStyleProp(p) && p !== 'isDangerous' && p !== 'aria-disabled',
})`
appearance: none;
- cursor: ${CURSOR_POINTER};
- color: ${props => (props.isDangerous ? COLORS.red50 : COLORS.blue50)};
+ cursor: ${props =>
+ props['aria-disabled'] ? CURSOR_DEFAULT : CURSOR_POINTER};
+ color: ${props => {
+ if (props['aria-disabled']) return COLORS.grey40
+ return props.isDangerous ? COLORS.red50 : COLORS.blue50
+ }};
border: ${BORDERS.lineBorder};
- border-color: ${props => (props.isDangerous ? COLORS.red50 : 'initial')};
+ border-color: ${props => {
+ if (props['aria-disabled']) return COLORS.grey30
+ return props.isDangerous ? COLORS.red50 : 'initial'
+ }};
border-radius: ${BORDERS.borderRadius8};
padding: ${SPACING.spacing8} ${SPACING.spacing16};
text-transform: ${TYPOGRAPHY.textTransformNone};
@@ -26,28 +35,45 @@ export const SecondaryButton = styled.button.withConfig({
&:hover,
&:focus {
- box-shadow: 0px 3px 6px 0px rgba(0, 0, 0, 0.23);
+ box-shadow: ${props =>
+ props['aria-disabled'] ? 'none' : '0px 3px 6px 0px rgba(0, 0, 0, 0.23)'};
}
&:hover {
- color: ${props => (props.isDangerous ? COLORS.red50 : COLORS.blue60)};
- border-color: ${props =>
- props.isDangerous ? COLORS.red50 : COLORS.blue55};
- box-shadow: 0 0 0;
+ color: ${props => {
+ if (props['aria-disabled']) return COLORS.grey40
+ return props.isDangerous ? COLORS.red50 : COLORS.blue60
+ }};
+ border-color: ${props => {
+ if (props['aria-disabled']) return COLORS.grey30
+ return props.isDangerous ? COLORS.red50 : COLORS.blue55
+ }};
+ box-shadow: ${props => (props['aria-disabled'] ? 'none' : '0 0 0')};
}
&:focus-visible {
- color: ${props => (props.isDangerous ? COLORS.red60 : COLORS.blue60)};
- border-color: ${props =>
- props.isDangerous ? COLORS.red50 : COLORS.blue60};
- box-shadow: 0 0 0 3px ${COLORS.yellow50};
+ color: ${props => {
+ if (props['aria-disabled']) return COLORS.grey40
+ return props.isDangerous ? COLORS.red60 : COLORS.blue60
+ }};
+ border-color: ${props => {
+ if (props['aria-disabled']) return COLORS.grey30
+ return props.isDangerous ? COLORS.red50 : COLORS.blue60
+ }};
+ box-shadow: ${props =>
+ props['aria-disabled'] ? 'none' : `0 0 0 3px ${COLORS.yellow50}`};
}
&:active {
box-shadow: none;
- color: ${props => (props.isDangerous ? COLORS.red60 : COLORS.blue55)};
- border-color: ${props =>
- props.isDangerous ? COLORS.red60 : COLORS.blue55};
+ color: ${props => {
+ if (props['aria-disabled']) return COLORS.grey40
+ return props.isDangerous ? COLORS.red60 : COLORS.blue55
+ }};
+ border-color: ${props => {
+ if (props['aria-disabled']) return COLORS.grey30
+ return props.isDangerous ? COLORS.red60 : COLORS.blue55
+ }};
}
&:disabled,
diff --git a/protocol-designer/cypress/support/MixSteps.ts b/protocol-designer/cypress/support/MixSteps.ts
index 92b3d0c96d7..8b4eb633153 100644
--- a/protocol-designer/cypress/support/MixSteps.ts
+++ b/protocol-designer/cypress/support/MixSteps.ts
@@ -90,7 +90,7 @@ enum MixLocators {
AspWellOrder = '[data-testid="WellsOrderField_ListButton_aspirate"]',
ResetToDefault = 'button:contains("Reset to default")',
PrimaryOrderDropdown = 'div[tabindex="0"].sc-bqWxrE jKLbYH iFjNDq',
- CancelAspSettings = '[class="SecondaryButton-sc-1opt1t9-0 kjpcRL"]',
+ CancelAspSettings = 'button:contains("Done")',
MixTipPos = '[data-testid="PositionField_ListButton_mix"]',
XpositionInput = '[data-testid="TipPositionModal_x_custom_input"]',
YpositionInput = '[id="TipPositionModal_y_custom_input"]',