-
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.
chore(app): add one col simple buttons component (#15366)
This is a subcomponent that can provide modal content for InterventionModal. It lives inside the InterventionModal molecule (at least for now). It's a radio button list where you can select one of the provided buttons. The argument passing style is a little weird but it's because there must be at least two buttons, and this is a nice way to make it hard to do anything else. They aren't child nodes because the whole point of this component is to enforce a single button style. Figma: https://www.figma.com/design/8dMeu8MuPfXoORtOV6cACO/Feature%3A-Error-Recovery?node-id=2523-42677&t=yaBnNzMY2BoDzZZm-0 Storybook: https://s3-us-west-2.amazonaws.com/opentrons-components/exec-490-one-col-simple-buttons/index.html?path=%2Fdocs%2Fdesign-tokens-borderradius--docs Closes [EXEC-490](https://opentrons.atlassian.net/browse/EXEC-490) [EXEC-490]: https://opentrons.atlassian.net/browse/EXEC-490?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
- Loading branch information
Showing
5 changed files
with
230 additions
and
0 deletions.
There are no files selected for viewing
49 changes: 49 additions & 0 deletions
49
app/src/molecules/InterventionModal/ModalContentOneColSimpleButtons.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,49 @@ | ||
import * as React from 'react' | ||
import { ModalContentOneColSimpleButtons as ModalContentOneColSimpleButtonsComponent } from './ModalContentOneColSimpleButtons' | ||
|
||
import type { Meta, StoryObj } from '@storybook/react' | ||
|
||
const meta: Meta<typeof ModalContentOneColSimpleButtonsComponent> = { | ||
title: 'App/Molecules/InterventionModal/ModalContentOneColSimpleButtons', | ||
component: ModalContentOneColSimpleButtonsComponent, | ||
argTypes: { | ||
firstButton: { | ||
control: { type: 'text' }, | ||
}, | ||
secondButton: { | ||
control: { type: 'text' }, | ||
}, | ||
furtherButtons: { | ||
control: { type: 'array' }, | ||
}, | ||
}, | ||
} | ||
|
||
export default meta | ||
|
||
type Story = StoryObj<typeof ModalContentOneColSimpleButtonsComponent> | ||
|
||
export const ModalContentOneColSimpleButtons: Story = { | ||
args: { | ||
topText: 'This is the top text area.', | ||
firstButton: 'This is the first button', | ||
secondButton: 'This is the second button', | ||
furtherButtons: ['this is the third button', 'this is the fourth button'], | ||
}, | ||
render: (args, context) => { | ||
return ( | ||
<ModalContentOneColSimpleButtonsComponent | ||
topText={args.topText} | ||
firstButton={{ label: args.firstButton, value: args.firstButton }} | ||
secondButton={{ label: args.secondButton, value: args.secondButton }} | ||
furtherButtons={args.furtherButtons | ||
.map(label => | ||
label === '' || label == null | ||
? null | ||
: { label: label, value: label } | ||
) | ||
.filter(val => val != null)} | ||
/> | ||
) | ||
}, | ||
} |
57 changes: 57 additions & 0 deletions
57
app/src/molecules/InterventionModal/ModalContentOneColSimpleButtons.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,57 @@ | ||
import * as React from 'react' | ||
import { | ||
Flex, | ||
DIRECTION_COLUMN, | ||
SPACING, | ||
StyledText, | ||
TYPOGRAPHY, | ||
} from '@opentrons/components' | ||
import { RadioButton } from '../../atoms/buttons/RadioButton' | ||
|
||
export interface ButtonProps { | ||
label: string | ||
value: string | ||
onChange?: React.ChangeEventHandler<HTMLInputElement> | ||
} | ||
|
||
export interface ModalContentOneColSimpleButtonsProps { | ||
topText: string | ||
firstButton: ButtonProps | ||
secondButton: ButtonProps | ||
furtherButtons?: ButtonProps[] | ||
onSelect?: React.ChangeEventHandler<HTMLInputElement> | ||
} | ||
|
||
export function ModalContentOneColSimpleButtons( | ||
props: ModalContentOneColSimpleButtonsProps | ||
): JSX.Element { | ||
const [selected, setSelected] = React.useState<string | null>(null) | ||
const furtherButtons = props.furtherButtons ?? [] | ||
const buttons = [props.firstButton, props.secondButton, ...furtherButtons] | ||
return ( | ||
<Flex flexDirection={DIRECTION_COLUMN} gap={SPACING.spacing16}> | ||
<StyledText | ||
fontSize={TYPOGRAPHY.fontSize28} | ||
fontWeight={TYPOGRAPHY.fontWeightSemiBold} | ||
lineHeight={TYPOGRAPHY.lineHeight36} | ||
> | ||
{props.topText} | ||
</StyledText> | ||
<Flex flexDirection={DIRECTION_COLUMN} gap={SPACING.spacing4}> | ||
{buttons.map((buttonProps, idx) => ( | ||
<RadioButton | ||
key={`button${idx}-${buttonProps.value}`} | ||
buttonLabel={buttonProps.label} | ||
buttonValue={buttonProps.value} | ||
isSelected={selected === buttonProps.value} | ||
onChange={event => { | ||
setSelected(event.target.value) | ||
buttonProps?.onChange && buttonProps.onChange(event) | ||
props?.onSelect && props.onSelect(event) | ||
}} | ||
/> | ||
))} | ||
</Flex> | ||
</Flex> | ||
) | ||
} |
120 changes: 120 additions & 0 deletions
120
app/src/molecules/InterventionModal/__tests__/ModalContentOneColSimpleButtons.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,120 @@ | ||
import * as React from 'react' | ||
import { vi, describe, it, expect } from 'vitest' | ||
import { render, screen, fireEvent } from '@testing-library/react' | ||
|
||
import { ModalContentOneColSimpleButtons } from '../ModalContentOneColSimpleButtons' | ||
|
||
/* eslint-disable testing-library/no-node-access */ | ||
const inputElForButtonFromButtonText = (text: string): HTMLInputElement => | ||
((screen.getByText(text)?.parentElement?.parentElement | ||
?.firstChild as any) as HTMLInputElement) || | ||
(() => { | ||
throw new Error(`Could not find el for ${text}`) | ||
})() | ||
/* eslint-enable testing-library/no-node-access */ | ||
|
||
describe('InterventionModal', () => { | ||
it('renders top text', () => { | ||
render( | ||
<ModalContentOneColSimpleButtons | ||
topText={'top text'} | ||
firstButton={{ label: 'first button', value: 'first' }} | ||
secondButton={{ label: 'second button', value: 'second' }} | ||
/> | ||
) | ||
expect(screen.getByText('top text')).not.toBeNull() | ||
}) | ||
it('renders buttons', () => { | ||
render( | ||
<ModalContentOneColSimpleButtons | ||
topText={'top text'} | ||
firstButton={{ label: 'first button', value: 'first' }} | ||
secondButton={{ label: 'second button', value: 'second' }} | ||
furtherButtons={[ | ||
{ label: 'third button', value: 'third' }, | ||
{ label: 'fourth button', value: 'fourth' }, | ||
]} | ||
/> | ||
) | ||
expect(screen.getByText('first button')).not.toBeNull() | ||
expect(screen.getByText('second button')).not.toBeNull() | ||
expect(screen.getByText('third button')).not.toBeNull() | ||
expect(screen.getByText('fourth button')).not.toBeNull() | ||
}) | ||
it('enforces single-item selection', () => { | ||
render( | ||
<ModalContentOneColSimpleButtons | ||
topText={'top text'} | ||
firstButton={{ label: 'first button', value: 'first' }} | ||
secondButton={{ label: 'second button', value: 'second' }} | ||
furtherButtons={[{ label: 'third button', value: 'third' }]} | ||
/> | ||
) | ||
expect(inputElForButtonFromButtonText('first button').checked).toBeFalsy() | ||
expect(inputElForButtonFromButtonText('second button').checked).toBeFalsy() | ||
expect(inputElForButtonFromButtonText('third button').checked).toBeFalsy() | ||
|
||
fireEvent.click(inputElForButtonFromButtonText('first button')) | ||
expect(inputElForButtonFromButtonText('first button').checked).toBeTruthy() | ||
expect(inputElForButtonFromButtonText('second button').checked).toBeFalsy() | ||
expect(inputElForButtonFromButtonText('third button').checked).toBeFalsy() | ||
|
||
fireEvent.click(inputElForButtonFromButtonText('third button')) | ||
expect(inputElForButtonFromButtonText('first button').checked).toBeFalsy() | ||
expect(inputElForButtonFromButtonText('second button').checked).toBeFalsy() | ||
expect(inputElForButtonFromButtonText('third button').checked).toBeTruthy() | ||
}) | ||
|
||
it('propagates individual button onChange', () => { | ||
const onChange = vi.fn() | ||
render( | ||
<ModalContentOneColSimpleButtons | ||
topText={'top text'} | ||
firstButton={{ | ||
label: 'first button', | ||
value: 'first', | ||
onChange: onChange as React.ChangeEventHandler<HTMLInputElement>, | ||
}} | ||
secondButton={{ label: 'second button', value: 'second' }} | ||
furtherButtons={[{ label: 'third button', value: 'third' }]} | ||
/> | ||
) | ||
fireEvent.click(inputElForButtonFromButtonText('first button')) | ||
expect(onChange).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
target: expect.objectContaining({ value: 'first' }), | ||
}) | ||
) | ||
vi.restoreAllMocks() | ||
|
||
fireEvent.click(inputElForButtonFromButtonText('second button')) | ||
expect(onChange).not.toHaveBeenCalled() | ||
}) | ||
|
||
it('propagates whole-list onSelect', () => { | ||
const onSelect = vi.fn() | ||
render( | ||
<ModalContentOneColSimpleButtons | ||
topText={'top text'} | ||
firstButton={{ label: 'first button', value: 'first' }} | ||
secondButton={{ label: 'second button', value: 'second' }} | ||
furtherButtons={[{ label: 'third button', value: 'third' }]} | ||
onSelect={onSelect} | ||
/> | ||
) | ||
|
||
fireEvent.click(inputElForButtonFromButtonText('first button')) | ||
expect(onSelect).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
target: expect.objectContaining({ value: 'first' }), | ||
}) | ||
) | ||
vi.restoreAllMocks() | ||
fireEvent.click(inputElForButtonFromButtonText('third button')) | ||
expect(onSelect).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
target: expect.objectContaining({ value: 'third' }), | ||
}) | ||
) | ||
}) | ||
}) |
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
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