Skip to content

Commit

Permalink
Support responsive aspect via function
Browse files Browse the repository at this point in the history
  • Loading branch information
weotch committed Nov 23, 2023
1 parent 59ffc30 commit d565134
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 12 deletions.
17 changes: 14 additions & 3 deletions packages/react/cypress/component/ReactVisual.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,11 @@ describe('sources', () => {
cy.mount(<ReactVisual
image={{
landscape: {
url: 'https://placehold.co/500x250?text=landscape+image',
url: 'https://placehold.co/500x255?text=landscape+image',
aspect: 2,
},
portrait: {
url: 'https://placehold.co/500x500?text=portrait+image',
url: 'https://placehold.co/500x505?text=portrait+image',
aspect: 1,
}
}}
Expand All @@ -222,7 +222,12 @@ describe('sources', () => {
return `https://placehold.co/${dimensions}${ext}?text=`+
encodeURIComponent(text)
}}
width='100%'
aspect={({ image, media }) => {
return media?.includes('landscape') ?
image.landscape.aspect :
image.portrait.aspect
}}
data-cy='react-visual'
alt=''/>)

// Generates a default from the first asset found
Expand All @@ -234,12 +239,18 @@ describe('sources', () => {
.should('contain', 'https://placehold.co/640x320')
.should('contain', 'landscape')

// Check that the aspect is informing the size, not the image size
cy.get('[data-cy=react-visual]').hasDimensions(500, 250)

// Switch to portrait, which should load the other source
cy.viewport(500, 600)
cy.get('img').its('[0].currentSrc')
.should('contain', 'https://placehold.co/640x640')
.should('contain', 'portrait')

// Check aspect again
cy.get('[data-cy=react-visual]').hasDimensions(500, 500)

})

})
28 changes: 28 additions & 0 deletions packages/react/cypress/component/VisualWrapper.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,34 @@ it('supports aspect', () => {
cy.get('.wrapper').hasDimensions(VW, VH / 2)
})

it('supports respponsive aspect function', () => {
cy.mount(<VisualWrapper
{...sharedProps }
image={{
landscape: {
aspect: 2,
},
portrait: {
aspect: 1,
}
}}
sourceMedia={[
'(orientation: landscape)',
'(orientation: portrait)'
]}
aspect={({ image, media }) => {
return media?.includes('landscape') ?
image.landscape.aspect :
image.portrait.aspect
}}
/>)
cy.viewport(500, 400)
cy.get('.wrapper').hasDimensions(500, 250)
cy.viewport(400, 500)
cy.get('.wrapper').hasDimensions(400, 400)
})


