我们可以只使用 html 和 css 创建树视图(可折叠列表) ,而不需要 JavaScript。可访问性软件将看到树形视图作为列表嵌套在披露窗口小部件中,并且自动支持标准键盘交互。
1、HTML
我们就从简单嵌套列表的 html 开始:
<ul>
<li>
Giant planets
<ul>
<li>
Gas giants
<ul>
<li>Jupiter</li>
<li>Saturn</li>
</ul>
</li>
<li>
Ice giants
<ul>
<li>Uranus</li>
<li>Neptune</li>
</ul>
</li>
</ul>
</li>
</ul>
然后,我们向最外层的 < ul > 元素添加一个类,对于每个包含嵌套列表的列表项,我们将列表项的内容放在 < Details > 和 < Summary > 元素中,使用 open 属性来控制最初展开哪些嵌套列表:
<ul class="tree">
<li>
<details open>
<summary>Giant planets</summary>
<ul>
<li>
<details>
<summary>Gas giants</summary>
<ul>
<li>Jupiter</li>
<li>Saturn</li>
</ul>
</details>
</li>
<li>
<details>
<summary>Ice giants</summary>
<ul>
<li>Uranus</li>
<li>Neptune</li>
</ul>
</details>
</li>
</ul>
</details>
</li>
</ul>
没有任何样式,这个 html 生成:
浏览器将 < Details > 元素实现为一个披露窗口小部件,使其能够展开和折叠嵌套列表,但是项目符号和披露箭头的组合会产生一个令人困惑的用户界面。
2、自定义属性:
有两个维度影响树视图的布局: 行间距(等于文本的行高)和标记的半径。我们首先为这些维度创建 CSS 自定义属性:
.tree{
--spacing : 1.5rem;
--radius : 10px;
}
虽然我们通常使用相对单位来缩放基于文本大小的用户界面控件,但对于标记,这可能导致控件过小或过大,所以我们使用一个合理的固定大小。
3、padding
然后我们设计列表项和嵌套列表的样式,为行和标记腾出空间:
.tree li{
display : block;
position : relative;
padding-left : calc(2 * var(--spacing) - var(--radius) - 2px);
}
.tree ul{
margin-left : calc(var(--radius) - var(--spacing));
padding-left : 0;
}
第7行从列表项中删除项目符号点。第8行建立了一个新的堆叠上下文和包含块,我们将使用它来定位行和标记。
第9行缩进列表项。缩进等于两倍的间距,减去标记半径,减去两个像素的行宽。其结果是,列表项中的文本将与其下方的标记的左侧对齐。
第13行使用负边距来补偿第9行引入的缩进,确保嵌套列表只按所需的间距缩进。第14行删除浏览器应用于列表的默认填充。
在最初展开所有嵌套列表的树视图中,应用此样式生成:
4、垂直线属性
接下来,我们添加构成连接每个列表项标记的线的一部分的垂直线到其嵌套列表的标记:
.tree ul li{
border-left : 2px solid #ddd;
}
.tree ul li:last-child{
border-color : transparent;
}
我们使用一个边框来创建这条线,并将其隐藏在每个列表中的最后一个项目中,因为这条线不应该继续超过这个项目的标记。使边框透明,而不是完全删除它,避免了增加填充以进行补偿的需要。
应用这种风格生成:
5、水平线
我们使用生成的内容来添加将垂直线连接到每个列表项的标记的水平线:
.tree ul li::before {
content: '';
display: block;
position: absolute;
top: calc(var(--spacing) / -2);
left: -2px;
width: calc(var(--spacing) + 2px);
height: calc(var(--spacing) + 1px);
border: solid #ddd;
border-width: 0 0 2px 2px;
}
这段代码还创建了短的垂直线,因为之前创建的垂直线不会一直延伸到顶部和底部的标记。
第26行和第27行生成一个块,第28行到第30行将其定位为从前一行文本的中点开始,与左侧的垂直线重叠。
第31和32行设置块的大小。它需要比间距宽两个像素,因为它与左侧的垂直线重叠,并且比间距高一个像素,因为水平线宽度的一半位于文本行的中点以下。注意,我们假设使用边框大小,因此这些尺寸包括边框。
第33和34行在块的左边和底边创建一个边框。
应用这种风格生成:
总结:
接下来,我们从摘要中删除默认样式:
.tree summary {
display: block;
cursor: pointer;
}
.tree summary::marker,
.tree summary::-webkit-details-marker {
display: none;
}
.tree summary:focus {
outline: none;
}
.tree summary:focus-visible {
outline: 1px dotted #000;
}
第38行和第44行删除了公开箭头。Safari 需要第44行,第42行和第43行的两个选择器覆盖了不同版本的浏览器。第39行更改光标以指示可以单击摘要以与其交互。
Safari 在摘要周围显示了一个焦点指示器,即使是在使用指针而不是键盘导航的情况下,所以我们移除了第48行的焦点样式,然后使用 :focus-visible 伪类将其添加回来,供访问者使用第52行的键盘导航。
应用这种风格生成:
6、标识
我们再次使用生成的内容来创建标记:
.tree li::after,
.tree summary::before {
content: '';
display: block;
position: absolute;
top: calc(var(--spacing) / 2 - var(--radius));
left: calc(var(--spacing) - var(--radius) - 1px);
width: calc(2 * var(--radius));
height: calc(2 * var(--radius));
border-radius: 50%;
background: #ddd;
}
注意,我们同时为 < li > 元素(不包含嵌套列表的列表项)和 < 摘要元素生成标记,允许包含嵌套列表的列表项具有不同的标记样式,具体取决于嵌套列表是展开的还是折叠的。
第57行和第58行生成一个块,第59行到第61行将其置于水平线和垂直线交汇处的中心。顶部位于文本行的中点,减去半径。左边定位在垂直线的边缘,减去半径,减去对应于线宽一半的一个像素。
第62和63行设置块的大小,第64和65行将其设置为圆形。
应用这种风格生成:
7、展开和折叠按钮
最后,我们添加了展开和折叠按钮:
.tree summary::before {
content: '+';
z-index: 1;
background: #696;
color: #fff;
line-height: calc(2 * var(--radius) - 2px);
text-align: center;
}
.tree details[open]>summary::before {
content: '−';
}
第69行和第78行显示了按钮中的加号与减号。请注意,我们使用真正的减号(-)而不是连字符(-) ,因为这与加号的外观相匹配,而在大多数字体中,连字符越窄越小。
第70行使按钮显示在前面创建的标记之上。因为标记是使用: : 创建的,否则它将显示在按钮的顶部。
应用此样式生成完成的树视图:
8、这是完成的代码
将以上所有内容结合起来,就得到了最终的代码:
CSS
<style>
.tree {
--spacing: 1.5rem;
--radius: 10px;
}
.tree li {
display: block;
position: relative;
padding-left: calc(2 * var(--spacing) - var(--radius) - 2px);
}
.tree ul {
margin-left: calc(var(--radius) - var(--spacing));
padding-left: 0;
}
.tree ul li {
border-left: 2px solid #ddd;
}
.tree ul li:last-child {
border-color: transparent;
}
.tree ul li::before {
content: '';
display: block;
position: absolute;
top: calc(var(--spacing) / -2);
left: -2px;
width: calc(var(--spacing) + 2px);
height: calc(var(--spacing) + 1px);
border: solid #ddd;
border-width: 0 0 2px 2px;
}
.tree summary {
display: block;
cursor: pointer;
}
.tree summary::marker,
.tree summary::-webkit-details-marker {
display: none;
}
.tree summary:focus {
outline: none;
}
.tree summary:focus-visible {
outline: 1px dotted #000;
}
.tree li::after,
.tree summary::before {
content: '';
display: block;
position: absolute;
top: calc(var(--spacing) / 2 - var(--radius));
left: calc(var(--spacing) - var(--radius) - 1px);
width: calc(2 * var(--radius));
height: calc(2 * var(--radius));
border-radius: 50%;
background: #ddd;
}
.tree summary::before {
content: '+';
z-index: 1;
background: #696;
color: #fff;
line-height: calc(2 * var(--radius) - 2px);
text-align: center;
}
.tree details[open]>summary::before {
content: '−';
}
</style>
HTML
<body>
<ul class="tree">
<li>
<details open>
<summary>Giant planets</summary>
<ul>
<li>
<details>
<summary>Gas giants</summary>
<ul>
<li>Jupiter</li>
<li>Saturn</li>
</ul>
</details>
</li>
<li>
<details>
<summary>Ice giants</summary>
<ul>
<li>Uranus</li>
<li>Neptune</li>
</ul>
</details>
</li>
</ul>
</details>
</li>
</ul>
</body>