From d229e390d475ea399249a23abaa4c1b26b61c092 Mon Sep 17 00:00:00 2001 From: A1lo Date: Sat, 29 Jul 2023 11:39:51 +0800 Subject: [PATCH] sync translation of 'build custom form controls' (#14568) Co-authored-by: Jason Ren <40999116+jasonren0403@users.noreply.github.com> --- .../example_1/index.md | 64 +- .../example_2/index.md | 28 +- .../example_3/index.md | 82 +- .../example_4/index.md | 96 +- .../example_5/index.md | 286 +++ .../index.md | 2176 ++++++++++++++--- 6 files changed, 2178 insertions(+), 554 deletions(-) create mode 100644 files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_5/index.md diff --git a/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_1/index.md b/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_1/index.md index 44f9c81511596b..b03f2b6ddefca6 100644 --- a/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_1/index.md +++ b/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_1/index.md @@ -1,9 +1,9 @@ --- -title: Example 1 +title: 示例 1 slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 --- -这是第一个[如果构建自定义表单小部件](/zh-CN/docs/Web/Guide/HTML/Forms/How_to_build_custom_form_widgets)的代码解释事例。 +这是第一个[如何构建自定义表单控件](/zh-CN/docs/Learn/Forms/How_to_build_custom_form_controls)的代码解释事例。 ## 基本状态 @@ -25,9 +25,9 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 ### CSS ```css -/* --------------- */ -/* Required Styles */ -/* --------------- */ +/*--------- */ +/* 所需的样式 */ +/* -------- */ .select { position: relative; @@ -51,15 +51,14 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 visibility: hidden; } -/* ------------ */ -/* Fancy Styles */ -/* ------------ */ +/* ------- */ +/* 美化样式 */ +/* ------- */ .select { font-size: 0.625em; /* 10px */ font-family: Verdana, Arial, sans-serif; - -moz-box-sizing: border-box; box-sizing: border-box; padding: 0.1em 2.5em 0.2em 0.5em; /* 1px 25px 2px 5px */ @@ -71,7 +70,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 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); } @@ -85,7 +83,7 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 vertical-align: top; } -.select:after { +.select::after { content: "▼"; position: absolute; z-index: 1; @@ -96,7 +94,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 padding-top: 0.1em; - -moz-box-sizing: border-box; box-sizing: border-box; text-align: center; @@ -122,7 +119,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4); - -moz-box-sizing: border-box; box-sizing: border-box; min-width: 100%; @@ -141,7 +137,7 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 } ``` -### 结果 +### 基本状态结果 {{ EmbedLiveSample('基本状态', 120, 130) }} @@ -165,9 +161,9 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 ### CSS ```css -/* --------------- */ -/* Required Styles */ -/* --------------- */ +/*--------- */ +/* 所需的样式 */ +/* -------- */ .select { position: relative; @@ -191,15 +187,14 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 visibility: hidden; } -/* ------------ */ -/* Fancy Styles */ -/* ------------ */ +/* ------- */ +/* 美化样式 */ +/* ------- */ .select { font-size: 0.625em; /* 10px */ font-family: Verdana, Arial, sans-serif; - -moz-box-sizing: border-box; box-sizing: border-box; padding: 0.1em 2.5em 0.2em 0.5em; /* 1px 25px 2px 5px */ @@ -211,7 +206,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 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); } @@ -225,7 +219,7 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 vertical-align: top; } -.select:after { +.select::after { content: "▼"; position: absolute; z-index: 1; @@ -236,7 +230,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 padding-top: 0.1em; - -moz-box-sizing: border-box; box-sizing: border-box; text-align: center; @@ -262,7 +255,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4); - -moz-box-sizing: border-box; box-sizing: border-box; min-width: 100%; @@ -281,7 +273,7 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 } ``` -### 结果 +### 活动状态结果 {{ EmbedLiveSample('活动状态', 120, 130) }} @@ -305,9 +297,9 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 ### CSS ```css -/* --------------- */ -/* Required Styles */ -/* --------------- */ +/*--------- */ +/* 所需的样式 */ +/* -------- */ .select { position: relative; @@ -331,15 +323,14 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 visibility: hidden; } -/* ------------ */ -/* Fancy Styles */ -/* ------------ */ +/* ------- */ +/* 美化样式 */ +/* ------- */ .select { font-size: 0.625em; /* 10px */ font-family: Verdana, Arial, sans-serif; - -moz-box-sizing: border-box; box-sizing: border-box; padding: 0.1em 2.5em 0.2em 0.5em; /* 1px 25px 2px 5px */ @@ -351,7 +342,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 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); } @@ -365,7 +355,7 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 vertical-align: top; } -.select:after { +.select::after { content: "▼"; position: absolute; z-index: 1; @@ -376,7 +366,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 padding-top: 0.1em; - -moz-box-sizing: border-box; box-sizing: border-box; text-align: center; @@ -402,7 +391,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4); - -moz-box-sizing: border-box; box-sizing: border-box; min-width: 100%; @@ -421,6 +409,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_1 } ``` -### 结果 +### 展开状态结果 {{ EmbedLiveSample('展开状态', 120, 130) }} diff --git a/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_2/index.md b/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_2/index.md index 9b6e35b8c417c4..1b8a8cf421ccef 100644 --- a/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_2/index.md +++ b/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_2/index.md @@ -1,9 +1,9 @@ --- -title: Example 2 +title: 示例 2 slug: Learn/Forms/How_to_build_custom_form_controls/Example_2 --- -这是解释 [如何构建自定义表单小部件](/zh-CN/docs/HTML/Forms/How_to_build_custom_form_widgets)的第二个示例。 +这是解释[如何构建自定义表单控件](/zh-CN/docs/Learn/Forms/How_to_build_custom_form_controls)的第二个示例。 ## 使用 JS @@ -29,7 +29,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_2
  • Apple
  • -
    ``` @@ -44,9 +43,9 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_2 overflow: hidden; } -/* --------------- */ -/* Required Styles */ -/* --------------- */ +/*--------- */ +/* 所需的样式 */ +/* -------- */ .select { position: relative; @@ -70,15 +69,14 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_2 visibility: hidden; } -/* ------------ */ -/* Fancy Styles */ -/* ------------ */ +/* ------- */ +/* 美化样式 */ +/* ------- */ .select { font-size: 0.625em; /* 10px */ font-family: Verdana, Arial, sans-serif; - -moz-box-sizing: border-box; box-sizing: border-box; padding: 0.1em 2.5em 0.2em 0.5em; /* 1px 25px 2px 5px */ @@ -90,7 +88,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_2 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); } @@ -104,7 +101,7 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_2 vertical-align: top; } -.select:after { +.select::after { content: "▼"; position: absolute; z-index: 1; @@ -115,7 +112,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_2 padding-top: 0.1em; - -moz-box-sizing: border-box; box-sizing: border-box; text-align: center; @@ -141,7 +137,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_2 box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4); - -moz-box-sizing: border-box; box-sizing: border-box; min-width: 100%; @@ -163,8 +158,8 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_2 ### JavaScript ```js -window.addEventListener("load", function () { - var form = document.querySelector("form"); +window.addEventListener("load", () => { + const form = document.querySelector("form"); form.classList.remove("no-widget"); form.classList.add("widget"); @@ -199,7 +194,6 @@ window.addEventListener("load", function () {
  • Apple
  • -
    ``` diff --git a/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_3/index.md b/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_3/index.md index 1081b79b78b885..722b41335b7952 100644 --- a/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_3/index.md +++ b/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_3/index.md @@ -3,11 +3,11 @@ title: Example 3 slug: Learn/Forms/How_to_build_custom_form_controls/Example_3 --- -这是解释 [如何构建自定义表单小部件](/zh-CN/docs/HTML/Forms/How_to_build_custom_form_widgets) 的第三个示例。 +这是解释[如何构建自定义表单控件](/zh-CN/docs/Learn/Forms/How_to_build_custom_form_controls)的第三个示例。 -## Change states +## 改变状态 -### HTML 内容 +### HTML ```html
    @@ -32,7 +32,7 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_3
    ``` -### CSS 内容 +### CSS ```css .widget select, @@ -43,9 +43,9 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_3 overflow: hidden; } -/* --------------- */ -/* Required Styles */ -/* --------------- */ +/*--------- */ +/* 所需的样式 */ +/* -------- */ .select { position: relative; @@ -69,15 +69,14 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_3 visibility: hidden; } -/* ------------ */ -/* Fancy Styles */ -/* ------------ */ +/* ------- */ +/* 美化样式 */ +/* ------- */ .select { font-size: 0.625em; /* 10px */ font-family: Verdana, Arial, sans-serif; - -moz-box-sizing: border-box; box-sizing: border-box; padding: 0.1em 2.5em 0.2em 0.5em; /* 1px 25px 2px 5px */ @@ -89,7 +88,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_3 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); } @@ -103,7 +101,7 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_3 vertical-align: top; } -.select:after { +.select::after { content: "▼"; position: absolute; z-index: 1; @@ -114,7 +112,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_3 padding-top: 0.1em; - -moz-box-sizing: border-box; box-sizing: border-box; text-align: center; @@ -140,7 +137,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_3 box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4); - -moz-box-sizing: border-box; box-sizing: border-box; min-width: 100%; @@ -159,25 +155,17 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_3 } ``` -### JavaScript 内容 +### JavaScript ```js // ------- // -// HELPERS // +// 函数定义 // // ------- // -NodeList.prototype.forEach = function (callback) { - Array.prototype.forEach.call(this, callback); -}; - -// -------------------- // -// Function definitions // -// -------------------- // - function deactivateSelect(select) { if (!select.classList.contains("active")) return; - var optList = select.querySelector(".optList"); + const optList = select.querySelector(".optList"); optList.classList.add("hidden"); select.classList.remove("active"); @@ -191,63 +179,69 @@ function activeSelect(select, selectList) { } function toggleOptList(select, show) { - var optList = select.querySelector(".optList"); + const optList = select.querySelector(".optList"); optList.classList.toggle("hidden"); } function highlightOption(select, option) { - var optionList = select.querySelectorAll(".option"); + const optionList = select.querySelectorAll(".option"); - optionList.forEach(function (other) { + optionList.forEach((other) => { other.classList.remove("highlight"); }); option.classList.add("highlight"); } -// ------------- // -// Event binding // -// ------------- // +// ------- // +// 事件绑定 // +// ------- // -window.addEventListener("load", function () { - var form = document.querySelector("form"); +window.addEventListener("load", () => { + const form = document.querySelector("form"); form.classList.remove("no-widget"); form.classList.add("widget"); }); -window.addEventListener("load", function () { - var selectList = document.querySelectorAll(".select"); +window.addEventListener("load", () => { + const selectList = document.querySelectorAll(".select"); - selectList.forEach(function (select) { - var optionList = select.querySelectorAll(".option"); + selectList.forEach((select) => { + const optionList = select.querySelectorAll(".option"); - optionList.forEach(function (option) { - option.addEventListener("mouseover", function () { + optionList.forEach((option) => { + option.addEventListener("mouseover", () => { highlightOption(select, option); }); }); select.addEventListener( "click", - function (event) { + (event) => { toggleOptList(select); }, false, ); - select.addEventListener("focus", function (event) { + select.addEventListener("focus", (event) => { activeSelect(select, selectList); }); - select.addEventListener("blur", function (event) { + select.addEventListener("blur", (event) => { deactivateSelect(select); }); + + select.addEventListener("keyup", (event) => { + if (event.key === "Escape") { + deactivateSelect(select); + } + }); }); }); ``` ### 结果 -{{ EmbedLiveSample('Change_states') }} +{{ EmbedLiveSample('改变状态') }} diff --git a/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_4/index.md b/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_4/index.md index 35b15b27158193..d525a374019bad 100644 --- a/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_4/index.md +++ b/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_4/index.md @@ -3,7 +3,7 @@ title: Example 4 slug: Learn/Forms/How_to_build_custom_form_controls/Example_4 --- -这是解释 [如何构建自定义表单小部件](/zh-CN/docs/Learn/HTML/Forms/How_to_build_custom_form_widgets) 的第四个示例。 +这是解释[如何构建自定义表单控件](/zh-CN/docs/Learn/Forms/How_to_build_custom_form_controls)的第四个示例。 ## 改变状态 @@ -43,9 +43,9 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_4 overflow: hidden; } -/* --------------- */ -/* Required Styles */ -/* --------------- */ +/*--------- */ +/* 所需的样式 */ +/* -------- */ .select { position: relative; @@ -69,15 +69,14 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_4 visibility: hidden; } -/* ------------ */ -/* Fancy Styles */ -/* ------------ */ +/* ------- */ +/* 美化样式 */ +/* ------- */ .select { font-size: 0.625em; /* 10px */ font-family: Verdana, Arial, sans-serif; - -moz-box-sizing: border-box; box-sizing: border-box; padding: 0.1em 2.5em 0.2em 0.5em; /* 1px 25px 2px 5px */ @@ -89,7 +88,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_4 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); } @@ -103,7 +101,7 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_4 vertical-align: top; } -.select:after { +.select::after { content: "▼"; position: absolute; z-index: 1; @@ -114,7 +112,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_4 padding-top: 0.1em; - -moz-box-sizing: border-box; box-sizing: border-box; text-align: center; @@ -140,7 +137,6 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_4 box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4); - -moz-box-sizing: border-box; box-sizing: border-box; min-width: 100%; @@ -163,21 +159,13 @@ slug: Learn/Forms/How_to_build_custom_form_controls/Example_4 ```js // ------- // -// HELPERS // +// 函数定义 // // ------- // -NodeList.prototype.forEach = function (callback) { - Array.prototype.forEach.call(this, callback); -}; - -// -------------------- // -// Function definitions // -// -------------------- // - function deactivateSelect(select) { if (!select.classList.contains("active")) return; - var optList = select.querySelector(".optList"); + const optList = select.querySelector(".optList"); optList.classList.add("hidden"); select.classList.remove("active"); @@ -191,15 +179,15 @@ function activeSelect(select, selectList) { } function toggleOptList(select, show) { - var optList = select.querySelector(".optList"); + const optList = select.querySelector(".optList"); optList.classList.toggle("hidden"); } function highlightOption(select, option) { - var optionList = select.querySelectorAll(".option"); + const optionList = select.querySelectorAll(".option"); - optionList.forEach(function (other) { + optionList.forEach((other) => { other.classList.remove("highlight"); }); @@ -207,9 +195,9 @@ function highlightOption(select, option) { } 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"); nativeWidget.selectedIndex = index; value.innerHTML = optionList[index].innerHTML; @@ -217,74 +205,76 @@ function updateValue(select, index) { } function getIndex(select) { - var nativeWidget = select.previousElementSibling; + const nativeWidget = select.previousElementSibling; return nativeWidget.selectedIndex; } // ------------- // -// Event binding // +// 事件绑定 // // ------------- // -window.addEventListener("load", function () { - var form = document.querySelector("form"); +window.addEventListener("load", () => { + const form = document.querySelector("form"); form.classList.remove("no-widget"); form.classList.add("widget"); }); -window.addEventListener("load", function () { - var selectList = document.querySelectorAll(".select"); +window.addEventListener("load", () => { + const selectList = document.querySelectorAll(".select"); - selectList.forEach(function (select) { - var optionList = select.querySelectorAll(".option"); + selectList.forEach((select) => { + const optionList = select.querySelectorAll(".option"); - optionList.forEach(function (option) { - option.addEventListener("mouseover", function () { + optionList.forEach((option) => { + option.addEventListener("mouseover", () => { highlightOption(select, option); }); }); - select.addEventListener("click", function (event) { + select.addEventListener("click", (event) => { toggleOptList(select); }); - select.addEventListener("focus", function (event) { + select.addEventListener("focus", (event) => { activeSelect(select, selectList); }); - select.addEventListener("blur", function (event) { + select.addEventListener("blur", (event) => { deactivateSelect(select); }); }); }); -window.addEventListener("load", function () { - var selectList = document.querySelectorAll(".select"); +window.addEventListener("load", () => { + const selectList = document.querySelectorAll(".select"); - selectList.forEach(function (select) { - var optionList = select.querySelectorAll(".option"), - selectedIndex = getIndex(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(function (option, index) { - option.addEventListener("click", function (event) { + optionList.forEach((option, index) => { + option.addEventListener("click", (event) => { updateValue(select, index); }); }); - select.addEventListener("keyup", function (event) { - var length = optionList.length, - index = getIndex(select); + select.addEventListener("keyup", (event) => { + let index = getIndex(select); - if (event.keyCode === 40 && index < length - 1) { + if (event.key === "Escape") { + deactivateSelect(select); + } + if (event.key === "ArrowDown" && index < optionList.length - 1) { index++; } - if (event.keyCode === 38 && index > 0) { + if (event.key === "ArrowUp" && index > 0) { index--; } diff --git a/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_5/index.md b/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_5/index.md new file mode 100644 index 00000000000000..e938053a23f161 --- /dev/null +++ b/files/zh-cn/learn/forms/how_to_build_custom_form_controls/example_5/index.md @@ -0,0 +1,286 @@ +--- +title: 示例 5 +slug: Learn/Forms/How_to_build_custom_form_controls/Example_5 +page-type: learn-module-chapter +--- + +这是解释[如何构建自定义表单控件](/zh-CN/docs/Learn/Forms/How_to_build_custom_form_controls)的最后一个示例。 + +## 改变状态 + +### HTML + +```html +
    + + +
    + Cherry + +
    +
    +``` + +### CSS + +```css +.widget select, +.no-widget .select { + position: absolute; + left: -5000em; + height: 0; + overflow: 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: #ffffff; +} +``` + +### JavaScript + +```js +// ------- // +// 函数定义 // +// ------- // + +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; +} + +// ------------- // +// 事件绑定 // +// ------------- // + +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", () => { + 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.key === "Escape") { + deactivateSelect(select); + } + if (event.key === "ArrowDown" && index < optionList.length - 1) { + index++; + } + if (event.key === "ArrowUp" && index > 0) { + index--; + } + + updateValue(select, index); + }); + }); +}); +``` + +### 结果 + +{{ EmbedLiveSample('改变状态') }} diff --git a/files/zh-cn/learn/forms/how_to_build_custom_form_controls/index.md b/files/zh-cn/learn/forms/how_to_build_custom_form_controls/index.md index 8608f9b9e078ed..c5463f394658c6 100644 --- a/files/zh-cn/learn/forms/how_to_build_custom_form_controls/index.md +++ b/files/zh-cn/learn/forms/how_to_build_custom_form_controls/index.md @@ -5,80 +5,89 @@ slug: Learn/Forms/How_to_build_custom_form_controls {{LearnSidebar}} -在许多情况下,可用的原生 HTML 表单控件是不够的。如果要在某些控件(例如 {{HTMLElement("select")}} 元素)上执行[高级样式](/zh-CN/docs/Learn/Forms/Advanced_form_styling),或者如果要提供自定义表现,则别无选择,只能构建自己的控件。 +在许多情况下,可用的原生 HTML 表单控件是不够的。如果要在某些控件(例如 {{HTMLElement("select")}} 元素)上[设置高级样式](/zh-CN/docs/Learn/Forms/Advanced_form_styling),或者如果要提供自定义行为,你就需要考虑构建自己的控件。 -在本文中,我们会看到如何构建这样的组件。为此,我们将使用这样一个例子:重建 {{HTMLElement("select")}} 元素。 +在本文中,我们会看到如何构建自定义控件。为此,我们将使用这样一个示例:重建 {{HTMLElement("select")}} 元素。我们还将讨论如何构建、何时构建自定义控件、构建是否存在意义,以及构建控件的相关注意事项。 -> **备注:** 我们将专注于构建小部件,而不是怎样让代码更通用或可复用;那会涉及一些非基础的 JavaScript 代码和未知环境下的 DOM 操作,这超过了这篇文章的范围。 +> **备注:** 我们将专注于构建控件,而不是怎样让代码更通用或可复用;那会涉及一些非基础的 JavaScript 代码和未知上下文下的 DOM 操作,这超过了这篇文章的范畴。 ## 设计、结构和语义 -在构建一个自定义控件之前,首先你要确切的知道你要什么。这将为您节省宝贵的时间。特别地,清楚地定义控件的所有状态非常重要。为了做到这一点,从状态和行为表现都众所周知的现有小控件开始是很好的选择,这样你可以轻松的尽量模仿这些控件。 +在构建一个自定义控件之前,首先你要确切的知道你要什么。这将为你节省宝贵的时间。特别地,清楚地定义控件的所有状态非常重要。要做到这一点,从状态和行为表现都为人所熟知的现有控件开始是很好的选择,这样你可以充分地模仿这些控件。 在我们的示例中,我们将重建 {{HTMLElement("select")}} 元素,这是我们希望实现的结果: ![选择框的三种状态](custom-select.png) -上面图片显示了我们控件的三个主要状态:正常状态(左); 活动状态(中)和打开状态(右)。 +上面图片显示了我们控件的三个主要状态:正常状态(左)、活动状态(中)和展开状态(右)。 -在行为方面,我们希望我们的控件像任何原生控件一样对鼠标和键盘都可用。让我们从定义控件如何到达每个状态开始: +在行为方面,我们正在重建原始 HTML 元素,因此它应该具有与原生 HTML 元素相同的行为和语义。我们要求我们的控件可以通过鼠标和键盘进行使用,并且可以被屏幕阅读器所识别,就像任何原生控件一样。首先让我们定义控件如何进入每种状态: -- 以下情况控件将会呈现正常状态: +**在以下情况下,控件处于正常状态:** - - 页面加载 - - 控件处于活动状态,但用户点击控件以外的任何位置 - - 控件是活动状态,但用户使用键盘将焦点移动到另一个小部件 +- 页面加载。 +- 控件处于活动状态,但用户点击了控件以外的任何位置。 +- 控件处于活动状态,但用户使用键盘(例如 Tab 键)将焦点移动到另一个控件。 - > **备注:** 在页面上移动焦点通常是通过按 Tab 键来完成的,但这并不是哪都通用的标准。例如,在 Safari 中页面上的链接间的循环切换默认下是通过使用[组合键 Option + Tab](http://www.456bereastreet.com/archive/200906/enabling_keyboard_navigation_in_mac_os_x_web_browsers/)完成的。 +**在以下情况下,控件处于活动状态:** -- 以下情况控件将会呈现活动状态: +- 用户点击或在触摸屏上触摸控件。 +- 用户按下 tab 键使控件获得了焦点。 +- 控件处于展开状态然后用户点击控件。 - - 用户点击 - - 用户按下 tab 让控件获得了焦点。 - - 控件呈现打开状态然后用户点击控件。 +**在以下情况下,控件处于展开状态:** -- 以下情况控件将会呈现打开状态: +- 控件处于非展开状态时被用户点击。 - - 控件在非打开状态时被用户点击。 +我们知道如何改变状态后,定义如何改变控件的值同样重要: -我们知道如何改变状态后,定义如何改变小工具的值还很重要: +**在以下情况下,其值将会被改变:** -- 以下情况控件的值将会被改变: +- 控件在展开状态下用户点击一个选项。 +- 控件在活动状态下用户按下键盘的上/下方向键。 - - 控件在打开状态下用户点击一个选项 - - 控件在活动状态下用户按下键盘上方向键或者下方向键 +**在以下情况下,其值不会被改变:** + +- 在选择第一个选项时,用户按下键盘的上方向键。 +- 在选择最后一个选项时,用户按下键盘的下方向键。 最后,让我们定义控件的选项将要怎么表现: -- 当控件在打开状态时,被选中的选项将被突出显示 -- 当鼠标悬停在某个选项上时,该选项将被突出显示,并且之前突出显示的选项将返回正常的状态 +- 当控件处于展开状态时,被选中的选项将被突出显示 +- 当鼠标悬停在某个选项上时,该选项将被突出显示,并且之前突出显示的选项将返回其正常的状态 + +对于我们的示例的目的,我们将就此结束;但是,如果你是一个认真的读者,你会注意到我们省略了一些东西,例如,你认为用户在控件处于展开状态时点击 tab 键会发生什么?答案是:_什么也不会发生_。好吧,似乎很明显这就是正确的行为,但事实是,因为在我们的规范中没有定义这种情况,我们很容易忽略这种行为。在团队环境中尤其是这样,因为设计控件行为的人与实现的人通常是不同的。 + +另外一个有趣的例子是:当控件处于展开状态时,用户按下键盘上方向键和下方向键将会发生什么?这个问题有些棘手,如果你认为活动状态和展开状态是完全不同的,那么答案又是“什么都不会发生”,因为我们没有定义任何在展开状态下键盘的交互行为。从另一个方面看,如果你认为活动状态和展开状态是有重叠的部分,那么控件的值可能会改变,但是被选中的选项肯定不会相应的进行突出显示,同样是因为我们没有定义在控件展开状态下的任何键盘交互事件(我们仅仅定义了控件打开会发生什么,而没有定义在其打开后会发生什么)。 + +我们必须进一步思考:按退出键会发生什么?按下 Esc 键会关闭一个打开的选择框。记住,如果你想要提供与现有的原生 {{htmlelement('select')}} 相同的功能,那么它应该对所有用户都有相同的行为,不论是键盘、鼠标、触摸、屏幕阅读器,还是其他任何输入设备。 -对于我们的例子的目的,我们将就此结束;但是,如果你是一个认真的读者,你会注意到我们省略了一些东西,例如,你认为用户在小部件处于打开状态时点击 tab 键会发生什么?答案是:什么也不会发生。好吧,似乎很明显这就是正确的行为,但事实是,因为在我们的规范中没有定义这种情况,我们很容易忽略这种行为。在团队环境中尤其是这样,因为设计小部件行为的人与实现的人通常是不同的。 +在我们的示例中,规范的缺失是显而易见的,所以我们将着手处理它们,但是对于一些没有人想到去定义正确行为的控件而言,这的确是一个问题。所以在元素(例如 {{htmlelement('select')}})标准化阶段,规范作者花费了大量的时间来定义每个输入设备每个用例的所有涉及的交互。创建新的控件并不容易,特别是你正在创建以前从未做过的东西,没有人知道其预期的行为和相关的交互是什么。至少 select 已经完成了这些设计,所以我们知道它应该如何表现! -另外一个有趣的例子是:当小部件处于打开状态时,用户按下键盘上方向键和下方向键将会发生什么?这个问题有些棘手,如果你认为活动状态和打开状态是完全不同的,那么答案就是“什么都不会发生”,因为我们没有定义任何在打开状态下键盘的交互行为。从另一个方面看,如果你认为活动状态和打开状态是有重叠的部分,那么控件的值可能会改变,但是被选中的选项肯定不会相应的进行突出显示,同样是因为我们没有定义在控件打开状态下的任何键盘交互事件(我们仅仅定义了控件打开会发生什么,而没有定义在其打开后会发生什么) +设计新的交互方式只是行业中重要参与者的一种选择,他们有足够的影响力来推动他们创建的交互方式成为标准。例如,Apple 于 2001 年在 iPod 中推出了滚轮。他们拥有足够的市场份额,而成功推出了一种全新的设备交互方式,这是大多数设备公司无法做到的。 -在我们的例子中,缺失的规范是显而易见的,所以我们将着手处理他们,但是对于一些没有人想到去定义正确行为的小部件而言,这的确是一个问题。所以在设计阶段花费时间是值得的,因为如果你定义的行为不够好,或者忘记定义了一个行为,那么在用户开始实际使用时,将会很难去重新定义它们。如果你在定义时有疑问,请征询他人的意见,如果你有预算,请不要犹豫的去进行[用户可用性测试](https://zh.wikipedia.org/wiki/可用性测试),这个过程被称为用户体验设计(UX Design),如果你想要深入的学习相关的内容,请查阅下面这些有用资源: +最好不要发明新的用户交互方式。对于你添加的任何交互方式,在设计阶段花费时间至关重要;如果你对一种行为的定义不够合适,或者忘记定义了某种行为,那么在用户习惯之后,将很难去重新定义它们。如果你在定义时有疑问,请征询他人的意见,如果你有预算,请不要犹豫去进行[用户可用性测试](https://zh.wikipedia.org/wiki/可用性测试),这个过程被称为用户体验设计(UX Design),如果你想要深入的学习相关的内容,请查阅下面这些有用资源: -- [UXMatters.com](http://www.uxmatters.com/) -- [UXDesign.com](http://uxdesign.com/) -- [The UX Design section of SmashingMagazine](http://uxdesign.smashingmagazine.com/) +- [UXMatters.com](https://www.uxmatters.com/) +- [UXDesign.com](https://uxdesign.com/) +- [SmashingMagazine 用户体验设计部分](https://www.smashingmagazine.com/) -> **备注:** 另外,在绝大多数系统中,还有一种方法能够打开{{HTMLElement("select")}}元素来观察其所有的选项(这和用鼠标点击{{HTMLElement("select")}}元素是一样的)。通过 Windows 下的 Alt + 向下箭头实现,在我们的例子中没有实现---但是这样做会很方便,因为鼠标点击事件就是由该原理实现的。 +> **备注:** 此外,在绝大多数系统中,还有一种方法能够打开 {{HTMLElement("select")}} 元素来观察其所有的选项(这和用鼠标点击 {{HTMLElement("select")}} 元素是一样的)。这可以通过 Windows 下的 Alt + Down 实现。这没有在我们的示例中实现,但是这样做会很方便,因为鼠标点击(`click`)事件就是由该原理实现的。 ### 定义语义化的 HTML 结构 -现在控件的基本功能已经决定了,可以开始构建自定义控件了。第一步就是去确定它的 HTML 结构并给予一些基本的语义规则。重构 {{HTMLElement("select")}} 元素需要这样做: +现在控件的基本功能已经决定,可以开始构建自定义控件了。第一步就是去确定它的 HTML 结构并给予一些基本的语义规则。重构 {{HTMLElement("select")}} 元素需要这样做: ```html -
    - + Cherry - +
    ``` -注意类名的使用:不管实际使用了哪种底层 HTML 元素,它们都标识每个相关的部分。这很重要,因为这样做能确保我们的 CSS 和 JavaScript 不会和 HTML 结构强绑定,这样我们就可以在不破坏使用小部件的代码的情况下进行实现更改。比如,如果你希望增加一个等价的{{HTMLElement("optgroup")}}元素。 +注意类名的使用:不管实际使用了哪种底层 HTML 元素,它们都标识每个相关的部分。这很重要,因为这样做能确保我们的 CSS 和 JavaScript 不会和 HTML 结构强绑定,这样我们就可以在不破坏使用控件的代码的情况下进行实现更改。比如,如果你希望增加一个等价的 {{HTMLElement("optgroup")}} 元素。 -### 使用 CSS 创建外观 +然而,类名并不提供语义值。到现在为止,屏幕阅读器的用户只能“看到”无序列表。我们后面会为其添加 ARIA 语义。 -现在我们有了控件结构,我们可以开始设计我们的控件了。构建自定义控件的重点是能够完全按照我们的期望设置它的样式。为了达到这个目的,我们将 CSS 部分的工作分为两部分:第一部分是让我们的控件表现得像一个{{HTMLElement("select")}}元素所必需的的 CSS 规则,第二部分包含了让组件看起来像我们所希望那样的精妙样式。 +## 使用 CSS 创建外观 -#### 所需的样式 +现在我们有了结构,我们可以开始设计我们的控件了。构建自定义控件的重点是能够完全按照我们的期望设置它的样式。为了达到这个目的,我们将 CSS 部分的工作分为两部分:第一部分是让我们的控件表现得像一个 {{HTMLElement("select")}} 元素所必需的的 CSS 规则,第二部分包含了让控件看起来像我们所希望那样的精妙样式。 -所需的样式是那些用以处理我们组件的三种状态的必须样式。 +### 所需的样式 + +所需的样式是那些用以处理我们控件的三种状态的必须样式。 ```css .select { - /* 这将为选项列表创建一个上下文定位 */ + /* 这将为选项列表创建一个上下文定位;如果完全支持 focus-within, + 则将其添加到“.select:focus-within”是个更好的选择 + */ position: relative; - /* 这将使我们的组件成为文本流的一部分,同时又可以调整大小 */ + /* 这将使我们的控件成为文本流的一部分,同时又可以调整大小 */ display: inline-block; } ``` -我们需要一个额外的类 `active` 来定义我们的组件处于其激活状态时的的界面外观。因为我们的组件是可以聚焦的,我们通过{{cssxref(":focus")}} 伪类重复自定义样式来确保它们表现得一样。 +我们需要一个额外的 `active` 类来定义我们的控件处于其激活状态时的的界面外观。因为我们的控件是可以聚焦的,我们通过 {{cssxref(":focus")}} 伪类复用自定义样式来确保它们表现得一样。 ```css .select .active, .select:focus { outline: none; - /* 这里的 box-shadow 属性并非必须,但确保活动状态能看出来非常重要---我们 + /* 这里的 box-shadow 属性并非必须,但确保活动状态能看出来非常重要——我们 将其作为一个默认值,你可以随意地覆盖掉它。*/ box-shadow: 0 0 3px 1px #227755; } @@ -127,93 +140,85 @@ slug: Learn/Forms/How_to_build_custom_form_controls 现在,让我们处理选项列表: ```css -/* 这里的 .select 选择器是一个糖衣语法,用来确保我们定义的类是 - 在我们的组件里的那个。 */ +/* 这里的 .select 选择器帮助我们确保定义的类是 + 在我们的控件里的那个。 */ .select .optList { /* 这可以确保我们的选项列表将会显示在值的下面,并且会处在 - HTML 流之外*/ + HTML 流之外 */ position: absolute; top: 100%; left: 0; } ``` -我们需要一个额外的类来处理选项列表隐藏时的情况。为了管理没有完全匹配的活动状态和打开状态之间的差异,这是有必要的。 +我们需要一个额外的类来处理选项列表隐藏时的情况。为了管理没有完全匹配的活动状态和展开状态之间的差异,这是有必要的。 ```css .select .optList.hidden { - /* 这是一个以可访问形式隐藏列表的简单方法, + /* 这是一个以无障碍的形式隐藏列表的简单方法, 对无障碍我们将在最后进一步拓展 */ max-height: 0; visibility: hidden; } ``` -#### 美化 +> **备注:** 我们也可以使用 `transform: scale(1, 0)` 来指定选项列表的高度为零,但宽度不变。 + +### 美化 -所以现在我们的基本功能已经就位,有趣的事情就可以开始了。下面是一个可行的简单的例子,和本文开头的截图是相对应的。不管怎样,你可以随意的体验一下看看能收获什么。 +所以现在我们的基本功能已经就位,有趣的事情就可以开始了。下面是一个可行的简单示例,这和本文开头的截图是相对应的。不管怎样,你可以随意尝试看看能想出什么。 ```css .select { - /* 出于无障碍方面的原因,所有尺寸都会由 em 值表示 - (用来确保用户在文本模式下使用浏览器缩放时组件的可缩放性). - 在大多数浏览器下的默认换算是 1em == 16px. - 如果你对 em 和 px 的转换感到疑惑,请参考 http://riddle.pl/emcalc/ */ - font-size: 0.625em; /* 这个(=10px)是以 em 方式表达的这个环境里的字体大小 */ + /* 假设的单位换算是 1em == 16px,这是大多数浏览器的默认值。 + 如果你对 em 和 px 的转换感到疑惑,请参考 https://nekocalc.com/px-to-em-converter */ + font-size: 0.625em; /* 这个(10px)是以 em 值表达这个上下文的字体大小 */ font-family: Verdana, Arial, sans-serif; - -moz-box-sizing: border-box; box-sizing: border-box; /* 我们需要为将要添加的向下箭头准备一些额外的空间 */ - padding: 0.1em 2.5em 0.2em 0.5em; /* 1px 25px 2px 5px */ + padding: 0.1em 2.5em 0.2em 0.5em; 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 */ + border: 0.2em solid #000; + border-radius: 0.4em; + box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); - /* 第一段声明是为了不支持线性梯度填充的浏览器准备的。 - 第二段声明是因为基于 WebKit 的浏览器没有预先定义它。 - 如果你想为过时的浏览器提供支持,请参阅 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); } .select .value { - /* 因为值的宽度可能超过组件的宽度,我们需要确保他不会改变组件的宽度 */ + /* 因为值的宽度可能超过控件的宽度,我们需要确保它不会改变控件的宽度。如果内容溢出了,我们显示省略号 */ display: inline-block; width: 100%; overflow: hidden; - - vertical-align: top; - - /* 如果内容溢出了,最好有一个恰当的缩写。*/ white-space: nowrap; text-overflow: ellipsis; + vertical-align: top; } ``` -我们不需要一个额外的元素来设计向下的箭头,而使用{{cssxref(":after")}} 伪类来替代。然而,这也可以通过使用一张加在`select` class 上的简单的背景图像来实现。 +我们不需要一个额外的元素来设计向下箭头,而使用 {{cssxref("::after")}} 伪类替代。这也可以通过使用一张加在 `select` 类上的简单的背景图像来实现。 ```css -.select:after { - content: "▼"; /* 我们使用了 unicode 编码的字符 U+25BC,确保设置了 charset meta 标签 */ +.select::after { + content: "▼"; /* 我们使用了 unicode 字符 U+25BC,请确保设置了 charset meta 标签 */ position: absolute; z-index: 1; /* 这对于防止箭头覆盖选项列表很重要 */ top: 0; right: 0; - -moz-box-sizing: border-box; box-sizing: border-box; height: 100%; - width: 2em; /* 20px */ - padding-top: 0.1em; /* 1px */ + width: 2em; + padding-top: 0.1em; - border-left: 0.2em solid #000; /* 2px */ - border-radius: 0 0.1em 0.1em 0; /* 0 1px 1px 0 */ + border-left: 0.2em solid #000; + border-radius: 0 0.1em 0.1em 0; background-color: #000; color: #fff; @@ -225,35 +230,34 @@ slug: Learn/Forms/How_to_build_custom_form_controls ```css .select .optList { - z-index: 2; /* 我们明确的表示选项列表会始终与向下箭头重叠 */ + z-index: 2; /* 我们显式定义选项列表始终与向下箭头重叠 */ /* 这会重置 ul 元素的默认样式 */ list-style: none; margin: 0; padding: 0; - -moz-box-sizing: border-box; box-sizing: border-box; - /* 这会确保即使数值比组件小,选项列表仍能变得跟组件自身一样大*/ + /* 这会确保即使数值比控件小,选项列表仍能变得跟控件自身一样宽 */ min-width: 100%; - /* 万一列表太长了,它的内容会从垂直方向溢出 (会自动添加一个竖向滚动条) - 但是水平方向不会 (因为我们没有设定宽度,列表会自适应宽度。如果不能的话,内容会被截断) */ + /* 万一列表太长了,它的内容会从垂直方向溢出(会自动添加一个竖向滚动条) + 但是水平方向不会(因为我们没有设定宽度,列表会自适应宽度。如果不能的话,内容会被截断) */ max-height: 10em; /* 100px */ overflow-y: auto; overflow-x: hidden; - border: 0.2em solid #000; /* 2px */ - border-top-width: 0.1em; /* 1px */ - border-radius: 0 0 0.4em 0.4em; /* 0 0 4px 4px */ + 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); /* 0 2px 4px */ + box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4); background: #f0f0f0; } ``` -对于选项,我们需要添加一个 `highlight` 类以便能标明用户将要选择的值或者已经选择的值。 +对于选项,我们需要添加一个 `highlight` 类以便能标明用户将要选择或者已经选择的值。 ```css .select .option { @@ -266,363 +270,1353 @@ slug: Learn/Forms/How_to_build_custom_form_controls } ``` -这是三种状态的结果: - - - - - - - - - - - - - - - - - - - -
    基本状态活动状态打开状态
    - {{EmbedLiveSample("基本状态",120,130, "", "Learn/Forms/How_to_build_custom_form_controls/Example_1")}} - - {{EmbedLiveSample("活动状态",120,130, "", "Learn/Forms/How_to_build_custom_form_controls/Example_1")}} - - {{EmbedLiveSample("展开状态",120,130, "", "Learn/Forms/How_to_build_custom_form_controls/Example_1")}} -
    - 查看源代码 -
    - -## 通过 JavaScript 让您的小部件动起来 - -现在我们的设计和结构已经完成了。我们可以写些 JavaScript 代码来让这个部件真正生效。 - -> **警告:** 下面的代码仅仅是教学性质的,并且不应该照搬使用。在许多方面,正如我们所看到的,这种方案不具有前瞻性,而且可能在旧浏览器上会不工作。这里面还有冗余的部分,在生产环境下,代码需要优化。 - -> **备注:** 创建可复用的组件可能是一件需要些技巧的事情。[W3C 网络组件草案](http://dvcs.w3.org/hg/webcomponents/raw-file/tip/explainer/index.html) 是对这类特定问题的答案之一。[X-Tag 项目](http://x-tags.org/) 是对这一规格的实验性实现;我们建议你看看它。 +这是我们的三种状态的结果([在此处查看源代码](/zh-CN/docs/Learn/Forms/How_to_build_custom_form_controls/Example_1)): -### 它为什么不生效? +#### 正常状态 -在我们开始之前,要记住一件和 JavaScript 有关的非常重要的事情:在浏览器中,**这是一种不可靠的技术**。当你构建一个自定义组件时,你会不得不得依赖于 JavaScript,因为这是将所有的东西联系在一起的线索。但是,很多情况下,JavaScript 不能在浏览器中运行。 +```html hidden +
    + Cherry + +
    +``` -- 用户关掉了 JavaScript:这是最不常见的情形。现在只有很少的人会关掉 JavaScript。 -- 脚本没有加载。这是最常见的情形,特别是在移动端上,在那些网络非常不可靠的地方。 -- 脚本是有问题的。你应该总是考虑这种可能性。 -- 脚本和第三方脚本冲突。这可能会由用户使用的跟踪脚本和一些书签工具引发。 -- 脚本与一个浏览器的拓展冲突,或者受其影响。 (比如 Firefox 的 [NoScript](https://addons.mozilla.org/fr/firefox/addon/noscript/) 拓展 或者 Chrome 的 [NotScripts](https://chrome.google.com/webstore/detail/notscripts/odjhifogjcknibkahlpidmdajjpkkcfn) 拓展)。 -- 用户在使用老旧的浏览器,而且你需要的一些功能没有被支持。当你使用一些最新的 API 时,这种情况会经常发生。 +```css hidden +.select { + position: relative; + display: inline-block; +} -因为这些风险,认真考虑 JavaScript 不生效时会发生什么是很重要的。处理这个问题的细节超出了这篇文章的范围,因为这与你有多么想使你的脚本具有通用性和可复用性更加相关,不过我们将在我们的例子中考虑与其相关的基本内容。 +.select.active, +.select:focus { + box-shadow: 0 0 3px 1px #227755; + outline: none; +} -在我们的例子中,如果 JavaScript 代码没有运行,我们会回退到显示一个标准的 {{HTMLElement("select")}} 元素。为了实现这一点,我们需要两样东西。 +.select .optList { + position: absolute; + top: 100%; + left: 0; +} -首先,在每次使用我们的自定义部件前,我们需要添加一个标准的 {{HTMLElement("select")}} 元素。实际上,为了能将来自我们自定义的表单组件和以及其他部分的表单数据发送出去,这个元素也是需要的。我们随后会详细的解释这一点。 +.select .optList.hidden { + max-height: 0; + visibility: hidden; +} -```html - -
    - +.select { + font-size: 0.625em; /* 10px */ + font-family: Verdana, Arial, sans-serif; -
    - Cherry - -
    -
    - -``` + box-sizing: border-box; -第二,我们需要两个新的 classes 来隐藏不需要的元素 (即,当我们的脚本没有运行时的自定义组件,或是脚本正常运行时的"真正的" {{HTMLElement("select")}} 元素)。注意默认情况下,我们的 HTML 代码会隐藏我们的自定义组件。 + padding: 0.1em 2.5em 0.2em 0.5em; /* 1px 25px 2px 5px */ + width: 10em; /* 100px */ -```css -.widget select, -.no-widget .select { - /* 这个 CSS 选择器大体上说的是: - - 要么我们将 body 的 class 设置为"widget",隐藏真实的{{HTMLElement("select")}}元素 - - 或是我们没有改变 body 的 class,这样 body 的 class 还是"no-widget", - 因此 class 为"select"的元素需要被隐藏 */ - position: absolute; - left: -5000em; - height: 0; - overflow: hidden; -} -``` + border: 0.2em solid #000; /* 2px */ + border-radius: 0.4em; /* 4px */ -接下来我们需要一个 JavaScript 开关来决定脚本是否运行。这个开关非常简单:如果页面加载时,我们的脚本运行了,它将会移除 `no-widget` class,并添加 `widget` class,由此切换 {{HTMLElement("select")}} 元素和自定义组件的可视性。 + box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /* 0 1px 2px */ -```js -window.addEventListener("load", function () { - document.body.classList.remove("no-widget"); - document.body.classList.add("widget"); -}); -``` + background: #f0f0f0; + background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0); +} - - - - - - - - - - - - - - - - -
    无 JS有 JS
    - {{EmbedLiveSample("不使用 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")}} -
    - 查看源代码 -
    - -> **备注:** 如果你真的想让你的代码变得通用和可重用,最好不要做一个 class 选择器开关,而是通过添加一个组件 class 的方式来隐藏 {{HTMLElement("select")}} 元素,并且动态地在每一个 {{HTMLElement("select")}} 元素后面添加代表页面中自定义组件的 DOM 树。 +.select .value { + display: inline-block; + width: 100%; + overflow: hidden; -### 让工作变得更简单 + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: top; +} -在我们将要构建的代码之中,我们将会使用标准的 DOM API 和 JavaScript 来完成要做的所有工作。我们准备使用的特性如下所示: +.select::after { + content: "▼"; + position: absolute; + z-index: 1; + height: 100%; + width: 2em; /* 20px */ + top: 0; + right: 0; -1. {{domxref("element.classList","classList")}} -2. {{domxref("EventTarget.addEventListener","addEventListener()")}} -3. {{domxref("NodeList.forEach()")}} -4. {{domxref("element.querySelector","querySelector()")}} 和 {{domxref("element.querySelectorAll","querySelectorAll()")}} + padding-top: 0.1em; -### 构造事件回调 + box-sizing: border-box; -基础已经准备好了,我们现在可以开始定义用户每次同我们的组件交互时会用到的所有函数了。 + text-align: center; -```js -// 这个函数会用在每当我们想要停用一个自定义组件的时候 -// 它需要一个参数: -// select :要停用的带有 'select' 类的节点 -function deactivateSelect(select) { - // 如果组件没有运行,不用进行任何操作 - if (!select.classList.contains("active")) return; + border-left: 0.2em solid #000; + border-radius: 0 0.1em 0.1em 0; - // 我们需要获取自定义组件的选项列表 - var optList = select.querySelector(".optList"); + background-color: #000; + color: #fff; +} - // 关闭选项列表 - optList.classList.add("hidden"); +.select .optList { + z-index: 2; - // 然后停用组件本身 - select.classList.remove("active"); -} + list-style: none; + margin: 0; + padding: 0; -// 每当用户想要激活(或停用)这个组件的时候,会调用这个函数 -// 它需要 2 个参数: -// select : 要激活的带有'select'类的 DOM 节点 -// selectList : 包含所有带'select'类的 DOM 节点的列表 -function activeSelect(select, selectList) { - // 如果组件已经激活了,不进行任何操作 - if (select.classList.contains("active")) return; + background: #f0f0f0; + border: 0.2em solid #000; + border-top-width: 0.1em; + border-radius: 0 0 0.4em 0.4em; - // 我们需要关闭所有自定义组件的活动状态 - // 因为 deactiveselect 函数满足 forEach 回调函数的所有请求, - // 我们直接使用它,不使用中间匿名函数 - selectList.forEach(deactivateSelect); + box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4); - // 然后我们激活特定的组件 - select.classList.add("active"); + box-sizing: border-box; + + min-width: 100%; + max-height: 10em; /* 100px */ + overflow-y: auto; + overflow-x: hidden; } -// 每当用户想要打开/关闭选项列表的时候,会调用这个函数 -// 它需要一个参数: -// select : 要触发的列表的 DOM 节点 -function toggleOptList(select) { - // 该列表不包含在组件中 - var optList = select.querySelector(".optList"); +.select .option { + padding: 0.2em 0.3em; +} - // 我们改变列表的class去显示/隐藏它 - optList.classList.toggle("hidden"); +.select .highlight { + background: #000; + color: #ffffff; } +``` -// 每当我们要高亮一个选项的时候,会调用该函数 -// 它需要两个参数: -// select : 带有'select'类的 DOM 节点,包含了需要高亮强调的选项 -// option : 需要高亮强调的带有'option'类的 DOM 节点 -function highlightOption(select, option) { - // 为我们的自定义 select 元素获取所有有效选项的列表 - var optionList = select.querySelectorAll(".option"); +{{EmbedLiveSample("基本状态",120,130)}} - // 我们移除所有选项的高亮强调 - optionList.forEach(function (other) { - other.classList.remove("highlight"); - }); +#### 活动状态 - // 我们高亮强调正确的选项 - option.classList.add("highlight"); -} +```html hidden +
    + Cherry + +
    ``` -这是你需要用来处理组件不同状态的所有代码。 +```css hidden +.select { + position: relative; + display: inline-block; +} -接下来,我们将这些函数绑定到合适的事件上: +.select.active, +.select:focus { + box-shadow: 0 0 3px 1px #227755; + outline: none; +} -```js -// 我们处理文档加载时的事件绑定。 -window.addEventListener("load", function () { - var selectList = document.querySelectorAll(".select"); +.select .optList { + position: absolute; + top: 100%; + left: 0; +} - // 每个自定义组件都需要初始化 - selectList.forEach(function (select) { - // 它的'option'元素也需要 - var optionList = select.querySelectorAll(".option"); +.select .optList.hidden { + max-height: 0; + visibility: hidden; +} - // 每当用户的鼠标悬停在一个选项上时,我们高亮这个指定的选项 - optionList.forEach(function (option) { - option.addEventListener("mouseover", function () { - // 注意:'select'和'option'变量是我们函数调用范围内有效的闭包。 - highlightOption(select, option); - }); - }); +.select { + font-size: 0.625em; /* 10px */ + font-family: Verdana, Arial, sans-serif; - // 每当用户点击一个自定义的 select 元素时 - select.addEventListener("click", function (event) { - // 注意:'select'变量是我们函数调用范围内有效的闭包。 + box-sizing: border-box; - // 我们改变选项列表的可见性 - toggleOptList(select); - }); + padding: 0.1em 2.5em 0.2em 0.5em; /* 1px 25px 2px 5px */ + width: 10em; /* 100px */ - // 如果组件获得了焦点 - // 每当用户点击它或是用 tab 键访问这个组件时,组件获得焦点 - select.addEventListener("focus", function (event) { - // 注意:'select'和'selectlist'变量是我们函数调用范围内有效的闭包。 + border: 0.2em solid #000; /* 2px */ + border-radius: 0.4em; /* 4px */ - // 我们激活这个组件 - activeSelect(select, selectList); - }); + box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /* 0 1px 2px */ - // 如果组件失去焦点 - select.addEventListener("blur", function (event) { - // 注意:'select'变量是我们函数调用范围内有效的闭包。 + background: #f0f0f0; + background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0); +} - // 我们关闭这个组件 - deactivateSelect(select); - }); - }); -}); -``` +.select .value { + display: inline-block; + width: 100%; + overflow: hidden; -此时,我们的组件会根据我们的设计改变状态,但是它的值仍然没有更新。我们接下来会处理这件事。 + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: top; +} -| 实时示例 | -| ------------------------------------------------------------------------------------------------------ | -| {{EmbedLiveSample("改变状态",120,130, "", "Learn/Forms/How_to_build_custom_form_controls/Example_3")}} | -| [查看源代码](/zh-CN/docs/Learn/Forms/How_to_build_custom_form_controls/Example_3) | +.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; -```js -// 这个函数更新显示的值并将其通过原生组件同步 -// 它需要 2 个参数: -// select : 含有要更新的值的'select'类的 DOM 节点 -// index : 要被选择的值的索引 -function updateValue(select, index) { - // 我们需要为了给定的自定义组件获取原生组件 - // 在我们的例子中,原生组件是自定义组件的‘同胞’ - var nativeWidget = select.previousElementSibling; + background-color: #000; + color: #fff; +} - // 我们也需要得到自定义组件的值占位符, - var value = select.querySelector(".value"); +.select .optList { + z-index: 2; - // 还有整个选项列表。 - var optionList = select.querySelectorAll(".option"); + list-style: none; + margin: 0; + padding: 0; - // 我们将被选择的索引设定为我们的选择的索引 - nativeWidget.selectedIndex = index; + background: #f0f0f0; + border: 0.2em solid #000; + border-top-width: 0.1em; + border-radius: 0 0 0.4em 0.4em; - // 更新相应的值占位符 - value.innerHTML = optionList[index].innerHTML; + box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4); - // 然后高亮我们自定义组件里对应的选项 - highlightOption(select, optionList[index]); -} + box-sizing: border-box; -// 这个函数返回原生组件里当前选定的索引 -// 它需要 1 个参数: -// select : 跟原生组件有关的'select'类 DOM 节点 -function getIndex(select) { - // 我们需要为了给定的自定义组件访问原生组件 - // 在我们的例子中,原生组件是自定义组件的一个“同胞” - var nativeWidget = select.previousElementSibling; + 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("活动状态",120,130)}} + +#### 展开状态 + +```html hidden +
    + Cherry + +
    +``` + +```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("展开状态",120,130)}} + +## 通过 JavaScript 让你的控件动起来 + +现在我们的设计和结构已经完成了。我们可以写些 JavaScript 代码来让这个控件真正生效。 + +> **警告:** 下面的代码仅仅是教学性质的,不是生产环境的代码,并且不应该照搬使用。这种方案不具有前瞻性,而且可能在旧式浏览器上会不工作。这里面还有冗余的部分,在生产环境下,代码需要优化。 + +### 它为什么不生效? + +在我们开始之前,要记住一件和 JavaScript 有关的非常重要的事情:**在浏览器中,这是一种不可靠的技术**。当你构建一个自定义控件时,你会不得不依赖于 JavaScript,因为这是将所有的东西联系在一起的线索。但是,很多情况下,JavaScript 不能在浏览器中运行。 + +- 用户禁用了 JavaScript:这是最不常见的情形。现在只有很少的人会禁用 JavaScript。 +- 脚本没有加载:这是最常见的情形,特别是在移动端上,在那些网络非常不可靠的地方。 +- 脚本是有问题的:你应该总是考虑这种可能性。 +- 脚本和第三方脚本冲突:这可能会由用户使用的跟踪脚本和一些书签工具引发。 +- 脚本与一个浏览器的扩展冲突,或者受其影响。(比如 Firefox 的 [NoScript](https://addons.mozilla.org/fr/firefox/addon/noscript/) 扩展或者 Chrome 的 [ScriptBlock](https://chrome.google.com/webstore/detail/scriptblock/hcdjknjpbnhdoabbngpmfekaecnpajba) 扩展)。 +- 用户在使用旧版浏览器,而且你需要的一些特性没有被支持。当你使用一些最新的 API 时,这种情况会经常发生。 +- 在 JavaScript 完全下载、解析和执行前,用户已经开始与内容进行交互。 + +因为这些风险,认真考虑 JavaScript 不生效时会发生什么是很重要的。处理这个问题的细节超出了这篇文章的范围,因为这与你有多么想使你的脚本具有通用性和可复用性更加相关,不过我们将在我们的示例中考虑与其相关的基本内容。 + +在我们的示例中,如果 JavaScript 代码没有运行,我们会回退到显示一个标准的 {{HTMLElement("select")}} 元素。包括我们的控件和 {{HTMLElement("select")}};显示哪个取决于 body 元素的类,当加载成功时,脚本会更新 body 元素的类以使得控件生效。 + +为了实现这一点,我们需要两样东西。 + +首先,在每次使用我们的自定义控件前,我们需要添加一个标准的 {{HTMLElement("select")}} 元素。即使我们的 JavaScript 按预期工作,这个“额外”的 select 也是有好处的:我们可以使用这个 select 来将来自我们自定义的表单控件以及其他部分的表单数据发送出去。我们随后会详细的解释这一点。 + +```html + +
    + + +
    + Cherry + +
    +
    + +``` + +第二,我们需要两个新的类来隐藏不需要的元素:如果脚本未运行,我们会在视觉上隐藏自定义控件;如果脚本正常运行,则隐藏“真正”的 {{HTMLElement("select")}} 元素)。注意默认情况下,我们的 HTML 代码会隐藏我们的自定义控件。 + +```css +.widget select, +.no-widget .select { + /* 这个 CSS 选择器大体上说的是: + - 要么我们将 body 的类设置为“widget”,隐藏真实的 + + + + + + + +
    + Cherry + +
    + +``` + +```css hidden +.widget select, +.no-widget .select { + position: absolute; + left: -5000em; + height: 0; + overflow: hidden; +} +``` + +{{EmbedLiveSample("不使用 JS",120,130)}} + +#### 使用 JS + +查看[完整源代码](/zh-CN/docs/Learn/Forms/How_to_build_custom_form_controls/Example_2#使用_js)。 + +```html hidden +
    + + +
    + Cherry + +
    +
    +``` + +```css hidden +.widget select, +.no-widget .select { + position: absolute; + left: -5000em; + height: 0; + overflow: 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: #ffffff; +} +``` + +```js hidden +window.addEventListener("load", () => { + const form = document.querySelector("form"); + + form.classList.remove("no-widget"); + form.classList.add("widget"); +}); +``` + +{{EmbedLiveSample("使用 JS",120,130)}} + +> **备注:** 如果你真的想让你的代码变得通用和可复用,最好不要仅仅是添加一个类选择器开关,而是通过添加一个控件类的方式来隐藏 {{HTMLElement("select")}} 元素,并且动态地在每一个 {{HTMLElement("select")}} 元素后面添加代表页面中自定义控件的 DOM 树。 + +### 让工作变得更简单 + +在我们将要构建的代码之中,我们将会使用标准的 DOM API 和 JavaScript 来完成要做的所有工作。我们准备使用的特性如下所示: + +1. {{domxref("element.classList","classList")}} +2. {{domxref("EventTarget.addEventListener","addEventListener()")}} +3. {{domxref("NodeList.forEach()")}} +4. {{domxref("element.querySelector","querySelector()")}} 和 {{domxref("element.querySelectorAll","querySelectorAll()")}} + +### 构造事件回调 + +基础已经准备好了。我们现在可以开始定义用户每次同我们的控件交互时会用到的所有函数了。 + +```js +// 这个函数会用在每当我们想要停用一个自定义控件的时候 +// 它需要一个参数: +// select:要停用的带有 `select` 类的节点 +function deactivateSelect(select) { + // 如果控件没有运行,不用进行任何操作 + if (!select.classList.contains("active")) return; + + // 我们需要获取自定义控件的选项列表 + const optList = select.querySelector(".optList"); + + // 关闭选项列表 + optList.classList.add("hidden"); + + // 然后停用控件本身 + select.classList.remove("active"); +} + +// 每当用户想要激活(或停用)这个控件的时候,会调用这个函数 +// 它需要 2 个参数: +// select:要激活的带有 `select` 类的 DOM 节点 +// selectList:包含所有带 `select` 类的 DOM 节点的列表 +function activeSelect(select, selectList) { + // 如果控件已经激活了,不进行任何操作 + if (select.classList.contains("active")) return; + + // 我们需要关闭所有自定义控件的活动状态 + // 因为 deactiveselect 函数满足 forEach 回调函数的所有请求, + // 我们直接使用它,不使用中间匿名函数 + selectList.forEach(deactivateSelect); + + // 然后我们激活指定的控件 + select.classList.add("active"); +} + +// 每当用户想要打开/关闭选项列表的时候,会调用这个函数 +// 它需要一个参数: +// select:要反转状态的列表的 DOM 节点 +function toggleOptList(select) { + // 该列表不包含在控件中 + const optList = select.querySelector(".optList"); + + // 我们改变列表的类去显示/隐藏它 + optList.classList.toggle("hidden"); +} + +// 每当我们要高亮一个选项的时候,会调用该函数 +// 它需要两个参数: +// select:带有 `select` 类的 DOM 节点,包含了需要高亮强调的选项 +// option:需要高亮强调的带有'option'类的 DOM 节点 +function highlightOption(select, option) { + // 为我们的自定义 select 元素获取所有有效选项的列表 + const optionList = select.querySelectorAll(".option"); + + // 我们移除所有选项的高亮强调 + optionList.forEach(function (other) { + other.classList.remove("highlight"); + }); + + // 我们高亮强调正确的选项 + option.classList.add("highlight"); +} +``` + +这是你需要用来处理控件不同状态的所有代码。 + +接下来,我们将这些函数绑定到合适的事件上: + +```js +// 我们处理文档加载时的事件绑定。 +window.addEventListener("load", function () { + const selectList = document.querySelectorAll(".select"); + + // 每个自定义控件都需要初始化 + selectList.forEach(function (select) { + // 它的 `option` 元素也需要 + const optionList = select.querySelectorAll(".option"); + + // 每当用户的鼠标悬停在一个选项上时,我们高亮这个指定的选项 + optionList.forEach((option) => { + option.addEventListener("mouseover", () => { + // 注意:`select` 和 `option` 变量是我们函数调用范围内有效的闭包。 + highlightOption(select, option); + }); + }); + + // 每当用户点击一个自定义的 select 元素时 + select.addEventListener("click", (event) => { + // 注意:`select` 变量是我们函数调用范围内有效的闭包。 + + // 我们改变选项列表的可见性 + toggleOptList(select); + }); + + // 如果控件获得了焦点 + // 每当用户点击它或是用 tab 键访问这个控件时,其获得焦点 + select.addEventListener("focus", (event) => { + // 注意:`select` 和 `selectlist` 变量是我们函数调用范围内有效的闭包。 + + // 我们激活这个控件 + activeSelect(select, selectList); + }); + + // 如果控件失去焦点 + select.addEventListener("blur", (event) => { + // 注意:`select` 变量是我们函数调用范围内有效的闭包。 + + // 我们关闭这个控件 + deactivateSelect(select); + }); + + // 如果用户按下 `esc` 键,失去焦点 + select.addEventListener("keyup", (event) => { + // 在松开 `esc` 键时关闭控件 + if (event.key === "Escape") { + deactivateSelect(select); + } + }); + }); +}); +``` + +现在,我们的控件会根据我们的设计改变状态,但是它的值仍然没有更新。我们接下来会处理这件事。 + +#### 实时示例 + +查看[完整源代码](/zh-CN/docs/Learn/Forms/How_to_build_custom_form_controls/Example_3)。 + +```html hidden +
    + + +
    + Cherry + +
    +
    +``` + +```css hidden +.widget select, +.no-widget .select { + position: absolute; + left: -5000em; + height: 0; + overflow: 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: #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"); +} + +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, + ); + + select.addEventListener("focus", (event) => { + activeSelect(select, selectList); + }); + + select.addEventListener("blur", (event) => { + deactivateSelect(select); + }); + + select.addEventListener("keyup", (event) => { + if (event.keyCode === 27) { + deactivateSelect(select); + } + }); + }); +}); +``` + +{{EmbedLiveSample("实时示例",120,130)}} + +### 处理控件的值 + +既然我们的控件已经开始工作了,我们必须添加代码,使其能够根据用户的输入更新取值,并且能将取值随表单数据一同发送。 + +实现这一点最简单的方法是在后台使用原生控件。这样的控件会使用浏览器提供的所有内置控件来跟踪值,并且在表单提交时,取值也会像往常一样发送。当有现成的功能时,我们再做重复工作就毫无意义了。 + +像前面所看到的那样,出于无障碍的原因,我们已经使用了一个原生的选择(select)控件作为回退显示内容;我们可轻松的将它的值与我们的自定义控件之间的值同步。 + +```js +// 这个函数更新显示的值并将其通过原生控件同步 +// 它需要 2 个参数: +// select:含有要更新的值的 `select` 类的 DOM 节点 +// index:要被选择的值的索引 +function updateValue(select, index) { + // 我们需要为了给定的自定义控件获取原生控件 + // 在我们的示例中,原生控件是自定义控件的“兄弟” + const nativeWidget = select.previousElementSibling; + + // 我们也需要得到自定义控件的值占位符 + const value = select.querySelector(".value"); + + // 还有整个选项列表 + const optionList = select.querySelectorAll(".option"); + + // 我们将被选择的索引设定为我们的选择的索引 + nativeWidget.selectedIndex = index; + + // 更新相应的值占位符 + value.innerHTML = optionList[index].innerHTML; + + // 然后高亮我们自定义控件里对应的选项 + highlightOption(select, optionList[index]); +} + +// 这个函数返回原生控件里当前选定的索引 +// 它需要 1 个参数: +// select:跟原生控件有关的 `select` 类 DOM 节点 +function getIndex(select) { + // 我们需要为了给定的自定义控件访问原生控件 + // 在我们的示例中,原生控件是自定义控件的“兄弟” + const nativeWidget = select.previousElementSibling; + + return nativeWidget.selectedIndex; +} +``` + +通过这两个函数,我们可以将原生控件绑定到自定义的控件上。 + +```js +// 我们在文档加载时处理事件的绑定。 +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("click", (event) => { + updateValue(select, index); + }); + }); + + // 每当用户在获得焦点的控件上用键盘操作时,更新相应的值 + select.addEventListener("keyup", (event) => { + let index = getIndex(select); + + // 当用户点击 esc 键时,关闭自定义控件 + if (event.key === "Escape") { + deactivateSelect(select); + } + + // 当用户点击向下箭头时,跳转到下一个选项 + if (event.key === "ArrowDown" && index < optionList.length - 1) { + index++; + } + + // 当用户点击向上箭头时,跳转到上一个选项 + if (event.key === "ArrowUp" && index > 0) { + index--; + } + + updateValue(select, index); + }); + }); +}); +``` + +在上面的代码里,值得注意的是 [`tabIndex`](/zh-CN/docs/Web/API/HTMLElement/tabIndex) 属性的使用。使用这个属性是很有必要的,这可以确保原生控件将永远不会获得焦点,而且还可以确保当用户使用键盘和鼠标时,我们的自定义控件能够获得焦点。 + +做完上面这些后,我们就完成了! + +#### 实时示例 + +查看[完整源代码](/zh-CN/docs/Learn/Forms/How_to_build_custom_form_controls/Example_4)。 + +```html hidden +
    + + +
    + Cherry + +
    +
    +``` + +```css hidden +.widget select, +.no-widget .select { + position: absolute; + left: -5000em; + height: 0; + overflow: 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: #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"); +} + +function updateValue(select, index) { + const nativeWidget = select.previousElementSibling; + const value = select.querySelector(".value"); + const optionList = select.querySelectorAll(".option"); + + nativeWidget.selectedIndex = index; + value.innerHTML = optionList[index].innerHTML; + highlightOption(select, optionList[index]); +} + +function getIndex(select) { + const nativeWidget = select.previousElementSibling; + + return nativeWidget.selectedIndex; +} + +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"); - return nativeWidget.selectedIndex; -} -``` + optionList.forEach((option) => { + option.addEventListener("mouseover", () => { + highlightOption(select, option); + }); + }); -通过这两个函数,我们可以将原生组件绑定到自定义的组件上。 + select.addEventListener("click", (event) => { + toggleOptList(select); + }); -```js -// 我们在文档加载时处理事件的绑定。 -window.addEventListener("load", function () { - var selectList = document.querySelectorAll(".select"); + select.addEventListener("focus", (event) => { + activeSelect(select, selectList); + }); - // 每个自定义组件都需要初始化 - selectList.forEach(function (select) { - var optionList = select.querySelectorAll(".option"), - selectedIndex = getIndex(select); + select.addEventListener("blur", (event) => { + deactivateSelect(select); + }); + }); +}); - // 使我们的自定义组件可以获得焦点 - select.tabIndex = 0; +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(function (option, index) { - option.addEventListener("click", function (event) { + optionList.forEach((option, index) => { + option.addEventListener("click", (event) => { updateValue(select, index); }); }); - // 每当用户在获得焦点的组件上用键盘操作时,更新相应的值 - select.addEventListener("keyup", function (event) { - var length = optionList.length, - index = getIndex(select); + select.addEventListener("keyup", (event) => { + let index = getIndex(select); - // 当用户点击向下箭头时,跳转到下一个选项 - if (event.keyCode === 40 && index < length - 1) { + if (event.key === "Escape") { + deactivateSelect(select); + } + if (event.key === "ArrowDown" && index < optionList.length - 1) { index++; } - - // 当用户点击向上箭头时,跳转到上一个选项 - if (event.keyCode === 38 && index > 0) { + if (event.key === "ArrowUp" && index > 0) { index--; } @@ -632,38 +1626,31 @@ window.addEventListener("load", function () { }); ``` -在上面的代码里,值得注意的是 [`tabIndex`](/zh-CN/docs/Web/API/HTMLElement/tabIndex) 属性的使用。使用这个属性是很有必要的,这可以确保原生组件将永远不会获得焦点,而且还可以确保当用户使用键盘和鼠标时,我们的自定义组件能够获得焦点。 - -做完上面这些后,我们就完成了!下面是结果: - -| 实时示例 | -| ------------------------------------------------------------------------------------------------------ | -| {{EmbedLiveSample("改变状态",120,130, "", "Learn/Forms/How_to_build_custom_form_controls/Example_4")}} | -| [查看源代码](/zh-CN/docs/Learn/Forms/How_to_build_custom_form_controls/Example_4) | +{{EmbedLiveSample("实时示例_2",120,130)}} 但是等等,我们真的做完了嘛? -## 使其具有无障碍 +## 使其变得无障碍 -我们构建了一个能够生效的东西,尽管这离一个特性齐全的选择框还差得远,但是它效果不错。但是我们已经完成的事情只不过是摆弄 DOM。这个组件并没有真正的语义,即使它看起来像一个选择框,但是从浏览器的角度来看并不是,所以辅助技术并不能明白这是一个选择框。简单来说,这个全新的选择框并不具备无障碍! +我们构建了一个能够生效的东西,尽管这离一个特性齐全的选择框还差得远,但是它效果不错。但是我们已经完成的事情只不过是摆弄 DOM。这个控件并没有真正的语义,即使它看起来像一个选择框,但是从浏览器的角度来看并不是,所以辅助技术并不能明白这是一个选择框。简单来说,这个全新的选择框并不具备无障碍! -幸运的是,有一种解决方案叫做 [ARIA](/zh-CN/docs/Web/Accessibility/ARIA)。ARIA 代表“无障碍富互联网应用”。这是一个专为我们现在做的事情设计的 [W3C 规范](https://www.w3.org/TR/wai-aria/):使网络应用和自定义组件易于访问,它本质上是一组用来拓展 HTML 的属性集,以便我们能够更好的描述角色、状态和属性,就像我们刚才设计的元素是它试图传递的原生元素一样。使用这些属性非常简单,所以让我们来试试看。 +幸运的是,有一种解决方案叫做 [ARIA](/zh-CN/docs/Web/Accessibility/ARIA)。ARIA 代表“无障碍富互联网应用”。这是一个专为我们现在做的事情设计的 [W3C 规范](https://www.w3.org/TR/wai-aria/):使 web 应用和自定义控件可以无障碍访问,它本质上是一组用来拓展 HTML 的属性集,以便我们能够更好的描述角色、状态和属性,就像我们刚才设计的元素是它试图传递的原生元素一样。只要编辑 HTML 标记就可以使用这些属性。我们也可以通过 JavaScript 在用户更新选择的值时更新 ARIA 属性。 ### `role` 属性 -[ARIA](/zh-CN/docs/Accessibility/ARIA) 使用的关键属性是 [`role`](/zh-CN/docs/Accessibility/ARIA/ARIA_Techniques) 属性。[`role`](/zh-CN/docs/Accessibility/ARIA/ARIA_Techniques) 属性接受一个值,该值定义了一个元素的用途。每一个 role 定义了它自己的需求和行为。在我们的例子中,我们会使用 [`listbox`](/zh-CN/docs/Accessibility/ARIA/ARIA_Techniques/Using_the_listbox_role) 这一 role。这是一个 "合成角色",表示具有该 role 的元素应该有子元素,每个子元素都有特定的角色。(在这个案例中,至少有一个具有`option` 角色的子元素)。 +[ARIA](/zh-CN/docs/Web/Accessibility/ARIA) 使用的关键属性是角色([`role`](/zh-CN/docs/Web/Accessibility/ARIA/ARIA_Techniques))属性。[`role`](/zh-CN/docs/Web/Accessibility/ARIA/ARIA_Techniques) 属性接受一个值,该值定义了一个元素的用途。每一个角色定义了它自己的需求和行为。在我们的示例中,我们会使用 [`listbox`](/zh-CN/docs/Web/Accessibility/ARIA/Roles/listbox_role) 这一角色。这是一个“复合角色(composite role)”,表示具有该角色的元素应该有子元素,每个子元素都有特定的角色。(在这个案例中,至少有一个具有`option` 角色的子元素)。 -同样值得注意的是,ARIA 定义了默认应用于标准 HTML 标记的角色。例如,{{HTMLElement("table")}} 元素与角色 `grid` 相匹配,而 {{HTMLElement("ul")}} 元素与角色 `list` 相匹配。由于我们使用了一个 {{HTMLElement("ul")}} 元素,我们想要确保我们组件的 `listbox` 角色能替代 {{HTMLElement("ul")}} 元素的`list` 角色。为此,我们会使用角色 `presentation`。这个角色被设计成让我们来表示一个元素没有特殊的含义,并且仅仅用于提供信息。我们会将其应用到{{HTMLElement("ul")}} 元素上。 +同样值得注意的是,ARIA 定义了默认应用于标准 HTML 标记的角色。例如,{{HTMLElement("table")}} 元素与角色 `grid` 相匹配,而 {{HTMLElement("ul")}} 元素与角色 `list` 相匹配。由于我们使用了一个 {{HTMLElement("ul")}} 元素,我们想要确保我们控件的 `listbox` 角色能替代 {{HTMLElement("ul")}} 元素的 `list` 角色。为此,我们会使用角色 `presentation`。这个角色被设计成让我们来表示一个元素没有特殊的含义,并且仅仅用于提供信息。我们会将其应用到 {{HTMLElement("ul")}} 元素上。 -为了支持 [`listbox`](/zh-CN/docs/Accessibility/ARIA/ARIA_Techniques/Using_the_listbox_role) 角色,我们只需要将我们 HTML 改成这样: +为了支持 [`listbox`](/zh-CN/docs/Web/Accessibility/ARIA/Roles/listbox_role) 角色,我们只需要将我们的 HTML 改成这样: ```html - +
    Cherry
    ``` -> **备注:** 只有当你想要为不支持 [CSS 属性选择器的](/zh-CN/docs/CSS/Attribute_selectors)旧浏览器提供支持时,才有必要同时包含 `role` 属性和一个`class` 属性。 +> **备注:** 不需要同时包含 `role` 属性和 `class` 属性。你可以在 CSS 中使用 `[role="option"]` [属性选择器](/zh-CN/docs/Web/CSS/Attribute_selectors)来代替 `.option` 类。 ### `aria-selected` 属性 -仅仅使用 [`role`](/zh-CN/docs/Accessibility/ARIA/ARIA_Techniques) 属性是不够的。 [ARIA](/zh-CN/docs/Accessibility/ARIA) 还提供了许多状态和属性的内部特征。你能更好更充分的利用它们,你的组件就会能够被辅助技术更好的理解。在我们的例子中,我们会把使用限制在一个属性上:`aria-selected`。 +仅仅使用 [`role`](/zh-CN/docs/Web/Accessibility/ARIA/ARIA_Techniques) 属性是不够的。[ARIA](/zh-CN/docs/Web/Accessibility/ARIA/ARIA_Techniques) 还提供了许多状态和属性特征。你能更好更充分的利用它们,你的控件就会能够被辅助技术更好地理解。在我们的示例中,我们会把使用限制在一个属性上:`aria-selected`。 `aria-selected` 属性被用来标记当前被选中的选项;这可以让辅助技术告知用户当前的选项是什么。我们会通过 JavaScript 动态地使用该属性,每当用户选择一个选项时标记选中的选项。为了达到这一目的,我们需要修正我们的 `updateValue()` 函数: ```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('[role="option"]'); // 我们确保所有的选项都没有被选中 - optionList.forEach(function (other) { + optionList.forEach((other) => { other.setAttribute("aria-selected", "false"); }); @@ -701,21 +1688,406 @@ function updateValue(select, index) { } ``` -这是经过所有的改变之后的最终结果。(藉由 [NVDA](https://www.nvaccess.org/) 或 [VoiceOver](https://www.apple.com/accessibility/vision/) 这样的辅助技术尝试它,你会对此有更好的体会): +让屏幕阅读器聚焦于不可见的 select 而忽略我们的添加样式后的 select 似乎更简单,但这不是一个无障碍的解决方案。屏幕阅读器的用户不仅限于盲人;弱视甚至视力没问题的人也使用它们。因此,你不能让屏幕阅读器聚焦于不可见的元素。 + +下面是经过所有的改变之后的最终结果(藉由 [NVDA](https://www.nvaccess.org/) 或 [VoiceOver](https://www.apple.com/accessibility/vision/) 这样的辅助技术尝试它,你会对此有更好的体会)。 + +#### 实时示例 + +查看[完整源代码](/zh-CN/docs/Learn/Forms/How_to_build_custom_form_controls/Example_5)。 + +```html hidden +
    + + +
    + Cherry + +
    +
    +``` + +```css hidden +.widget select, +.no-widget .select { + position: absolute; + left: -5000em; + height: 0; + overflow: 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: #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"); +} + +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; +} + +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", () => { + 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("实时示例_3",120,130)}} + +如果你想继续前进,此示例中的代码需要进行一些改进才能变得通用和可复用。你可以尝试进行这方面的练习。有两个提示可以帮助你:我们所有函数的第一个参数都是相同的,这意味着这些函数需要相同的上下文。构建一个对象来共享该上下文是明智的。 + +## 代替方法:使用单选按钮 + +在上面的示例中,我们使用非语义化的 HTML、CSS 和 JavaScript 重新构建了一个 {{htmlelement('select')}} 元素。这个 select 从有限的选项中选择一个选项,这和一组同名的 {{htmlelement('input/radio', 'radio')}} 按钮的功能是一样的。 + +我们可以使用单选按钮重新实现这个功能,让我们看看这个方法。 + +我们可以从一个完全语义化、无障碍且无序的{{htmlelement('input/radio','单选')}}按钮列表开始,使用一对语义化的 {{htmlelement('fieldset')}} 和 {{htmlelement('legend')}} 对来标记整个组。 + +```html +
    + 选择一种水果 + +
    +``` + +我们将对单选按钮列表(不是 legend/fieldset)设置一些样式,使其看起来有点像前面的示例,这里只是为了表明它可以实现: + +```css +.styledSelect { + display: inline-block; + padding: 0; +} +.styledSelect li { + list-style-type: none; + padding: 0; + display: flex; +} +.styledSelect [type="radio"] { + position: absolute; + left: -100vw; + top: -100vh; +} +.styledSelect label { + margin: 0; + line-height: 2; + padding: 0 0 0 4px; +} +.styledSelect:not(:focus-within) input:not(:checked) + label { + height: 0; + outline: none; + overflow: hidden; +} +.styledSelect:not(:focus-within) input:checked + label { + border: 0.2em solid #000; + border-radius: 0.4em; + box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); +} +.styledSelect:not(:focus-within) input:checked + label::after { + content: "▼"; + background: black; + float: right; + color: white; + padding: 0 4px; + margin: 0 -4px 0 4px; +} +.styledSelect:focus-within { + border: 0.2em solid #000; + border-radius: 0.4em; + box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); +} +.styledSelect:focus-within input:checked + label { + background-color: #333; + color: #fff; + width: 100%; +} +``` + +无需 JavaScript,只需一点点 CSS,我们就可以为单选按钮列表添加样式,使其只显示选中的项目。当焦点在 `
    ` 中的 `