Créer un tableau de status pour vos composants dans Storybook

⚠ La totalitĂ© de cet article a Ă©tĂ© rĂ©digĂ© pour ĂȘtre mis en place avec @storybook/html en utilisant le format MDX.

Le contexte

Notre besoin Ă©tait de visualiser les diffĂ©rents status (UI, HTML – Vanilla, A11Y, Doc, Framework) de l’ensemble de nos composants Ă  l’aide d’un tableau rĂ©capitulatif.

La centralisation de l’information avait pour objectif d’offrir de l’autonomie aux personnes pour trouver et lire facilement les informations qu’ils recherchaient. Evitant ainsi les A/R entre Confluence, Figma et Storybook.

Le résultat attendu était le suivant :

storybook-status-table

Les contraintes

  • Le tableau doit ĂȘtre gĂ©rer par une page Storybook au format MDX.
  • La solution doit ĂȘtre Ă©volutive (nouveau status, nouveau framework, etc).
  • La mise Ă  jour des status d’un composant doit ĂȘtre simple.
  • Notre bibliothĂšque est monobloc et nos composants ne disposent pas d’un package.json indĂ©pendant.

Les implémentations

1. La simplicité

La premiĂšre idĂ©e fut la plus simple : un tableau HTML statique dans le fichier MDX. Quelqu’un a dit « Un tableau HTML c’est pas si compliquĂ© Ă  maintenir » đŸ˜‚ D’autres ne m’ont pas cru


MĂȘme si la mise en place a Ă©tĂ© plutot rapide et plĂ©bicitĂ©e, ce fut rapidement un Ă©chec sans surprise.

Ce fut une vraie galĂšre Ă  maintenir et faire Ă©voluer mĂȘme avec seulement une dizaine de composants.

Il y aurait bien eu la solution de gestion de tableaux HTML avec un outil en ligne mais l’idĂ©e a Ă©tĂ© tuĂ©e avant d’y investir cinq minutes.

2. La praticité

Pour que toute notre histoire devienne pratique, il nous fallait savoir trois choses :

  1. Storybook est-il en mesure de nous fournir la liste de toutes nos stories ?
  2. OĂč peut-on stocker les informations dans une story ?
  3. Peut-on rendre un tableau HTML dynamique dans un fichier MDX avec Storybook ?

1. La rĂ©ponse est oui

Comme je n’ai absolument rien tracĂ© pendant mes recherches, je ne sais plus vraiment comment je suis arrivĂ© Ă  trouver ça. Certainement en passant quelques heures sur GitHub et Stack Overflow.

Le paquet @storybook/html met Ă  disposition deux mĂ©thodes pour rĂ©cupĂ©rer l’ensemble des contenus :

  • getStorybook va retourner un tableau contenant l’ensemble des stories et des pages. Malheureusement pour nous, les informations retournĂ©es sont limitĂ©es et ne vont pas couvrir notre futur besoin.
  • raw va retourner un tableau contenant uniquement l’ensemble des stories avec toutes leurs informations. đŸ„ł

J’ai encore quelques regrets sur le fait de ne pas avoir trouvĂ© de solution avec la mĂ©thode getStorybook surtout quand je sais qu’il existe un @deprecated devant la mĂ©thode raw ici.

Il existe bien une mĂ©thode selectStory dans le paquet @storybook/api mais rien ne semble fonctionnel au niveau d’une story (j’ai peut ĂȘtre loupĂ© un truc) đŸ˜ . Alors que ça semble tout Ă  fait possible dans une addon : Addon API – useStorybookApi [en]

Pour aller au bout de notre idĂ©e, nous profiterons de raw tant que la mĂ©thode est disponible đŸ™

2. Les paramĂštres d’une story sont sans limite

On trouve souvent notre bonheur et notre dĂ©livrance dans la documentation d’un projet. Dans notre cas, ce sera l’objet parameters [en]. Un espace de stockage qui va servir Ă  diffĂ©rentes choses dans Storybook et qui semble sans restriction. Let’s go.

L’autre avantage de passer par cet objet est la notion d’hĂ©ritage (cf. : Rules of parameter inheritance [en]). Celle qui nous intĂ©ressera le plus dans notre cas sera celle entre le composant et ses stories.

Du coup, dans chaque composant au niveau voulu (composant, story ou les deux), nous aurons dans parameters un objet de la forme :

parameters={{
  uiLibraryName: {
    status: {
      ui: ['ready'],
      html: ['ready', 'includes-js'],
      a11y: [
        {
          value: 'ready',
          description: 'modĂšle [accordion](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/) "Accordion (Sections With Show/Hide Functionality) - ARIA Authoring Practices")',
        }
      ],
      docs: ['in-progress'],
      frameworkA: ['no-available'],
      frameworkB: ['in-progress'],
      frameworkC: ['scheduled'],
    }
  }
}}

Ce dernier nous pemettra d’avoir la liste de tous nos status avec la possibilitĂ© d’avoir un ou N Ă©tats pour chacun. Nous avons Ă©galement laissĂ© la possibilitĂ© de saisir une description pour un Ă©tat Ă  l’aide d’un objet (qui accepte du Markdown đŸ˜›).

Autre dĂ©tail, les status ne sont pas Ă  la racine de notre objet uiLibraryName (vous y donnerez le nom que vous voulez) pour se laisser d’autres possibilitĂ©s dans le futur.

3. Avec html-react-parser rien ne semble impossible

