Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve accessibility with ratio breakpoints & LazyImage component #187

Merged
merged 5 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading