-
Notifications
You must be signed in to change notification settings - Fork 178
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
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
- Loading branch information
Showing
4 changed files
with
225 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">Here's some error content</StyledText>, | ||
} | ||
|
||
export const InterventionRequiredIntervention = Template.bind({}) | ||
InterventionRequiredIntervention.args = { | ||
robotName: 'Otie', | ||
type: 'intervention-required', | ||
heading: <StyledText as="h3">Looks like there's something to do</StyledText>, | ||
children: <StyledText as="p">You've got to intervene!</StyledText>, | ||
} |
56 changes: 56 additions & 0 deletions
56
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,56 @@ | ||
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 '../' | ||
|
||
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'].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('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,114 @@ | ||
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 === 'intervention-required' | ||
? INTERVENTION_REQUIRED_COLOR | ||
: ERROR_COLOR | ||
const border = `${BORDER_STYLE_BASE} ${ | ||
props.type === 'intervention-required' | ||
? INTERVENTION_REQUIRED_COLOR | ||
: ERROR_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