Le paquet html-react-parser nous sert dĂ©jĂ  Ă  afficher le rĂ©sultat d’une story ou plutĂŽt de son template Ă  l’intĂ©rieur d’un Ă©lĂ©ment <Canvas> pour ne pas polluer l’arborescence dans la sidebar (cf. : storybookjs/storybook#9209 (comment))

Du coup, nous n’avons pas cherchĂ© plus loin pour la crĂ©ation de notre tableau.

Voici l’exemple pour notre lĂ©gende :

import htmlReactParser from 'html-react-parser'
import { Meta } from '@storybook/addon-docs'
import includeJS from './assets/media/js.svg'

export const componentStatus = {
  scheduled: { icon: '🚀', label: 'ProgrammĂ©' },
  ready: { icon: '✅', label: 'ValidĂ©' },
  'in-progress': { icon: '⏳', label: 'En cours' },
  alert: { icon: '⚠', label: 'Point de vigilance' },
  blocked: { icon: '🔒', label: 'BloquĂ©' },
  'not-available': { icon: '➖', label: 'Non implĂ©mentĂ©' },
  experimental: { icon: 'đŸ§Ș', label: 'ExpĂ©rimental' },
  'includes-js': {
    icon: `<img src="${includeJS}" style="width:1.25em;height:1.25em;vertical-align:middle" />`,
    label: 'Inclus du JavaScript',
  },
}
 
<Meta
  title="Composants/Status des composants"
  parameters={{
    options: { isToolshown: false },
  }}
/>

# Statut des composants
 
<br />
 
**LĂ©gende** :
 
<ul>
  {htmlReactParser(Object.values(componentStatus)
    .map(
      (s, i) =>
        `<li style="font-size:14px${i ? ';margin-top:.25em' : ''}">${
          s.icon
        }&nbsp;${s.label}</li>`
    )
    .join(''))}
</ul>

4. Le petit tips pour le plaisir

Pour restituer le markdown dans la description de nos status, nous avons utilisĂ©s le paquet marked.

La derniĂšre petite chose qui nous resterait Ă  faire et de gĂ©rer l’attribut target="_blank" grĂące Ă  ce contournement : markedjs/marked#655 (comment). En attendant, on va expliquer aux gens qu’on peut faire ça aussi avec un raccourci clavier đŸ˜‰

En espĂ©rant que tout cela pourra vous ĂȘtre utile :bowtie:

La page complĂšte est dispo ici : components.stories.mdx.

Administrer le quatriĂšme format d’images de WordPress

Le sujet des images dans WordPress peut ĂȘtre sensible surtout quand on se prĂ©occupe un peu de son espace disque 😅

Si on laisse de cĂŽtĂ© les thĂšmes premium et leur mille formats d’images gĂ©nĂ©rĂ©s pour pas grand chose (je hais les thĂšmes premium !) , il faut savoir que lorsqu’on tĂ©lĂ©verse une image dans WordPress ce dernier va par dĂ©faut en crĂ©er quatre autres.
MĂȘme s’il vous semble que l’administration WordPress vous offre la possibilitĂ© de prĂ©ciser vos tailles d’images : « Taille des miniatures », « Taille moyenne » et « Grande taille Â», WordPress gĂšre en fait une quatriĂšme taille medium_large qu’il utilise pour les images responsives đŸ€”

Si vous avez loupĂ© ce dĂ©tail datant du 22 dĂ©cembre 2015 (version 4.4 de WordPress), voici l’article qui y fait rĂ©fĂ©rence : Responsive Images in WordPress 4.4.

Du coup, en parcourant le code du cƓur, il est facile de trouver dans wp-admin/includes/schema.php les deux options qui font rĂ©fĂ©rence Ă  la taille de cette image :

// 4.4.0
'medium_large_size_w' => 768,
'medium_large_size_h' => 0,

Je n’ai pas poussĂ© jusqu’Ă  trouver le ticket qui fait rĂ©fĂ©rence Ă  ce choix de 768px mais je suis presque sĂ»r que c’est parce que cela correspond Ă  la largeur CSS d’un Ipad đŸ˜†
Quel malheur đŸ˜„

Comme j’ai pu ĂȘtre mauvaise langue 😜 en disant que je n’avais jamais rien trouvĂ© dans le codex au sujet de ce format, je m’en excuse car tout y est, il suffit d’ouvrir les yeux : Featured Images & Post Thumbnails

Bon bĂ© maintenant qu’on est au courant, il ne reste plus qu’Ă  nous offrir la possibilitĂ© de prĂ©ciser les dimensions maximales (en pixels) Ă  utiliser pour ce fameux format Â« Medium-large » comme ci-dessous.

La premiĂšre chose Ă  faire est de rajouter les champs de saisie au bon endroit :

/**
 * Adds a new field into the media settings page to manage
 * the fourth image size: "Medium Large".
 *
 * Introduced by WordPress 4.4, this format has by default a `768px` width
 * and is used for responsive images (through the `srcset` attribute).
 * I know that it's not a good idea to define `srcset` and `size`
 * attributes according to this site breakpoint
 * (cf.: )
 * but as WP handles this format (and uses space disc) without telling us,
 * we should be able to edit it to enjoy it even for one of our breakpoints.
 *
 * If you want to escape this format, you just have to set its width
 * and height to zero ;)
 */
function thistle_add_medium_large_size_settings() {
    add_settings_field(
        'medium_large_size',
        __( 'Large size' ),
        '_thistle_output_medium_large_size_settings',
        'media',
        'default',
        array()
    );
}

function _thistle_output_medium_large_size_settings() {
    ?>
    <fieldset>
        <legend class="screen-reader-text">
            <span><?php _e( 'Large size' ); ?></span>
        </legend>
        <label for="medium_large_size_w"><?php _e( 'Max Width' ); ?></label>
        <input name="medium_large_size_w" type="number" step="1" min="0" id="medium_large_size_w" value="<?php form_option( 'medium_large_size_w' ); ?>" class="small-text" />
        <br>
        <label for="medium_large_size_h"><?php _e( 'Max Height' ); ?></label>
        <input name="medium_large_size_h" type="number" step="1" min="0" id="medium_large_size_h" value="<?php form_option( 'medium_large_size_h' ); ?>" class="small-text" />
    </fieldset>
    <?php
}
add_action( 'admin_init', 'thistle_add_medium_large_size_settings' );

Je l’avoue, j’ai pris une initiative supplĂ©mentaire dans mon code 😚

Comme en bon François Â« Medium-large » ne semble pas avoir d’Ă©quivalent, j’ai dĂ©cidĂ© que ce format s’appellerait maintenant « Grande taille » et que je renommerais le prĂ©cĂ©dent Â« Grande taille » en « TrĂšs grande taille » đŸ€“

Procédons donc au renommage :

/**
 * Renames "Large" label into "Extra large" via JavaScript 
 * because we can't do it with the help of PHP.
 */
function thistle_rename_large_size_settings() {
    ?>
    <script>
        ( function( window, $, undefined ) {
            if ( typeof $ !== 'undefined' ) {
                $( document ).ready( function () {
                    var $lss = $( '#large_size_w' ).parents('tr');

                    $lss
                        .find('th, legend span')
                            .html('<?php _e( 'Extra large size', YOUR_TEXT_DOMAIN ); ?>');
                } );
            }
        } )( window, window.jQuery );
    </script>
    <?php
}
add_action( 'admin_head-options-media.php', 'thistle_rename_large_size_settings' );

/**
 * Retrieves the names and labels of the default image sizes including
 * the fourth (or fifth) one: "Medium Large".
 * Because we don't have a good translation for "Medium Large" in French,
 * I decided to rename "Large" into "Extra large" and
 * give the "Large" label to the medium_large size.
 *
 * @param array $size_names Array of image sizes and their names. Default values
 *                          include 'Thumbnail', 'Medium', 'Large', 'Full Size'.
 * @return array
 */
function thistle_change_image_size_names( $size_names ) {
    $index = array_search( 'large' , array_keys( $size_names ) );

    $size_names = array_merge( array_slice( $size_names, 0, $index, true), array( 'medium_large' => _( 'Large' ) ), $size_names );
    $size_names['large'] = __( 'Extra large', YOUR_TEXT_DOMAIN );

    return $size_names;
}
add_filter( 'image_size_names_choose', 'thistle_change_image_size_names' );

Je sais que ce choix peut ĂȘtre sujet Ă  un troll monumental mais je l’assume đŸ˜Ž On aurait aussi pu traduire Â« Medium-large » par Â« Taille intermĂ©diaire » mais bon.

Nous y sommes presque.
Quand nous avons ajoutĂ© nos champs via add_settings_field, ces derniers se sont placĂ©s Ă  la fin de notre page et ce n’est pas vraiment logique par rapport Ă  l’ordre des tailles d’images.

Remettons cela dans le bon ordre :

/**
 * Changes the order of the size settings to have "Medium Large" before
 * "Large" via JavaScript because we can't do it with the help of PHP.
 */
function thistle_change_size_settings_order() {
    ?>
    <script>
        ( function( window, $, undefined ) {
            if ( typeof $ !== 'undefined' ) {
                $( document ).ready( function () {
                    var $lss = $( '#large_size_w' ).parents('tr');
                    var $mlss = $( '#medium_large_size_w' ).parents('tr');

                    $lss
                        .insertAfter($mlss);
                } );
            }
        } )( window, window.jQuery );
    </script>
    <?php
}
add_action( 'admin_head-options-media.php', 'thistle_change_size_settings_order' );

Je ne vous cache pas que l’utilisation de JavaScript pour faire ce genre de choses ne m’enchante jamais mais quand WordPress ne nous laisse pas le choix
 đŸ˜©

Sauf que, mĂȘme si nos nouveaux champs se trouvent bien sur notre page d’options et  sont dans le bon ordre, les nouvelles dimensions ne s’enregistrent pas en base de donnĂ©es đŸ€”
Pour que cela fonctionne, nous devons whitelister celles-ci de la façon suivante :

/**
 * Adds the `medium_large_size_w` and `medium_large_size_h` options
 * to the white list to be authorised to edit them.
 *
 * @param array White list options.
 * @return array.
 */
function thistle_media_whitelist_options( $whitelist_options ) {
    $whitelist_options['media'][] = 'medium_large_size_w';
    $whitelist_options['media'][] = 'medium_large_size_h';
    
    return $whitelist_options;
}
add_filter( 'whitelist_options', 'thistle_media_whitelist_options' );

Voila, c’est fini đŸ˜

CrĂ©er des raccourcis vers des pages « importantes » dans le menu d’administration de WordPress

Ce besoin est venu d’une ancienne chef de projet qui en avait marre de chercher certaines pages « importantes » dans la liste des pages đŸ˜„ Ă‡a peut se comprendre. Entre nous, avec l’extension Simple Page Ordering, elle aurait pu remonter les pages concernĂ©es tout en haut mais pas question (pour tout le monde) de voir la liste des pages dans le plan du site (via wp_list_pages) ĂȘtre impactĂ©e par ce choix/besoin cĂŽtĂ© back-office.

Alors d’un commun accord, nous avons dĂ©cidĂ© d’ajouter des raccourcis dans le menu « Pages » du back-office de WordPress. Juste entre l’entrĂ©e « Toutes les pages » et « Ajouter » pour que ça soit plus « pratique » et « logique » đŸ˜… Je savais que ça risquerait d’ĂȘtre chiant mais j’Ă©tais d’accord avec elle đŸ‘ De toutes façons, quand je rĂ©flĂ©chis avec les CdP, j’Ă©vite de penser Ă  la technique car ça bloque/tend souvent les discussions.

Donc voilĂ  comment j’ai rĂ©pondu Ă  notre besoin :

La premiĂšre chose Ă  faire est de pouvoir rĂ©cupĂ©rer et modifier une liste des pages qui vont faire l’objet d’un raccourci en tant que sous-menu. 
Par dĂ©faut, nous avons dĂ©cidĂ© d’ajouter la page d’accueil ainsi que la page « Politique de confidentialité ».

/**
 * Retrieves the list of pages which will have a shortcut into the "Page"
 * admin menu.
 *
 * @param void
 * @return array An associate array (`post_id => label`) of pages.
 */
function thistle_get_admin_submenu_page_shortcuts() {
    $shortcuts =  apply_filters( 'thistle_admin_submenu_page_shortcuts', array(
		get_option( 'page_on_front' )              => __( 'Front Page' ),
		get_option( 'wp_page_for_privacy_policy' ) => __( 'Privacy Policy' )
    ) );

    // Be sure that the page ID asked for is not zero ;)
    $shortcuts = array_filter( $shortcuts, function( $sk ) { return !! intval( $sk ); }, ARRAY_FILTER_USE_KEY );

    return is_array( $shortcuts ) ? $shortcuts : array();
}

La façon la plus « propre » et responsable d’ajouter ces pages serait d’utiliser la fonction add_pages_page. Mais malheureusement pour nous, cette fonction ne nous permet pas de choisir la position de nos Ă©lĂ©ments
 La faute Ă  add_submenu_page qui ne gĂšre pas de prioritĂ©/position contrairement Ă  sa grande soeur add_menu_page đŸ˜© On garde les doigts croisĂ©s (ça ne fait qu’un an et demi) pour que le ticket 39776 fasse parti de WordPress 5.0 mais en attendant il va falloir bricoler
 
J’ai donc dĂ©cidĂ© de me mettre sur l’action admin_menu et d’attaquer directement la variable globale $submenu sur l’index edit.php?post_type=page đŸ˜Ź Sauf que mĂȘme si ça parait plutot simple, il y a un hic đŸ˜‰ 
Voici comment est organisé le tableau pour construire le sous-menu :

Array
(
    [5] => Array
        (
            [0] => All pages
            [1] => edit_pages
            [2] => edit.php?post_type=page
        )

    [10] => Array
        (
            [0] => Add new
            [1] => edit_pages
            [2] => post-new.php?post_type=page
        )

)

MĂȘme si cela ne me choque pas vraiment (cf. : Menu Structure), il va falloir tenir compte de ce dĂ©tail pour que notre cinquiĂšme raccourci n’Ă©crase/efface pas l’entrĂ©e « Ajouter ». 
Donc Ă  la place d’insĂ©rer nos raccourcis sur un pas de 1, nous les ajouterons sur un pas de 0.1 đŸ˜œ Sachez le, la fonction add_menu_page accepte dĂ©jĂ  ce genre de valeurs (cf. : add_menu_page supports decimal positions) et c’est super pratique.

/**
 * Adds page shortcuts into the "Page" admin menu between "All Pages" and "Add new".
 *
 * @param void
 * @return void
 */
function thistle_add_admin_submenu_page_shortcuts() {
    global $submenu, $wp_post_types;

    $page_shortcuts = thistle_get_admin_submenu_page_shortcuts();
    $page_shortcuts_count = count( $page_shortcuts );

    // Exit early if no shortcut is needed.
    if ( empty( $page_shortcuts ) ) {
        return;
    }

    // Start to add shortcuts just after "all Pages" submenu.
    $index = (int) array_search( $wp_post_types['page']->labels->all_items, array_combine( array_keys( $submenu['edit.php?post_type=page'] ), array_column( $submenu['edit.php?post_type=page'], 0 ) ) );

    foreach ( $page_shortcuts as $id => $label ) {
        $index += .1;

        $submenu['edit.php?post_type=page'][(string) $index] = array(
            $label,
            'edit_pages',
            'post.php?post=' . $id . '&action=edit'
        );

      //add_pages_page( $label, $label, 'edit_pages', 'post.php?post=' . $id . '&action=edit', (string) $index );
    }

    ksort( $submenu['edit.php?post_type=page'] );
}
add_action( 'admin_menu', 'thistle_add_admin_submenu_page_shortcuts', 5 );

Il ne reste plus qu’Ă  sĂ©lectionner/illuminer le bon sous-menu car par dĂ©faut lors de l’Ă©dition d’un contenu, WordPress sĂ©lectionne/illumine l’entrĂ©e « gĂ©nĂ©rale » (dans notre cas « Toutes les pages »). 
Pour faire cela, il y a plusieurs Ă©coles : via admin_head, via parent_file ou celle ci-dessous đŸ˜™

/**
 * Highlights the shortcut submenu into the "Page" admin menu instead of "All pages".
 *
 * @param string $submenu_file The submenu file.
 * @param string $parent_file  The submenu item's parent file.
 * @return string
 */
function thistle_highlight_admin_submenu_page_shortcut( $submenu_file, $parent_file ) {
    global $post_type;

    $page_shortcuts = thistle_get_admin_submenu_page_shortcuts();

    if ( $post_type == 'page' && isset( $_GET['post'], $_GET['action'] ) && array_key_exists( $_GET['post'], $page_shortcuts ) && $_GET['action'] == 'edit' ) {
        return 'post.php?post=' . $_GET['post'] . '&action=edit';
    }

    return $submenu_file;
}
add_filter( 'submenu_file', 'thistle_highlight_admin_submenu_page_shortcut', 10, 2 );

Cela n’Ă©tait pas notre besoin lors du dĂ©veloppement mais l’ensemble de ce code pourrait tout Ă  fait ĂȘtre fonctionnel pour l’ensemble des types de contenu avec quelques ajustements.

Voila, c’est fini đŸ˜

Cacher son URL de connexion dans les emails RGPD de WordPress (et un peu plus)

Nous le savons, WordPress est un outil formidable qui propose des Ă©volutions toujours apprĂ©ciables (ou pas). La derniĂšre en date, ne dĂ©roge pas Ă  la rĂšgle surtout quand elle concerne la conformitĂ© RGPD 😃 
Si vous n’avez pas encore fait le tour du sujet, voici un article de monsieur Jb Audras : WordPress version 4.9.6/7 : de nouveaux outils pour l’application du RGPD, un autre article concernant WooCommerce : WooCommerce 3.4 GDPR featureset la liste des choses Ă  venir : Proposed Privacy Roadmap 
C’est quand mĂȘme cool tout ça !

Comme toutes nouvelles fonctionnalitĂ©s, mĂȘme si celle-ci facilite bien la vie aux administrateurs grĂące Ă  une interface soignĂ©e, l’envers du dĂ©cor pour les dĂ©veloppeurs est un peu moins rĂ©jouissant :

  1. WordPress ne fournit aucune aide pour enregistrer une demande venant de votre site. Pourquoi WP le fait-il pour les commentaires et pas avec ça finalement ? Je vous rassure, la communauté a déjà pensé à ce point : GDPR Data Request Form
  2. La page « Politique de confidentialité » est considérée comme uniquement éditable par les administrateurs : https://boiteaweb.fr/manage-privacy-options-roles-gdpr-105235.html
  3. Lorsqu’un utilisateur WP demande la suppression de ses donnĂ©es, son compte n’est ni supprimĂ©, ni anonymisĂ© et ses contenus lui sont toujours associĂ©s (vaste sujet hein).
  4. Tout le reste… Exemple le ticket 44354 et ceux listĂ©s dedans.

Mais le plus problĂ©matique pour moi Ă  ce moment est la divulgation de l’URL de connexion dans un email « public ». 
Voici ce que WordPress envoye aux visiteurs (n’importe qui) qui vont effectuer une demande d’export :

Bonjour,

Une demande a Ă©tĂ© faite afin d’effectuer les actions suivantes sur votre compte :

Exporter les données

Pour confirmer cela, veuillez cliquer sur le lien suivant :
https://www.monsite.test/wp-admin/wp-login.php?action=confirmaction&request_id=289&confirm_key=8Gq7SGilB2blScR7owW4

Si vous ne souhaitez pas effectuer cette action, vous pouvez ignorer et supprimer cet e-mail en tout sécurité.

Cet e-mail a été envoyé à xavier@monsite.test.

Cordialement,
L’équipe de monsite_test
https://www.monsite.test

Quel malheur de voir apparaĂźtre son URL de connexion et cela mĂȘme si on utilise une extension pour la cacherÂ đŸ˜„Â 
En attendant que les extentions concernĂ©es (ex.: Move Login) se mettent Ă  jour et pour ceux qui n’utilisent pas d’extension, voici comment j’ai abordĂ© le sujet/problĂšme :

Avant toutes choses, nous allons demander Ă  WP de stocker les archives d’exports ailleurs que dans le rĂ©pertoire par dĂ©faut wp-personal-data-exports. C’est un peu parano mais aprĂšs tout ce sont des donnĂ©es bien plus sensibles que les images de notre site.

/**
 * Returns the directory used to store personal data export files.
 *
 * @return string Exports directory.
 */
function thistle_privacy_exports_dir() {
    $upload_dir  = wp_upload_dir();
    $exports_dir = trailingslashit( $upload_dir['basedir'] ) . 'gprd-exports/';

    return apply_filters( 'thistle_privacy_exports_dir', $exports_dir );
}

/**
 * Returns the URL of the directory used to store personal data export files.
 *
 * @return string Exports directory URL.
 */
function thistle_privacy_exports_url() {
    $upload_dir  = wp_upload_dir();
    $exports_url = trailingslashit( $upload_dir['baseurl'] ) . 'gprd-exports/';

    return apply_filters( 'thistle_privacy_exports_url', $exports_url );
}

/**
 * Overrides some default WP choices:
 * - the directory used to store personal data export files.
 * - the URL of the directory used to store personal data export files.
 */
add_filter( 'wp_privacy_exports_dir', 'thistle_privacy_exports_dir' );
add_filter( 'wp_privacy_exports_url', 'thistle_privacy_exports_url' );

Evidemment, vous pouvez remplacer gprd-exports par ce que vous voulez 😉

Maintenant, le plus important, nous allons demander Ă  WP d’enregistrer deux alias :

  1. le premier pour cacher wp-login.php. Ce qui signifie qu’en nous rendant Ă  cette adresse https://www.monsite.test/donnes-personnelles/, nous verrons notre page de confirmation d’action RGPD.
  2. le second pour cacher le dossier oĂč sont stockĂ©es les archives des donnĂ©es exportĂ©es. Les archives stockĂ©es dans le rĂ©pertoire gprd-exports seront donc aussi accessibles sur l’URL https://www.monsite.test/donnes-personnelles/export/*.zip.
/**
 *
 *
 * @return string
 */
function thistle_gprd_make_link_relative( $link ) {
  return ltrim( wp_make_link_relative( $link ), '/' );
}

/**
 * Returns the public URL which will be used by the users to confirm
 * their action about their personal data.
 *
 * @return string
 */
function thistle_privacy_confirm_public_url() {
  $confirm_url = home_url( 'donnees-personnelles/' );

  return apply_filters( 'thistle_privacy_confirm_public_url', $confirm_url );
}

/**
 * Returns the public URL which will be used by the users to download
 * their personal data export file.
 *
 * @return string
 */
function thistle_privacy_exports_public_url() {
  $exports_url = trailingslashit( thistle_privacy_confirm_public_url() ) . 'export/';

  return apply_filters( 'thistle_privacy_exports_public_url', $exports_url );
}

/**
 * Adds rewrite rules to hide :
 *  - the URL of login page
 *  - the URL of the directory used to store personal data export files.
 *
 * for non-registered users. It's a bit paranoiac, but if you have activated a plugin
 * to hide/move your login page, you will not want to display it in a public email ;)
 *
 * @param void
 * @return void
 */
function thistle_rewrite_privacy_urls() {
    // Export URL
    $exports_rel_url = thistle_gprd_make_link_relative( thistle_privacy_exports_public_url() );
    $wp_privacy_exports_rel_url = thistle_gprd_make_link_relative( wp_privacy_exports_url() );

    add_rewrite_rule( $exports_rel_url . '([^/]+)\.zip$', $wp_privacy_exports_rel_url . '$1.zip', 'top' );

    // Confirm URL
    $confirm_rel_url = thistle_gprd_make_link_relative( thistle_privacy_confirm_public_url() );
    $wp_privacy_confirm_rel_url = thistle_gprd_make_link_relative( site_url() . '/wp-login.php' );

    add_rewrite_rule( $confirm_rel_url . '?$', $wp_privacy_confirm_rel_url, 'top' );
}
add_action( 'init', 'thistle_rewrite_privacy_urls' );

Ces rĂšgles concidĂ©rĂ©es comme non_wp_rules (car elles ne concernent pas index.php) iront s’Ă©crire dans votre fichier .htaccess entre # BEGIN WordPress et # END WordPress comme ci-dessous :

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteRule ^donnees-personnelles/export/([^/]+)\.zip$ /wp-content/uploads/gprd-exports/$1.zip [QSA,L]
RewriteRule ^donnees-personnelles/?$ /wp-admin/wp-login.php [QSA,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

# END WordPress

C’est bien joli d’avoir cette nouvelle URL https://www.monsite.test/donnes-personnelles/ mais il va falloir imposer quelques rĂšgles pour que les gens mal-intentionnĂ©s ne puissent pas y accĂ©der directement et voir notre boite de connexion. De toute façon, il n’y a aucun intĂ©rĂȘt Ă  accĂ©der Ă  cette page si ce n’est pas pour confirmer une action RGPD.

/**
 * Handles access to the new public URL which receives confirm actions from users for their personal data.
 *
 * Forbids direct or wrong formatted requests.
 *
 * @param void
 * @return void
 */
function thistle_handle_privacy_confirm_action() {
  $confirm_url_pattern = '/' . trim( thistle_gprd_make_link_relative( thistle_privacy_confirm_public_url() ), '/' );
  if ( mb_strpos( $_SERVER['REQUEST_URI'], $confirm_url_pattern ) !== 0 ) {
    return;
  }

  $wp_privacy_action = isset( $_REQUEST['action'], $_REQUEST['request_id'], $_REQUEST['confirm_key'] ) && ! empty( $_REQUEST['action'] ) && ! empty( $_REQUEST['request_id'] ) && ! empty( $_REQUEST['confirm_key'] );
  if ( ! $wp_privacy_action ) {
    wp_die( __( 'Invalid request.' ) );
  }

  if ( $_REQUEST['action'] != 'confirmaction' || ! intval( $_REQUEST['request_id'] ) ) {
    wp_die( __( 'Invalid request.' ) );
  }
}
add_filter( 'init', 'thistle_handle_privacy_confirm_action', 9 );

Avec ces quelques lignes, si quelqu’un arrive sur notre nouvelle URL sans faire une demande de confirmation sur une action RGPD valide, celui-ci sera en tĂȘte Ă  tĂȘte avec un message d’erreur.

AprĂšs tout ça, il ne reste plus qu’Ă  gĂ©rer l’essentiel : remplacer les URLs dans les emails envoyĂ©s par WordPress.

  1. l’URL de confirmation d’une action RGPD via le filtre user_request_action_email_content.
  2. l’URL de tĂ©lĂ©chargement de l’archive contenant nos donnĂ©es personnelles via le filtre user_request_action_email_content.
/**
 * Overrides the confirmation link into the email sent to the user
 * when an account action is attempted.
 *
 * @param string $email_text Text in the email.
 * @param array  $email_data {
 *     Data relating to the account action email.
 *
 *     @type WP_User_Request $request     User request object.
 *     @type string          $email       The email address this is being sent to.
 *     @type string          $description Description of the action being performed so the user knows what the email is for.
 *     @type string          $confirm_url The link to click on to confirm the account action.
 *     @type string          $sitename    The site name sending the mail.
 *     @type string          $siteurl     The site URL sending the mail.
 * }
 * @return string Text in the email.
 */
function thistle_override_privacy_confirm_url( $email_text, $email_data ) {
    $confirm_url_query = parse_url( $email_data['confirm_url'], PHP_URL_QUERY );
    $confirm_url = thistle_privacy_confirm_public_url() . '?' . $confirm_url_query;

    $email_text = str_replace( '###CONFIRM_URL###', esc_url_raw( $confirm_url ), $email_text );

    return $email_text;
}
add_filter( 'user_request_action_email_content', 'thistle_override_privacy_confirm_url', 10, 2 );

/**
 * Overrides the URL of the personal data export file into the email sent to the user.
 *
 * @param string $email_text Text in the email.
 * @param int    $request_id The request ID for this personal data export.
 * @return string
 */
function thistle_override_privacy_file_url( $email_text, $request_id ) {
    $export_file_url = get_post_meta( $request_id, '_export_file_url', true );
    $export_file_url = str_replace( wp_privacy_exports_url(), thistle_privacy_exports_public_url(), $export_file_url );

    $email_text = str_replace( '###LINK###', esc_url_raw( $export_file_url ), $email_text );

    return $email_text;
}
add_filter( 'wp_privacy_personal_data_email_content', 'thistle_override_privacy_file_url', 10, 2 );

Je pensais qu’avec ce ticket 44353 nous accĂšderions Ă  une façon plus « simple » de procĂ©der au remplacement de l’URL de confirmation mais non. Et les tickets 44376 et 44235 n’ont pas Ă©tĂ© plus chanceux


Pour finir, si vous avez envie d’aller plus loin avec la partie RGPD de WordPress pour rajouter vos donnĂ©es personnalisĂ©es aux exports par exemple, pensez à https://developer.wordpress.org/plugins/privacy/.

Voila, c’est fini 😁

Améliorer le choix des dispositions dans un contenu flexible ACF

Je pense que tout est parti d’un screenshot que j’ai vu passer sur le slack WordPress-fr et oĂč je me suis dit que ça serait gĂ©nial d’avoir la mĂȘme chose pour nos contributeurs 😍

Au boulot, nous adorons les contenus flexibles ACF mais il faut bien se l’avouer, la popin pour choisir les dispositions n’est pas trĂšs « sexy » pour l’utilisateur final.

Je pensais que cela serait assez simple car comme nous le savons tous, ACF est trĂšs extensible et bien codĂ©, sauf qu’il a fallu rĂ©flĂ©chir un peu plus que prĂ©vu 😅 
ACF ne propose pas encore la possibilitĂ© d’ajouter une option Ă  une disposition d’un contenu flexible. Pour les curieux, ça se passe ici : plugins/advanced-custom-fields-pro/pro/fields/class-acf-field-flexible-content.php#L564-648 et vraiment aucun hook dans les parages
 De toute façon, en y rĂ©flĂ©chissant un peu, rajouter une image depuis l’interface qu’il faudrait ensuite stocker dans la mĂ©diathĂšque (alors que c’est dĂ©jĂ  assez le bordel) ou ailleurs n’est pas une idĂ©e si simple que ça.

Alors regardons du cÎté de la popin qui affiche cette fameuse liste des dispositions :

<script type="text-html" class="tmpl-popup"><?php 
	?><div class="acf-fc-popup"><ul><?php foreach( $layouts as $layout ): 
		
		$atts = array(
			'href'			=> '#',
			'data-layout'	=> $layout['name'],
			'data-min' 		=> $layout['min'],
			'data-max' 		=> $layout['max'],
		);
		
		?><li><a <?php acf_esc_attr_e( $atts ); ?>><?php echo $layout['label']; ?></a></li><?php 
	
	endforeach; ?></ul></div>
</script>

Toujours pas de hook mais nous savons maintenant qu’il va falloir bidouiller le nom de nos dispositions 😂 Mais attention cette opĂ©ration n’est pas si simple que ça. La documentation d’ACF acf/fields/flexible_content/layout_title nous dit que cette donnĂ©e $layout['label'] est aussi utilisĂ©e pour composer le titre principal pour nos dispositions


Alors comment fait-on ?

Code

Dans un premier temps, nous amenons tout le luxe dont nous aurons besoin pour la personnalisation des dispositions de nos contenus flexibles. Nous pourrons donc choisir :

  • le rĂ©pertoire qui contient les images pour tous les champs ou pour chaque champ individuellement ;
  • l’extension de l’ensemble des images (on ne mĂ©lange pas les formats) ;
  • l’URL spĂ©cifique pour une disposition en particulier.

Et nous disposerons de donnĂ©es supplĂ©mentaires sur chaque disposition comme l’url de l’image de remplacement et le titre.

/**
 * Improve client’s experience by providing placeholder images instead of basic texts
 * to choose layouts in flexible content fields.
 *
 * @param array $field The field settings array. This can be modified and then returned
 * @return array $field The field settings array
 */
function swp_acf_override_flexible_content_layout_labels( $field ) {
    if ( $field['type'] != 'flexible_content' || (isset( $field['layout'] ) && empty( $field['layout'] )) ) {
        return $field;
    }

    $placeholders_rel_path = apply_filters( "swp_acf/fields/flexible_content/placeholders_rel_path", '', $field );
    $placeholders_rel_path = apply_filters( "swp_acf/fields/flexible_content/placeholders_rel_path/name={$field['_name']}", $placeholders_rel_path, $field );
    $placeholders_rel_path = apply_filters( "swp_acf/fields/flexible_content/placeholders_rel_path/key={$field['key']}", $placeholders_rel_path, $field );
    $placeholders_rel_path = rtrim( $placeholders_rel_path, '/' );

    if ( $placeholders_rel_path == '' ) {
        return $field;
    }

    $placeholders_extension = apply_filters( "swp_acf/fields/flexible_content/placeholders_extension", 'jpg', $field );
    $placeholders_extension = trim( $placeholders_extension, '.  \t\n\r\0\x0B' );
    $theme_file_uri = get_theme_file_uri();

    foreach ( $field['layouts'] as $key => $layout ) {
        $old_label = $field['layouts'][ $key ]['label'];

        $palceholder_url = $theme_file_uri . '/' . $placeholders_rel_path . '/' . $layout['name'] . '.' . $placeholders_extension;
        $palceholder_url = apply_filters( "swp_acf/fields/flexible_content/placeholder_url/name={$field['_name']}", $palceholder_url, $layout['name'], $layout, $field );
        $palceholder_url = apply_filters( "swp_acf/fields/flexible_content/placeholder_url/key={$field['key']}", $palceholder_url, $layout['name'], $layout, $field );

        $field['layouts'][ $key ]['title'] = $old_label;
        $field['layouts'][ $key ]['placeholder'] = $palceholder_url;
        $field['layouts'][ $key ]['label'] = '<img src="' . $palceholder_url . '">';
    }

    return $field;

}
add_filter( 'acf/prepare_field/type=flexible_content', 'swp_acf_override_flexible_content_layout_labels', 9, 1 );

Sans risque, nous rendons son titre Ă  la disposition si notre nouvelle donnĂ©e existe. Pas question d’aller afficher une image Ă  la place du titre 😉

/**
 * Reset the text (HTML) displayed at the top of each flexible content layout
 * with the first textual label instead of keeping the image placeholder.
 *
 * @param string $title  The layout title text. Defaults to the layout title
 * @param array  $field  The flexible content field settings array
 * @param array  $layout The current layout settings array
 * @param int    $i      The current layout index. The first layout index is 0
 * @return string $title The layout title text
 */
function swp_acf_reset_flexible_content_layout_title( $title, $field, $layout, $i ) {
    if ( isset( $layout['title'] ) && ! empty( $layout['title'] ) ) {
        return $layout['title'];
    }

    return $title;
}
add_filter( 'acf/fields/flexible_content/layout_title', 'swp_acf_reset_flexible_content_layout_title', 9, 4 );

Et pour finir avec quelques styles « inline », nous adaptons l’affichage des nouveaux contenus de la popin pour qu’il colle au fonctionnement par dĂ©faut Ă©tabli par ACF â˜ș

/**
 * Add some inline style to correctly display layout's image placeholder 
 * and follow the default ACF behaviour in the flexible content field popup.
 *
 * @param void
 * @return void
 */
function swp_acf_add_flexible_content_layout_placeholder_styles() {
    $inline_styles = <<<CSS
.acf-fc-popup a.disabled {
  cursor: default;
  pointer-events: none;
}
.acf-fc-popup img {
  display: block;
  height: auto;
  margin: -6px -5px;
  max-width: 135px;
  width: auto;
}
.acf-fc-popup /*a*/.disabled img {
  opacity: .5;
  filter: grayscale(100%);
}
.acf-fc-popup a:hover {
  background: 0;
}
.acf-fc-popup a:hover img {
  outline: 6px solid #0073aa;
}
CSS;

    wp_add_inline_style( 'acf-input', $inline_styles );
}
add_action( 'admin_enqueue_scripts', 'swp_acf_add_flexible_content_layout_placeholder_styles', PHP_INT_MAX );

Usage

function tc_acf_set_font_page_placeholders_rel_path( $rel_path ) {
    return '/assets/media/placeholders/acf/front-page/';
}
add_filter( 'swp_acf/fields/flexible_content/placeholders_rel_path/key=field_5a83f8cc4b135', 'tc_acf_set_font_page_placeholders_rel_path' );

Voila, c’est fini 😁

Edit 6 juin 2018

En Ă©crivant ce post, je n’avais pas connaissance de ressources similaires mais je viens d’en trouver deux en mĂȘme temps 😄

Ma mĂ©thode aborde le problĂšme diffĂ©remment mais au final l’utilisateur sera ravi 😘

Remplacement “responsive” d’image en CSS

Comme nous le savons, la crĂ©ation graphique d’un site passe toujours souvent par la rĂ©alisation d’Ă©lĂ©ments inutilement visuellement complexes.
Avec un peu de chance, voici sur quoi vous pourriez ou avez pu tomber en ouvrant un fichier :


Cette construction artistique se situant certainement dans un mouvement abstrait, futuriste ou art nouveau ne donne malheureusement pas la mĂȘme satisfaction Ă  toutes les Ă©tapes de la conception. Quoique.

Pour ĂȘtre franc, mon point de vue sur ce genre de choses (que je considĂšre dĂ©nuĂ©es de tout bon sens) est bien arrĂȘtĂ© : « Elles ne devront jamais ĂȘtre reprĂ©sentĂ©es par une structure HTML complexe et des centaines de lignes CSS imbuvables » .

Pour les autres, je vous laisse en tĂȘte Ă  tĂȘte avec la citation de Kevin Rocher :

Laisse les donc prouver qu’ils peuvent se faire un croque-monsieur au micro-ondes.

Sur le papier

La demande paraĂźt justifiĂ©e et rĂ©alisable. Cependant, une fois le story rĂ©alisĂ© (pour l’occasion et par moi mĂȘme), celui-ci a identifiĂ© quelques contraintes :

  • L’image d’arriĂšre-plan doit pouvoir ĂȘtre dessinĂ©e dans une surface contenant le contenu de taille variable
  • L’image de remplacement ne doit pas ĂȘtre Ă©tirĂ©e ou dĂ©formĂ©e (le ratio original doit toujours ĂȘtre prĂ©servĂ©)
  • Le ratio intrinsĂšque doit ĂȘtre calculĂ© en fonction de la largeur de la surface contenant le contenu et non celle de son parent
  • MĂȘme si il nous serait facile d’utiliser une balise <img /> (cf : « Un reset “responsive” pour les images. »), il n’y a aucun intĂ©rĂȘt Ă  ce que cette information soit prĂ©sente sous forme d’image si le style n’est pas appliquĂ© (dans le code source de la page).

L’une des techniques les plus connues pour remplacement du texte par une image en CSS est certainement celle du text-indent: -9999px; pour laquelle il a Ă©tĂ© proposĂ© une rĂ©cente alternative tout Ă  fait honorable mais qui a toutefois ses limites.
Pour ceux qui aime les choses plus « .ir » , vous avez la solution proposĂ©e par Nicolas Gallagher (“Another CSS image replacement technique”) qui a elle aussi Ă©tĂ© remplacĂ©e sans trop de bruit par celle-ci dans le projet HTML5 Boilerplate.

Le fait est qu’aucune de ces solutions ne rĂ©pond Ă  mon besoin. Elles nĂ©cessitent toutes de dĂ©finir une hauteur (et une largeur dans beaucoup de cas) Ă  notre contenant pour afficher l’image de remplacement.
En m’arrĂȘtant lĂ , je serai inĂ©vitablement entraĂźnĂ© (pour ne pas dire obligĂ© d’aller) vers la mise en place de solutions spĂ©cifiques qui peuvent ĂȘtre dĂ©multipliĂ©es en fonction des cas.

Dans la pratique

.rwd-ir {
  max-width: 308px; /* Natural width */
  background-image: url(le-client-et-le-graphiste-ont-toujours-raison.png);
  background-size: 100%;

  font: 0/0 a;
}
.rwd-ir:before {
  display: block;
  padding: 32.4675% 0 0; /* Height in percent (Natural height: 100px) */
  content: "";
}

Ça se passe plutît bien. Ça ne paraüt pas !

Que font chaques déclarations ?

  • max-width: 308px; – Permet de s’assurer que la largeur naturelle de l’image ne sera jamais dĂ©passĂ©e.
  • background-size: 100%; – SpĂ©cifie que l’image d’arriĂšre-plan doit ĂȘtre mise Ă  l’Ă©chelle pour ĂȘtre aussi grande que possible sur la largeur de la zone de contenu.
    Dans le cas oĂč une/des marges intĂ©rieures seraient appliquĂ©es, les dĂ©clarations background-origin: content-box; et background-repeat: no-repeat; devront ĂȘtre ajoutĂ©es pour que le fond soit peint uniquement dans la zone de contenu et Ă©viter la rĂ©pĂ©tition.
  • .rwd-ir:before {} – Donne Ă  la surface contenant le contenu un format spĂ©cifique qui respecte le ratio de l’image de remplacement et cela par rapport Ă  sa propre largeur.
    Cette mĂ©thode est inspirĂ©e de celle utilisĂ©e dans SUIT flexible embed ou FitVids.js pour ne citer qu’eux. À la seule diffĂ©rence que je profite d’un pseudo-Ă©lĂ©ment.

Ce qu’il faut savoir

Aucun hack de remplacement n’est parfait.
Celui au dessus a lui aussi ses limites (mĂȘme s’il a plutĂŽt bien marchĂ© lors de mon dernier projet) :

  • L’utilisation de la propriĂ©tĂ© background-size impose ses limites dans IE 8. Dans les cas simples, un polyfill suffira. Dans les plus complexes, dans les plus complexes

  • Cette approche exclut complĂštement le support d’IE 7. Je n’ai pas de regrets Ă  cela car elle n’a pas Ă©tĂ© pensĂ©e pour.
  • Comme beaucoup de mĂ©thodes de remplacement, elles ne fonctionnent pas si les CSS sont chargĂ©es mais que les images ne le sont pas. Que voulez-vous, nous ne pouvons pas tout avoir.

Pour finir

Les possibilitĂ©s pour rĂ©soudre ce problĂšme sont Ă  coup sĂ»r infinies mais en restant raisonnable (et en sachant ce que vous faites) cette technique s’utilisera sans encombre.

outline – une spec approximative

Honorable dĂ©fenseur de la propriĂ©tĂ© CSS outline (du moins jusque lĂ ), j’ai toujours pensĂ©/prĂ©tendu que celle-ci Ă©tait sous-estimĂ©e. Afin appuyer cette affirmation contestable, voici deux utilisations qui ne manquent pas d’imagination :

BriĂšvement, la propriĂ©té outline a Ă©tĂ© Ă©tablie lors de la spĂ©cification CSS2 et n’a que trĂšs peu Ă©voluĂ© depuis. Le seul gros changement est amenĂ© par la propriĂ©tĂ© complĂ©mentaire outline-offset dans le niveau 3 de CSS (source : CSS3 UI). Je vous l’accorde, il n’y a rien de bien transcendant surtout quand on ne la voit mĂȘme pas apparaĂźtre dans la notation raccourcie outline


Nous pourrions dĂ©battre longuement sur le fonctionnement qui se veut trĂšs minimaliste (sans rĂ©elles explications) mais ce n’est pas le sujet.

Dans la pratique

Nous utilisons que trĂšs rarement cette propriĂ©tĂ©. A tort certainement. L’usage le plus courant et le plus sauvage/horrible/dĂ©sespĂ©rant reste : * { outline: none; }. Dans mon cas, j’ai pris l’initiative de profiter de cette propriĂ©tĂ© pour remplacer/simuler une bordure.

Pourquoi ?

  1. Une envie est une envie. Ne pas y céder ne serait que plus frustrant.
  2. Un graphiste a prend le droit de faire des bordures externes.
  3. L’intĂ©grateur rĂąle mais n’a pas le choix parce qu’il le veut bien

  4. Le support de box-shadow commence qu’Ă  partir de IE9 (source : caniuse.com)

A premiĂšre vue, ça semble gĂ©nial. Mais ne nous y trompons pas, le web va rarement dans le mĂȘme sens (ça serait trop facile). Cette fois, c’est Opera qui ne semble/veut pas avoir la mĂȘme vision des choses : table des rendus, table des rendus mobiles

Un grand nombre (des dĂ©veloppeurs pour la plupart) diront : “Opera bla bla bla, les stats sur nos serveurs bla bla bla”. Je ne suis pas de cet avis, Opera reprĂ©sente aujourd’hui 300 millions d’utilisateurs qui vont prochainement devoir faire avec Webkit mais qui restent des utilisateurs dont nous devons nous prĂ©occuper.

Et alors ?

Il y a peu de ressources qui relatent le sujet :

Le bug problĂšme est connu et cela depuis la version 9.5 d’Opera. Difficile d’en dire plus (sans se tromper) car jusque lĂ  je n’ai pas trouvĂ© de billet/ticket rapportant le problĂšme officiellement.

La premiÚre ressource est certainement la plus intéressante. Pour ne pas changer, la vérité vraie se trouve dans les documentations (CSS 2.1, CSS3). Et voila de quoi nous laisser dubitatif :

The outline created with the outline properties is drawn “over” a box, i.e., the outline is always on top, and does not influence the position or size of the box, or of any other boxes.

This specification does not define how multiple overlapping outlines are drawn, or how outlines are drawn for boxes that are partially obscured behind other elements.

Since the outline does not affect formatting (i.e., no space is left for it in the box model), it may well overlap other elements on the page.

Difficile alors de parler de BUG aprĂšs ces trois extraits. Chacun est libre d’y voir ce que bon lui semble.

Cependant, il est peut ĂȘtre exagĂ©rer de garder un tel contre pied Ă  travers 11 versions de navigateur et 9 versions de moteur (source : WikipĂ©dia). Malheureusement, ce ne sera pas la derniĂšre fois que ce genre de choses se passe surtout quand tout le monde Ă  les yeux tournĂ©s vers les nouveautĂ©s CSS3.

Mais aprĂšs tout, pourquoi OpĂ©ra n’aurait-il pas raison sur son implĂ©mentation de la propriĂ©tĂ© outline ?

Faisons attention, une prochaine version de WebKit pourrait avoir droit à cette interprétation.