-
Notifications
You must be signed in to change notification settings - Fork 179
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(app): split InterventionModal to molecule (#15174)
The implementation of the intervention modal (i.e., manual move labware) in the desktop app was sort of manually open-coded in the intervention modal organism. We're going to need modals styled in this way elsewhere, so split it out into an app molecule with some overridable styles. Also, make its background wrapper position: absolute instead of position: fixed, for the same reasons as #15166 - when this modal is hung off of the modal portal rather than the top portal, it should allow interaction with the navbar and the breadcrumbs. This should not affect the modal's use in the actual InterventionRequired organism, since in the organism the modal is hung off of the top portal. Closes RSQ-6 ## Testing - [x] The new storybook component should render - [x] Move labware and pause modals on desktop should still render as in ~figma~ the shipping app - [x] Move labware and pause modals on desktop should continue to ~(a) visually overlap and (b)~ inhibit interaction with the navbar and breadcrumbs the shipping app has these modals confined to the route component area, while figma has them overlapping everything with a small padding to the viewport boundary. this pr doesn't change that. the navbar and route breadcrumbs are still non interactive.
- Loading branch information
1 parent
18d7d71
commit b2fd523
Showing
4 changed files
with
232 additions
and
83 deletions.
There are no files selected for viewing
31 changes: 31 additions & 0 deletions
31
app/src/molecules/InterventionModal/InterventionModal.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import * as React from 'react' | ||
|
||
import { StyledText } from '@opentrons/components' | ||
import { InterventionModal as InterventionModalComponent } from './' | ||
import type { Story, Meta } from '@storybook/react' | ||
|
||
export default { | ||
title: 'App/Molecules/InterventionModal', | ||
component: InterventionModalComponent, | ||
} as Meta | ||
|
||
const Template: Story< | ||
React.ComponentProps<typeof InterventionModalComponent> | ||
> = args => <InterventionModalComponent {...args} /> | ||
|
||
export const ErrorIntervention = Template.bind({}) | ||
ErrorIntervention.args = { | ||
robotName: 'Otie', | ||
type: 'error', | ||
heading: <StyledText as="h3">Oh no, an error!</StyledText>, | ||
iconName: 'alert-circle', | ||
children: <StyledText as="p">Heres some error content</StyledText>, | ||
} | ||
|
||
export const InterventionRequiredIntervention = Template.bind({}) | ||
InterventionRequiredIntervention.args = { | ||
robotName: 'Otie', | ||
type: 'intervention-required', | ||
heading: <StyledText as="h3">Looks like theres something to do</StyledText>, | ||
children: <StyledText as="p">Youve got to intervene!</StyledText>, | ||
} |
66 changes: 66 additions & 0 deletions
66
app/src/molecules/InterventionModal/__tests__/InterventionModal.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import * as React from 'react' | ||
import { describe, it, expect, beforeEach } from 'vitest' | ||
import '@testing-library/jest-dom/vitest' | ||
import { screen } from '@testing-library/react' | ||
import { COLORS, BORDERS } from '@opentrons/components' | ||
import { renderWithProviders } from '../../../__testing-utils__' | ||
import { InterventionModal } from '../' | ||
import type { ModalType } from '../' | ||
|
||
const render = (props: React.ComponentProps<typeof InterventionModal>) => { | ||
return renderWithProviders(<InterventionModal {...props} />)[0] | ||
} | ||
|
||
describe('InterventionModal', () => { | ||
let props: React.ComponentProps<typeof InterventionModal> | ||
|
||
beforeEach(() => { | ||
props = { | ||
heading: 'mock intervention heading', | ||
children: 'mock intervention children', | ||
iconName: 'alert-circle', | ||
type: 'intervention-required', | ||
} | ||
}) | ||
;(['intervention-required', 'error'] as ModalType[]).forEach(type => { | ||
const color = | ||
type === 'intervention-required' ? COLORS.blue50 : COLORS.red50 | ||
it(`renders with the ${type} style`, () => { | ||
render({ ...props, type }) | ||
const header = screen.getByTestId('__otInterventionModalHeader') | ||
expect(header).toHaveStyle(`background-color: ${color}`) | ||
const modal = screen.getByTestId('__otInterventionModal') | ||
expect(modal).toHaveStyle(`border: 6px ${BORDERS.styleSolid} ${color}`) | ||
}) | ||
}) | ||
it('uses intervention-required if prop is not passed', () => { | ||
render({ ...props, type: undefined }) | ||
const header = screen.getByTestId('__otInterventionModalHeader') | ||
expect(header).toHaveStyle(`background-color: ${COLORS.blue50}`) | ||
const modal = screen.getByTestId('__otInterventionModal') | ||
expect(modal).toHaveStyle( | ||
`border: 6px ${BORDERS.styleSolid} ${COLORS.blue50}` | ||
) | ||
}) | ||
it('renders passed elements', () => { | ||
render(props) | ||
screen.getByText('mock intervention children') | ||
screen.getByText('mock intervention heading') | ||
}) | ||
it('renders an icon if an icon is specified', () => { | ||
const { container } = render(props) | ||
// eslint-disable-next-line testing-library/no-node-access, testing-library/no-container | ||
const icon = container.querySelector( | ||
'[aria-roledescription="alert-circle"]' | ||
) | ||
expect(icon).not.toBeNull() | ||
}) | ||
it('does not render an icon if no icon is specified', () => { | ||
const { container } = render({ ...props, iconName: undefined }) | ||
// eslint-disable-next-line testing-library/no-node-access, testing-library/no-container | ||
const icon = container.querySelector( | ||
'[aria-roledescription="alert-circle"]' | ||
) | ||
expect(icon).toBeNull() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import * as React from 'react' | ||
import { | ||
ALIGN_CENTER, | ||
BORDERS, | ||
Box, | ||
COLORS, | ||
Flex, | ||
Icon, | ||
JUSTIFY_CENTER, | ||
OVERFLOW_AUTO, | ||
POSITION_ABSOLUTE, | ||
POSITION_RELATIVE, | ||
POSITION_STICKY, | ||
SPACING, | ||
} from '@opentrons/components' | ||
import type { IconName } from '@opentrons/components' | ||
|
||
export type ModalType = 'intervention-required' | 'error' | ||
|
||
const BASE_STYLE = { | ||
position: POSITION_ABSOLUTE, | ||
alignItems: ALIGN_CENTER, | ||
justifyContent: JUSTIFY_CENTER, | ||
top: 0, | ||
right: 0, | ||
bottom: 0, | ||
left: 0, | ||
width: '100%', | ||
height: '100%', | ||
'data-testid': '__otInterventionModalHeaderBase', | ||
} as const | ||
|
||
const BORDER_STYLE_BASE = `6px ${BORDERS.styleSolid}` | ||
|
||
const MODAL_STYLE = { | ||
backgroundColor: COLORS.white, | ||
position: POSITION_RELATIVE, | ||
overflowY: OVERFLOW_AUTO, | ||
maxHeight: '100%', | ||
width: '47rem', | ||
borderRadius: BORDERS.borderRadius8, | ||
boxShadow: BORDERS.smallDropShadow, | ||
'data-testid': '__otInterventionModal', | ||
} as const | ||
|
||
const HEADER_STYLE = { | ||
alignItems: ALIGN_CENTER, | ||
gridGap: SPACING.spacing12, | ||
padding: `${SPACING.spacing20} ${SPACING.spacing32}`, | ||
color: COLORS.white, | ||
position: POSITION_STICKY, | ||
top: 0, | ||
'data-testid': '__otInterventionModalHeader', | ||
} as const | ||
|
||
const WRAPPER_STYLE = { | ||
position: POSITION_ABSOLUTE, | ||
left: '0', | ||
right: '0', | ||
top: '0', | ||
bottom: '0', | ||
zIndex: '1', | ||
backgroundColor: `${COLORS.black90}${COLORS.opacity40HexCode}`, | ||
cursor: 'default', | ||
'data-testid': '__otInterventionModalWrapper', | ||
} as const | ||
|
||
const INTERVENTION_REQUIRED_COLOR = COLORS.blue50 | ||
const ERROR_COLOR = COLORS.red50 | ||
|
||
export interface InterventionModalProps { | ||
/** optional modal heading **/ | ||
heading?: React.ReactNode | ||
/** overall style hint */ | ||
type?: ModalType | ||
/** optional icon name */ | ||
iconName?: IconName | null | undefined | ||
/** modal contents */ | ||
children: React.ReactNode | ||
} | ||
|
||
export function InterventionModal(props: InterventionModalProps): JSX.Element { | ||
const modalType = props.type ?? 'intervention-required' | ||
const headerColor = | ||
modalType === 'error' ? ERROR_COLOR : INTERVENTION_REQUIRED_COLOR | ||
const border = `${BORDER_STYLE_BASE} ${ | ||
modalType === 'error' ? ERROR_COLOR : INTERVENTION_REQUIRED_COLOR | ||
}` | ||
return ( | ||
<Flex {...WRAPPER_STYLE}> | ||
<Flex {...BASE_STYLE} zIndex={10}> | ||
<Box | ||
{...MODAL_STYLE} | ||
border={border} | ||
onClick={(e: React.MouseEvent) => { | ||
e.stopPropagation() | ||
}} | ||
> | ||
<Flex {...HEADER_STYLE} backgroundColor={headerColor}> | ||
{props.iconName != null ? ( | ||
<Icon name={props.iconName} size={SPACING.spacing32} /> | ||
) : null} | ||
{props.heading != null ? props.heading : null} | ||
</Flex> | ||
{props.children} | ||
</Box> | ||
</Flex> | ||
</Flex> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters