Skip to content

Commit 05ff7af

Browse files
committed
feat(NavTabs): 支持受控模式并改进可访问性
- 为 NavTabs 组件添加 `modelValue` 属性和 `update:modelValue`、`change` 事件,以支持 Vue 的双向绑定和受控模式 - 将导航项从 div 重构为 button 元素,并添加适当的 ARIA 角色和属性(role="tablist"、role="tab"、aria-selected、tabindex),提升可访问性 - 实现内部状态管理,当未提供 `modelValue` 时使用内部状态,并处理 items 变化时的状态重置 - 更新主页面的导航项数组格式以提高可读性
1 parent 129e518 commit 05ff7af

File tree

3 files changed

+81
-8
lines changed

3 files changed

+81
-8
lines changed

src/components/ui/NavTabs/index.vue

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,76 @@
11
<template>
2-
<nav class="ui-nav-tabs" :aria-label="props.ariaLabel">
3-
<div
2+
<nav class="ui-nav-tabs" :aria-label="props.ariaLabel" role="tablist">
3+
<button
44
v-for="item in props.items"
55
:key="item.id"
6+
type="button"
67
class="ui-nav-tabs__item"
7-
:class="{ 'ui-nav-tabs__item--active': item.active }">
8-
<span class="ui-nav-tabs__icon material-symbols-outlined" :class="{ 'fill-1': item.active }">
8+
role="tab"
9+
:aria-selected="isActive(item)"
10+
:tabindex="isActive(item) ? 0 : -1"
11+
:class="{ 'ui-nav-tabs__item--active': isActive(item) }"
12+
@click="handleItemClick(item.id)">
13+
<span class="ui-nav-tabs__icon material-symbols-outlined" :class="{ 'fill-1': isActive(item) }">
914
{{ item.icon }}
1015
</span>
1116
<span class="ui-nav-tabs__label">{{ item.label }}</span>
12-
</div>
17+
</button>
1318
</nav>
1419
</template>
1520

1621
<script setup lang="ts">
22+
import { computed, ref, watch } from 'vue';
23+
1724
import type { NavTabsProps } from './types';
1825
1926
const props = withDefaults(defineProps<NavTabsProps>(), {
2027
ariaLabel: 'Main navigation'
2128
});
29+
30+
const emit = defineEmits<{
31+
(event: 'update:modelValue', value: string): void;
32+
(event: 'change', value: string): void;
33+
}>();
34+
35+
const internalActiveId = ref(resolveInitialActiveId());
36+
const activeId = computed(() => props.modelValue ?? internalActiveId.value);
37+
38+
watch(
39+
() => props.modelValue,
40+
nextValue => {
41+
if (nextValue !== undefined) {
42+
internalActiveId.value = nextValue;
43+
}
44+
},
45+
{ immediate: true }
46+
);
47+
48+
watch(
49+
() => props.items,
50+
items => {
51+
if (!items.some(item => item.id === activeId.value)) {
52+
internalActiveId.value = resolveInitialActiveId();
53+
}
54+
},
55+
{ deep: true }
56+
);
57+
58+
function resolveInitialActiveId() {
59+
const preferredItem = props.items.find(item => item.active) ?? props.items[0];
60+
return preferredItem?.id ?? '';
61+
}
62+
63+
function isActive(item: NavTabsProps['items'][number]) {
64+
return activeId.value === item.id;
65+
}
66+
67+
function handleItemClick(id: string) {
68+
if (props.modelValue === undefined) {
69+
internalActiveId.value = id;
70+
}
71+
emit('update:modelValue', id);
72+
emit('change', id);
73+
}
2274
</script>
2375

2476
<style scoped lang="css">
@@ -38,6 +90,9 @@ const props = withDefaults(defineProps<NavTabsProps>(), {
3890
}
3991
4092
.ui-nav-tabs__item {
93+
appearance: none;
94+
background: transparent;
95+
width: 100%;
4196
min-height: 56px;
4297
display: flex;
4398
flex-direction: column;
@@ -46,6 +101,10 @@ const props = withDefaults(defineProps<NavTabsProps>(), {
46101
gap: 3px;
47102
color: var(--ui-nav-tabs-text);
48103
border-bottom: 2px solid transparent;
104+
border-left: 0;
105+
border-right: 0;
106+
border-top: 0;
107+
cursor: pointer;
49108
}
50109
51110
.ui-nav-tabs__item--active {

src/components/ui/NavTabs/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ export interface NavTabItem {
77

88
export interface NavTabsProps {
99
items: NavTabItem[];
10+
modelValue?: string;
1011
ariaLabel?: string;
1112
}

src/pages/index.vue

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,22 @@ const store = useAppStore();
171171
const { t } = useI18n();
172172
const appVersion = packageJson.version;
173173
const mainNavItems = computed(() => [
174-
{ id: 'monitor', label: t('overlay.mainNavMonitor'), icon: 'monitor_heart', active: true },
175-
{ id: 'toolkit', label: t('overlay.mainNavToolkit'), icon: 'construction' },
176-
{ id: 'settings', label: t('overlay.mainNavSettings'), icon: 'settings' }
174+
{
175+
id: 'monitor',
176+
label: t('overlay.mainNavMonitor'),
177+
icon: 'monitor_heart',
178+
active: true
179+
},
180+
{
181+
id: 'toolkit',
182+
label: t('overlay.mainNavToolkit'),
183+
icon: 'construction'
184+
},
185+
{
186+
id: 'settings',
187+
label: t('overlay.mainNavSettings'),
188+
icon: 'settings'
189+
}
177190
]);
178191
const {
179192
updateAvailable,

0 commit comments

Comments
 (0)