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"]',