Skip to content

Commit

Permalink
Feature: ZoomContent customization
Browse files Browse the repository at this point in the history
  • Loading branch information
rpearce committed Aug 9, 2022
1 parent 61e8d4c commit 5fd0a82
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 41 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export interface UncontrolledProps {
// Default: 'Expand image'
a11yNameButtonZoom?: string

// Your image
// Your image (required)
children: ReactNode

// Provide your own unzoom button icon
Expand All @@ -83,6 +83,13 @@ export interface UncontrolledProps {
// Default: window
scrollableEl?: Window | HTMLElement

// Provide your own custom modal content component
ZoomContent?: (props: {
img: ReactElement | null;
buttonUnzoom: ReactElement<HTMLButtonElement>;
onUnzoom: () => void;
}) => ReactElement;

// Higher quality image attributes to use on zoom
zoomImg?: ImgHTMLAttributes<HTMLImageElement>

Expand Down
98 changes: 63 additions & 35 deletions source/Controlled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import React, {
ElementType,
ImgHTMLAttributes,
KeyboardEvent,
MouseEvent,
ReactElement,
ReactNode,
createRef,
} from 'react'
Expand Down Expand Up @@ -41,6 +43,11 @@ export interface ControlledProps {
isZoomed: boolean
onZoomChange?: (value: boolean) => void
scrollableEl?: Window | HTMLElement
ZoomContent?: (data: {
img: ReactElement | null
buttonUnzoom: ReactElement<HTMLButtonElement>
onUnzoom: () => void
}) => ReactElement
zoomImg?: ImgHTMLAttributes<HTMLImageElement>
zoomMargin?: number
}
Expand Down Expand Up @@ -86,6 +93,7 @@ class ControlledBase extends Component<ControlledPropsWithDefaults, ControlledSt

private refContent = createRef<HTMLDivElement>()
private refDialog = createRef<HTMLDialogElement>()
private refModalContent = createRef<HTMLDivElement>()
private refModalImg = createRef<HTMLImageElement>()
private refWrap = createRef<HTMLDivElement>()

Expand All @@ -95,6 +103,7 @@ class ControlledBase extends Component<ControlledPropsWithDefaults, ControlledSt

render() {
const {
handleDialogClick,
handleDialogKeyDown,
handleUnzoom,
handleZoom,
Expand All @@ -106,11 +115,13 @@ class ControlledBase extends Component<ControlledPropsWithDefaults, ControlledSt
IconUnzoom,
IconZoom,
isZoomed,
ZoomContent,
zoomImg,
zoomMargin,
},
refContent,
refDialog,
refModalContent,
refModalImg,
refWrap,
state: {
Expand Down Expand Up @@ -173,6 +184,47 @@ class ControlledBase extends Component<ControlledPropsWithDefaults, ControlledSt

// =========================================================================

const modalImg = isImg || isDiv
? <img
alt={imgAlt}
sizes={imgSizes}
src={imgSrc}
srcSet={imgSrcSet}
{...isZoomImgLoaded && modalState === ModalState.LOADED ? zoomImg : {}}
data-rmiz-modal-img
height={this.styleModalImg.height}
id={idModalImg}
ref={refModalImg}
style={this.styleModalImg}
width={this.styleModalImg.width}
/>
: isSvg
? <div
data-rmiz-modal-img
ref={refModalImg}
style={this.styleModalImg}
/>
: null

const modalBtnUnzoom = <button
aria-label={a11yNameButtonUnzoom}
data-rmiz-btn-unzoom
onClick={handleUnzoom}
type="button"
>
<IconUnzoom />
</button>

const modalContent = ZoomContent
? <ZoomContent
buttonUnzoom={modalBtnUnzoom}
img={modalImg}
onUnzoom={handleUnzoom}
/>
: <>{modalImg}{modalBtnUnzoom}</>

// =========================================================================

return (
<div data-rmiz ref={refWrap}>
<div data-rmiz-content ref={refContent} style={styleContent}>
Expand All @@ -193,46 +245,13 @@ class ControlledBase extends Component<ControlledPropsWithDefaults, ControlledSt
aria-modal="true"
data-rmiz-modal
ref={refDialog}
onClick={handleUnzoom}
onClick={handleDialogClick}
onClose={handleUnzoom}
onKeyDown={handleDialogKeyDown}
role="dialog"
>
<div data-rmiz-modal-overlay={dataOverlayState} />
<div data-rmiz-modal-content>
{isImg || isDiv
? <img
alt={imgAlt}
sizes={imgSizes}
src={imgSrc}
srcSet={imgSrcSet}
{...isZoomImgLoaded && modalState === ModalState.LOADED ? zoomImg : {}}
data-rmiz-modal-img
height={this.styleModalImg.height}
id={idModalImg}
ref={refModalImg}
style={this.styleModalImg}
width={this.styleModalImg.width}
/>
: undefined
}
{isSvg
? <div
data-rmiz-modal-img
ref={refModalImg}
style={this.styleModalImg}
/>
: undefined
}
<button
aria-label={a11yNameButtonUnzoom}
data-rmiz-btn-unzoom
onClick={handleUnzoom}
type="button"
>
<IconUnzoom />
</button>
</div>
<div data-rmiz-modal-content ref={refModalContent}>{modalContent}</div>
</dialog>
</div>
)
Expand Down Expand Up @@ -343,6 +362,15 @@ class ControlledBase extends Component<ControlledPropsWithDefaults, ControlledSt
this.props.onZoomChange?.(false)
}

// ===========================================================================
// Have dialog.click() only close in certain situations

handleDialogClick = (e: MouseEvent<HTMLDialogElement>) => {
if (e.target === this.refModalContent.current || e.target === this.refModalImg.current) {
this.handleUnzoom()
}
}

// ===========================================================================
// Intercept default dialog.close() and use ours so we can animate

Expand Down
7 changes: 4 additions & 3 deletions source/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@
[data-rmiz-modal-overlay] {
position: absolute;
inset: 0;
pointer-events: all;
transition: background-color 0.3s;
}
[data-rmiz-modal-overlay="hidden"] {
Expand All @@ -78,14 +77,16 @@
[data-rmiz-modal-overlay="visible"] {
background-color: rgba(255, 255, 255, 1);
}
[data-rmiz-modal][open] [data-rmiz-modal-content] {
[data-rmiz-modal-content] {
position: relative;
pointer-events: all;
width: 100%;
height: 100%;
}
[data-rmiz-modal-img] {
position: absolute;
cursor: zoom-out;
image-rendering: high-quality;
pointer-events: all;
transform-origin: top left;
transition: transform 0.3s;
will-change: transform;
Expand Down
82 changes: 80 additions & 2 deletions stories/Img.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from 'react'
import React, { ReactElement, useLayoutEffect, useMemo, useState } from 'react'

import { ComponentStory, ComponentMeta } from '@storybook/react'
import { waitFor, within, userEvent } from '@storybook/testing-library'
import { expect } from '@storybook/jest'

import Zoom from '../source'
import Zoom, { UncontrolledProps } from '../source'
import '../source/styles.css'
import './base.css'

Expand Down Expand Up @@ -176,6 +176,69 @@ export const CustomModalStyles: ComponentStory<typeof Zoom> = (props) => (
</div>
)

export const ModalFigureCaption: ComponentStory<typeof Zoom> = (props) => (
<div>
<h1>Modal With Figure And Caption</h1>
<p>
If you want more control over the zoom modal&apos;s content, you can pass
a <code>ZoomContent</code> component
</p>
<div className="mw-600">
<Zoom {...props} ZoomContent={CustomZoomContent}>
<img
alt={imgThatWanakaTree.alt}
src={imgThatWanakaTree.src}
height="320"
loading="lazy"
/>
</Zoom>
</div>
</div>
)

const CustomZoomContent: UncontrolledProps['ZoomContent'] = ({ buttonUnzoom, img }) => {
const [isLoaded, setIsLoaded] = useState(false)

const imgProps = (img as ReactElement<HTMLImageElement>)?.props
const imgWidth = imgProps?.width
const imgHeight = imgProps?.height

const classCaption = useMemo(() => {
const hasWidthHeight = imgWidth && imgHeight
const imgRatioLargerThanWindow = imgWidth / imgHeight > window.innerWidth / window.innerHeight

return cx({
'zoom-caption': true,
'zoom-caption--loaded': isLoaded,
'zoom-caption--bottom': hasWidthHeight && imgRatioLargerThanWindow,
'zoom-caption--left': hasWidthHeight && !imgRatioLargerThanWindow,
})
}, [imgWidth, imgHeight, isLoaded])

// @TODO: this needs to be set on load/unload
useLayoutEffect(() => {
setIsLoaded(true)
}, [])

return <>
{buttonUnzoom}

<figure>
{img}
<figcaption className={classCaption}>
That Wanaka Tree, also known as the Wanaka Willow, is a willow tree
located at the southern end of Lake Wānaka in the Otago region of New
Zealand.
<cite className="zoom-caption-cite">
Wikipedia, <a className="zoom-caption-link" href="https://en.wikipedia.org/wiki/That_Wanaka_Tree">
That Wanaka Tree
</a>
</cite>
</figcaption>
</figure>
</>
}

export const CustomButtonIcons: ComponentStory<typeof Zoom> = (props) => (
<div>
<h1>An image with custom zoom &amp; unzoom icons</h1>
Expand Down Expand Up @@ -215,3 +278,18 @@ WithRegularZoomed.play = async ({ canvasElement }) => {
await expect(canvas.getByLabelText('Minimize image')).toHaveFocus()
})
}

// =============================================================================
// HELPERS

const cx = (mods) => {
const cns = []

for (const k in mods) {
if (mods[k]) {
cns.push(k)
}
}

return cns.join(' ')
}
28 changes: 28 additions & 0 deletions stories/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,31 @@ img {
width: 15px;
line-height: 0;
}
.zoom-caption {
position: absolute;
background-color: rgba(0, 0, 0, 0.85);
color: #fff;
font-size: 1.4rem;
padding: 1.7rem 2.5rem;
opacity: 0.0001;
transition: opacity 1s;
}
.zoom-caption--loaded {
opacity: 1;
}
.zoom-caption--bottom {
inset: auto 0 0 0;
}
.zoom-caption--left {
max-width: 40rem;
top: 50%;
transform: translateY(-50%);
}
.zoom-caption-cite {
display: block;
margin-top: 1.5rem;
}
.zoom-caption-link {
color: #fff;
text-underline-offset: 0.5rem;
}

0 comments on commit 5fd0a82

Please sign in to comment.