Skip to content

Commit f9ba7a4

Browse files
authored
feat(app): TouchControlButton story and refactored implementation (#20177)
# Overview Added `TouchControlButton` story to app atoms and adjusted props ## Test Plan and Hands on Testing Smoke tested app <img width="356" height="164" alt="Screenshot 2025-11-17 at 12 40 05 PM" src="https://github.com/user-attachments/assets/47661dfc-d290-4190-9ba3-3a09dc44d33a" /> Smoke tested storybook <img width="1040" height="203" alt="Screenshot 2025-11-17 at 12 35 16 PM" src="https://github.com/user-attachments/assets/aafa471d-d1d4-42fb-a49f-e2c6e02cdceb" /> <img width="575" height="701" alt="Screenshot 2025-11-17 at 12 35 23 PM" src="https://github.com/user-attachments/assets/7d5e75f0-b218-462d-9d24-a2897e1c1fe9" /> ## Changelog - Added isOnDevice to be a prop of the touch control button so that it rendered properly on storybook ## Risk assessment low Closes EXEC-1981
1 parent 3ef89b5 commit f9ba7a4

File tree

7 files changed

+190
-67
lines changed

7 files changed

+190
-67
lines changed
Lines changed: 81 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useSelector } from 'react-redux'
21
import styled from 'styled-components'
32

