-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add payment method logos to card label (#10005)
Co-authored-by: Guilherme Pressutto <gpressutto5@gmail.com>
- Loading branch information
1 parent
78b4854
commit 1ff074e
Showing
26 changed files
with
760 additions
and
38 deletions.
There are no files selected for viewing
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,4 @@ | ||
Significance: minor | ||
Type: add | ||
|
||
Add payment method logos to checkout block card label. |
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,4 @@ | ||
Significance: minor | ||
Type: update | ||
|
||
Update Credit Card / Debit Card label to Cards |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { PaymentMethodsLogos } from './payment-methods-logos'; |
133 changes: 133 additions & 0 deletions
133
client/checkout/blocks/payment-methods-logos/logo-popover.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,133 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import React, { | ||
useEffect, | ||
useLayoutEffect, | ||
useRef, | ||
useState, | ||
useCallback, | ||
} from 'react'; | ||
|
||
interface LogoPopoverProps { | ||
id: string; | ||
className?: string; | ||
children: React.ReactNode; | ||
anchor: HTMLElement | null; | ||
open: boolean; | ||
onClose?: () => void; | ||
dataTestId?: string; | ||
} | ||
|
||
export const LogoPopover: React.FC< LogoPopoverProps > = ( { | ||
id, | ||
className, | ||
children, | ||
anchor, | ||
open, | ||
onClose, | ||
dataTestId, | ||
} ) => { | ||
const popoverRef = useRef< HTMLDivElement >( null ); | ||
const [ isPositioned, setIsPositioned ] = useState( false ); | ||
|
||
const updatePosition = useCallback( () => { | ||
const popover = popoverRef.current; | ||
if ( ! popover || ! anchor ) { | ||
return; | ||
} | ||
|
||
const label = anchor.closest( 'label' ); | ||
if ( ! label ) return; | ||
|
||
const labelRect = label.getBoundingClientRect(); | ||
const labelStyle = window.getComputedStyle( label ); | ||
const labelPaddingRight = parseInt( labelStyle.paddingRight, 10 ); | ||
|
||
popover.style.position = 'fixed'; | ||
popover.style.right = `${ | ||
window.innerWidth - ( labelRect.right - labelPaddingRight ) | ||
}px`; | ||
popover.style.top = `${ labelRect.top - 30 }px`; | ||
popover.style.left = 'auto'; | ||
|
||
setIsPositioned( true ); | ||
}, [ anchor ] ); | ||
|
||
useLayoutEffect( () => { | ||
if ( open && anchor ) { | ||
// Use requestAnimationFrame to ensure the DOM has updated before positioning | ||
requestAnimationFrame( updatePosition ); | ||
} | ||
}, [ open, anchor, updatePosition ] ); | ||
|
||
useEffect( () => { | ||
if ( open && anchor ) { | ||
const observer = new MutationObserver( updatePosition ); | ||
observer.observe( anchor, { | ||
attributes: true, | ||
childList: true, | ||
subtree: true, | ||
} ); | ||
|
||
window.addEventListener( 'resize', updatePosition ); | ||
window.addEventListener( 'scroll', updatePosition ); | ||
|
||
const handleOutsideClick = ( event: MouseEvent ) => { | ||
if ( | ||
popoverRef.current && | ||
! popoverRef.current.contains( event.target as Node ) && | ||
! anchor.contains( event.target as Node ) | ||
) { | ||
onClose?.(); | ||
} | ||
}; | ||
|
||
const handleEscapeKey = ( event: KeyboardEvent ) => { | ||
if ( event.key === 'Escape' ) { | ||
onClose?.(); | ||
} | ||
}; | ||
|
||
document.addEventListener( 'mousedown', handleOutsideClick ); | ||
document.addEventListener( 'keydown', handleEscapeKey ); | ||
|
||
return () => { | ||
observer.disconnect(); | ||
window.removeEventListener( 'resize', updatePosition ); | ||
window.removeEventListener( 'scroll', updatePosition ); | ||
document.removeEventListener( 'mousedown', handleOutsideClick ); | ||
document.removeEventListener( 'keydown', handleEscapeKey ); | ||
}; | ||
} | ||
}, [ open, anchor, updatePosition, onClose ] ); | ||
|
||
if ( ! open ) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<div | ||
id={ id } | ||
ref={ popoverRef } | ||
className={ `logo-popover ${ className || '' }` } | ||
style={ { | ||
position: 'fixed', | ||
zIndex: 1000, | ||
opacity: isPositioned ? 1 : 0, | ||
transition: 'opacity 0.2s', | ||
gridTemplateColumns: `repeat(${ | ||
React.Children.count( children ) > 5 | ||
? 5 | ||
: React.Children.count( children ) | ||
}, 38px)`, | ||
left: 'auto', | ||
} } | ||
role="dialog" | ||
aria-label="Supported Credit Card Brands" | ||
data-testid={ dataTestId } | ||
> | ||
{ children } | ||
</div> | ||
); | ||
}; |
143 changes: 143 additions & 0 deletions
143
client/checkout/blocks/payment-methods-logos/payment-methods-logos.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,143 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import React, { useState, useEffect, useCallback, useRef } from 'react'; | ||
/** | ||
* Internal dependencies | ||
*/ | ||
import { LogoPopover } from './logo-popover'; | ||
import './style.scss'; | ||
|
||
interface BreakpointConfig { | ||
breakpoint: number; | ||
maxElements: number; | ||
} | ||
|
||
interface PaymentMethodsLogosProps { | ||
maxElements: number; | ||
paymentMethods: { name: string; component: string }[]; | ||
breakpointConfigs?: BreakpointConfig[]; | ||
} | ||
|
||
const breakpointConfigsDefault = [ | ||
{ breakpoint: 480, maxElements: 5 }, | ||
{ breakpoint: 768, maxElements: 7 }, | ||
]; | ||
const paymentMethodsDefault: never[] = []; | ||
export const PaymentMethodsLogos: React.FC< PaymentMethodsLogosProps > = ( { | ||
maxElements = 10, | ||
paymentMethods = paymentMethodsDefault, | ||
breakpointConfigs = breakpointConfigsDefault, | ||
} ) => { | ||
const [ maxShownElements, setMaxShownElements ] = useState( maxElements ); | ||
const [ | ||
popoverAnchor, | ||
setPopoverAnchor, | ||
] = useState< HTMLDivElement | null >( null ); | ||
const [ popoverOpen, setPopoverOpen ] = useState( false ); | ||
const [ shouldHavePopover, setShouldHavePopover ] = useState( false ); | ||
|
||
const togglePopover = () => setPopoverOpen( ! popoverOpen ); | ||
|
||
const anchorRef = useCallback( ( node: HTMLDivElement | null ) => { | ||
if ( node !== null ) { | ||
setPopoverAnchor( node ); | ||
} | ||
}, [] ); | ||
|
||
const buttonRef = useRef< HTMLDivElement | null >( null ); | ||
|
||
const handlePopoverClose = useCallback( () => { | ||
setPopoverOpen( false ); | ||
buttonRef.current?.focus(); | ||
}, [] ); | ||
|
||
useEffect( () => { | ||
const updateMaxElements = () => { | ||
const sortedConfigs = [ ...breakpointConfigs ].sort( | ||
( a, b ) => a.breakpoint - b.breakpoint | ||
); | ||
const config = sortedConfigs.find( | ||
( cfg ) => window.innerWidth <= cfg.breakpoint | ||
); | ||
|
||
setMaxShownElements( config ? config.maxElements : maxElements ); | ||
}; | ||
|
||
updateMaxElements(); | ||
window.addEventListener( 'resize', updateMaxElements ); | ||
|
||
return () => window.removeEventListener( 'resize', updateMaxElements ); | ||
}, [ breakpointConfigs, maxElements ] ); | ||
|
||
useEffect( () => { | ||
if ( popoverAnchor ) { | ||
buttonRef.current = popoverAnchor; | ||
} | ||
}, [ popoverAnchor ] ); | ||
|
||
useEffect( () => { | ||
setShouldHavePopover( paymentMethods.length > maxShownElements ); | ||
}, [ maxShownElements, paymentMethods.length ] ); | ||
|
||
return ( | ||
<> | ||
<div className="payment-methods--logos"> | ||
<div | ||
ref={ anchorRef } | ||
{ ...( shouldHavePopover && { | ||
onClick: togglePopover, | ||
onKeyDown: ( e ) => { | ||
if ( e.key === 'Enter' || e.key === ' ' ) { | ||
e.preventDefault(); | ||
togglePopover(); | ||
} | ||
}, | ||
role: 'button', | ||
tabIndex: 0, | ||
'aria-expanded': popoverOpen, | ||
'aria-controls': 'payment-methods-popover', | ||
} ) } | ||
data-testid="payment-methods-logos" | ||
> | ||
{ paymentMethods | ||
.slice( 0, maxShownElements ) | ||
.map( ( pm ) => ( | ||
<img | ||
key={ pm.name } | ||
alt={ pm.name } | ||
src={ pm.component } | ||
width={ 38 } | ||
height={ 24 } | ||
/> | ||
) ) } | ||
{ shouldHavePopover && ( | ||
<div className="payment-methods--logos-count"> | ||
+ { paymentMethods.length - maxShownElements } | ||
</div> | ||
) } | ||
</div> | ||
</div> | ||
{ shouldHavePopover && popoverOpen && ( | ||
<LogoPopover | ||
id="payment-methods-popover" | ||
className="payment-methods--logos-popover" | ||
anchor={ popoverAnchor } | ||
open={ popoverOpen } | ||
onClose={ handlePopoverClose } | ||
dataTestId="payment-methods-popover" | ||
> | ||
{ paymentMethods.slice( maxShownElements ).map( ( pm ) => ( | ||
<img | ||
key={ pm.name } | ||
alt={ pm.name } | ||
src={ pm.component } | ||
width={ 38 } | ||
height={ 24 } | ||
/> | ||
) ) } | ||
</LogoPopover> | ||
) } | ||
</> | ||
); | ||
}; |
Oops, something went wrong.