Skip to content

Commit

Permalink
Improve accessibility with ratio breakpoints & LazyImage component (#187
Browse files Browse the repository at this point in the history
)

* Improved accessibility

* Fixed LazyImage imports

* Fixed deployment

* Add custom aspect ratio param to LazyImage

* Update ratio breakpoints to cover larger sizes landscape and portrait

---------

Co-authored-by: Bastien Cornier <bastien@cher-ami.tv>
  • Loading branch information
theoplawinski and Bastou authored Oct 15, 2024
1 parent 884fa85 commit 03a0794
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 168 deletions.
18 changes: 18 additions & 0 deletions apps/front/src/components/lazyImage/LazyImage.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,21 @@
:global(.lazyloaded) {
opacity: 1;
}

.imageWrapper {
position: relative;
width: 100%;
height: 0;
}

.image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
&:global(.lazyJs) {
opacity: 0;
}
}
75 changes: 60 additions & 15 deletions apps/front/src/components/lazyImage/LazyImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ interface IProps {
dataSrcSet?: string
className?: string
alt?: string
aspectRatio?: number
style?: CSSProperties
aspectRatio?: string // ex: "16/9" "4/3"
width: number
height: number
onLoaded?: (img: HTMLImageElement) => void
}

export type Lazy = "lazyload" | "lazyloading" | "lazyloaded"

