From d56513480b41a66dbc581d0f9f6a5b4f107c972c Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Wed, 22 Nov 2023 17:21:02 -0800 Subject: [PATCH] Support responsive aspect via function --- .../cypress/component/ReactVisual.cy.tsx | 17 +++- .../cypress/component/VisualWrapper.cy.tsx | 28 +++++++ packages/react/src/ReactVisual.tsx | 9 ++- packages/react/src/VisualWrapper.tsx | 78 +++++++++++++++++-- packages/react/src/lib/styles.ts | 10 +++ packages/react/src/types/reactVisualTypes.ts | 9 ++- .../react/src/types/visualWrapperTypes.ts | 17 ++++ 7 files changed, 156 insertions(+), 12 deletions(-) create mode 100644 packages/react/src/types/visualWrapperTypes.ts diff --git a/packages/react/cypress/component/ReactVisual.cy.tsx b/packages/react/cypress/component/ReactVisual.cy.tsx index 31d8c0d..fdb5645 100644 --- a/packages/react/cypress/component/ReactVisual.cy.tsx +++ b/packages/react/cypress/component/ReactVisual.cy.tsx @@ -192,11 +192,11 @@ describe('sources', () => { cy.mount( { 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 @@ -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) + }) }) diff --git a/packages/react/cypress/component/VisualWrapper.cy.tsx b/packages/react/cypress/component/VisualWrapper.cy.tsx index 8d3de66..a7d7827 100644 --- a/packages/react/cypress/component/VisualWrapper.cy.tsx +++ b/packages/react/cypress/component/VisualWrapper.cy.tsx @@ -45,6 +45,34 @@ it('supports aspect', () => { cy.get('.wrapper').hasDimensions(VW, VH / 2) }) +it('supports respponsive aspect function', () => { + cy.mount( { + 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(

Hey

diff --git a/packages/react/src/ReactVisual.tsx b/packages/react/src/ReactVisual.tsx index 94aa0e9..3247881 100644 --- a/packages/react/src/ReactVisual.tsx +++ b/packages/react/src/ReactVisual.tsx @@ -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 @@ -42,6 +43,9 @@ export default function ReactVisual( width, height, aspect, + sourceMedia, + image, + video, className, style, dataAttributes: collectDataAttributes(props), @@ -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, } }} /> } diff --git a/packages/react/src/VisualWrapper.tsx b/packages/react/src/VisualWrapper.tsx index 98326d2..9877067 100644 --- a/packages/react/src/VisualWrapper.tsx +++ b/packages/react/src/VisualWrapper.tsx @@ -1,11 +1,32 @@ 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 & { + sourceMedia: Required['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 @@ -13,17 +34,62 @@ export default function VisualWrapper({ 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 (
{ children } + { aspectStyleTag }
) } + +// 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 = ( + + ) + + // Return completed objects + return { aspectClasses, aspectStyleTag} +} diff --git a/packages/react/src/lib/styles.ts b/packages/react/src/lib/styles.ts index 8aab650..42516a7 100644 --- a/packages/react/src/lib/styles.ts +++ b/packages/react/src/lib/styles.ts @@ -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() +} diff --git a/packages/react/src/types/reactVisualTypes.ts b/packages/react/src/types/reactVisualTypes.ts index 9a13e36..e8608dd 100644 --- a/packages/react/src/types/reactVisualTypes.ts +++ b/packages/react/src/types/reactVisualTypes.ts @@ -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 @@ -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' diff --git a/packages/react/src/types/visualWrapperTypes.ts b/packages/react/src/types/visualWrapperTypes.ts new file mode 100644 index 0000000..63f8975 --- /dev/null +++ b/packages/react/src/types/visualWrapperTypes.ts @@ -0,0 +1,17 @@ +import type { ReactNode } from 'react' +import type { ReactVisualProps } from './reactVisualTypes' + +export type VisualWrapperProps = Pick & { + dataAttributes?: object + children?: ReactNode +}