Skip to content

Commit d6bbafb

Browse files
authored
Migrate Payment and PaymentForm to Chakra V3 (#2661)
Migrate Payment and PaymentForm to Chakra V3 (#2661)
1 parent 0e0735a commit d6bbafb

File tree

8 files changed

+303
-120
lines changed

8 files changed

+303
-120
lines changed

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ module.exports = {
9494
},
9595
setupFilesAfterEnv: [path.join(__dirname, 'jest-setup.js')],
9696
collectCoverageFrom: [
97+
'src/**/*.{js,jsx}',
9798
'app/**/*.{js,jsx}',
9899
'non-pwa/**/*.{js,jsx}',
99100
'worker/**/*.{js,jsx}',

src/components/field/index.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ const Field = ({
7676
>
7777
<PasswordIcon color="gray.500" boxSize={6} />
7878
</IconButton>
79-
) : undefined
79+
) : (
80+
_inputProps?.endElement
81+
)
8082
}
8183
>
8284
<>

src/components/field/index.test.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,64 @@ test('renders Field component without ref and works correctly', () => {
6969
fireEvent.change(emailInput, {target: {value: 'testuser@example.com'}})
7070
expect(emailInput.value).toBe('testuser@example.com')
7171
})
72+
73+
test('renders Field component with endElement', () => {
74+
const EndElementComponent = () => <span data-testid="end-element">USD</span>
75+
76+
renderWithProviders(
77+
<TestComponent defaultValues={{amount: ''}}>
78+
{({control}) => (
79+
<Field
80+
name="amount"
81+
label="Amount"
82+
type="text"
83+
control={control}
84+
placeholder="Enter amount"
85+
inputProps={{
86+
endElement: <EndElementComponent />
87+
}}
88+
/>
89+
)}
90+
</TestComponent>
91+
)
92+
93+
const amountInput = screen.getByPlaceholderText('Enter amount')
94+
const endElement = screen.getByTestId('end-element')
95+
96+
expect(amountInput).toBeInTheDocument()
97+
expect(endElement).toBeInTheDocument()
98+
expect(endElement).toHaveTextContent('USD')
99+
})
100+
101+
test('renders Field component with password type shows password toggle endElement', () => {
102+
renderWithProviders(
103+
<TestComponent defaultValues={{password: ''}}>
104+
{({control}) => (
105+
<Field
106+
name="password"
107+
label="Password"
108+
type="password"
109+
control={control}
110+
placeholder="Enter your password"
111+
/>
112+
)}
113+
</TestComponent>
114+
)
115+
116+
const passwordInput = screen.getByPlaceholderText('Enter your password')
117+
const toggleButton = screen.getByLabelText('Show password')
118+
119+
expect(passwordInput).toBeInTheDocument()
120+
expect(passwordInput).toHaveAttribute('type', 'password')
121+
expect(toggleButton).toBeInTheDocument()
122+
123+
// Click toggle to show password
124+
fireEvent.click(toggleButton)
125+
expect(passwordInput).toHaveAttribute('type', 'text')
126+
expect(screen.getByLabelText('Hide password')).toBeInTheDocument()
127+
128+
// Click toggle again to hide password
129+
fireEvent.click(screen.getByLabelText('Hide password'))
130+
expect(passwordInput).toHaveAttribute('type', 'password')
131+
expect(screen.getByLabelText('Show password')).toBeInTheDocument()
132+
})

src/components/forms/credit-card-fields.jsx

Lines changed: 32 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@
44
* SPDX-License-Identifier: BSD-3-Clause
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7-
import React, {useState} from 'react'
7+
import React from 'react'
88
import PropTypes from 'prop-types'
99
import ccValidator from 'card-validator'
1010
import {useIntl} from 'react-intl'
11-
import {Box, Flex, FormLabel, InputRightElement, SimpleGrid, Stack, Tooltip} from '@chakra-ui/react'
11+
import {Box, Flex, SimpleGrid, Stack, Field as ChakraField} from '@chakra-ui/react'
12+
import Tooltip from '../../components/tooltip'
1213
import {formatCreditCardNumber, getCreditCardIcon} from '../../utils/cc-utils'
1314
import useCreditCardFields from '../../components/forms/useCreditCardFields'
1415
import Field from '../../components/field'
1516
import {AmexIcon, DiscoverIcon, MastercardIcon, VisaIcon, InfoIcon} from '../../components/icons'
1617

1718
const CreditCardFields = ({form, prefix = ''}) => {
1819
const {formatMessage} = useIntl()
19-
const [isTooltipOpen, setIsTooltipOpen] = useState(false)
2020
const fields = useCreditCardFields({form, prefix})
2121

2222
// Rerender the fields when we `cardType` changes so the detected
@@ -42,31 +42,15 @@ const CreditCardFields = ({form, prefix = ''}) => {
4242
description: 'Generic credit card security code help text'
4343
})
4444

45-
const handleTooltipClose = () => {
46-
setIsTooltipOpen(false)
47-
if (document) {
48-
document.removeEventListener('click', handleTooltipClose)
49-
document.removeEventListener('keydown', handleTooltipClose)
50-
}
51-
}
52-
53-
const handleTooltipOpen = () => {
54-
setIsTooltipOpen(true)
55-
if (document) {
56-
document.addEventListener('click', handleTooltipClose)
57-
document.addEventListener('keydown', handleTooltipClose)
58-
}
59-
}
60-
6145
return (
6246
<Box>
63-
<Stack spacing={5}>
47+
<Stack gap={5}>
6448
<Field
6549
{...fields.number}
6650
formLabel={
67-
<Flex justify="space-between">
68-
<FormLabel>{fields.number.label}</FormLabel>
69-
<Stack direction="row" spacing={1}>
51+
<Flex justify="space-between" align="center" w="full">
52+
<Box>{fields.number.label}</Box>
53+
<Stack direction="row" gap={1}>
7054
<VisaIcon layerStyle="ccIcon" />
7155
<MastercardIcon layerStyle="ccIcon" />
7256
<AmexIcon layerStyle="ccIcon" />
@@ -84,19 +68,17 @@ const CreditCardFields = ({form, prefix = ''}) => {
8468
: number
8569
form.setValue('cardType', card?.type || '')
8670
return onChange(formattedNumber)
87-
}
71+
},
72+
endElement:
73+
CardIcon && form.getValues().number?.length > 2 ? (
74+
<CardIcon layerStyle="ccIcon" />
75+
) : undefined
8876
})}
89-
>
90-
{CardIcon && form.getValues().number?.length > 2 && (
91-
<InputRightElement width="60px">
92-
<CardIcon layerStyle="ccIcon" />
93-
</InputRightElement>
94-
)}
95-
</Field>
77+
/>
9678