it('supports children', () => {
cy.mount(<VisualWrapper {...sharedProps }>
<h1>Hey</h1>
Expand Down
9 changes: 7 additions & 2 deletions packages/react/src/ReactVisual.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import PictureImage from './PictureImage'

import { collectDataAttributes } from './lib/attributes'
import { ReactVisualProps } from './types/reactVisualTypes'
import { fillStyles } from './lib/styles'

export default function ReactVisual(
props: ReactVisualProps
Expand Down Expand Up @@ -42,6 +43,9 @@ export default function ReactVisual(
width,
height,
aspect,
sourceMedia,
image,
video,
className,
style,
dataAttributes: collectDataAttributes(props),
Expand All @@ -59,8 +63,9 @@ export default function ReactVisual(
sourceTypes,
sourceMedia,
style: { // Expand to wrapper when wrapper has layout
width: expand || width || aspect ? '100%': undefined,
height: expand || height ? '100%' : undefined,
...(aspect || expand ? fillStyles : undefined),
width: width ? '100%': undefined,
height: height ? '100%' : undefined,
}
}} /> }

Expand Down
78 changes: 72 additions & 6 deletions packages/react/src/VisualWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,95 @@
import type { CSSProperties, ReactElement } from 'react'
import { fillStyles } from './lib/styles'
import { fillStyles, cx } from './lib/styles'
import { isNumeric } from './lib/values'
import type { VisualWrapperProps } from './types/visualWrapperTypes'
import type { AspectCalculator } from './types/reactVisualTypes'

type MakeResponsiveAspectsProps = Pick<VisualWrapperProps,
'image' | 'video'
> & {
sourceMedia: Required<VisualWrapperProps>['sourceMedia']
aspectCalculator: AspectCalculator
}

// Wraps media elements and applys layout and other functionality
export default function VisualWrapper({
expand, width, height, aspect, children, className, style, dataAttributes
}: any): ReactElement {
expand, width, height,
aspect, sourceMedia, image, video,
children, className, style, dataAttributes
}: VisualWrapperProps): ReactElement {

// If aspect is a function, invoke it to determine the aspect ratio
let aspectRatio, aspectStyleTag, aspectClasses
if (typeof aspect == 'function' && sourceMedia?.length) {
({ aspectStyleTag, aspectClasses } = makeResponsiveAspects({
aspectCalculator: aspect,
sourceMedia, image, video
}))
console.log(aspectClasses, aspectStyleTag )
} else aspectRatio = aspect

// Make the wrapper style. If expanding, use normal fill rules. Otherwise,
// apply width, height and aspect
const layoutStyles = expand ? fillStyles : {
position: 'relative', // For expanded elements
width: isNumeric(width) ? `${width}px` : width,
height: isNumeric(height) ? `${height}px` : height,
aspectRatio: aspect,
maxWidth: '100%', // Don't exceed container width
aspectRatio,
maxWidth: '100%', // Never exceed container width
} as CSSProperties

// Render wrapping component
return (
<div
className={ className }
className={ cx(className, aspectClasses) }
style={{ ...layoutStyles, ...style }}
{ ...dataAttributes } >
{ children }
{ aspectStyleTag }
</div>
)
}

// Create a style tag that applies responsive aspect ratio values
function makeResponsiveAspects({
aspectCalculator, sourceMedia, image, video
}: MakeResponsiveAspectsProps): {
aspectClasses: string
aspectStyleTag: ReactElement
} {

// Make CSS classes and related rules that are specific to the query and
// aspect value.
const styles = sourceMedia.map(mediaQuery => {

// Calculate the asepct for this query state
const aspect = aspectCalculator({ media: mediaQuery, image, video })

// Make a CSS class name from the media query string
const mediaClass = mediaQuery
.replace(/[^\w]/ig, '-') // Replace special chars with "-"
const cssClass = `rv-${mediaClass}-${aspect}`
.replace(/\-{2,}/g, '-') // Reduce multiples of `-`

// Make the CSS rule
const cssRule = `@media ${mediaQuery} {
.${cssClass} {
aspect-ratio: ${aspect};
}
}`
return { cssClass, cssRule}
})

// Make an array of the classes to add
const aspectClasses = styles.map(({ cssClass }) => cssClass).join(' ')

// Make the style tag
const aspectStyleTag = (
<style>
{styles.map(({ cssRule }) => cssRule).join(' ')}
</style>
)

// Return completed objects
return { aspectClasses, aspectStyleTag}
}
10 changes: 10 additions & 0 deletions packages/react/src/lib/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,13 @@ export const fillStyles = {
// Transparent gif to use own image as poster
// https://stackoverflow.com/a/13139830/59160
export const transparentGif = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'

// Combine classes
// https://dev.to/gugaguichard/replace-clsx-classnames-or-classcat-with-your-own-little-helper-3bf
export function cx(...args: unknown[]) {
return args
.flat()
.filter(x => typeof x === 'string')
.join(' ')
.trim()
}
9 changes: 8 additions & 1 deletion packages/react/src/types/reactVisualTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type ReactVisualProps= {
video?: AssetSrc

expand?: boolean
aspect?: number // An explict aspect ratio
aspect?: number | AspectCalculator // An explict aspect ratio
width?: number | string
height?: number | string
fit?: ObjectFitOption | ObjectFit
Expand Down Expand Up @@ -44,6 +44,13 @@ export type VideoLoader = ({ src, media }: {
media?: SourceMedia
}) => string

// Callback for producing the aspect ratio
export type AspectCalculator = ({ media, image, video }: {
media: SourceMedia
image?: AssetSrc
video?: AssetSrc
}) => number

export type ObjectFitOption = 'cover' | 'contain'

export type SourceType = 'image/jpeg'
Expand Down
17 changes: 17 additions & 0 deletions packages/react/src/types/visualWrapperTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { ReactNode } from 'react'
import type { ReactVisualProps } from './reactVisualTypes'

export type VisualWrapperProps = Pick<ReactVisualProps,
'expand' |
'width' |
'height' |
'aspect' |
'sourceMedia' |
'image' |
'video' |
'className' |
'style'
> & {
dataAttributes?: object
children?: ReactNode
}

0 comments on commit d565134

Please sign in to comment.