diff --git a/.autocorrectignore b/.autocorrectignore new file mode 100644 index 00000000000000..cabe9364ee1033 --- /dev/null +++ b/.autocorrectignore @@ -0,0 +1,7 @@ +# AutoCorrect Link ignore rules. +# https://github.com/huacnlee/autocorrect +# +# Like `.gitignore`, this file to tell AutoCorrect which files need to check, some need to ignore. +files/ +docs/ +!files/zh-cn/ diff --git a/.autocorrectrc b/.autocorrectrc new file mode 100644 index 00000000000000..8b8f715da76117 --- /dev/null +++ b/.autocorrectrc @@ -0,0 +1,4 @@ +textRules: + 一二三,四五六.七八九: 0 + 一二三,四五六,七八九,一二三,四五六,七八九: 0 + 9.9亿: 0 diff --git a/.github/workflows/autocorrect-lint.yml b/.github/workflows/autocorrect-lint.yml new file mode 100644 index 00000000000000..5c28e32aaf0d37 --- /dev/null +++ b/.github/workflows/autocorrect-lint.yml @@ -0,0 +1,44 @@ +# This workflow to use AutoCorrect tool for checking the copywriting, correct spaces and punctuations for CJK contents. +# +# For example: +# +# - incorrect: "欢迎阅读MDN文档." +# - correct: "欢迎阅读 MDN 文档。" +# +# - incorrect: "Welcome,this is MDN Web Docs。" +# - correct: "Welcome, to read MDN Web Docs." +# +# More details: +# https://github.com/huacnlee/autocorrect +name: AutoCorrect Lint +on: + pull_request: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Get changed files + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + # Use the GitHub API to get the list of changed files + # documenation: https://docs.github.com/rest/commits/commits#compare-two-commits + DIFF_DOCUMENTS=$(gh api repos/{owner}/{repo}/compare/${{ env.BASE_SHA }}...${{ env.HEAD_SHA }} \ + --jq '.files | .[] | select(.status|IN("added", "modified", "renamed", "copied", "changed")) | .filename') + # filter out files that are not markdown + DIFF_DOCUMENTS=$(echo "${DIFF_DOCUMENTS}" | egrep -i "^files/zh-cn/" | xargs) + echo "DIFF_DOCUMENTS=${DIFF_DOCUMENTS}" >> $GITHUB_ENV + + - name: AutoCorrect changed content + if: ${{ env.DIFF_DOCUMENTS }} + uses: huacnlee/autocorrect-action@v2.6.2 + with: + args: ${{ env.DIFF_DOCUMENTS }} --lint --no-diff-bg-color diff --git a/.prettierignore b/.prettierignore index f0d814407f8119..7b22224faa1977 100644 --- a/.prettierignore +++ b/.prettierignore @@ -20,6 +20,10 @@ build/ /files/fr/glossary/grid_lines/index.md /files/fr/glossary/main_axis/index.md /files/fr/learn/server-side/django/forms/index.md +/files/fr/web/css/align-self/index.md +/files/fr/web/css/justify-content/index.md +/files/fr/web/css/place-items/index.md +/files/fr/web/css/place-self/index.md /files/pt-br/learn/server-side/django/forms/index.md /files/ru/learn/server-side/django/forms/index.md /files/ru/learn/server-side/django/introduction/index.md @@ -35,10 +39,6 @@ build/ /files/es/web/javascript/reference/**/*.md # fr -/files/fr/mozilla/add-ons/webextensions/api/**/*.md -/files/fr/web/api/**/*.md -/files/fr/web/css/**/*.md -/files/fr/web/html/**/*.md /files/fr/web/javascript/**/*.md # ja diff --git a/files/fr/learn/forms/how_to_build_custom_form_controls/index.md b/files/fr/learn/forms/how_to_build_custom_form_controls/index.md index 829eacf73cc91f..620fd9bd4159f2 100644 --- a/files/fr/learn/forms/how_to_build_custom_form_controls/index.md +++ b/files/fr/learn/forms/how_to_build_custom_form_controls/index.md @@ -168,52 +168,44 @@ Maintenant que nous avons mis en place les fonctionnalités de base, le divertis ```css .select { - /* Toutes les tailles seront exprimées en valeurs em (lettre M, étalon - du cadratin : cadre dans lequel sont dessinées toutes les lettres d'une - police de caractères) pour des raisons d'accessibilité (pour être sûrs - que le widget reste redimensionnable si l'utilisateur utilise le zoom - du navigateur en mode texte exclusif). Les calculs sont faits en - supposant que 1em==16px qui est la valeur par défaut dans la majorité - des navigateurs. Si vous êtes perdus avec les conversions entre px et - em, essayez http://riddle.pl/emcalc/ */ - font-size : 0.625em; /* ceci (10px) est le nouveau contexte de taille de + /* Les calculs sont faits en supposant que 1em==16px qui est la valeur + par défaut dans la majorité des navigateurs. */ + font-size: 0.625em; /* ceci (10px) est le nouveau contexte de taille de police pour la valeur em dans ce contexte. */ - font-family : Verdana, Arial, sans-serif; + font-family: Verdana, Arial, sans-serif; - -moz-box-sizing : border-box; - box-sizing : border-box; + box-sizing: border-box; /* Nous avons besoin de plus d'espace pour la flèche vers le bas que nous allons ajouter. */ - padding : .1em 2.5em .2em .5em; /* 1px 25px 2px 5px */ - width : 10em; /* 100px */ + padding: 0.1em 2.5em 0.2em 0.5em; /* 1px 25px 2px 5px */ + width: 10em; /* 100px */ - border : .2em solid #000; /* 2px */ - border-radius : .4em; /* 4px */ - box-shadow : 0 .1em .2em rgba(0,0,0,.45); /* 0 1px 2px */ + border: 0.2em solid #000; /* 2px */ + border-radius: 0.4em; /* 4px */ + box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /* 0 1px 2px */ /* La première déclaration concerne les navigateurs qui ne prennent pas en charge les gradients linéaires. La deuxième déclaration est parce que les navigateurs basés sur WebKit ne l'ont pas encore préfixé. Si vous souhaitez prendre en charge les anciens navigateurs, essayez http://www.colorzilla.com/gradient-editor/ */ - background : #F0F0F0; - background : -webkit-linear-gradient(90deg, #E3E3E3, #fcfcfc 50%, #f0f0f0); - background : linear-gradient(0deg, #E3E3E3, #fcfcfc 50%, #f0f0f0); + background: #f0f0f0; + background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0); } .select .value { /* Comme la valeur peut être plus large que le widget, nous devons nous assurer qu'elle ne changera pas la largeur du widget. */ - display : inline-block; - width : 100%; - overflow : hidden; - - vertical-align: top; + display: inline-block; + width: 100%; + overflow: hidden; /* Et si le contenu déborde, c'est mieux d'avoir une jolie abreviation. */ - white-space : nowrap; + white-space: nowrap; text-overflow: ellipsis; + vertical-align: top; +} ``` Nous n'avons pas besoin d'un élément supplémentaire pour concevoir la flèche vers le bas ; à la place, nous utilisons le pseudo-élément {{cssxref(":after:after")}}. Cependant, elle pourrait également être mise en œuvre à l'aide d'une simple image de fond sur la classe `select`. @@ -227,7 +219,6 @@ Nous n'avons pas besoin d'un élément supplémentaire pour concevoir la flèche top: 0; right: 0; - -moz-box-sizing: border-box; box-sizing: border-box; height: 100%; @@ -254,7 +245,6 @@ Maintenant, composons la décoration de la liste des options : margin: 0; padding: 0; - -moz-box-sizing: border-box; box-sizing: border-box; /* Cela nous assure que même si les valeurs sont plus petites que le widget, @@ -291,408 +281,1403 @@ Pour les options, nous devons ajouter une classe `highlight` pour pouvoir identi } ``` -Donc, voici le résultat avec les trois états : - - - - - - - - - - - - - - - - - - - -
État initialÉtat actifÉtat ouvert
- {{EmbedLiveSample('État_initial',120,130, "", "Learn/Forms/How_to_build_custom_form_controls/Example_1")}} - - {{EmbedLiveSample("État_actif",120,130, "", "Learn/Forms/How_to_build_custom_form_controls/Example_1")}} - - {{EmbedLiveSample("État_ouvert",120,130, "", "Learn/Forms/How_to_build_custom_form_controls/Example_1")}} -
- Voir le code source -
- -## Donnez vie à votre widget avec JavaScript +Donc, voici le résultat avec les trois états ([consultez le code source ici](/fr/docs/Learn/Forms/How_to_build_custom_form_controls/Example_1)): -Maintenant que le design et la structure sont prêts, nous pouvons écrire le code JAvaScript pour que le widget fonctionne vraiment. +#### État initial -> **Attention :** Le code qui suit a été conçu à des fins éducatives et ne doit pas être utilisé tel quel. Entre autres choses, comme nous le verrons, il n'est pas à l'épreuve du temps et ne fonctionnera pas sur des navigateurs historiques. Il comporte également des parties redondantes. Elles devraient être optimisées pour du code de production. +```html hidden +
+ Cerise + +
+``` -> **Note :** Créer des widgets réutilisables peut se révéler un peu délicat. L'ébauche de la norme « [W3C Web Component](http://dvcs.w3.org/hg/webcomponents/raw-file/tip/explainer/index.html) » apporte des réponses à cette question particulière. Le [projet X-Tag](http://x-tags.org/) est un essai de mise en œuvre de cette spécification ; nous vous encourageons à y jeter un coup d'œil. +```css hidden +.select { + position: relative; + display: inline-block; +} -### Pourquoi ne fonctionne-t-il pas ? +.select.active, +.select:focus { + box-shadow: 0 0 3px 1px #227755; + outline: none; +} -Avant de commencer, il est important de se rappeler quelque chose de très important à propos de JavaScript : dans un navigateur, c'est une technique peu fiable. Lorsque vous créez des widgets personnalisés, vous êtes obligé de faire appel à JavaScript parce que c'est un fil nécessaire pour tout lier ensemble. Cependant, il existe de nombreux cas dans lesquels JavaScript n'est pas capable de s'exécuter dans le navigateur : +.select .optList { + position: absolute; + top: 100%; + left: 0; +} -- L'utilisateur a désactivé le JavaScript : c'est un cas assez inhabituel, peu de personnes désactivent le JavaScript de nos jours. -- Le script ne se charge pas. La chose est très courante, en particulier dans le domaine des mobiles pour lesquels le réseau n'est pas sûr. -- Le script est bogué. Il faut toujours prendre en considération cette éventualité. -- Le script est en conflit avec un autre script tierce‑partie. Cela peut se produire avec des suites de scripts ou n'importe quel marque page utilisé par l'utilisateur. -- Le script est en conflit avec, ou est affecté par un extension de navigateur (comme l'extension « [No script](https://addons.mozilla.org/fr/firefox/addon/noscript/) » de Firefox ou « [Scripts »](https://chrome.google.com/webstore/detail/notscripts/odjhifogjcknibkahlpidmdajjpkkcfn) de Chrome). -- L'utilisateur utilise un navigateur ancien et l'une des fonctions dont vous avez besoin n'est pas prise en charge. Cela se produira fréquemment lorsque vous utiliserez des API de pointe.s. +.select .optList.hidden { + max-height: 0; + visibility: hidden; +} -En raison de ces aléas, il est vraiment important de considérer avec sérieux ce qui se passe si JavaScript ne fonctionne pas. Traiter en détail cette question est hors de la portée de cet article parce qu'elle est étroitement liée à la façon dont vous voulez rendre votre script générique et réutilisable, mais nous prendrons en considération les bases de ce sujet dans notre exemple. +.select { + font-size: 0.625em; /* 10px */ + font-family: Verdana, Arial, sans-serif; -Ainsi, si notre code JavaScript ne s'exécute pas, nous reviendrons à l'affichage d'un élément {{HTMLElement("select")}} standard. Pour y parvenir, nous avons besoin de deux choses. + box-sizing: border-box; -Tout d'abord, nous devons ajouter un élément {{HTMLElement("select")}} régulier avant chaque utilisation de notre widget personnalisé. Ceci est également nécessaire pour pouvoir envoyer les données de notre widget personnalisé avec le reste de nos données du formulaire ; nous reviendrons sur ce point plus tard. + padding: 0.1em 2.5em 0.2em 0.5em; /* 1px 25px 2px 5px */ + width: 10em; /* 100px */ -```html - -
- + border: 0.2em solid #000; /* 2px */ + border-radius: 0.4em; /* 4px */ -
- Cerise - -
-
- -``` + box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /* 0 1px 2px */ -Deuxièmement, nous avons besoin de deux nouvelles classes pour nous permettre de cacher l'élément qui ne sert pas (c'est-à-dire l'élément{{HTMLElement("select")}} « réel » si notre script ne fonctionne pas, ou le widget personnalisé s'il fonctionne). Notez que par défaut, le code HTML cache le widget personnalisé. + background: #f0f0f0; + background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0); +} -```css -.widget select, -.no-widget .select { - /* Ce sélecteur CSS dit fondamentalement : - - soit la classe body est "widget" et donc l'élément {{HTMLElement("select")}} réel sera caché - - soit la classe body n'a pas changé, elle est toujours "no-widget", - et donc les éléments, dont la classe est « select », doivent être cachés */ - position: absolute; - left: -5000em; - height: 0; +.select .value { + display: inline-block; + width: 100%; overflow: hidden; + + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: top; } -``` -Maintenant nous avons juste besoin d'un commutateur JavaScript pour déterminer si le script est en cours d'exécution ou non. Cette bascule est très simple : si au moment du chargement de la page notre script est en cours d'exécution, il supprime la classe no-widget et ajoute la classe widget, échangeant ainsi la visibilité de l'élément {{HTMLElement("select")}} et du widget personnalisé. +.select:after { + content: "▼"; + position: absolute; + z-index: 1; + height: 100%; + width: 2em; /* 20px */ + top: 0; + right: 0; -```js -window.addEventListener("load", function () { - document.body.classList.remove("no-widget"); - document.body.classList.add("widget"); -}); -``` + padding-top: 0.1em; - - - - - - - - - - - - - - - - -
Sans JavaScriptAvec JavaScript
- {{EmbedLiveSample("Sans_JS",120,130, "", "Learn/Forms/How_to_build_custom_form_controls/Example_2")}} - - {{EmbedLiveSample("JS",120,130, "", "Learn/Forms/How_to_build_custom_form_controls/Example_2")}} -
- Voir le code source -
+ box-sizing: border-box; -> **Note :** Si vous voulez vraiment rendre votre code générique et réutilisable, au lieu de faire un changement de classe, il est préférable d'ajouter la classe widget pour cacher les éléments {{HTMLElement("select")}} et d'ajouter dynamiquement l'arbre DOM représentant le widget personnalisé après chaque élément {{HTMLElement("select")}} dans la page. + text-align: center; -### Rendre le travail plus facile + border-left: 0.2em solid #000; + border-radius: 0 0.1em 0.1em 0; -Dans le code que nous sommes sur le point de construire, nous utiliserons l'API standard DOM pour faire tout le travail dont nous avons besoin. Cependant, bien que la prise en charge de l'API DOM se soit améliorée dans les navigateurs, il y a toujours des problèmes avec les anciens navigateurs (surtout avec le bon vieux navigateur Internet Explorer). + background-color: #000; + color: #fff; +} -Si vous voulez éviter les problèmes avec les navigateurs anciens, il y a deux façons de le faire : en utilisant un framework dédié tel que jQuery, $dom, prototype, Dojo, YUI ou similaire, ou bien en remplissant la fonctionnalité manquante que vous voulez utiliser (ce qui peut facilement être fait par un chargement conditionnel, avec la bibliothèque yepnope par exemple). +.select .optList { + z-index: 2; -Les fonctionnalités que nous prévoyons d'utiliser sont les suivantes (classées de la plus risquée à la plus sûre) : + list-style: none; + margin: 0; + padding: 0; -1. {{domxref("element.classList","classList")}} -2. {{domxref("EventTarget.addEventListener","addEventListener")}} -3. [`forEach`](/fr/docs/JavaScript/Reference/Global_Objects/Array/forEach) (ce n'est pas du DOM mais du JavaScript moderne) -4. {{domxref("element.querySelector","querySelector")}} et {{domxref("element.querySelectorAll","querySelectorAll")}} + background: #f0f0f0; + border: 0.2em solid #000; + border-top-width: 0.1em; + border-radius: 0 0 0.4em 0.4em; -Au-delà de la disponibilité de ces fonctionnalités spécifiques, il reste encore un problème avant de commencer. L'objet retourné par la fonction {{domxref("element.querySelectorAll","querySelectorAll()")}} est une {{domxref("NodeList")}} plutôt qu'un [`Array`](/fr/docs/JavaScript/Reference/Global_Objects/Array). C'est important, car les objets `Array` acceptent la fonction [`forEach`](/fr/docs/JavaScript/Reference/Global_Objects/Array/forEach), mais {{domxref("NodeList")}} ne le fait pas. Comme {{domxref("NodeList")}} ressemble vraiment à un `Array` et que `forEach` est d'utilisation si commode, nous pouvons facilement ajouter la prise en charge de `forEach à` {{domxref("NodeList")}} pour nous faciliter la vie, comme ceci : + box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4); -```js -NodeList.prototype.forEach = function (callback) { - Array.prototype.forEach.call(this, callback); -}; -``` + box-sizing: border-box; -On ne plaisantait pas quand on a dit que c'était facile à faire. + min-width: 100%; + max-height: 10em; /* 100px */ + overflow-y: auto; + overflow-x: hidden; +} -### Construction des fonctions de rappel d'événements +.select .option { + padding: 0.2em 0.3em; +} -Les fondations sont prêtes, nous pouvons maintenant commencer à définir toutes les fonctions à utiliser chaque fois que l'utilisateur interagit avec notre widget. +.select .highlight { + background: #000; + color: #ffffff; +} +``` -```js -// Cette fonction est utilisée chaque fois que nous voulons désactiver un -// widget personnalisé. Elle prend un paramètre -// select : le nœud DOM avec la classe select à désactiver -function deactivateSelect(select) { - // Si le widget n'est pas actif, il n'y a rien à faire - if (!select.classList.contains("active")) return; +{{EmbedLiveSample("État_initial",120,130)}} - // Nous devons obtenir la liste des options pour le widget personnalisé - var optList = select.querySelector(".optList"); +#### État actif - // Nous cachons la liste des options - optList.classList.add("hidden"); +```html hidden +
+ Cerise + +
+``` - // et nous désactivons le widget personnalisé lui-même - select.classList.remove("active"); +```css hidden +.select { + position: relative; + display: inline-block; } -// Cette fonction sera utilisée chaque fois que l'utilisateur veut (des)activer le widget -// Elle prend deux paramètres : -// select : le nœud DOM de la classe `select` à activer -// selectList : la liste de tous les nœuds DOM de la classe `select` -function activeSelect(select, selectList) { - // Si le widget est déjà actif il n'y a rien à faire - if (select.classList.contains("active")) return; +.select.active, +.select:focus { + box-shadow: 0 0 3px 1px #227755; + outline: none; +} - // Nous devons désactiver tous les widgets personnalisés - // comme la fonction deactivateSelect remplit toutes les fonctionnalités de la - // fonction de rappel forEach, nous l'utilisons directement sans utiliser - // une fonction anonyme intermédiaire. - selectList.forEach(deactivateSelect); +.select .optList { + position: absolute; + top: 100%; + left: 0; +} - // Et nous activons l'état du widget donné - select.classList.add("active"); +.select .optList.hidden { + max-height: 0; + visibility: hidden; } -// Cette fonction sera utilisée chaque fois que l'utilisateur veut enrouler/dérouler la -// liste des options -// Elle prend un paramètre : -// select : le nœud DOM de la liste à basculer -function toggleOptList(select) { - // La liste est prise à partir du widget - var optList = select.querySelector(".optList"); +.select { + font-size: 0.625em; /* 10px */ + font-family: Verdana, Arial, sans-serif; - // Nous changeons la classe de la liste pour l'enrouler/dérouler - optList.classList.toggle("hidden"); -} + box-sizing: border-box; -// Cett fonction sera utilisée chaque fois qu'il faut mettre en surbrillance -// une option. Elle prend deux paramètres : -// select : le nœud DOM de la classe `select` -// contenant l'option à mettre en surbrillance -// option : le nœud DOM de la classe `option` à mettre en surbrillance -function highlightOption(select, option) { - // Obtenir la liste de toutes les options disponibles pour l'élémént sélectionné - var optionList = select.querySelectorAll(".option"); + padding: 0.1em 2.5em 0.2em 0.5em; /* 1px 25px 2px 5px */ + width: 10em; /* 100px */ - // Supprimer la surbrillance pour toutes les options - optionList.forEach(function (other) { - other.classList.remove("highlight"); - }); + border: 0.2em solid #000; /* 2px */ + border-radius: 0.4em; /* 4px */ - // Mettre en surbrillance l'option correcte - option.classList.add("highlight"); + box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /* 0 1px 2px */ + + background: #f0f0f0; + background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0); } -``` -C'est tout ce dont on a besoin pour gérer les différents états du widget personnalisé. +.select .value { + display: inline-block; + width: 100%; + overflow: hidden; -Ensuite, nous assujettissons ces fonctions aux événement appropriés : + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: top; +} -```js -// Nous lions le widget aux événements dès le chargement du document -window.addEventListener("load", function () { - var selectList = document.querySelectorAll(".select"); +.select:after { + content: "▼"; + position: absolute; + z-index: 1; + height: 100%; + width: 2em; /* 20px */ + top: 0; + right: 0; - // Chaque widget personnalisé doit être initialisé - selectList.forEach(function (select) { - // de même que tous les éléments `option` - var optionList = select.querySelectorAll(".option"); + padding-top: 0.1em; - // Chaque fois que l'utilisateur passe le pointeur de souris - // sur une option, nous mettons en surbrillance la dite option + box-sizing: border-box; - optionList.forEach(function (option) { - option.addEventListener("mouseover", function () { - // Note : les variables `select` et `option` sont des "closures" - // disponibles dans la portée de notre appel de fonction. - highlightOption(select, option); - }); - }); + text-align: center; - // Chaque fois que l'utilisateur clique sur un élément personnalisé - select.addEventListener("click", function (event) { - // Note : la variable `select` est une "closure" - // available dans la portée de notre appel de fonction. + border-left: 0.2em solid #000; + border-radius: 0 0.1em 0.1em 0; - // Nous basculons la visibilité de la liste des options + background-color: #000; + color: #fff; +} + +.select .optList { + z-index: 2; + + list-style: none; + margin: 0; + padding: 0; + + background: #f0f0f0; + border: 0.2em solid #000; + border-top-width: 0.1em; + border-radius: 0 0 0.4em 0.4em; + + box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4); + + box-sizing: border-box; + + min-width: 100%; + max-height: 10em; /* 100px */ + overflow-y: auto; + overflow-x: hidden; +} + +.select .option { + padding: 0.2em 0.3em; +} + +.select .highlight { + background: #000; + color: #ffffff; +} +``` + +{{EmbedLiveSample("État_actif",120,130)}} + +#### État ouvert + +```html hidden +
+ Cerise + +
+``` + +```css hidden +.select { + position: relative; + display: inline-block; +} + +.select.active, +.select:focus { + box-shadow: 0 0 3px 1px #227755; + outline: none; +} + +.select .optList { + position: absolute; + top: 100%; + left: 0; +} + +.select .optList.hidden { + max-height: 0; + visibility: hidden; +} + +.select { + font-size: 0.625em; /* 10px */ + font-family: Verdana, Arial, sans-serif; + + box-sizing: border-box; + + padding: 0.1em 2.5em 0.2em 0.5em; /* 1px 25px 2px 5px */ + width: 10em; /* 100px */ + + border: 0.2em solid #000; /* 2px */ + border-radius: 0.4em; /* 4px */ + + box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /* 0 1px 2px */ + + background: #f0f0f0; + background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0); +} + +.select .value { + display: inline-block; + width: 100%; + overflow: hidden; + + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: top; +} + +.select:after { + content: "▼"; + position: absolute; + z-index: 1; + height: 100%; + width: 2em; /* 20px */ + top: 0; + right: 0; + + padding-top: 0.1em; + + box-sizing: border-box; + + text-align: center; + + border-left: 0.2em solid #000; + border-radius: 0 0.1em 0.1em 0; + + background-color: #000; + color: #fff; +} + +.select .optList { + z-index: 2; + + list-style: none; + margin: 0; + padding: 0; + + background: #f0f0f0; + border: 0.2em solid #000; + border-top-width: 0.1em; + border-radius: 0 0 0.4em 0.4em; + + box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4); + + box-sizing: border-box; + + min-width: 100%; + max-height: 10em; /* 100px */ + overflow-y: auto; + overflow-x: hidden; +} + +.select .option { + padding: 0.2em 0.3em; +} + +.select .highlight { + background: #000; + color: #fff; +} +``` + +{{EmbedLiveSample("État_ouvert",120,130)}} + +## Donnez vie à votre widget avec JavaScript + +Maintenant que le design et la structure sont prêts, nous pouvons écrire le code JAvaScript pour que le widget fonctionne vraiment. + +> **Attention :** Le code qui suit a été conçu à des fins éducatives et ne doit pas être utilisé tel quel. Entre autres choses, comme nous le verrons, il n'est pas à l'épreuve du temps et ne fonctionnera pas sur des navigateurs historiques. Il comporte également des parties redondantes. Elles devraient être optimisées pour du code de production. + +> **Note :** Créer des widgets réutilisables peut se révéler un peu délicat. L'ébauche de la norme « [W3C Web Component](http://dvcs.w3.org/hg/webcomponents/raw-file/tip/explainer/index.html) » apporte des réponses à cette question particulière. Le [projet X-Tag](http://x-tags.org/) est un essai de mise en œuvre de cette spécification ; nous vous encourageons à y jeter un coup d'œil. + +### Pourquoi ne fonctionne-t-il pas ? + +Avant de commencer, il est important de se rappeler quelque chose de très important à propos de JavaScript : dans un navigateur, c'est une technique peu fiable. Lorsque vous créez des widgets personnalisés, vous êtes obligé de faire appel à JavaScript parce que c'est un fil nécessaire pour tout lier ensemble. Cependant, il existe de nombreux cas dans lesquels JavaScript n'est pas capable de s'exécuter dans le navigateur : + +- L'utilisateur a désactivé le JavaScript : c'est un cas assez inhabituel, peu de personnes désactivent le JavaScript de nos jours. +- Le script ne se charge pas. La chose est très courante, en particulier dans le domaine des mobiles pour lesquels le réseau n'est pas sûr. +- Le script est bogué. Il faut toujours prendre en considération cette éventualité. +- Le script est en conflit avec un autre script tierce‑partie. Cela peut se produire avec des suites de scripts ou n'importe quel marque page utilisé par l'utilisateur. +- Le script est en conflit avec, ou est affecté par un extension de navigateur (comme l'extension « [No script](https://addons.mozilla.org/fr/firefox/addon/noscript/) » de Firefox ou « [Scripts »](https://chrome.google.com/webstore/detail/notscripts/odjhifogjcknibkahlpidmdajjpkkcfn) de Chrome). +- L'utilisateur utilise un navigateur ancien et l'une des fonctions dont vous avez besoin n'est pas prise en charge. Cela se produira fréquemment lorsque vous utiliserez des API de pointe.s. + +En raison de ces aléas, il est vraiment important de considérer avec sérieux ce qui se passe si JavaScript ne fonctionne pas. Traiter en détail cette question est hors de la portée de cet article parce qu'elle est étroitement liée à la façon dont vous voulez rendre votre script générique et réutilisable, mais nous prendrons en considération les bases de ce sujet dans notre exemple. + +Ainsi, si notre code JavaScript ne s'exécute pas, nous reviendrons à l'affichage d'un élément {{HTMLElement("select")}} standard. Pour y parvenir, nous avons besoin de deux choses. + +Tout d'abord, nous devons ajouter un élément {{HTMLElement("select")}} régulier avant chaque utilisation de notre widget personnalisé. Ceci est également nécessaire pour pouvoir envoyer les données de notre widget personnalisé avec le reste de nos données du formulaire ; nous reviendrons sur ce point plus tard. + +```html + +
+ + +
+ Cerise + +
+
+ +``` + +Deuxièmement, nous avons besoin de deux nouvelles classes pour nous permettre de cacher l'élément qui ne sert pas (c'est-à-dire l'élément{{HTMLElement("select")}} « réel » si notre script ne fonctionne pas, ou le widget personnalisé s'il fonctionne). Notez que par défaut, le code HTML cache le widget personnalisé. + +```css +.widget select, +.no-widget .select { + /* Ce sélecteur CSS dit fondamentalement : + - soit la classe body est "widget" et donc l'élément {{HTMLElement("select")}} réel sera caché + - soit la classe body n'a pas changé, elle est toujours "no-widget", + et donc les éléments, dont la classe est « select », doivent être cachés */ + position: absolute; + left: -5000em; + height: 0; + overflow: hidden; +} +``` + +Maintenant nous avons juste besoin d'un commutateur JavaScript pour déterminer si le script est en cours d'exécution ou non. Cette bascule est très simple : si au moment du chargement de la page notre script est en cours d'exécution, il supprime la classe no-widget et ajoute la classe widget, échangeant ainsi la visibilité de l'élément {{HTMLElement("select")}} et du widget personnalisé. + +```js +window.addEventListener("load", function () { + document.body.classList.remove("no-widget"); + document.body.classList.add("widget"); +}); +``` + +#### Sans JS + +Consultez le [code source complet ici](/fr/docs/Learn/Forms/How_to_build_custom_form_controls/Example_2#sans_js). + +```html hidden +
+ + +
+ Cerise + +
+
+ +``` + +```css hidden +.widget select, +.no-widget .select { + position: absolute; + left: -5000em; + height: 0; + overflow: hidden; +} +``` + +{{EmbedLiveSample("Sans_JS", 120, 130)}} + +#### Avec JS + +Consultez le [code source complet ici](/fr/docs/Learn/Forms/How_to_build_custom_form_controls/Example_2#avec_js). + +```html hidden +
+ + +
+ Cerise + +
+
+ +``` + +```css hidden +.widget select, +.no-widget .select { + position: absolute; + left: -5000em; + height: 0; + overflow: hidden; +} + +/* --------------- */ +/* Styles requis */ +/* --------------- */ + +.select { + position: relative; + display: inline-block; +} + +.select.active, +.select:focus { + box-shadow: 0 0 3px 1px #227755; + outline: none; +} + +.select .optList { + position: absolute; + top: 100%; + left: 0; +} + +.select .optList.hidden { + max-height: 0; + visibility: hidden; +} + +/* ------------ */ +/* Styles décor */ +/* ------------ */ + +.select { + font-size: 0.625em; /* 10px */ + font-family: Verdana, Arial, sans-serif; + + box-sizing: border-box; + + padding: 0.1em 2.5em 0.2em 0.5em; /* 1px 25px 2px 5px */ + width: 10em; /* 100px */ + + border: 0.2em solid #000; /* 2px */ + border-radius: 0.4em; /* 4px */ + + box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /* 0 1px 2px */ + + background: #f0f0f0; + background: -webkit-linear-gradient(90deg, #e3e3e3, #fcfcfc 50%, #f0f0f0); + background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0); +} + +.select .value { + display: inline-block; + width: 100%; + overflow: hidden; + + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: top; +} + +.select:after { + content: "▼"; + position: absolute; + z-index: 1; + height: 100%; + width: 2em; /* 20px */ + top: 0; + right: 0; + + padding-top: 0.1em; + + box-sizing: border-box; + + text-align: center; + + border-left: 0.2em solid #000; + border-radius: 0 0.1em 0.1em 0; + + background-color: #000; + color: #fff; +} + +.select .optList { + z-index: 2; + + list-style: none; + margin: 0; + padding: 0; + + background: #f0f0f0; + border: 0.2em solid #000; + border-top-width: 0.1em; + border-radius: 0 0 0.4em 0.4em; + + box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4); + + box-sizing: border-box; + + min-width: 100%; + max-height: 10em; /* 100px */ + overflow-y: auto; + overflow-x: hidden; +} + +.select .option { + padding: 0.2em 0.3em; +} + +.select .highlight { + background: #000; + color: #ffffff; +} +``` + +```js hidden +window.addEventListener("load", function () { + const form = document.querySelector("form"); + + form.classList.remove("no-widget"); + form.classList.add("widget"); +}); +``` + +{{EmbedLiveSample("Avec_JS", 120, 130)}} + +> **Note :** Si vous voulez vraiment rendre votre code générique et réutilisable, au lieu de faire un changement de classe, il est préférable d'ajouter la classe widget pour cacher les éléments {{HTMLElement("select")}} et d'ajouter dynamiquement l'arbre DOM représentant le widget personnalisé après chaque élément {{HTMLElement("select")}} dans la page. + +### Rendre le travail plus facile + +Dans le code que nous sommes sur le point de construire, nous utiliserons l'API standard DOM pour faire tout le travail dont nous avons besoin. Cependant, bien que la prise en charge de l'API DOM se soit améliorée dans les navigateurs, il y a toujours des problèmes avec les anciens navigateurs (surtout avec le bon vieux navigateur Internet Explorer). + +Si vous voulez éviter les problèmes avec les navigateurs anciens, il y a deux façons de le faire : en utilisant un framework dédié tel que jQuery, $dom, prototype, Dojo, YUI ou similaire, ou bien en remplissant la fonctionnalité manquante que vous voulez utiliser (ce qui peut facilement être fait par un chargement conditionnel, avec la bibliothèque yepnope par exemple). + +Les fonctionnalités que nous prévoyons d'utiliser sont les suivantes (classées de la plus risquée à la plus sûre) : + +1. {{domxref("element.classList","classList")}} +2. {{domxref("EventTarget.addEventListener","addEventListener")}} +3. {{domxref("NodeList.forEach()")}} +4. {{domxref("element.querySelector","querySelector")}} et {{domxref("element.querySelectorAll","querySelectorAll")}} + +### Construction des fonctions de rappel d'événements + +Les fondations sont prêtes, nous pouvons maintenant commencer à définir toutes les fonctions à utiliser chaque fois que l'utilisateur interagit avec notre widget. + +```js +// Cette fonction est utilisée chaque fois que nous voulons désactiver un +// widget personnalisé. Elle prend un paramètre +// select : le nœud DOM avec la classe select à désactiver +function deactivateSelect(select) { + // Si le widget n'est pas actif, il n'y a rien à faire + if (!select.classList.contains("active")) return; + + // Nous devons obtenir la liste des options pour le widget personnalisé + const optList = select.querySelector(".optList"); + + // Nous cachons la liste des options + optList.classList.add("hidden"); + + // et nous désactivons le widget personnalisé lui-même + select.classList.remove("active"); +} + +// Cette fonction sera utilisée chaque fois que l'utilisateur veut (des)activer le widget +// Elle prend deux paramètres : +// select : le nœud DOM de la classe `select` à activer +// selectList : la liste de tous les nœuds DOM de la classe `select` +function activeSelect(select, selectList) { + // Si le widget est déjà actif il n'y a rien à faire + if (select.classList.contains("active")) return; + + // Nous devons désactiver tous les widgets personnalisés + // comme la fonction deactivateSelect remplit toutes les fonctionnalités de la + // fonction de rappel forEach, nous l'utilisons directement sans utiliser + // une fonction anonyme intermédiaire. + selectList.forEach(deactivateSelect); + + // Et nous activons l'état du widget donné + select.classList.add("active"); +} + +// Cette fonction sera utilisée chaque fois que l'utilisateur veut enrouler/dérouler la +// liste des options +// Elle prend un paramètre : +// select : le nœud DOM de la liste à basculer +function toggleOptList(select) { + // La liste est prise à partir du widget + const optList = select.querySelector(".optList"); + + // Nous changeons la classe de la liste pour l'enrouler/dérouler + optList.classList.toggle("hidden"); +} + +// Cett fonction sera utilisée chaque fois qu'il faut mettre en surbrillance +// une option. Elle prend deux paramètres : +// select : le nœud DOM de la classe `select` +// contenant l'option à mettre en surbrillance +// option : le nœud DOM de la classe `option` à mettre en surbrillance +function highlightOption(select, option) { + // Obtenir la liste de toutes les options disponibles pour l'élémént sélectionné + const optionList = select.querySelectorAll(".option"); + + // Supprimer la surbrillance pour toutes les options + optionList.forEach((other) => { + other.classList.remove("highlight"); + }); + + // Mettre en surbrillance l'option correcte + option.classList.add("highlight"); +} +``` + +C'est tout ce dont on a besoin pour gérer les différents états du widget personnalisé. + +Ensuite, nous assujettissons ces fonctions aux événement appropriés : + +```js +// Nous lions le widget aux événements dès le chargement du document +window.addEventListener("load", function () { + const selectList = document.querySelectorAll(".select"); + + // Chaque widget personnalisé doit être initialisé + selectList.forEach((select) => { + // de même que tous les éléments `option` + const optionList = select.querySelectorAll(".option"); + + // Chaque fois que l'utilisateur passe le pointeur de souris + // sur une option, nous mettons en surbrillance la dite option + + optionList.forEach((option) => { + option.addEventListener("mouseover", function () { + // Note : les variables `select` et `option` sont des "closures" + // disponibles dans la portée de notre appel de fonction. + highlightOption(select, option); + }); + }); + + // Chaque fois que l'utilisateur clique sur un élément personnalisé + select.addEventListener("click", function (event) { + // Note : la variable `select` est une "closure" + // available dans la portée de notre appel de fonction. + + // Nous basculons la visibilité de la liste des options toggleOptList(select); }); - // Dans le cas où le widget obtient le focus - // Le widget obtient le focus chaque fois que l'utilisateur clique dessus - // ou presse la touche Tab pour avoir accès au widget - select.addEventListener("focus", function (event) { - // Note : les variable `select` et `selectList` sont des "closures" - // disponibles dans la portée de notre appel de fonction. + // Dans le cas où le widget obtient le focus + // Le widget obtient le focus chaque fois que l'utilisateur clique dessus + // ou presse la touche Tab pour avoir accès au widget + select.addEventListener("focus", function (event) { + // Note : les variable `select` et `selectList` sont des "closures" + // disponibles dans la portée de notre appel de fonction. + + // Nous activons le widget + activeSelect(select, selectList); + }); + + // Dans le cas où le widget perd le focus + select.addEventListener("blur", function (event) { + // Note : la variable `select` est une "closure" + // disponible dans la portée de notre appel de fonction. + + // Nous désactivons le widget + deactivateSelect(select); + }); + + // Relâcher le focus si la personne utilise la touche Echap + select.addEventListener("keyup", (event) => { + // Désactivation sur appui sur Echap + if (event.key === "Escape") { + deactivateSelect(select); + } + }); + }); +}); +``` + +A ce stade, notre widget change d'état comme nous l'avons conçu, mais sa valeur n'est pas encore mise à jour. On s'en occupera après. + +#### Exemple en direct + +Consultez le [code source complet ici](/fr/docs/Learn/Forms/How_to_build_custom_form_controls/Example_3). + +```html hidden +
+ + +
+ Cerise + +
+
+``` + +```css hidden +.widget select, +.no-widget .select { + position: absolute; + left: -5000em; + height: 0; + overflow: hidden; +} + +/* --------------- */ +/* Styles Requis */ +/* --------------- */ + +.select { + position: relative; + display: inline-block; +} + +.select.active, +.select:focus { + box-shadow: 0 0 3px 1px #227755; + outline: none; +} + +.select .optList { + position: absolute; + top: 100%; + left: 0; +} + +.select .optList.hidden { + max-height: 0; + visibility: hidden; +} + +/* ------------ */ +/* Style chic */ +/* ------------ */ + +.select { + font-size: 0.625em; /* 10px */ + font-family: Verdana, Arial, sans-serif; + + box-sizing: border-box; + + padding: 0.1em 2.5em 0.2em 0.5em; /* 1px 25px 2px 5px */ + width: 10em; /* 100px */ + + border: 0.2em solid #000; /* 2px */ + border-radius: 0.4em; /* 4px */ + + box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /* 0 1px 2px */ + + background: #f0f0f0; + background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0); +} + +.select .value { + display: inline-block; + width: 100%; + overflow: hidden; + + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: top; +} + +.select:after { + content: "▼"; + position: absolute; + z-index: 1; + height: 100%; + width: 2em; /* 20px */ + top: 0; + right: 0; + + padding-top: 0.1em; + + box-sizing: border-box; + + text-align: center; + + border-left: 0.2em solid #000; + border-radius: 0 0.1em 0.1em 0; + + background-color: #000; + color: #fff; +} + +.select .optList { + z-index: 2; + + list-style: none; + margin: 0; + padding: 0; + + background: #f0f0f0; + border: 0.2em solid #000; + border-top-width: 0.1em; + border-radius: 0 0 0.4em 0.4em; + + box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4); + + box-sizing: border-box; + + min-width: 100%; + max-height: 10em; /* 100px */ + overflow-y: auto; + overflow-x: hidden; +} + +.select .option { + padding: 0.2em 0.3em; +} + +.select .highlight { + background: #000; + color: #ffffff; +} +``` + +```js hidden +function deactivateSelect(select) { + if (!select.classList.contains("active")) return; + + const optList = select.querySelector(".optList"); + + optList.classList.add("hidden"); + select.classList.remove("active"); +} + +function activeSelect(select, selectList) { + if (select.classList.contains("active")) return; + + selectList.forEach(deactivateSelect); + select.classList.add("active"); +} + +function toggleOptList(select, show) { + const optList = select.querySelector(".optList"); + + optList.classList.toggle("hidden"); +} + +function highlightOption(select, option) { + const optionList = select.querySelectorAll(".option"); + + optionList.forEach((other) => { + other.classList.remove("highlight"); + }); + + option.classList.add("highlight"); +} + +// ------------------- // +// Lien aux événements // +// ------------------- // + +window.addEventListener("load", () => { + const form = document.querySelector("form"); + + form.classList.remove("no-widget"); + form.classList.add("widget"); +}); + +window.addEventListener("load", () => { + const selectList = document.querySelectorAll(".select"); + + selectList.forEach((select) => { + const optionList = select.querySelectorAll(".option"); + + optionList.forEach((option) => { + option.addEventListener("mouseover", () => { + highlightOption(select, option); + }); + }); + + select.addEventListener( + "click", + (event) => { + toggleOptList(select); + }, + false, + ); - // Nous activons le widget + select.addEventListener("focus", (event) => { activeSelect(select, selectList); }); - // Dans le cas où le widget perd le focus - select.addEventListener("blur", function (event) { - // Note : la variable `select` est une "closure" - // disponible dans la portée de notre appel de fonction. - - // Nous désactivons le widget + select.addEventListener("blur", (event) => { deactivateSelect(select); }); + + select.addEventListener("keyup", (event) => { + if (event.keyCode === 27) { + deactivateSelect(select); + } + }); }); }); ``` -A ce stade, notre widget change d'état comme nous l'avons conçu, mais sa valeur n'est pas encore mise à jour. On s'en occupera après. +{{EmbedLiveSample("Exemple_en_direct",120,130)}} + +### Gérer la valeur du widget + +Maintenant que notre widget fonctionne, nous devons ajouter du code pour mettre à jour la valeur en fonction des entrées utilisateur et envoyer cette valeur avec les données du formulaire. + +La façon la plus simple de le faire est d'utiliser un widget natif sous‑jacent. Un tel widget gardera une trace de la valeur avec tous les contrôles intégrés fournis par le navigateur, et la valeur sera envoyée comme d'habitude lorsque le formulaire sera soumis. Il ne sert à rien de réinventer la roue alors que tout cela peut être fait pour nous. + +Comme nous l'avons vu précédemment, nous utilisons déjà un widget de sélection natif comme solution de repli pour des raisons d'accessibilité ; nous pouvons simplement synchroniser sa valeur avec celle de notre widget personnalisé : + +```js +// Cette fonction met à jour la valeur affichée et la synchronise avec celle +// du widget natif. Elle prend deux paramètres : +// select : le nœud DOM de la classe `select` contenant la valuer à mettre à jour +// index : l'index de la valeur choisie +function updateValue(select, index) { + // Nous devons obtenir le widget natif correspondant au widget personnalisé + // Dans notre exemple, le widget natif est un parent du widget personnalisé + const nativeWidget = select.previousElementSibling; + + // Nou devons aussi obtenir la valeur de remplacement du widget personnalisé + const value = select.querySelector(".value"); + + // Et nous avons besoin de toute la liste des options + const optionList = select.querySelectorAll(".option"); + + // Nous définissons l'index choisi à l'index du choix + nativeWidget.selectedIndex = index; + + // Nous mettons à jour la valeur de remplacement en accord + value.innerHTML = optionList[index].innerHTML; + + // Et nous mettons en surbrillance l'option correspondante du widget personnalisé + highlightOption(select, optionList[index]); +} + +// Cette fonction renvoie l'index courant dans le widget natif +// Elle prend un paramètre : +// select : le nœud DOM avec la classe `select` relative au widget natif +function getIndex(select) { + // Nous avons besoin d'avoir accès au widget natif pour le widget personnalisé + // Dans notre exemple, le widget natif est un parent du widget personnalisé + const nativeWidget = select.previousElementSibling; + + return nativeWidget.selectedIndex; +} +``` + +Avec ces deux fonctions, nous pouvons lier les widgets natifs avec les personnalisés : + +```js +// Nous lions le widget aux événements dès le chargement du document +window.addEventListener("load", function () { + const selectList = document.querySelectorAll(".select"); + + // Chaque widget personnalisé doit être initialisé + selectList.forEach((select) => { + const optionList = select.querySelectorAll(".option"); + const selectedIndex = getIndex(select); + + // Nous rendons le widget personnalisé capable d'avoir le focus + select.tabIndex = 0; + + // Nous faisons en sorte que le widget natif ne puisse plus avoir le focus + select.previousElementSibling.tabIndex = -1; + + // Nous nous assurons que la valeur sélectionnée par défaut est bien affichée + updateValue(select, selectedIndex); + + // Chaque fois que l'utilisateur clique sur une option, nous mettons à + // jour la valeur en accord + optionList.forEach((option, index) => { + option.addEventListener("click", (event) => { + updateValue(select, index); + }); + }); + + // Chaque fois que l'utilisateur utilise le clavier sur un widget + // avec focus, les valeurs sont mises à jour en accord + + select.addEventListener("keyup", (event) => { + let index = getIndex(select); + + // Lorsque l'utilisateur utilise la touche Echap + // le contrôle est désactivé + if (event.key === "Escape") { + deactivateSelect(select); + } + + // Quand l'utilisateur presse la flèche bas, nous allons à l'option suivante + if (event.key === "ArrowDown" && index < optionList.length - 1) { + index++; + } + + // Quand l'utilisateur presse la flèche haut, nous sautons à l'option suivante + if (event.key === "ArrowUp" && index > 0) { + index--; + } + + updateValue(select, index); + }); + }); +}); +``` + +Dans le code ci-dessus, il faut noter l'utilisation de la propriété [`tabIndex`](/fr/docs/Web/API/HTMLElement/tabIndex). Utiliser cette propriété est nécessaire pour être sûr que le widget natif n'obtiendra jamais le focus et que le widget personnalisé l'obtiendra quand l'utilisateur utilise le clavier ou la souris. + +Et voilà, nous avons terminé ! Voici le résultat ([consultez le code source ici](/fr/docs/Learn/Forms/How_to_build_custom_form_controls/Example_4)) : + +```html hidden +
+ + +
+ Cerise + +
+
+``` + +```css hidden +.widget select, +.no-widget .select { + position: absolute; + left: -5000em; + height: 0; + overflow: hidden; +} + +/* --------------- */ +/* Styles Requis */ +/* --------------- */ + +.select { + position: relative; + display: inline-block; +} + +.select.active, +.select:focus { + box-shadow: 0 0 3px 1px #227755; + outline: none; +} + +.select .optList { + position: absolute; + top: 100%; + left: 0; +} + +.select .optList.hidden { + max-height: 0; + visibility: hidden; +} + +/* ------------ */ +/* Styles chic */ +/* ------------ */ + +.select { + font-size: 0.625em; /* 10px */ + font-family: Verdana, Arial, sans-serif; + + box-sizing: border-box; + + padding: 0.1em 2.5em 0.2em 0.5em; /* 1px 25px 2px 5px */ + width: 10em; /* 100px */ + + border: 0.2em solid #000; /* 2px */ + border-radius: 0.4em; /* 4px */ + + box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /* 0 1px 2px */ + + background: #f0f0f0; + background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0); +} + +.select .value { + display: inline-block; + width: 100%; + overflow: hidden; + + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: top; +} + +.select:after { + content: "▼"; + position: absolute; + z-index: 1; + height: 100%; + width: 2em; /* 20px */ + top: 0; + right: 0; + + padding-top: 0.1em; + + box-sizing: border-box; + + text-align: center; + + border-left: 0.2em solid #000; + border-radius: 0 0.1em 0.1em 0; + + background-color: #000; + color: #fff; +} + +.select .optList { + z-index: 2; + + list-style: none; + margin: 0; + padding: 0; + + background: #f0f0f0; + border: 0.2em solid #000; + border-top-width: 0.1em; + border-radius: 0 0 0.4em 0.4em; + + box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4); + + box-sizing: border-box; + + min-width: 100%; + max-height: 10em; /* 100px */ + overflow-y: auto; + overflow-x: hidden; +} + +.select .option { + padding: 0.2em 0.3em; +} + +.select .highlight { + background: #000; + color: #ffffff; +} +``` + +```js hidden +// ------------------------- // +// Définitions des fonctions // +// ------------------------- // + +function deactivateSelect(select) { + if (!select.classList.contains("active")) return; + + const optList = select.querySelector(".optList"); + + optList.classList.add("hidden"); + select.classList.remove("active"); +} - - - - - - - - - - - - - - -
Exemple en direct
- {{EmbedLiveSample("Changement_détat",120,130, "", "Learn/Forms/How_to_build_custom_form_controls/Example_3")}} -
- Voir le code source -
+function activeSelect(select, selectList) { + if (select.classList.contains("active")) return; -### Gérer la valeur du widget + selectList.forEach(deactivateSelect); + select.classList.add("active"); +} -Maintenant que notre widget fonctionne, nous devons ajouter du code pour mettre à jour la valeur en fonction des entrées utilisateur et envoyer cette valeur avec les données du formulaire. +function toggleOptList(select, show) { + const optList = select.querySelector(".optList"); -La façon la plus simple de le faire est d'utiliser un widget natif sous‑jacent. Un tel widget gardera une trace de la valeur avec tous les contrôles intégrés fournis par le navigateur, et la valeur sera envoyée comme d'habitude lorsque le formulaire sera soumis. Il ne sert à rien de réinventer la roue alors que tout cela peut être fait pour nous. + optList.classList.toggle("hidden"); +} -Comme nous l'avons vu précédemment, nous utilisons déjà un widget de sélection natif comme solution de repli pour des raisons d'accessibilité ; nous pouvons simplement synchroniser sa valeur avec celle de notre widget personnalisé : +function highlightOption(select, option) { + const optionList = select.querySelectorAll(".option"); -```js -// Cette fonction met à jour la valeur affichée et la synchronise avec celle -// du widget natif. Elle prend deux paramètres : -// select : le nœud DOM de la classe `select` contenant la valuer à mettre à jour -// index : l'index de la valeur choisie -function updateValue(select, index) { - // Nous devons obtenir le widget natif correspondant au widget personnalisé - // Dans notre exemple, le widget natif est un parent du widget personnalisé - var nativeWidget = select.previousElementSibling; + optionList.forEach((other) => { + other.classList.remove("highlight"); + }); - // Nou devons aussi obtenir la valeur de remplacement du widget personnalisé - var value = select.querySelector(".value"); + option.classList.add("highlight"); +} - // Et nous avons besoin de toute la liste des options - var optionList = select.querySelectorAll(".option"); +function updateValue(select, index) { + const nativeWidget = select.previousElementSibling; + const value = select.querySelector(".value"); + const optionList = select.querySelectorAll(".option"); - // Nous définissons l'index choisi à l'index du choix nativeWidget.selectedIndex = index; - - // Nous mettons à jour la valeur de remplacement en accord value.innerHTML = optionList[index].innerHTML; - - // Et nous mettons en surbrillance l'option correspondante du widget personnalisé highlightOption(select, optionList[index]); } -// Cette fonction renvoie l'index courant dans le widget natif -// Elle prend un paramètre : -// select : le nœud DOM avec la classe `select` relative au widget natif function getIndex(select) { - // Nous avons besoin d'avoir accès au widget natif pour le widget personnalisé - // Dans notre exemple, le widget natif est un parent du widget personnalisé - var nativeWidget = select.previousElementSibling; + const nativeWidget = select.previousElementSibling; return nativeWidget.selectedIndex; } -``` -Avec ces deux fonctions, nous pouvons lier les widgets natifs avec les personnalisés : +// -------------------- // +// Liens aux événements // +// -------------------- // -```js -// Nous lions le widget aux événements dès le chargement du document -window.addEventListener("load", function () { - var selectList = document.querySelectorAll(".select"); +window.addEventListener("load", () => { + const form = document.querySelector("form"); - // Chaque widget personnalisé doit être initialisé - selectList.forEach(function (select) { - var optionList = select.querySelectorAll(".option"), - selectedIndex = getIndex(select); + form.classList.remove("no-widget"); + form.classList.add("widget"); +}); - // Nous rendons le widget personnalisé capable d'avoir le focus - select.tabIndex = 0; +window.addEventListener("load", () => { + const selectList = document.querySelectorAll(".select"); - // Nous faisons en sorte que le widget natif ne puisse plus avoir le focus + selectList.forEach((select) => { + const optionList = select.querySelectorAll(".option"); + + optionList.forEach((option) => { + option.addEventListener("mouseover", () => { + highlightOption(select, option); + }); + }); + + select.addEventListener("click", (event) => { + toggleOptList(select); + }); + + select.addEventListener("focus", (event) => { + activeSelect(select, selectList); + }); + + select.addEventListener("blur", (event) => { + deactivateSelect(select); + }); + }); +}); + +window.addEventListener("load", () => { + const selectList = document.querySelectorAll(".select"); + + selectList.forEach((select) => { + const optionList = select.querySelectorAll(".option"); + const selectedIndex = getIndex(select); + + select.tabIndex = 0; select.previousElementSibling.tabIndex = -1; - // Nous nous assurons que la valeur sélectionnée par défaut est bien affichée updateValue(select, selectedIndex); - // Chaque fois que l'utilisateur clique sur une option, nous mettons à - // jour la valeur en accord - optionList.forEach(function (option, index) { - option.addEventListener("click", function (event) { + optionList.forEach((option, index) => { + option.addEventListener("click", (event) => { updateValue(select, index); }); }); - // Chaque fois que l'utilisateur utilise le clavier sur un widget - // avec focus, les valeurs sont mises à jour en accord - - select.addEventListener("keyup", function (event) { - var length = optionList.length, - index = getIndex(select); + select.addEventListener("keyup", (event) => { + let index = getIndex(select); - // Quand l'utilisateur presse ⇓, nous allons à l'option suivante - if (event.keyCode === 40 && index < length - 1) { + if (event.key === "Escape") { + deactivateSelect(select); + } + if (event.key === "ArrowDown" && index < optionList.length - 1) { index++; } - - // Quand l'utilisateur presse ⇑, nous sautons à l'option suivante - if (event.keyCode === 38 && index > 0) { + if (event.key === "ArrowUp" && index > 0) { index--; } @@ -702,32 +1687,7 @@ window.addEventListener("load", function () { }); ``` -Dans le code ci-dessus, il faut noter l'utilisation de la propriété [`tabIndex`](/fr/docs/Web/API/HTMLElement/tabIndex). Utiliser cette propriété est nécessaire pour être sûr que le widget natif n'obtiendra jamais le focus et que le widget personnalisé l'obtiendra quand l'utilisateur utilise le clavier ou la souris. - -Et voilà, nous avons terminé ! Voici le résultat : - - - - - - - - - - - - - - - -
Exemple en direct
- {{EmbedLiveSample("Changement_détat",120,130, "", "Learn/Forms/How_to_build_custom_form_controls/Example_4")}} -
- Voir le code source -
+{{EmbedLiveSample("Gérer_la_valeur_du_widget",120,130)}} Mais attendez, avons‑nous vraiment terminé ? @@ -771,12 +1731,12 @@ L'attribut `aria-selected` s'utilise pour marquer l'option actuellement sélecti ```js function updateValue(select, index) { - var nativeWidget = select.previousElementSibling; - var value = select.querySelector(".value"); - var optionList = select.querySelectorAll(".option"); + const nativeWidget = select.previousElementSibling; + const value = select.querySelector(".value"); + const optionList = select.querySelectorAll(".option"); // Nous nous assurons qu'aucune option n'est sélectionnée - optionList.forEach(function (other) { + optionList.forEach((other) => { other.setAttribute("aria-selected", "false"); }); @@ -789,30 +1749,275 @@ function updateValue(select, index) { } ``` -Voici le résultat final de toutes ces modifications (vous obtiendrez un meilleur ressenti en les testant avec une technique d'assistance comme [NVDA](http://www.nvda-project.org/) ou [VoiceOver](http://www.apple.com/accessibility/voiceover/)) : - - - - - - - - - - - - - - - -
Exemple en direct
- {{EmbedLiveSample("Changement_détat",120,130, "", "Learn/Forms/How_to_build_custom_form_controls/Example_5")}} -
- Voir le code source -
+Voici le résultat final de toutes ces modifications (vous obtiendrez un meilleur ressenti en les testant avec une technique d'assistance comme [NVDA](http://www.nvda-project.org/) ou [VoiceOver](http://www.apple.com/accessibility/voiceover/)). Consultez le [code complet source ici](/fr/docs/Learn/Forms/How_to_build_custom_form_controls/Example_5) : + +```html hidden +
+ + +
+ Cerise + +
+
+``` + +```css hidden +.widget select, +.no-widget .select { + position: absolute; + left: -5000em; + height: 0; + overflow: hidden; +} + +/* --------------- */ +/* Styles Requis */ +/* --------------- */ + +.select { + position: relative; + display: inline-block; +} + +.select.active, +.select:focus { + box-shadow: 0 0 3px 1px #227755; + outline: none; +} + +.select .optList { + position: absolute; + top: 100%; + left: 0; +} + +.select .optList.hidden { + max-height: 0; + visibility: hidden; +} + +/* ------------ */ +/* Styles Chic */ +/* ------------ */ + +.select { + font-size: 0.625em; /* 10px */ + font-family: Verdana, Arial, sans-serif; + + box-sizing: border-box; + + padding: 0.1em 2.5em 0.2em 0.5em; /* 1px 25px 2px 5px */ + width: 10em; /* 100px */ + + border: 0.2em solid #000; /* 2px */ + border-radius: 0.4em; /* 4px */ + + box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /* 0 1px 2px */ + + background: #f0f0f0; + background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0); +} + +.select .value { + display: inline-block; + width: 100%; + overflow: hidden; + + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: top; +} + +.select:after { + content: "▼"; + position: absolute; + z-index: 1; + height: 100%; + width: 2em; /* 20px */ + top: 0; + right: 0; + + padding-top: 0.1em; + + box-sizing: border-box; + + text-align: center; + + border-left: 0.2em solid #000; + border-radius: 0 0.1em 0.1em 0; + + background-color: #000; + color: #fff; +} + +.select .optList { + z-index: 2; + + list-style: none; + margin: 0; + padding: 0; + + background: #f0f0f0; + border: 0.2em solid #000; + border-top-width: 0.1em; + border-radius: 0 0 0.4em 0.4em; + + box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4); + + box-sizing: border-box; + + min-width: 100%; + max-height: 10em; /* 100px */ + overflow-y: auto; + overflow-x: hidden; +} + +.select .option { + padding: 0.2em 0.3em; +} + +.select .highlight { + background: #000; + color: #ffffff; +} +``` + +```js hidden +// ------------------------- // +// Définitions des fonctions // +// ------------------------- // +function deactivateSelect(select) { + if (!select.classList.contains("active")) return; + + const optList = select.querySelector(".optList"); + + optList.classList.add("hidden"); + select.classList.remove("active"); +} + +function activeSelect(select, selectList) { + if (select.classList.contains("active")) return; + + selectList.forEach(deactivateSelect); + select.classList.add("active"); +} + +function toggleOptList(select, show) { + const optList = select.querySelector(".optList"); + + optList.classList.toggle("hidden"); +} + +function highlightOption(select, option) { + const optionList = select.querySelectorAll(".option"); + + optionList.forEach((other) => { + other.classList.remove("highlight"); + }); + + option.classList.add("highlight"); +} + +function updateValue(select, index) { + const nativeWidget = select.previousElementSibling; + const value = select.querySelector(".value"); + const optionList = select.querySelectorAll(".option"); + + optionList.forEach((other) => { + other.setAttribute("aria-selected", "false"); + }); + + optionList[index].setAttribute("aria-selected", "true"); + + nativeWidget.selectedIndex = index; + value.innerHTML = optionList[index].innerHTML; + highlightOption(select, optionList[index]); +} + +function getIndex(select) { + const nativeWidget = select.previousElementSibling; + + return nativeWidget.selectedIndex; +} + +// -------------------- // +// Liens aux événements // +// -------------------- // + +window.addEventListener("load", () => { + const form = document.querySelector("form"); + + form.classList.remove("no-widget"); + form.classList.add("widget"); +}); + +window.addEventListener("load", () => { + const selectList = document.querySelectorAll(".select"); + + selectList.forEach((select) => { + const optionList = select.querySelectorAll(".option"); + const selectedIndex = getIndex(select); + + select.tabIndex = 0; + select.previousElementSibling.tabIndex = -1; + + updateValue(select, selectedIndex); + + optionList.forEach((option, index) => { + option.addEventListener("mouseover", function () { + highlightOption(select, option); + }); + + option.addEventListener("click", (event) => { + updateValue(select, index); + }); + }); + + select.addEventListener("click", (event) => { + toggleOptList(select); + }); + + select.addEventListener("focus", (event) => { + activeSelect(select, selectList); + }); + + select.addEventListener("blur", (event) => { + deactivateSelect(select); + }); + + select.addEventListener("keyup", (event) => { + let index = getIndex(select); + + if (event.keyCode === 27) { + deactivateSelect(select); + } + if (event.keyCode === 40 && index < optionList.length - 1) { + index++; + } + if (event.keyCode === 38 && index > 0) { + index--; + } + + updateValue(select, index); + }); + }); +}); +``` + +{{EmbedLiveSample("Lattribut_aria-selected",120,130)}} ## Conclusion diff --git a/files/fr/learn/forms/how_to_structure_a_web_form/index.md b/files/fr/learn/forms/how_to_structure_a_web_form/index.md index 553a13bf78e6d7..fc8903b9906552 100644 --- a/files/fr/learn/forms/how_to_structure_a_web_form/index.md +++ b/files/fr/learn/forms/how_to_structure_a_web_form/index.md @@ -130,31 +130,35 @@ En fait, il est possible d'associer plusieurs étiquettes à un seul widget, mai Considérons cet exemple : ```html -

Les champs obligatoires sont suivis de *.

+

+ Les champs obligatoires sont suivis de *. +

-
+ -
+
- - + +
``` +{{EmbedLiveSample("", 120, 120)}} + Le paragraphe du haut définit la règle pour les éléments obligatoires. Ce doit être au début pour s'assurer que les techniques d'assistance telles que les lecteurs d'écran l'afficheront ou le vocaliseront à l'utilisateur avant qu'il ne trouve un élément obligatoire. Ainsi, ils sauront ce que signifie l'astérisque. Un lecteur d'écran mentionnera l'astérisque en disant « astérisque » ou « obligatoire », selon les réglages du lecteur d'écran — dans tous les cas, ce qui sera dit est clairement précisé dans le premier paragraphe. - Dans le premier exemple, l'étiquette n'est pas lue du tout avec l'entrée — vous obtenez simplement « texte édité vierge », puis les étiquettes réelles sont lues séparément. Les multiples éléments \