/**
* @name LazyImage
* @description Lazy load image component with srcset and src fallback
* @example <LazyImage dataSrcSet="image-600 600w, image-800 800w, image-1024 1024w" src="image-800" alt="image" width={800} height={600} aspectRatio={"4 / 3"} />
*/
function LazyImage(props: IProps) {
const imageRef = useRef<HTMLImageElement>(null)
Expand All @@ -30,8 +34,7 @@ function LazyImage(props: IProps) {
new Promise((resolve) => {
const dataSrc = image.dataset.src
const dataSrcSet = image.dataset.srcset
// create void image tag for start preload
// const img = document.createElement("img")

if (dataSrc) image.src = dataSrc
if (dataSrcSet) image.srcset = dataSrcSet

Expand All @@ -51,6 +54,21 @@ function LazyImage(props: IProps) {
const lazyStateRef = useRef<Lazy>("lazyload")

useEffect(() => {
// if img lazy is supported by the browser we don't need to use IntersectionObserver
if ("loading" in HTMLImageElement.prototype) {
// add src and srcset to image
if (imageRef.current) {
imageRef.current.srcset = props.dataSrcSet ?? ""
imageRef.current.src = props.src && !props.dataSrcSet ? props.src : ""
}
return
}

// add class lazyJs on imageRef
if (imageRef.current) {
imageRef.current.classList.add("lazyJs")
}

const observer = new IntersectionObserver((entries) => {
entries.forEach(async (entry) => {
if (entry.isIntersecting) {
Expand All @@ -62,6 +80,9 @@ function LazyImage(props: IProps) {
// Start preload
await preloadImage(image)

// Set new src fallback
image.src = props.src ?? "data:,"

// end!
setLazyState("lazyloaded")
props.onLoaded?.(image)
Expand All @@ -76,19 +97,43 @@ function LazyImage(props: IProps) {
}
}, [])

const aspectRatioPadding =
props.width && props.height ? (props.height / props.width) * 100 : 0

return (
<img
ref={imageRef}
className={cls(css.root, props.className, lazyState)}
src={props.src ?? "data:,"}
data-src={props?.dataSrc}
data-srcset={props?.dataSrcSet}
alt={props?.alt}
style={{
...(props.aspectRatio ? { aspectRatio: `${props.aspectRatio}` } : {}),
...(props.style || {})
}}
/>
<>
<div
className={cls(css.imageWrapper, props.className)}
style={{
paddingBottom: props.aspectRatio
? `calc((2 - ${props.aspectRatio})* 100%)`
: `${aspectRatioPadding}%`
}}
>
<img
ref={imageRef}
className={cls(css.image, lazyState)}
src={"data:,"}
data-src={props?.dataSrc}
data-srcset={props?.dataSrcSet}
alt={props?.alt ?? ""}
width={props.width}
height={props.height}
style={props.style}
loading={"lazy"}
/>
</div>
<noscript>
<img
className={cls(css.image, props.className)}
src={props.src}
srcSet={props.dataSrcSet}
alt={props.alt}
width={props.width}
height={props.height}
/>
</noscript>
</>
)
}

Expand Down
2 changes: 1 addition & 1 deletion apps/front/src/index-server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export async function render(
<head>
<meta charSet="UTF-8" />
<meta httpEquiv="x-ua-compatible" content="IE=Edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{meta?.title || "app"}</title>
<meta name="description" content={meta?.description} />
<link rel="canonical" href={meta?.url || url} />
Expand Down
2 changes: 2 additions & 0 deletions apps/front/src/styles/_breakpoints.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
$breakpoint-mobile: 320px;
$breakpoint-mobile-horizontal: 500px;
$breakpoint-tablet: 768px;
$breakpoint-tablet-height: 950px;
$breakpoint-laptop: 1024px;
$breakpoint-bigLaptop-min: 1366px;
$breakpoint-bigLaptop: 1440px;
$breakpoint-desktop: 1680px;
69 changes: 60 additions & 9 deletions apps/front/src/styles/_ratio.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@use "./_viewport" as viewport;
@use "./_breakpoints" as breakpoints;
@use "./_functions" as fn;
@use "./_utils" as utils;

/// Set property calculated with VH & VW ratio
/// @param {css property} $property
Expand All @@ -22,6 +23,20 @@
}
}

@mixin mobileVH(
$property,
$n1,
$ratioVH: viewport.$viewport-reference-height,
$ratioVW: viewport.$viewport-reference-width
) {
#{$property}: #{fn.ratioVH($n1, $ratioVH)};
@if $ratioVW > 0 {
@media (max-aspect-ratio: #{ #{$ratioVW} / #{$ratioVH+1} }) {
#{$property}: #{fn.ratioVW($n1, $ratioVW)};
}
}
}

/// Set property calculated with VW ratio
/// @param {css property} $property
/// @param {Number} $n1
Expand Down Expand Up @@ -87,19 +102,39 @@
//Mobile
@include propertyVW($property, $value1);

//Horizontal Desktop & Tablet
@include desktop($breakpoint) {
@include propertyVH($property, $value2, $ratioVW: 0);
// Landscape mobile
@include mobile-landscape {
@include propertyVH($property, $value2, viewport.$viewport-reference-width, 0);
}

// Portrait Tablet
@include tablet-portrait {
@include propertyVH(
$property,
$value2,
$ratioVW: viewport.$viewport-reference-tablet-width,
$ratioVH: viewport.$viewport-reference-tablet-height
);
}

// Landscape Tablet
@include tablet-landscape {
@include propertyVH(
$property,
$value2,
$ratioVW: viewport.$viewport-reference-tablet-width,
$ratioVH: viewport.$viewport-reference-tablet-height
);
}

//Portrait Desktop & Tablet
@include desktop-portrait {
@include propertyVW($property, $value2, viewport.$viewport-reference-tablet-width);
}

//Horizontal mobile
@include mobile-landscape {
@include propertyVH($property, $value2, viewport.$viewport-reference-width, 0);
//Horizontal Desktop & Tablet
@include desktop($breakpoint) {
@include propertyVH($property, $value2, $ratioVW: 0);
}

@if $cap {
Expand Down Expand Up @@ -136,15 +171,31 @@
/// Desktop & Tablet portrait media query
/// @param {string} [$breakpoint=breakpoints.$breakpoint-tablet] - Le point de rupture pour les médias queries (par défaut égal à breakpoints.$breakpoint-tablet).
@mixin desktop-portrait($breakpoint: breakpoints.$breakpoint-tablet) {
@media (min-width: $breakpoint) and (orientation: portrait) {
@media (min-width: $breakpoint) and (min-height: breakpoints.$breakpoint-bigLaptop) and (orientation: portrait) {
@content;
}
}

/// Mobile landscape media query.
/// @param {string} [$breakpoint=breakpoints.$breakpoint-tablet] - Le point de rupture pour les médias queries (par défaut égal à breakpoints.$breakpoint-laptop).
@mixin mobile-landscape($breakpoint: breakpoints.$breakpoint-tablet) {
@media (max-width: #{$breakpoint + 1}) and (orientation: landscape) {
@content;
}
}

/// Tablet landscape media query.
/// @param {string} [$breakpoint=breakpoints.$breakpoint-laptop] - Le point de rupture pour les médias queries (par défaut égal à breakpoints.$breakpoint-laptop).
@mixin mobile-landscape($breakpoint: breakpoints.$breakpoint-laptop) {
@media (max-width: #{$breakpoint - 1}) and (orientation: landscape) {
@mixin tablet-landscape($breakpoint: breakpoints.$breakpoint-laptop) {
@media (max-width: #{breakpoints.$breakpoint-bigLaptop-min + 1}) and (min-width: #{$breakpoint}) and (orientation: landscape) {
@content;
}
}

/// Tablet portrait media query.
/// @param {string} [$breakpoint=breakpoints.$breakpoints-tablet-height] - Le point de rupture pour les médias queries (par défaut égal à breakpoints.$breakpoint-laptop).
@mixin tablet-portrait($breakpoint: breakpoints.$breakpoint-tablet-height) {
@media (min-height: #{$breakpoint}) and (orientation: portrait) {
@content;
}
}
2 changes: 2 additions & 0 deletions apps/front/src/styles/breakpoints-inline.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
--breakpoint-mobile: #{$breakpoint-mobile};
--breakpoint-mobile-horizontal: #{$breakpoint-mobile-horizontal};
--breakpoint-tablet: #{$breakpoint-tablet};
--breakpoint-tablet-height: #{$breakpoint-tablet-height};
--breakpoint-laptop: #{$breakpoint-laptop};
--breakpoint-bigLaptop-min: #{$breakpoint-bigLaptop-min};
--breakpoint-bigLaptop: #{$breakpoint-bigLaptop};
--breakpoint-desktop: #{$breakpoint-desktop};
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const %%upperComponentName%% = forwardRef((props: IProps, handleRef: ForwardedRe

return (
<div className={css.root} ref={rootRef}>
{componentName}
<h1>{componentName}</h1>
</div>
);
});
Expand Down
Loading

0 comments on commit 03a0794

Please sign in to comment.