Pages :
Modérateur
(reprise du message précédent)

Salut Boteha,


Pourquoi veux-tu défaire le web component ? Pourquoi passer en procédurale/fonctionnel ? ça n'a pas de sens à priori.
Modifié par Niuxe (19 Jul 2026 - 12:20)
Bonjour Niuxe,

Merci de ton suivi.

La structure est la suivante ;

<figure role="main">
<div><img tabindex="0" src="im/pr/1721G.webp" alt="" width="100" height="58" loading="lazy" decoding="async" /></div>
<img src="im/pr/1721G.webp" alt="mon beya produit" width="400" height="231" fetchpriority="high" decoding="async" />
<div><img tabindex="0" src="im/mu/1721-1G.webp" alt="" width="100" height="46" loading="lazy" decoding="async" /></div>
<img src="im/mu/1721-1G.webp" alt="" width="400" height="182" loading="lazy" decoding="async" />
<div><img tabindex="0" src="im/mu/1721-2G.webp" alt="" width="100" height="49" loading="lazy" decoding="async" /></div>
<img src="im/mu/1721-2G.webp" alt="" width="400" height="193" loading="lazy" decoding="async" />
<p><span>Agrandir cette fen&ecirc;tre afin de mieux voir les d&eacute;tails.</span><button type="button">Fermer</button></p>
</figure>


Le focus sur une vignette (<div><img tabindex="0" ) transforme le premier frère (<img) en image principale, cela en CSS uniquement.

La molette sur l'image principale doit produire l'effet de ton web-componant.

Mais je ne vois pas comment intégrer le web-componant. à cette structure.

D'où mon idée de le faire en javascript "simple" que je maîtrise un peu mieux.
Modifié par boteha_2 (19 Jul 2026 - 12:46)
Modérateur
Ton tabindex est faux. Si tabindex, ça veut dire que ce sont des éléments html interactifs comme <a> <button> etc.

