Skip to content

Commit cfad590

Browse files
authored
Merge pull request #944 from rpearce/fix/prevent-zoom-click-event-propagation
fix: prevent zoom click even propagation
2 parents bfbd6ff + 83f7b9a commit cfad590

File tree

6 files changed

+161
-22
lines changed

6 files changed

+161
-22
lines changed

.changeset/red-dingos-know.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-medium-image-zoom": minor
3+
---
4+
5+
Add support for onZoomChange reporting to Uncontrolled components and include additional argument housing the fired event

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ export interface UncontrolledProps {
101101
// Default: false
102102
isDisabled?: boolean
103103

104+
// First argument: boolean value of a new zoomed state (Uncontrolled
105+
// component) or a suggested new state (Controlled component).
106+
// Second argument: object containing the event that triggered the change.
107+
// Default: undefined
108+
onZoomChange?: (
109+
value: boolean,
110+
data: { event: React.SyntheticEvent | Event }
111+
) => void
112+
104113
// Swipe gesture threshold after which to unzoom.
105114
// Default: 10
106115
swipeToUnzoomThreshold?: number
@@ -137,10 +146,6 @@ export interface ControlledProps {
137146
// Tell the component whether or not it should be zoomed
138147
// Default: false
139148
isZoomed: boolean
140-
141-
// Listen for hints from the component about when you
142-
// should zoom (`true` value) or unzoom (`false` value)
143-
onZoomChange?: (value: boolean) => void
144149
}
145150
```
146151

source/Controlled.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,15 @@ export interface ControlledProps {
6363
IconZoom?: React.ElementType
6464
isDisabled?: boolean
6565
isZoomed: boolean
66-
onZoomChange?: (value: boolean) => void
66+
onZoomChange?: (value: boolean, data: { event: React.SyntheticEvent | Event }) => void
6767
swipeToUnzoomThreshold?: number
6868
wrapElement?: 'div' | 'span'
6969
ZoomContent?: (data: {
7070
buttonUnzoom: React.ReactElement<HTMLButtonElement>
7171
img: React.ReactElement | null
7272
isZoomImgLoaded: boolean
7373
modalState: ModalState
74-
onUnzoom: () => void
74+
onUnzoom: (e: Event) => void
7575
}) => React.ReactElement
7676
zoomImg?: React.ImgHTMLAttributes<HTMLImageElement>
7777
zoomMargin?: number
@@ -527,18 +527,18 @@ class ControlledBase extends React.Component<ControlledPropsWithDefaults, Contro
527527
/**
528528
* Report that zooming should occur
529529
*/
530-
handleZoom = () => {
530+
handleZoom = (e: React.SyntheticEvent | Event) => {
531531
if (!this.props.isDisabled && this.hasImage()) {
532-
this.props.onZoomChange?.(true)
532+
this.props.onZoomChange?.(true, { event: e })
533533
}
534534
}
535535

536536
/**
537537
* Report that unzooming should occur
538538
*/
539-
handleUnzoom = () => {
539+
handleUnzoom = (e: React.SyntheticEvent | Event) => {
540540
if (!this.props.isDisabled) {
541-
this.props.onZoomChange?.(false)
541+
this.props.onZoomChange?.(false, { event: e })
542542
}
543543
}
544544

@@ -550,7 +550,7 @@ class ControlledBase extends React.Component<ControlledPropsWithDefaults, Contro
550550
handleBtnUnzoomClick = (e: React.MouseEvent<HTMLButtonElement>) => {
551551
e.preventDefault()
552552
e.stopPropagation()
553-
this.handleUnzoom()
553+
this.handleUnzoom(e)
554554
}
555555

556556
// ===========================================================================
@@ -570,7 +570,7 @@ class ControlledBase extends React.Component<ControlledPropsWithDefaults, Contro
570570
handleDialogClick = (e: React.MouseEvent<HTMLDialogElement>) => {
571571
if (e.target === this.refModalContent.current || e.target === this.refModalImg.current) {
572572
e.stopPropagation()
573-
this.handleUnzoom()
573+
this.handleUnzoom(e)
574574
}
575575
}
576576

@@ -581,7 +581,7 @@ class ControlledBase extends React.Component<ControlledPropsWithDefaults, Contro
581581
*/
582582
handleDialogClose = (e: React.SyntheticEvent<HTMLDialogElement>) => {
583583
e.stopPropagation()
584-
this.handleUnzoom()
584+
this.handleUnzoom(e)
585585
}
586586

587587
// ===========================================================================
@@ -593,7 +593,7 @@ class ControlledBase extends React.Component<ControlledPropsWithDefaults, Contro
593593
if (e.key === 'Escape' || e.keyCode === 27) {
594594
e.preventDefault()
595595
e.stopPropagation()
596-
this.handleUnzoom()
596+
this.handleUnzoom(e)
597597
}
598598
}
599599

@@ -608,7 +608,7 @@ class ControlledBase extends React.Component<ControlledPropsWithDefaults, Contro
608608

609609
e.stopPropagation()
610610
queueMicrotask(() => {
611-
this.handleUnzoom()
611+
this.handleUnzoom(e)
612612
})
613613
}
614614

@@ -650,7 +650,7 @@ class ControlledBase extends React.Component<ControlledPropsWithDefaults, Contro
650650
if (delta > this.props.swipeToUnzoomThreshold) {
651651
this.touchYStart = undefined
652652
this.touchYEnd = undefined
653-
this.handleUnzoom()
653+
this.handleUnzoom(e)
654654
}
655655
}
656656
}

source/Uncontrolled.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import React from 'react'
2-
import { Controlled, ControlledProps } from './Controlled'
2+
import { Controlled, type ControlledProps } from './Controlled'
33

44
// =============================================================================
55

66
export type UncontrolledProps =
7-
Omit<ControlledProps, 'isZoomed' | 'onZoomChange'>
7+
Omit<ControlledProps, 'isZoomed'>
88

9-
export function Uncontrolled (props: UncontrolledProps) {
9+
export function Uncontrolled ({ onZoomChange, ...props }: UncontrolledProps) {
1010
const [isZoomed, setIsZoomed] = React.useState(false)
1111

12-
return <Controlled {...props} isZoomed={isZoomed} onZoomChange={setIsZoomed} />
12+
const handleZoomChange = React.useCallback<
13+
NonNullable<ControlledProps['onZoomChange']>
14+
>((value, { event }) => {
15+
setIsZoomed(value)
16+
onZoomChange?.(value, { event })
17+
}, [onZoomChange])
18+
19+
return <Controlled {...props} isZoomed={isZoomed} onZoomChange={handleZoomChange} />
1320
}

stories/Img.stories.tsx

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Meta } from '@storybook/react-webpack5'
33

44
import { waitFor, within, userEvent, expect } from 'storybook/test'
55

6-
import Zoom, { UncontrolledProps } from '../source'
6+
import Zoom, { type UncontrolledProps } from '../source'
77
import '../source/styles.css'
88
import './base.css'
99

@@ -45,11 +45,17 @@ function shuffle<T extends unknown[]>(xs: T): T {
4545
// =============================================================================
4646

4747
export const Regular = (props: typeof Zoom) => {
48+
const handleZoomChange = React.useCallback<
49+
NonNullable<React.ComponentProps<typeof Zoom>['onZoomChange']>
50+
>((value, { event }) => {
51+
console.log('handleZoomChange info!', { value, event })
52+
}, [])
53+
4854
return (
4955
<main aria-label="Story">
5056
<h1>Zooming a regular image</h1>
5157
<div className="mw-600" style={{ display: 'flex', flexDirection: 'column' }}>
52-
<Zoom {...props} wrapElement="span">
58+
<Zoom {...props} onZoomChange={handleZoomChange} wrapElement="span">
5359
<img
5460
alt={imgThatWanakaTree.alt}
5561
src={imgThatWanakaTree.src}
@@ -734,6 +740,90 @@ export const SwipeToUnzoomThreshold = (props: typeof Zoom) => (
734740
</main>
735741
)
736742

743+
// =============================================================================
744+
745+
export const SelectCards = (props: typeof Zoom) => {
746+
return (
747+
<main aria-label="Story">
748+
<h1>Selecting cards and zooming without triggering selection state</h1>
749+
<div className="mw-600" style={{ display: 'flex', flexDirection: 'column' }}>
750+
<ul className="cards">
751+
<CardItem
752+
alt={imgThatWanakaTree.alt}
753+
src={imgThatWanakaTree.src}
754+
zoomProps={props}
755+
/>
756+
<CardItem
757+
alt={imgGlenorchyLagoon.alt}
758+
src={imgGlenorchyLagoon.src}
759+
zoomProps={props}
760+
/>
761+
</ul>
762+
</div>
763+
</main>
764+
)
765+
}
766+
767+
function CardItem({
768+
alt,
769+
src,
770+
zoomProps,
771+
}: {
772+
alt: string,
773+
src: string,
774+
zoomProps: typeof Zoom,
775+
}) {
776+
const [isSelected, setIsSelected] = React.useState(false)
777+
778+
const handleItemClick = React.useCallback(() => {
779+
setIsSelected(isSelected => !isSelected)
780+
}, [])
781+
782+
const handleInputClick: React.MouseEventHandler<HTMLInputElement> = React.useCallback((e) => {
783+
e.stopPropagation()
784+
}, [])
785+
786+
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((e) => {
787+
setIsSelected(e.currentTarget.checked)
788+
}, [])
789+
790+
const handleZoomChange = React.useCallback<
791+
NonNullable<React.ComponentProps<typeof Zoom>['onZoomChange']>
792+
>((value, { event }) => {
793+
event.stopPropagation()
794+
795+
console.log(
796+
'handleZoomChange (after event.stopPropagation())',
797+
{ value, event }
798+
)
799+
}, [])
800+
801+
return (
802+
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
803+
<li className="card" onClick={handleItemClick}>
804+
<label>
805+
<input
806+
aria-label="Select item"
807+
checked={isSelected}
808+
onChange={handleInputChange}
809+
onClick={handleInputClick}
810+
type="checkbox"
811+
/>
812+
</label>
813+
<Zoom {...zoomProps} onZoomChange={handleZoomChange} wrapElement="span">
814+
<img
815+
alt={alt}
816+
src={src}
817+
height="320"
818+
width="320"
819+
decoding="async"
820+
loading="lazy"
821+
/>
822+
</Zoom>
823+
</li>
824+
)
825+
}
826+
737827
// =============================================================================
738828
// INTERACTIONS
739829

stories/base.css

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,38 @@ img {
143143
border-radius: 50%;
144144
animation: spin 1s linear infinite;
145145
}
146+
.cards {
147+
align-items: baseline;
148+
display: flex;
149+
list-style: none;
150+
margin: 0;
151+
padding: 0;
152+
gap: 2rem;
153+
}
154+
.card {
155+
border: 1px solid #c0c0c0;
156+
border-radius: 8px;
157+
flex-shrink: 0;
158+
padding-block: 3.2rem 1.2rem;
159+
padding-inline: 1.2rem;
160+
position: relative;
161+
}
162+
.card label {
163+
align-items: center;
164+
background-color: #fff;
165+
display: flex;
166+
height: max-content;
167+
inset-block-start: 0.8rem;
168+
inset-inline: auto 1rem;
169+
justify-content: center;
170+
position: absolute;
171+
width: max-content;
172+
}
173+
.card img {
174+
object-fit: cover;
175+
object-position: center;
176+
user-select: none;
177+
}
146178
@keyframes spin {
147179
0% { transform: rotate(0deg); }
148180
100% { transform: rotate(360deg); }

0 commit comments

Comments
 (0)