diff --git a/components/src/constants.js b/components/src/constants.js index 71935e7fe31..cbdf4276514 100644 --- a/components/src/constants.js +++ b/components/src/constants.js @@ -2,6 +2,7 @@ // ========= STYLE ================ +export const AIR = '__air__' export const MIXED_WELL_COLOR = '#9b9b9b' // NOTE: matches `--c-med-gray` in colors.css // TODO factor into CSS or constants or elsewhere diff --git a/components/src/tooltips/HoverTooltip.js b/components/src/tooltips/HoverTooltip.js index a298074a8ce..1b06dbb6a23 100644 --- a/components/src/tooltips/HoverTooltip.js +++ b/components/src/tooltips/HoverTooltip.js @@ -17,10 +17,12 @@ export type HoverTooltipHandlers = { type PopperProps = React.ElementProps type Props = { tooltipComponent?: React.Node, + portal?: React.ComponentType<*>, placement?: $PropertyType, positionFixed?: $PropertyType, modifiers?: $PropertyType, children: (?HoverTooltipHandlers) => React.Node, + forceOpen?: boolean, // NOTE: mostly for debugging/positioning } type State = {isOpen: boolean} class HoverTooltip extends React.Component { @@ -34,6 +36,11 @@ class HoverTooltip extends React.Component { this.state = {isOpen: false} } + componentWillUnmount () { + if (this.closeTimeout) clearTimeout(this.closeTimeout) + if (this.openTimeout) clearTimeout(this.openTimeout) + } + delayedOpen = () => { if (this.closeTimeout) clearTimeout(this.closeTimeout) this.openTimeout = setTimeout(() => this.setState({isOpen: true}), OPEN_DELAY_MS) @@ -52,7 +59,7 @@ class HoverTooltip extends React.Component { {({ref}) => this.props.children({ref, onMouseEnter: this.delayedOpen, onMouseLeave: this.delayedClose})} { - this.state.isOpen && + (this.props.forceOpen || this.state.isOpen) && { if (placement === 'left' || placement === 'right') { arrowStyle = {top: '0.6em'} } - return ( + const tooltipContents = (
{this.props.tooltipComponent}
) + if (this.props.portal) { + const PortalClass = this.props.portal + return {tooltipContents} + } + return tooltipContents }} } diff --git a/components/src/utils.js b/components/src/utils.js index 78baa5c3498..14e51ad68be 100644 --- a/components/src/utils.js +++ b/components/src/utils.js @@ -5,6 +5,8 @@ import { MIXED_WELL_COLOR, } from '@opentrons/components' +import {AIR} from './constants' + export const humanizeLabwareType = startCase export const wellNameSplit = (wellName: string): [string, string] => { @@ -26,9 +28,9 @@ export const wellNameSplit = (wellName: string): [string, string] => { return [letters, numbers] } -// TODO Ian 2018-07-20: make sure '__air__' or other pseudo-ingredients don't get in here export const ingredIdsToColor = (groupIds: Array): ?string => { - if (groupIds.length === 0) return null - if (groupIds.length === 1) return swatchColors(Number(groupIds[0])) + const filteredIngredIds = groupIds.filter(id => id !== AIR) + if (filteredIngredIds.length === 0) return null + if (filteredIngredIds.length === 1) return swatchColors(Number(filteredIngredIds[0])) return MIXED_WELL_COLOR } diff --git a/protocol-designer/src/components/steplist/AspirateDispenseHeader.js b/protocol-designer/src/components/steplist/AspirateDispenseHeader.js index 60503d67eb5..143d82f9019 100644 --- a/protocol-designer/src/components/steplist/AspirateDispenseHeader.js +++ b/protocol-designer/src/components/steplist/AspirateDispenseHeader.js @@ -1,31 +1,51 @@ // @flow import * as React from 'react' import cx from 'classnames' -import {Icon} from '@opentrons/components' +import {Icon, HoverTooltip} from '@opentrons/components' import {PDListItem} from '../lists' import styles from './StepItem.css' +import LabwareTooltipContents from './LabwareTooltipContents' +import type {Labware} from '../../labware-ingred/types' +import {labwareToDisplayName} from '../../labware-ingred/utils' +import {Portal} from './TooltipPortal' type AspirateDispenseHeaderProps = { - sourceLabwareName: ?string, - destLabwareName: ?string, + sourceLabware: ?Labware, + destLabware: ?Labware, } function AspirateDispenseHeader (props: AspirateDispenseHeaderProps) { - const {sourceLabwareName, destLabwareName} = props + const {sourceLabware, destLabware} = props return (
  • - ASPIRATE - - DISPENSE + ASPIRATE + + DISPENSE
  • - {sourceLabwareName} + }> + {(hoverTooltipHandlers) => ( + + {sourceLabware && labwareToDisplayName(sourceLabware)} + + )} + {/* This is always a "transfer icon" (arrow pointing right) for any step: */} - {destLabwareName} + }> + {(hoverTooltipHandlers) => ( + + {destLabware && labwareToDisplayName(destLabware)} + + )} +
    ) diff --git a/protocol-designer/src/components/steplist/LabwareTooltipContents.js b/protocol-designer/src/components/steplist/LabwareTooltipContents.js new file mode 100644 index 00000000000..d3d869ff1d3 --- /dev/null +++ b/protocol-designer/src/components/steplist/LabwareTooltipContents.js @@ -0,0 +1,23 @@ +// @flow +import * as React from 'react' +import type {Labware} from '../../labware-ingred/types' +import {labwareToDisplayName} from '../../labware-ingred/utils' +import styles from './StepItem.css' + +type LabwareTooltipContentsProps = {labware: ?Labware} +const LabwareTooltipContents = ({labware}: LabwareTooltipContentsProps) => { + const displayName = labware && labwareToDisplayName(labware) + return ( +
    +

    {displayName}

    + {labware && labware.type !== displayName && + +
    +

    {labware && labware.type}

    + + } +
    + ) +} + +export default LabwareTooltipContents diff --git a/protocol-designer/src/components/steplist/MixHeader.js b/protocol-designer/src/components/steplist/MixHeader.js index ef3ba50664f..5b5a053f366 100644 --- a/protocol-designer/src/components/steplist/MixHeader.js +++ b/protocol-designer/src/components/steplist/MixHeader.js @@ -1,19 +1,34 @@ // @flow import * as React from 'react' +import cx from 'classnames' +import {HoverTooltip} from '@opentrons/components' import {PDListItem} from '../lists' import styles from './StepItem.css' +import type {Labware} from '../../labware-ingred/types' +import LabwareTooltipContents from './LabwareTooltipContents' +import {Portal} from './TooltipPortal' type Props = { volume: ?string, times: ?string, - labwareName: ?string, + labware: ?Labware, } export default function MixHeader (props: Props) { - const {volume, times, labwareName} = props - return - {labwareName} - {volume} uL - {times}x - + const {volume, times, labware} = props + return ( + + }> + {(hoverTooltipHandlers) => ( + + {labware && labware.name} + + )} + + {volume} uL + {times}x + + ) } diff --git a/protocol-designer/src/components/steplist/StepItem.css b/protocol-designer/src/components/steplist/StepItem.css index 7c9d1b6e1da..a28cee4992a 100644 --- a/protocol-designer/src/components/steplist/StepItem.css +++ b/protocol-designer/src/components/steplist/StepItem.css @@ -82,6 +82,7 @@ .liquid_tooltip_contents { margin: 0.5em; + max-width: 20rem; } .ingred_row { @@ -129,3 +130,24 @@ .multi_substep_header { font-style: italic; } + +.labware_name { + font-weight: var(--fw-semibold); +} + +.labware_spacer { + width: 0.5rem; + height: 0.5rem; +} + +.labware_tooltip_contents { + margin: 0.5rem; + max-width: 20rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.labware_display_name { + cursor: default; +} diff --git a/protocol-designer/src/components/steplist/StepItem.js b/protocol-designer/src/components/steplist/StepItem.js index 5c65abdf0c9..133bbfdc9ee 100644 --- a/protocol-designer/src/components/steplist/StepItem.js +++ b/protocol-designer/src/components/steplist/StepItem.js @@ -8,7 +8,7 @@ import MixHeader from './MixHeader' import PauseStepItems from './PauseStepItems' import StepDescription from '../StepDescription' import {stepIconsByType, type StepIdType} from '../../form-types' - +import type {Labware} from '../../labware-ingred/types' import type { SubstepIdentifier, StepItemData, @@ -28,7 +28,7 @@ type StepItemProps = { hoveredSubstep: ?SubstepIdentifier, ingredNames: WellIngredientNames, - getLabwareName: (labwareId: ?string) => ?string, + getLabware: (labwareId: ?string) => ?Labware, handleSubstepHover: SubstepIdentifier => mixed, onStepClick?: (event?: SyntheticEvent<>) => mixed, onStepItemCollapseToggle?: (event?: SyntheticEvent<>) => mixed, @@ -76,7 +76,7 @@ function getStepItemContents (stepItemProps: StepItemProps) { const { step, substeps, - getLabwareName, + getLabware, hoveredSubstep, handleSubstepHover, ingredNames, @@ -103,13 +103,13 @@ function getStepItemContents (stepItemProps: StepItemProps) { formData.stepType === 'distribute' ) ) { - const sourceLabwareName = getLabwareName(formData['aspirate_labware']) - const destLabwareName = getLabwareName(formData['dispense_labware']) + const sourceLabware = getLabware(formData['aspirate_labware']) + const destLabware = getLabware(formData['dispense_labware']) result.push( ) } @@ -119,7 +119,7 @@ function getStepItemContents (stepItemProps: StepItemProps) { ) } diff --git a/protocol-designer/src/components/steplist/StepList.js b/protocol-designer/src/components/steplist/StepList.js index 649e0f86227..e49e45efa3a 100644 --- a/protocol-designer/src/components/steplist/StepList.js +++ b/protocol-designer/src/components/steplist/StepList.js @@ -10,6 +10,7 @@ import {END_TERMINAL_TITLE} from '../../constants' import {END_TERMINAL_ITEM_ID} from '../../steplist' import type {StepIdType} from '../../form-types' +import {PortalRoot} from './TooltipPortal' type StepListProps = { orderedSteps: Array, @@ -18,18 +19,18 @@ type StepListProps = { export default function StepList (props: StepListProps) { return ( - - + + + - {props.orderedSteps.map((stepId: StepIdType) => ( - - ))} + {props.orderedSteps.map((stepId: StepIdType) => )} - - - + + + + +
    ) } diff --git a/protocol-designer/src/components/steplist/SubstepRow.js b/protocol-designer/src/components/steplist/SubstepRow.js index adbee30ca28..100bccb0424 100644 --- a/protocol-designer/src/components/steplist/SubstepRow.js +++ b/protocol-designer/src/components/steplist/SubstepRow.js @@ -11,6 +11,7 @@ import IngredPill from './IngredPill' import {PDListItem} from '../lists' import styles from './StepItem.css' import {formatVolume, formatPercentage} from './utils' +import {Portal} from './TooltipPortal' type SubstepRowProps = {| volume?: ?number | ?string, @@ -77,6 +78,7 @@ export default function SubstepRow (props: SubstepRowProps) { onMouseEnter={props.onMouseEnter} onMouseLeave={props.onMouseLeave}> {`${formatVolume(props.volume)} μL`} {props.dest && props.dest.well} global.document.getElementById(PORTAL_ROOT_ID) + +export function PortalRoot () { + return
    +} + +// the children of Portal are rendered into the PortalRoot if it exists in DOM +export class Portal extends React.Component { + $root: ?Element + + constructor (props: Props) { + super(props) + this.$root = getPortalRoot() + this.state = {hasRoot: !!this.$root} + } + + // on first launch, $portalRoot isn't in DOM; double check once we're mounted + // TODO(mc, 2018-10-08): prerender UI instead + componentDidMount () { + if (!this.$root) { + this.$root = getPortalRoot() + this.setState({hasRoot: !!this.$root}) + } + } + + render () { + if (!this.$root) return null + return ReactDom.createPortal(this.props.children, this.$root) + } +} diff --git a/protocol-designer/src/containers/ConnectedStepItem.js b/protocol-designer/src/containers/ConnectedStepItem.js index 87a2469e706..486d27540a3 100644 --- a/protocol-designer/src/containers/ConnectedStepItem.js +++ b/protocol-designer/src/containers/ConnectedStepItem.js @@ -26,7 +26,7 @@ type SP = {| selected: $PropertyType, hovered: $PropertyType, hoveredSubstep: $PropertyType, - getLabwareName: $PropertyType, + getLabware: $PropertyType, ingredNames: $PropertyType, |} @@ -61,7 +61,7 @@ function mapStateToProps (state: BaseState, ownProps: OP): SP { // user is not hovering on substep. hovered: (hoveredStep === stepId) && !hoveredSubstep, - getLabwareName: (labwareId: ?string) => labwareId && labwareIngredSelectors.getLabwareNames(state)[labwareId], + getLabware: (labwareId: ?string) => labwareId ? labwareIngredSelectors.getLabware(state)[labwareId] : null, ingredNames: labwareIngredSelectors.getIngredientNames(state), } } diff --git a/protocol-designer/src/labware-ingred/reducers/index.js b/protocol-designer/src/labware-ingred/reducers/index.js index 2fd72f3cc9e..bc24dc88088 100644 --- a/protocol-designer/src/labware-ingred/reducers/index.js +++ b/protocol-designer/src/labware-ingred/reducers/index.js @@ -1,5 +1,4 @@ // @flow -import {humanizeLabwareType} from '@opentrons/components' import {combineReducers} from 'redux' import {handleActions, type ActionType} from 'redux-actions' import {createSelector} from 'reselect' @@ -13,6 +12,7 @@ import isEmpty from 'lodash/isEmpty' import {sortedSlotnames, FIXED_TRASH_ID} from '../../constants.js' import {uuid} from '../../utils' +import {labwareToDisplayName} from '../utils' import type {DeckSlot} from '@opentrons/components' import {getIsTiprack} from '@opentrons/shared-data' @@ -335,7 +335,8 @@ const getLabwareNames: Selector<{[labwareId: string]: string}> = createSelector( getLabware, (_labware) => mapValues( _labware, - (l: Labware) => l.name || `${humanizeLabwareType(l.type)} (${l.disambiguationNumber})`) + labwareToDisplayName, + ) ) const getLabwareTypes: Selector = createSelector( diff --git a/protocol-designer/src/labware-ingred/utils.js b/protocol-designer/src/labware-ingred/utils.js new file mode 100644 index 00000000000..4af2e0617e8 --- /dev/null +++ b/protocol-designer/src/labware-ingred/utils.js @@ -0,0 +1,7 @@ +// @flow +import {humanizeLabwareType} from '@opentrons/components' +import type {Labware} from './types' + +export const labwareToDisplayName = (l: Labware) => ( + l.name || `${humanizeLabwareType(l.type)} (${l.disambiguationNumber})` +) diff --git a/protocol-designer/src/step-generation/utils.js b/protocol-designer/src/step-generation/utils.js index 55cd07f77cc..33b23e47da8 100644 --- a/protocol-designer/src/step-generation/utils.js +++ b/protocol-designer/src/step-generation/utils.js @@ -13,6 +13,9 @@ import type { LocationLiquidState, } from './types' +import {AIR} from '@opentrons/components' +export {AIR} + export function repeatArray (array: Array, repeats: number): Array { return flatMap(range(repeats), (i: number): Array => array) } @@ -88,8 +91,6 @@ export const commandCreatorsTimeline = (commandCreators: Array) type Vol = {volume: number} -export const AIR = '__air__' - /** Breaks a liquid volume state into 2 parts. Assumes all liquids are evenly mixed. */ export function splitLiquid (volume: number, sourceLiquidState: LocationLiquidState): { source: LocationLiquidState,