Tu cherches à faire une gallerie interactive ? Tu as une démo dans ce codepen (j'ai viré les imports et exports).

Pour les perfs, on peut nettement améliorer aussi. J'ai fait un peu à la va vite cette démo.

arborescence (pas de dépendances extérieur excepté vitejs pour le hot reload) :

.
|-- index.html
|-- package.json
|-- pnpm-lock.yaml
|-- public
|   |-- favicon.svg
|   `-- icons.svg
`-- src
    |-- ImageGallery.js
    |-- main.js
    |-- style.css
    `-- ZoomableImage.js


index.html :

<!doctype html>
<html lang="fr">
    <head>
        <meta charset="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>boteha-zoom</title>
    </head>
    <body>
        <main>
            <image-gallery>
                <div>
                    <zoomable-image slot="main-image" src="https://picsum.photos/id/145/800/600" alt="manche guitare" />
                </div>
                <nav slot="thumbnails">
                    <ul>
                        <li>
                            <button class="active">
                                <img src="https://picsum.photos/id/145/800/600" loading="lazy" alt="manche guitare">
                            </button>
                        </li>
                        <li>
                            <button>
                                <img src="https://picsum.photos/id/69/800/600" loading="lazy" alt="rails">
                            </button>
                        </li>
                        <li>
                            <button>
                                <img src="https://picsum.photos/id/89/800/600" loading="lazy" alt="herbes">
                            </button>
                        </li>
                    </ul>
                </nav>
            </image-gallery>
        </main>
        <script type="module" src="/src/main.js"></script>
    </body>
</html>


style.css :


/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */

/* Document
   ========================================================================== */

/**
 * 1. Correct the line height in all browsers.
 * 2. Prevent adjustments of font size after orientation changes in iOS.
 */

html {
    line-height: 1.15; /* 1 */
    -webkit-text-size-adjust: 100%; /* 2 */
}

/* Sections
   ========================================================================== */

/**
 * Remove the margin in all browsers.
 */

body {
margin: 0;
}

/**
 * Render the `main` element consistently in IE.
 */

main {
display: block;
}

/**
 * Correct the font size and margin on `h1` elements within `section` and
 * `article` contexts in Chrome, Firefox, and Safari.
 */

h1 {
    font-size: 2em;
margin: 0.67em 0;
}

/* Grouping content
   ========================================================================== */

/**
 * 1. Add the correct box sizing in Firefox.
 * 2. Show the overflow in Edge and IE.
 */

hr {
    box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}

/**
 * 1. Correct the inheritance and scaling of font size in all browsers.
 * 2. Correct the odd `em` font sizing in all browsers.
 */

pre {
    font-family: monospace, monospace; /* 1 */
    font-size: 1em; /* 2 */
}

/* Text-level semantics
   ========================================================================== */

/**
 * Remove the gray background on active links in IE 10.
 */

a {
    background-color: transparent;
}

/**
 * 1. Remove the bottom border in Chrome 57-
 * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
 */

abbr[title] {
    border-bottom: none; /* 1 */
    text-decoration: underline; /* 2 */
    text-decoration: underline dotted; /* 2 */
}

/**
 * Add the correct font weight in Chrome, Edge, and Safari.
 */

b,
    strong {
        font-weight: bolder;
    }

/**
 * 1. Correct the inheritance and scaling of font size in all browsers.
 * 2. Correct the odd `em` font sizing in all browsers.
 */

code,
    kbd,
    samp {
        font-family: monospace, monospace; /* 1 */
        font-size: 1em; /* 2 */
    }

/**
 * Add the correct font size in all browsers.
 */

small {
    font-size: 80%;
}

/**
 * Prevent `sub` and `sup` elements from affecting the line height in
 * all browsers.
 */

sub,
    sup {
        font-size: 75%;
        line-height: 0;
position: relative;
          vertical-align: baseline;
    }

sub {
bottom: -0.25em;
}

sup {
top: -0.5em;
}

/* Embedded content
   ========================================================================== */

/**
 * Remove the border on images inside links in IE 10.
 */

img {
    border-style: none;
}

/* Forms
   ========================================================================== */

/**
 * 1. Change the font styles in all browsers.
 * 2. Remove the margin in Firefox and Safari.
 */

button,
    input,
    optgroup,
    select,
    textarea {
        font-family: inherit; /* 1 */
        font-size: 100%; /* 1 */
        line-height: 1.15; /* 1 */
margin: 0; /* 2 */
    }

/**
 * Show the overflow in IE.
 * 1. Show the overflow in Edge.
 */

button,
    input { /* 1 */
overflow: visible;
    }

/**
 * Remove the inheritance of text transform in Edge, Firefox, and IE.
 * 1. Remove the inheritance of text transform in Firefox.
 */

button,
    select { /* 1 */
        text-transform: none;
    }

/**
 * Correct the inability to style clickable types in iOS and Safari.
 */

button,
    [type="button"],
    [type="reset"],
    [type="submit"] {
        -webkit-appearance: button;
        padding: 0;
        line-height: 0;
        cursor:pointer;

        &.active{
            border-color: #0066cc;
            box-shadow: 0 0 0 2px rgba(0,102,204,0.3);
        }
    }

/**
 * Remove the inner border and padding in Firefox.
 */

button::-moz-focus-inner,
    [type="button"]::-moz-focus-inner,
    [type="reset"]::-moz-focus-inner,
    [type="submit"]::-moz-focus-inner {
        border-style: none;
padding: 0;
    }

/**
 * Restore the focus styles unset by the previous rule.
 */

button:-moz-focusring,
    [type="button"]:-moz-focusring,
    [type="reset"]:-moz-focusring,
    [type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
    }

/**
 * Correct the padding in Firefox.
 */

fieldset {
padding: 0.35em 0.75em 0.625em;
}

/**
 * 1. Correct the text wrapping in Edge and IE.
 * 2. Correct the color inheritance from `fieldset` elements in IE.
 * 3. Remove the padding so developers are not caught out when they zero out
 *    `fieldset` elements in all browsers.
 */

legend {
    box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
         max-width: 100%; /* 1 */
padding: 0; /* 3 */
         white-space: normal; /* 1 */
}

/**
 * Add the correct vertical alignment in Chrome, Firefox, and Opera.
 */

progress {
    vertical-align: baseline;
}

/**
 * Remove the default vertical scrollbar in IE 10+.
 */

textarea {
overflow: auto;
}

/**
 * 1. Add the correct box sizing in IE 10.
 * 2. Remove the padding in IE 10.
 */

[type="checkbox"],
    [type="radio"] {
        box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
    }

/**
 * Correct the cursor style of increment and decrement buttons in Chrome.
 */

[type="number"]::-webkit-inner-spin-button,
    [type="number"]::-webkit-outer-spin-button {
height: auto;
    }

/**
 * 1. Correct the odd appearance in Chrome and Safari.
 * 2. Correct the outline style in Safari.
 */

[type="search"] {
    -webkit-appearance: textfield; /* 1 */
    outline-offset: -2px; /* 2 */
}

/**
 * Remove the inner padding in Chrome and Safari on macOS.
 */

[type="search"]::-webkit-search-decoration {
    -webkit-appearance: none;
}

/**
 * 1. Correct the inability to style clickable types in iOS and Safari.
 * 2. Change font properties to `inherit` in Safari.
 */

::-webkit-file-upload-button {
    -webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}

/* Interactive
   ========================================================================== */

/*
 * Add the correct display in Edge, IE 10+, and Firefox.
 */

details {
display: block;
}

/*
 * Add the correct display in all browsers.
 */

summary {
display: list-item;
}

/* Misc
   ========================================================================== */

/**
 * Add the correct display in IE 10+.
 */

template {
display: none;
}

/**
 * Add the correct display in IE 10.
 */

[hidden] {
display: none;
}


main{
    max-width: 800px;
    margin: 50px auto;
}

nav{
    margin: 25px 0;
    ul{
        list-style: none;
        margin: 0;
        padding: 0;
        display: flex;
        justify-content: center;

        li{
            margin: 0 10px;

            &:first-child, &:last-child{
                margin:0;
            }

            button{
                border-radius: 6px;
                img{
                    width: 100px;
                    height: 58px;
                }
            }
        }
    }
}


.zoomable-image {
    display: inline-block;
    border: 1px solid #bbb;
    border-radius: 6px;
    overflow: hidden;
    cursor: grab;
    width: 800px;
    height: 600px;
    position: relative;

    img {
        display: block;
        margin: auto;
        user-select: none;
        pointer-events: none;
        transform-origin: center center;
        transition: transform 0.1s ease-out;
    }
}


ImageGallery.js :

export class ImageGallery extends HTMLElement {
    constructor() {
        super()
        this.zoomableImage = null
        this.thumbnails = []
    }

    connectedCallback() {
        this.zoomableImage = this.querySelector('[slot="main-image"]')

        const nav = this.querySelector('[slot="thumbnails"]')
        if (nav) {
            this.thumbnails = nav.querySelectorAll('button')
            this.setupThumbnails()

            if (this.thumbnails.length > 0) {
                this.setActiveThumbnail(0)
            }
        }
    }

    setupThumbnails() {
        this.thumbnails.forEach((button, index) => {
            button.addEventListener('click', () => {
                this.setActiveThumbnail(index)
            })
        })
    }

    setActiveThumbnail(index) {
        if (!this.zoomableImage) return

        this.thumbnails.forEach((btn, i) => {
            btn.classList.toggle('active', i === index)
        })

        const img = this.thumbnails[index].querySelector('img')
        if (img && img.src) {
            this.zoomableImage.setAttribute('src', img.src)
            this.zoomableImage.setAttribute('alt', img.alt)
        }

        this.dispatchEvent(new CustomEvent('image-change', {
            detail: {
                index,
                src: img?.src,
                alt: img?.alt
            },
            bubbles: true
        }))
    }
}


ZoomableImage.js :

export class ZoomableImage extends HTMLElement {

    static get observedAttributes() {
        return [
            'src',
            'alt'
        ]
    }

    constructor() {
        super()
        this.img = document.createElement('img')
        this.appendChild(this.img)

        this.currentScale = 1
        this.pos = { x: 0, y: 0 }
        this.start = { x: 0, y: 0 }
        this.isDragging = false
    }

    connectedCallback() {
        if (this.hasAttribute('src')) {
            this.img.src = this.getAttribute('src')
        }
        if (this.hasAttribute('alt')) {
            this.img.alt = this.getAttribute('alt')
        }
        this.classList.add('zoomable-image')
        this.bindEvents()
    }

    bindEvents() {
        this.addEventListener('wheel', e => this.onWheel(e))
        this.addEventListener('mousedown', e => this.onMouseDown(e))
        this.addEventListener('mousemove', e => this.onMouseMove(e))
        this.addEventListener('mouseup', () => this.onMouseUp())
        this.addEventListener('mouseleave', () => this.onMouseLeave())
    }

    attributeChangedCallback(name, oldValue, newValue) {
        if (name === 'src' && oldValue !== newValue) {
            this.img.src = newValue
        }
        if (name === 'alt' && oldValue !== newValue) {
            this.img.alt = newValue
        }
    }

    onWheel(e) {
        e.preventDefault()
        const MIN_SCALE = 1
        const MAX_SCALE = 5
        const zoomSpeed = 0.0015
        const oldScale = this.currentScale

        this.currentScale -= e.deltaY * zoomSpeed
        this.currentScale = Math.min(Math.max(this.currentScale, MIN_SCALE), MAX_SCALE)

        const rect = this.getBoundingClientRect()
        const dx = e.clientX - rect.left - rect.width / 2
        const dy = e.clientY - rect.top - rect.height / 2
        this.pos.x -= dx * (this.currentScale / oldScale - 1)
        this.pos.y -= dy * (this.currentScale / oldScale - 1)

        this.updateTransform()
    }

    onMouseDown(e) {
        if (this.currentScale <= 1) return
        this.isDragging = true
        this.start.x = e.clientX - this.pos.x
        this.start.y = e.clientY - this.pos.y
        this.style.cursor = 'grabbing'
    }

    onMouseMove(e) {
        if (!this.isDragging) return
        this.pos.x = e.clientX - this.start.x
        this.pos.y = e.clientY - this.start.y
        this.updateTransform()
    }

    onMouseUp() {
        this.isDragging = false
        this.style.cursor = 'grab'
    }

    onMouseLeave() {
        this.isDragging = false
        this.style.cursor = 'grab'
    }

    updateTransform() {
        if (this.currentScale === 1) {
            this.pos.x = 0
            this.pos.y = 0
        }
        this.img.style.transform = `translate(${this.pos.x}px, ${this.pos.y}px) scale(${this.currentScale})`
    }
}


main.js :

import './style.css'
import { ZoomableImage } from './ZoomableImage'
import { ImageGallery } from './ImageGallery'

(() => {
    customElements.define('image-gallery', ImageGallery)
    customElements.define('zoomable-image', ZoomableImage)
})()


On peut encore améliorer ce principe. Le slot de navigation peut être encapsuler et on utilise un template ou on crée les éléments à la volée. Dans le slot de navigation, on insère un json (ou pas ?).

Un truc comme ça :

<image-gallery images='[
    {"src":"https://picsum.photos/id/145/800/600","alt":"manche guitare"},
    {"src":"https://picsum.photos/id/69/800/600","alt":"rails"},
    {"src":"https://picsum.photos/id/89/800/600","alt":"herbes"}
]'></image-gallery>


Pourquoi le Web Component est mieux que du procédural pour ce cas ?

1. Encapsulation

- Toute la logique de zoom/drag est dans la classe ZoomableImage
- Pas de variables globales, pas de sélecteurs qui trainent
- Le composant est autonome, réutilisable sans conflit

2. Réutilisabilité

- Tu peux poser <zoomable-image> n'importe où dans la page, plusieurs fois
- En procédural, il faudrait instancier manuellement, gérer des tableaux d'instances, attacher/détacher les événements...

3. Cycle de vie

- connectedCallback / disconnectedCallback : le composant se nettoie tout seul
- En procédural, bonne chance pour cleaner les event listeners quand l'élément est retiré du DOM

4. Réactivité native

- observedAttributes + attributeChangedCallback : changer src ou alt met à jour l'image automatiquement
- En procédural, il faudrait un MutationObserver ou des setters manuels

5. Intégration naturelle dans le DOM

- L'API du composant, c'est juste des attributs HTML
- N'importe quel dev frontend comprend <zoomable-image src="...">
- En procédural, il faut connaître l'API spécifique de ta fonction

Le procédural pour ce genre de comportement, c'est un retour en arrière. Le Web Component est pertinent ici.

Accessibilité : les points clés à corriger

1. Navigation clavier sur les miniatures

- Pattern tablist : role="tablist" sur le <ul>, role="tab" sur chaque <button>
- Flèches gauche/droite pour naviguer, Home/End pour premier/dernier
- aria-selected="true" sur l'actif, pas juste .active
- Un seul tabindex="0" dans la liste (le reste tabindex="-1")

2. Labels explicites

- aria-label sur les boutons (ex: "Afficher l'image : manche guitare")
- La miniature active annoncée comme "sélectionnée"

3. Image principale

- alt déjà géré via l'attribut, c'est bon
- Ajouter un role="img" si nécessaire
- Annoncer le changement d'image (aria-live="polite")

4. Zoom accessible (le plus dur)

- Boutons +/- visibles en plus de la molette
- Annonce du niveau de zoom (aria-live="assertive" ou aria-valuenow)
- Au pire, fallback : lien vers l'image plein écran

5. Focus visible

- :focus-visible sur les boutons et la zone zoomable
- Ne pas virer l'outline, la styliser

Le plus critique : navigation clavier + labels, le reste c'est du bonus.
Modifié par Niuxe (20 Jul 2026 - 17:06)
Bonjour Niuxe,

Merci beaucoup de ton suivi.

Je vais regarder ton code en détail.

Niuxe a écrit :
Ton tabindex est faux. Si tabindex, ça veut dire que ce sont des éléments html interactifs comme <a> <button> etc.


Le code a été conçu par gcyrillus.
L'objectif était d'avoir un carrousel 100 % CSS.

Cela fonctionne bien,.
Modifié par boteha_2 (21 Jul 2026 - 19:27)
Modérateur
boteha_2 a écrit :

Le code a été conçu par gcyrillus.
L'objectif était d'avoir un carrousel 100 % CSS.


1. Bidouiller les tabindex est une mauvaise pratique. Ça casse l'ordre naturel¹² de tabulation. ça crée de la confusion pour les utilisateurs de lecteurs d'écran, et ça demande du JS supplémentaire pour gérer les événements clavier qui devraient être natifs.

2. bien qu'universel, tabindex doit être réservé aux éléments interactifs natifs (<button>, <a>, <input>, <select>, <textarea>, etc.) et uniquement tabindex="0" ou tabindex="-1", jamais de valeurs positives.

_____
¹ Rendre les éléments focalisables
² Préserver l'ordre
Bonjour Niuxe,

Peut-être est-il possible de remplacer div par button et chercher le focus sur le button et non sur l'image.

<figure role="main">
<button tabindex="0"><img src="im/pr/1721G.webp" alt="" width="100" height="58" loading="lazy" decoding="async" /></button>
<img src="im/pr/1721G.webp" alt="mon beya produit" width="400" height="231" fetchpriority="high" decoding="async" />
</figure>


Cela oblige à pas mal de changements dans le CSS.
Mais img peut-il enfant de button, pas évident ?
Modifié par boteha_2 (21 Jul 2026 - 12:06)
Modérateur
boteha_2 a écrit :

Mais img peut-il enfant de button, pas évident ?


<button> peut avoir n'importe quels enfants excepté les éléments interactifs : <select>, <textarea>, <a>, <button>, <input>, etc.

Tu peux même mettre tout un DOM. C'est pour ça que dans un <form>, c'est plus intéressant de faire <button type="submit"> que <input type="submit">.
Modifié par Niuxe (21 Jul 2026 - 12:30)
Si tu penses que le changement de code html en vaut la peine je veux bien essayer de le faire...
Sachant que le code actuel fonctionne bien et ne provoque pas d'alerte du W3C.
Modérateur
boteha_2 a écrit :
Si tu penses que le changement de code html en vaut la peine je veux bien essayer de le faire...
Sachant que le code actuel fonctionne bien et ne provoque pas d'alerte du W3C.


syntaxiquement juste, mais sémantiquement faux ou pas adapté.

Exemple : « la terre est plate » Smiley biggol

Un <div onclick="..."> passe le validateur. Ce n'est pas pour autant un bouton accessible.
Si tu penses que le code ci-dessous est bon je vais essayer :

<figure role="main">
<button tabindex="0"><img src="im/pr/1721G.webp" alt="" width="100" height="58" loading="lazy" decoding="async" /></button>
<img src="im/pr/1721G.webp" alt="mon beya produit" width="400" height="231" fetchpriority="high" decoding="async" />
</figure>


Merci de confirmer que c'est bon car en plus du html très facile à changer je dois changer le CSS et aussi un peu de javascript qui repère l'élément qui a le focus (qui sera donc button au lieu d'img...).
Bonjour,

Bon, j'ai remplacé le code avec <div><img tabindex="0" ../> par <button<<img... />

Cela donne par exemple :

<figure role="main">
<button type="button"><img src="im/pr/1550G.avif" alt="vignette 1" width="100" height="77" loading="lazy" decoding="async" /></button>
<img src="im/pr/1550G.avif" alt="mon beau produit tout neuf" width="400" height="305" fetchpriority="high" decoding="async" />
<button type="button"><img src="im/mu/1550-1G.webp" alt="vignette 2" width="100" height="82" loading="lazy" decoding="async" /></button>
<img src="im/mu/1550-1G.webp" alt="imafge 2" width="400" height="328" loading="lazy" decoding="async" />
</figure>


Il y avait un code javascript qui donnait le focus à la bonne image.

Code avec la structure <div><img tabindex="0" ../> qui fonctionne.

<script>
document.querySelector('img[src$="1550-6G.webp"]').focus ();
const imgs = document.querySelectorAll("img[tabindex]");
const imgFocused = [];
imgFocused[0] = "im/mu/1550-1G.webp"
for (img of imgs) {
img.addEventListener("blur", function () {
document.querySelector('img[src="' + imgFocused[0] + '"]').focus();
});
img.addEventListener("mousedown", function () {
imgFocused[0] = this.getAttribute("src");
document.querySelector('img[src="' + imgFocused[0] + '"]').focus();
});
}
</script>


Code avec la nouvelle structure <button><img../> qui fonctionne pas....

<script>
document.querySelector('button:has(> img[src$="1550-6G.webp"])').focus ();
const imgs = document.querySelectorAll("figure > button");
const imgFocused = [];
imgFocused[0] = "im/mu/1550-1G.webp"
for (button of imgs) {
button.addEventListener("blur", function () {
document.querySelector('button:has(> img[src="' + imgFocused[0] + '"])').focus();
});
button.addEventListener("mousedown", function () {
imgFocused[0] = document.querySelector(figure > button > this).getAttribute("src");
document.querySelector('button:has(> img[src="' + imgFocused[0] + '"])').focus();
});
}
</script>


Voyez-vous où est l'erreur dans le nouveau code ?

J'ai un gros doute sur :

imgFocused[0] = document.querySelector(figure > button > this).getAttribute("src");

Modifié par boteha_2 (26 Jul 2026 - 14:18)