Skip to content

Commit

Permalink
fix(protocol-designer): step overflow menu positioning and prevent mu…
Browse files Browse the repository at this point in the history
…ltiple opened (#16829)

closes RQA-3408 RQA-3358, partially closes RQA-3402 (item 4 i think)
  • Loading branch information
jerader authored Nov 14, 2024
1 parent feeb999 commit 11cad09
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useDispatch, useSelector } from 'react-redux'
import type { Dispatch, SetStateAction } from 'react'
import { useTranslation } from 'react-i18next'
import { useConditionalConfirm } from '@opentrons/components'
import * as timelineWarningSelectors from '../../../../top-selectors/timelineWarnings'
Expand Down Expand Up @@ -33,7 +34,6 @@ import {
nonePressed,
} from './utils'

import type * as React from 'react'
import type { ThunkDispatch } from 'redux-thunk'
import type {
HoverOnStepAction,
Expand All @@ -47,10 +47,18 @@ export interface ConnectedStepInfoProps {
stepId: StepIdType
stepNumber: number
dragHovered?: boolean
openedOverflowMenuId?: string | null
setOpenedOverflowMenuId?: Dispatch<SetStateAction<string | null>>
}

export function ConnectedStepInfo(props: ConnectedStepInfoProps): JSX.Element {
const { stepId, stepNumber, dragHovered = false } = props
const {
stepId,
stepNumber,
dragHovered = false,
openedOverflowMenuId,
setOpenedOverflowMenuId,
} = props
const { t } = useTranslation('application')
const dispatch = useDispatch<ThunkDispatch<BaseState, any, any>>()
const stepIds = useSelector(getOrderedStepIds)
Expand Down Expand Up @@ -203,6 +211,8 @@ export function ConnectedStepInfo(props: ConnectedStepInfoProps): JSX.Element {
/>
)}
<StepContainer
openedOverflowMenuId={openedOverflowMenuId}
setOpenedOverflowMenuId={setOpenedOverflowMenuId}
hasError={hasError}
isStepAfterError={stepAfterError}
stepId={stepId}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef } from 'react'
import { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { useDrop, useDrag } from 'react-dnd'
Expand All @@ -14,6 +14,7 @@ import { selectors as stepFormSelectors } from '../../../../step-forms'
import { stepIconsByType } from '../../../../form-types'
import { StepContainer } from './StepContainer'
import { ConnectedStepInfo } from './ConnectedStepInfo'
import type { Dispatch, SetStateAction } from 'react'
import type { DragLayerMonitor, DropTargetMonitor } from 'react-dnd'
import type { StepIdType } from '../../../../form-types'
import type { ConnectedStepItemProps } from '../../../../containers/ConnectedStepItem'
Expand All @@ -23,14 +24,24 @@ interface DragDropStepProps extends ConnectedStepItemProps {
moveStep: (stepId: StepIdType, value: number) => void
findStepIndex: (stepId: StepIdType) => number
orderedStepIds: string[]
openedOverflowMenuId?: string | null
setOpenedOverflowMenuId?: Dispatch<SetStateAction<string | null>>
}

interface DropType {
stepId: StepIdType
}

function DragDropStep(props: DragDropStepProps): JSX.Element {
const { stepId, moveStep, findStepIndex, orderedStepIds, stepNumber } = props
const {
stepId,
moveStep,
findStepIndex,
orderedStepIds,
stepNumber,
openedOverflowMenuId,
setOpenedOverflowMenuId,
} = props
const stepRef = useRef<HTMLDivElement>(null)

const [{ isDragging }, drag] = useDrag(
Expand Down Expand Up @@ -73,6 +84,8 @@ function DragDropStep(props: DragDropStepProps): JSX.Element {
data-handler-id={handlerId}
>
<ConnectedStepInfo
openedOverflowMenuId={openedOverflowMenuId}
setOpenedOverflowMenuId={setOpenedOverflowMenuId}
stepNumber={stepNumber}
stepId={stepId}
dragHovered={hovered}
Expand All @@ -88,6 +101,9 @@ interface DraggableStepsProps {
export function DraggableSteps(props: DraggableStepsProps): JSX.Element | null {
const { orderedStepIds, reorderSteps } = props
const { t } = useTranslation('shared')
const [openedOverflowMenuId, setOpenedOverflowMenuId] = useState<
string | null
>(null)

const findStepIndex = (stepId: StepIdType): number =>
orderedStepIds.findIndex(id => stepId === id)
Expand Down Expand Up @@ -123,6 +139,8 @@ export function DraggableSteps(props: DraggableStepsProps): JSX.Element | null {
moveStep={moveStep}
findStepIndex={findStepIndex}
orderedStepIds={orderedStepIds}
openedOverflowMenuId={openedOverflowMenuId}
setOpenedOverflowMenuId={setOpenedOverflowMenuId}
/>
))}
<StepDragPreview />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,30 @@ import { LINE_CLAMP_TEXT_STYLE } from '../../../../atoms'
import { StepOverflowMenu } from './StepOverflowMenu'
import { capitalizeFirstLetterAfterNumber } from './utils'

import type {
SetStateAction,
Dispatch,
MouseEvent as ReactMouseEvent,
} from 'react'
import type { ThunkDispatch } from 'redux-thunk'
import type { IconName } from '@opentrons/components'
import type { StepIdType } from '../../../../form-types'
import type { BaseState } from '../../../../types'

const STARTING_DECK_STATE = 'Starting deck state'
const FINAL_DECK_STATE = 'Final deck state'

const PX_HEIGHT_TO_TOP_OF_CONTAINER = 32
export interface StepContainerProps {
title: string
iconName: IconName
openedOverflowMenuId?: string | null
setOpenedOverflowMenuId?: Dispatch<SetStateAction<string | null>>
stepId?: string
iconColor?: string
onClick?: (event: React.MouseEvent) => void
onDoubleClick?: (event: React.MouseEvent) => void
onMouseEnter?: (event: React.MouseEvent) => void
onMouseLeave?: (event: React.MouseEvent) => void
onClick?: (event: ReactMouseEvent) => void
onDoubleClick?: (event: ReactMouseEvent) => void
onMouseEnter?: (event: ReactMouseEvent) => void
onMouseLeave?: (event: ReactMouseEvent) => void
selected?: boolean
hovered?: boolean
hasError?: boolean
Expand All @@ -74,10 +81,11 @@ export function StepContainer(props: StepContainerProps): JSX.Element {
hasError = false,
isStepAfterError = false,
dragHovered = false,
setOpenedOverflowMenuId,
openedOverflowMenuId,
} = props
const [top, setTop] = useState<number>(0)
const menuRootRef = useRef<HTMLDivElement | null>(null)
const [stepOverflowMenu, setStepOverflowMenu] = useState<boolean>(false)
const isStartingOrEndingState =
title === STARTING_DECK_STATE || title === FINAL_DECK_STATE
const dispatch = useDispatch<ThunkDispatch<BaseState, any, any>>()
Expand All @@ -104,22 +112,21 @@ export function StepContainer(props: StepContainerProps): JSX.Element {
menuRootRef.current?.contains(event.target)
)

if (wasOutside && stepOverflowMenu) {
setStepOverflowMenu(false)
if (wasOutside) {
setOpenedOverflowMenuId?.(null)
}
}

const handleOverflowClick = (event: React.MouseEvent): void => {
const { clientY } = event

const handleOverflowClick = (event: ReactMouseEvent): void => {
const buttonRect = event.currentTarget.getBoundingClientRect()
const screenHeight = window.innerHeight
const rootHeight = menuRootRef.current
? menuRootRef.current.offsetHeight
: 0
const rootHeight = menuRootRef.current?.offsetHeight || 0

const spaceBelow = screenHeight - buttonRect.bottom
const top =
screenHeight - clientY > rootHeight
? clientY + 5
: clientY - rootHeight - 5
spaceBelow > rootHeight
? buttonRect.bottom - PX_HEIGHT_TO_TOP_OF_CONTAINER
: buttonRect.top - rootHeight + PX_HEIGHT_TO_TOP_OF_CONTAINER

setTop(top)
}
Expand All @@ -135,7 +142,7 @@ export function StepContainer(props: StepContainerProps): JSX.Element {
if (stepId != null) {
dispatch(populateForm(stepId))
}
setStepOverflowMenu(false)
setOpenedOverflowMenuId?.(null)
}

const onDeleteClickAction = (): void => {
Expand Down Expand Up @@ -168,7 +175,6 @@ export function StepContainer(props: StepContainerProps): JSX.Element {
)
}
}

const {
confirm: confirmDelete,
showConfirmation: showDeleteConfirmation,
Expand Down Expand Up @@ -242,10 +248,15 @@ export function StepContainer(props: StepContainerProps): JSX.Element {
<OverflowBtn
data-testid={`StepContainer_${stepId}`}
fillColor={COLORS.white}
onClick={(e: React.MouseEvent) => {
onClick={(e: ReactMouseEvent) => {
e.preventDefault()
e.stopPropagation()
setStepOverflowMenu(prev => !prev)
if (openedOverflowMenuId === stepId) {
setOpenedOverflowMenuId?.(null)
} else {
setOpenedOverflowMenuId?.(stepId ?? null)
}

handleOverflowClick(e)
}}
/>
Expand All @@ -262,10 +273,12 @@ export function StepContainer(props: StepContainerProps): JSX.Element {
/>
) : null}
</Flex>
{stepOverflowMenu && stepId != null
{stepId != null &&
openedOverflowMenuId === stepId &&
setOpenedOverflowMenuId != null
? createPortal(
<StepOverflowMenu
setStepOverflowMenu={setStepOverflowMenu}
setOpenedOverflowMenuId={setOpenedOverflowMenuId}
stepId={stepId}
menuRootRef={menuRootRef}
top={top}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ interface StepOverflowMenuProps {
stepId: string
menuRootRef: React.MutableRefObject<HTMLDivElement | null>
top: number
setStepOverflowMenu: React.Dispatch<React.SetStateAction<boolean>>
setOpenedOverflowMenuId: React.Dispatch<React.SetStateAction<string | null>>
handleEdit: () => void
confirmDelete: () => void
confirmMultiDelete: () => void
Expand All @@ -44,7 +44,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element {
stepId,
menuRootRef,
top,
setStepOverflowMenu,
setOpenedOverflowMenuId,
handleEdit,
confirmDelete,
confirmMultiDelete,
Expand Down Expand Up @@ -91,7 +91,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element {
ref={menuRootRef}
zIndex={12}
top={top}
left="19.5rem"
left="18.75rem"
position={POSITION_ABSOLUTE}
whiteSpace={NO_WRAP}
borderRadius={BORDERS.borderRadius8}
Expand All @@ -109,7 +109,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element {
disabled={batchEditFormHasUnstagedChanges}
onClick={() => {
duplicateMultipleSteps()
setStepOverflowMenu(false)
setOpenedOverflowMenuId(null)
}}
>
{t('duplicate_steps')}
Expand All @@ -118,7 +118,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element {
<MenuItem
onClick={() => {
confirmMultiDelete()
setStepOverflowMenu(false)
setOpenedOverflowMenuId(null)
}}
>
{t('delete_steps')}
Expand All @@ -133,7 +133,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element {
<MenuItem
disabled={formData != null}
onClick={() => {
setStepOverflowMenu(false)
setOpenedOverflowMenuId(null)
dispatch(hoverOnStep(stepId))
dispatch(toggleViewSubstep(stepId))
dispatch(analyticsEvent(selectViewDetailsEvent))
Expand All @@ -146,7 +146,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element {
disabled={singleEditFormHasUnsavedChanges}
onClick={() => {
duplicateStep(stepId)
setStepOverflowMenu(false)
setOpenedOverflowMenuId(null)
}}
>
{t('duplicate')}
Expand All @@ -155,7 +155,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element {
<MenuItem
onClick={() => {
confirmDelete()
setStepOverflowMenu(false)
setOpenedOverflowMenuId(null)
}}
>
{t('delete')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe('StepOverflowMenu', () => {
stepId: moveLiquidStepId,
top: 0,
menuRootRef: { current: null },
setStepOverflowMenu: vi.fn(),
setOpenedOverflowMenuId: vi.fn(),
multiSelectItemIds: [],
handleEdit: vi.fn(),
confirmDelete: mockConfirm,
Expand Down

0 comments on commit 11cad09

Please sign in to comment.