9779
<Field {...fields.holder} />
9880

99-
<SimpleGrid columns={[2, 2, 3]} spacing={5}>
81+
<SimpleGrid columns={[2, 2, 3]} gap={5}>
10082
<Field
10183
{...fields.expiry}
10284
inputProps={({onChange}) => ({
@@ -131,33 +113,31 @@ const CreditCardFields = ({form, prefix = ''}) => {
131113
<Field
132114
{...fields.securityCode}
133115
formLabel={
134-
<>
135-
<FormLabel display="inline" mr={1}>
136-
{fields.securityCode.label}
137-
</FormLabel>
138-
<Box
139-
onMouseEnter={handleTooltipOpen}
140-
onFocus={handleTooltipOpen}
141-
as="span"
142-
>
116+
<ChakraField.Label>
117+
<Flex align="center" justify="space-between">
118+
<Box>{fields.securityCode.label}</Box>
143119
<Tooltip
144-
hasArrow
145-
placement="top"
146-
label={securityCodeTooltipLabel}
147-
shouldWrapChildren={true}
148-
isOpen={isTooltipOpen}
120+
content={securityCodeTooltipLabel}
121+
contentProps={{
122+
css: {'--tooltip-bg': 'colors.blue.800'}
123+
}}
149124
>
150-
<InfoIcon
151-
boxSize={5}
152-
color="gray.700"
125+
<Box
126+
as="button"
153127
aria-label={formatMessage({
154128
id: 'credit_card_fields.tool_tip.security_code_aria_label',
155129
defaultMessage: 'Security code info'
156130
})}
157-
/>
131+
>
132+
<InfoIcon
133+
boxSize={4}
134+
color="gray.700"
135+
cursor="pointer"
136+
/>
137+
</Box>
158138
</Tooltip>
159-
</Box>
160-
</>
139+
</Flex>
140+
</ChakraField.Label>
161141
}
162142
/>
163143
</SimpleGrid>

src/components/tooltip/index.jsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright (c) 2025, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import React from 'react'
8+
import PropTypes from 'prop-types'
9+
import {Tooltip as ChakraTooltip} from '@chakra-ui/react'
10+
11+
const Tooltip = React.forwardRef(
12+
(
13+
{
14+
children,
15+
content,
16+
placement = 'top',
17+
showArrow = true,
18+
disabled = false,
19+
openDelay = 500,
20+
closeDelay = 500,
21+
contentProps,
22+
positioning
23+
},
24+
ref
25+
) => {
26+
if (disabled || !content) {
27+
return children
28+
}
29+
30+
const positioningConfig = positioning || {}
31+
const finalPositioning = {
32+
placement,
33+
...positioningConfig
34+
}
35+
36+
return (
37+
<ChakraTooltip.Root
38+
openDelay={openDelay}
39+
closeDelay={closeDelay}
40+
positioning={finalPositioning}
41+
>
42+
<ChakraTooltip.Trigger asChild ref={ref}>
43+
{children}
44+
</ChakraTooltip.Trigger>
45+
<ChakraTooltip.Positioner>
46+
<ChakraTooltip.Content {...contentProps}>
47+
{showArrow && (
48+
<ChakraTooltip.Arrow>
49+
<ChakraTooltip.ArrowTip />
50+
</ChakraTooltip.Arrow>
51+
)}
52+
{content}
53+
</ChakraTooltip.Content>
54+
</ChakraTooltip.Positioner>
55+
</ChakraTooltip.Root>
56+
)
57+
}
58+
)
59+
60+
Tooltip.displayName = 'Tooltip'
61+
62+
Tooltip.propTypes = {
63+
children: PropTypes.node.isRequired,
64+
content: PropTypes.node,
65+
placement: PropTypes.oneOf([
66+
'top',
67+
'top-start',
68+
'top-end',
69+
'bottom',
70+
'bottom-start',
71+
'bottom-end',
72+
'left',
73+
'left-start',
74+
'left-end',
75+
'right',
76+
'right-start',
77+
'right-end'
78+
]),
79+
showArrow: PropTypes.bool,
80+
disabled: PropTypes.bool,
81+
openDelay: PropTypes.number,
82+
closeDelay: PropTypes.number,
83+
contentProps: PropTypes.object,
84+
positioning: PropTypes.object
85+
}
86+
87+
export default Tooltip

src/pages/checkout/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ const Checkout = () => {
8686
/>
8787
<ShippingAddress />
8888
<ShippingOptions />
89-
{/* <Payment /> TODO: bring this back */}
89+
<Payment />
9090

9191
{step === 4 && (
9292
<Box pt={3} display={{base: 'none', lg: 'block'}}>

0 commit comments

Comments
 (0)