43
import {
@@ -10,51 +9,86 @@ import {
109
StyledText,
1110
} from '@opentrons/components'
1211

13-
import { getIsOnDevice } from '/app/redux/config'
14-
1512
interface TouchControlProps {
1613
title: string
1714
isActive: boolean
1815
onClick: () => void
16+
isOnDevice: boolean
1917
subText?: string
2018
}
2119

22-
const StyledTouchButton = styled(Btn)<{ isActive: boolean; isOnOdd: boolean }>`
23-
background-color: ${({ isActive, isOnOdd }) =>
24-
!isOnOdd ? COLORS.white : isActive ? COLORS.blue50 : COLORS.blue35};
25-
border: ${({ isActive, isOnOdd }) =>
26-
!isOnOdd
27-
? isActive
28-
? `1px ${COLORS.blue50} solid`
29-
: `1px ${COLORS.grey30} solid`
30-
: `1px ${COLORS.grey30} solid`};
20+
const getBgColor = (isActive: boolean, isOnDevice: boolean): string => {
21+
if (!isOnDevice) {
22+
return COLORS.white
23+
}
24+
25+
if (isActive) {
26+
return COLORS.blue50
27+
}
28+
29+
return COLORS.blue35
30+
}
31+
32+
const getBorderColor = (isActive: boolean, isOnDevice: boolean): string => {
33+
if (!isOnDevice) {
34+
if (isActive) {
35+
return COLORS.blue50
36+
}
37+
return COLORS.grey30
38+
}
39+
40+
return COLORS.grey30
41+
}
42+
43+
const getFocusBorderColor = (
44+
isActive: boolean,
45+
isOnDevice: boolean
46+
): string => {
47+
if (!isOnDevice) {
48+
if (isActive) {
49+
return COLORS.blue50
50+
}
51+
return COLORS.grey30
52+
}
53+
54+
return COLORS.white
55+
}
56+
57+
const StyledTouchButton = styled(Btn)<{
58+
isActive: boolean
59+
isOnDevice: boolean
60+
}>`
61+
background-color: ${({ isActive, isOnDevice }) =>
62+
getBgColor(isActive, isOnDevice)};
63+
64+
border: ${({ isActive, isOnDevice }) =>
65+
`1px ${getBorderColor(isActive, isOnDevice)} solid`};
66+
3167
cursor: ${CURSOR_DEFAULT};
32-
border-radius: ${({ isOnOdd }) =>
33-
isOnOdd ? BORDERS.borderRadius16 : BORDERS.borderRadius8};
68+
border-radius: ${({ isOnDevice }) =>
69+
isOnDevice ? BORDERS.borderRadius16 : BORDERS.borderRadius8};
3470
padding: ${SPACING.spacing8} ${SPACING.spacing20};
35-
text-align: ${({ isOnOdd }) => (isOnOdd ? 'left' : 'center')};
71+
text-align: ${({ isOnDevice }) => (isOnDevice ? 'left' : 'center')};
3672
3773
&:focus {
38-
background-color: ${({ isActive, isOnOdd }) =>
39-
!isOnOdd ? COLORS.white : isActive ? COLORS.blue50 : COLORS.blue35};
40-
border: ${({ isActive, isOnOdd }) =>
41-
!isOnOdd
42-
? isActive
43-
? `1px ${COLORS.blue50} solid`
44-
: `1px ${COLORS.grey30} solid`
45-
: `1px ${COLORS.white} solid`};
74+
background-color: ${({ isActive, isOnDevice }) =>
75+
getBgColor(isActive, isOnDevice)};
76+
77+
border: ${({ isActive, isOnDevice }) =>
78+
`1px ${getFocusBorderColor(isActive, isOnDevice)} solid`};
4679
}
4780
4881
&:active {
49-
background-color: ${({ isActive, isOnOdd }) =>
50-
!isOnOdd ? COLORS.white : isActive ? COLORS.blue50 : COLORS.blue35};
82+
background-color: ${({ isActive, isOnDevice }) =>
83+
getBgColor(isActive, isOnDevice)};
84+
5185
color: ${COLORS.blue50};
5286
border: 1px ${COLORS.blue50} solid;
5387
}
5488
5589
&:focus-visible {
56-
${({ isOnOdd }) =>
57-
!isOnOdd &&
90+
${({ isOnDevice }) =>
91+
!isOnDevice &&
5892
`
5993
color: ${COLORS.blue50};
6094
background-color: ${COLORS.white};
@@ -71,30 +105,36 @@ const StyledTouchButton = styled(Btn)<{ isActive: boolean; isOnOdd: boolean }>`
71105
`
72106

73107
export function TouchControlButton(props: TouchControlProps): JSX.Element {
74-
const { title, subText, isActive, onClick } = props
75-
const isOnOdd = useSelector(getIsOnDevice)
108+
const { title, isActive, onClick, subText, isOnDevice } = props
76109
return (
77-
<StyledTouchButton isActive={isActive} onClick={onClick} isOnOdd={isOnOdd}>
110+
<StyledTouchButton
111+
isActive={isActive}
112+
isOnDevice={isOnDevice}
113+
onClick={onClick}
114+
>
78115
<StyledText
79-
oddStyle={'bodyTextSemiBold'}
80-
desktopStyle={'bodyDefaultSemiBold'}
116+
oddStyle="bodyTextSemiBold"
117+
desktopStyle="bodyDefaultSemiBold"
81118
color={
82-
isActive && !isOnOdd
119+
isActive && !isOnDevice
83120
? COLORS.blue50
84-
: isOnOdd && isActive
121+
: isOnDevice && isActive
85122
? COLORS.white
86123
: COLORS.black90
87124
}
88125
>
89126
{title}
90127
</StyledText>
91-
<StyledText
92-
oddStyle={'bodyTextRegular'}
93-
desktopStyle={'captionRegular'}
94-
color={isActive && isOnOdd ? COLORS.white : COLORS.grey60}
95-
>
96-
{subText}
97-
</StyledText>
128+
129+
{subText && (
130+
<StyledText
131+
oddStyle="bodyTextRegular"
132+
desktopStyle="captionRegular"
133+
color={isActive && isOnDevice ? COLORS.white : COLORS.grey60}
134+
>
135+
{subText}
136+
</StyledText>
137+
)}
98138
</StyledTouchButton>
99139
)
100140
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { screen } from '@testing-library/react'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
4+
import { COLORS } from '@opentrons/components'
5+
6+
import { renderWithProviders } from '/app/__testing-utils__'
7+
8+
import { TouchControlButton } from '..'
9+
10+
import type { ComponentProps } from 'react'
11+
12+
const mockOnClick = vi.fn()
13+
14+
const render = (props: ComponentProps<typeof TouchControlButton>) => {
15+
return renderWithProviders(<TouchControlButton {...props} />)[0]
16+
}
17+
18+
describe('TouchControlButton', () => {
19+
let props: ComponentProps<typeof TouchControlButton>
20+
beforeEach(() => {
21+
props = {
22+
title: 'touch control button',
23+
isActive: true,
24+
onClick: mockOnClick,
25+
subText: 'touch control subtext',
26+
isOnDevice: false,
27+
}
28+
})
29+
it('renders touch control button active on desktop', () => {
30+
render(props)
31+
const button = screen.getByRole('button')
32+
expect(button).toHaveStyle(`background-color: ${COLORS.white}`)
33+
expect(button).toHaveStyle(`border: 1px ${COLORS.blue50} solid`)
34+
})
35+
it('renders touch control button inactive on desktop', () => {
36+
props.isActive = false
37+
render(props)
38+
const button = screen.getByRole('button')
39+
expect(button).toHaveStyle(`background-color: ${COLORS.white}`)
40+
expect(button).toHaveStyle(`border: 1px ${COLORS.grey30} solid`)
41+
})
42+
43+
it('renders touch control button active on ODD', () => {
44+
props.isOnDevice = true
45+
render(props)
46+
const button = screen.getByRole('button')
47+
expect(button).toHaveStyle(`background-color: ${COLORS.blue50}`)
48+
expect(button).toHaveStyle(`border: 1px ${COLORS.grey30} solid`)
49+
})
50+
it('renders touch control button inactive on ODD', () => {
51+
props.isActive = false
52+
props.isOnDevice = true
53+
render(props)
54+
const button = screen.getByRole('button')
55+
expect(button).toHaveStyle(`background-color: ${COLORS.blue35}`)
56+
expect(button).toHaveStyle(`border: 1px ${COLORS.grey30} solid`)
57+
})
58+
})

app/src/atoms/buttons/buttons.stories.tsx

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,14 @@ import {
2020
} from './index'
2121

2222
import type { Meta, Story } from '@storybook/react'
23+
import type { ComponentProps } from 'react'
2324

2425
export default {
2526
title: 'App/Atoms/Buttons',
2627
} as Meta
2728

28-
const TouchControlButtonTemplate: Story<
29-
React.ComponentProps<typeof TouchControlButton>
30-
> = args => {
31-
return (
32-
<Flex flexDirection={DIRECTION_ROW} gridGap={SPACING.spacing16}>
33-
<TouchControlButton {...args} />
34-
</Flex>
35-
)
36-
}
37-
38-
export const TouchControl = TouchControlButtonTemplate.bind({})
39-
TouchControl.args = {
40-
children: 'touch control button',
41-
}
42-
4329
const TertiaryButtonTemplate: Story<
44-
React.ComponentProps<typeof TertiaryButton>
30+
ComponentProps<typeof TertiaryButton>
4531
> = args => {
4632
const { children } = args
4733
return (
@@ -57,7 +43,7 @@ Tertiary.args = {
5743
}
5844

5945
const QuaternaryButtonTemplate: Story<
60-
React.ComponentProps<typeof QuaternaryButton>
46+
ComponentProps<typeof QuaternaryButton>
6147
> = args => {
6248
const { children } = args
6349
return (
@@ -73,7 +59,7 @@ Quaternary.args = {
7359
}
7460

7561
const SubmitPrimaryButtonTemplate: Story<
76-
React.ComponentProps<typeof SubmitPrimaryButton>
62+
ComponentProps<typeof SubmitPrimaryButton>
7763
> = args => {
7864
return (
7965
<Flex flexDirection={DIRECTION_ROW} width="15rem">
@@ -91,8 +77,30 @@ SubmitPrimary.args = {
9177
},
9278
disabled: false,
9379
}
80+
81+
const TouchControlButtonTemplate: Story<
82+
ComponentProps<typeof TouchControlButton>
83+
> = args => {
84+
return (
85+
<Flex>
86+
<TouchControlButton {...args} />
87+
</Flex>
88+
)
89+
}
90+
91+
export const TouchControl = TouchControlButtonTemplate.bind({})
92+
TouchControl.args = {
93+
title: 'touch control button',
94+
subText: 'touch control subtext',
95+
isActive: true,
96+
isOnDevice: false,
97+
onClick: () => {
98+
console.log('touch control button clicked')
99+
},
100+
}
101+
94102
const ToggleButtonTemplate: Story<
95-
React.ComponentProps<typeof ToggleButton>
103+
ComponentProps<typeof ToggleButton>
96104
> = args => {
97105
const { onClick, ...rest } = args
98106
const [isToggled, setIsToggled] = React.useState<boolean>(false)
@@ -113,7 +121,7 @@ Toggle.args = {
113121
}
114122

115123
const LongPressButtonTemplate: Story<
116-
React.ComponentProps<typeof PrimaryButton>
124+
ComponentProps<typeof PrimaryButton>
117125
> = args => {
118126
const { children } = args
119127
const longPress = useLongPress()
@@ -154,7 +162,7 @@ LongPress.args = {
154162
}
155163

156164
const TextOnlyButtonTemplate: Story<
157-
React.ComponentProps<typeof TextOnlyButton>
165+
ComponentProps<typeof TextOnlyButton>
158166
> = () => {
159167
const [count, setCount] = React.useState<number>(0)
160168
return (

app/src/molecules/JogControls/DirectionControl.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// jog controls component
22
import { useState } from 'react'
33
import { useTranslation } from 'react-i18next'
4+
import { useSelector } from 'react-redux'
45
import styled, { css } from 'styled-components'
56

67
import {
@@ -29,6 +30,7 @@ import {
2930
} from '@opentrons/components'
3031

3132
import { TouchControlButton } from '/app/atoms/buttons/TouchControlButton'
33+
import { getIsOnDevice } from '/app/redux/config'
3234

3335
import { HORIZONTAL_PLANE, VERTICAL_PLANE } from './constants'
3436
import { ControlContainer } from './ControlContainer'
@@ -455,7 +457,7 @@ export function TouchDirectionControl(
455457
initialPlane ?? planes[0]
456458
)
457459
const { i18n, t } = useTranslation(['robot_calibration'])
458-
460+
const isOnDevice = useSelector(getIsOnDevice)
459461
return (
460462
<Flex
461463
flex="1"
@@ -479,6 +481,7 @@ export function TouchDirectionControl(
479481
onClick={() => {
480482
setCurrentPlane(plane)
481483
}}
484+
isOnDevice={isOnDevice}
482485
title={CONTROLS_CONTENTS_BY_PLANE[plane].title}
483486
/>
484487
)

app/src/molecules/JogControls/StepSizeControl.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,14 @@ interface StepSizeControlProps {
5151
stepSizes: StepSize[]
5252
currentStepSize: StepSize
5353
setCurrentStepSize: (stepSize: StepSize) => void
54+
isOnDevice: boolean
5455
}
5556

5657
function StepSizeButtons({
5758
stepSizes,
5859
currentStepSize,
5960
setCurrentStepSize,
61+
isOnDevice,
6062
}: StepSizeControlProps): JSX.Element {
6163
const { t } = useTranslation('robot_calibration')
6264
return (
@@ -70,14 +72,15 @@ function StepSizeButtons({
7072
}}
7173
title={t(stepSizeTranslationKeyByStep[stepSize])}
7274
subText={`${stepSize} mm`}
75+
isOnDevice={isOnDevice}
7376
/>
7477
))}
7578
</>
7679
)
7780
}
7881

7982
export function StepSizeControl(props: StepSizeControlProps): JSX.Element {
80-
const { stepSizes, currentStepSize, setCurrentStepSize } = props
83+
const { stepSizes, currentStepSize, setCurrentStepSize, isOnDevice } = props
8184
const { t } = useTranslation(['robot_calibration'])
8285

8386
const increaseStepSize: () => void = () => {
@@ -124,6 +127,7 @@ export function StepSizeControl(props: StepSizeControlProps): JSX.Element {
124127
stepSizes={stepSizes}
125128
currentStepSize={currentStepSize}
126129
setCurrentStepSize={setCurrentStepSize}
130+
isOnDevice={isOnDevice}
127131
/>
128132
</Box>
129133
</Flex>

0 commit comments

Comments
 (0)