Skip to content

Commit 87d500c

Browse files
refactor: rebuild Tab/TabList on reka-ui TabsTrigger for ARIA and keyboard nav
Amp-Thread-ID: https://ampcode.com/threads/T-019cdb2f-a7a6-74fb-bc4b-f2c1dfa428f2 Co-authored-by: Amp <amp@ampcode.com>
1 parent c9ccfc0 commit 87d500c

File tree

4 files changed

+65
-82
lines changed

4 files changed

+65
-82
lines changed

src/components/sidebar/tabs/NodeLibrarySidebarTabV2.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { mount } from '@vue/test-utils'
22
import { createTestingPinia } from '@pinia/testing'
3+
import { TabsTrigger } from 'reka-ui'
34
import { ref } from 'vue'
45
import { beforeEach, describe, expect, it, vi } from 'vitest'
56
import { createI18n } from 'vue-i18n'
@@ -105,8 +106,8 @@ describe('NodeLibrarySidebarTabV2', () => {
105106
it('should render with tabs', () => {
106107
const wrapper = mountComponent()
107108

108-
const tabs = wrapper.findAll('[role="tab"]')
109-
expect(tabs).toHaveLength(3)
109+
const triggers = wrapper.findAllComponents(TabsTrigger)
110+
expect(triggers).toHaveLength(3)
110111
})
111112

112113
it('should render search box', () => {

src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -106,42 +106,44 @@
106106
</DropdownMenuRoot>
107107
</template>
108108
</SidebarTopArea>
109-
<!-- Tab list in header (fixed) -->
110-
<div class="border-b border-comfy-input px-2 pt-2 pb-1 2xl:px-4">
111-
<TabList v-model="selectedTab">
112-
<Tab v-for="tab in tabs" :key="tab.value" :value="tab.value">
113-
{{ tab.label }}
114-
</Tab>
115-
</TabList>
116-
</div>
117109
</template>
118110
<template #body>
119111
<NodeDragPreview />
120-
<!-- Tab content (scrollable) -->
121-
<TabsRoot v-model="selectedTab" class="h-full py-2">
122-
<EssentialNodesPanel
123-
v-if="
124-
flags.nodeLibraryEssentialsEnabled && selectedTab === 'essentials'
125-
"
126-
v-model:expanded-keys="expandedKeys"
127-
:root="renderedEssentialRoot"
128-
:flat-nodes="essentialFlatNodes"
129-
@node-click="handleNodeClick"
130-
/>
131-
<AllNodesPanel
132-
v-if="selectedTab === 'all'"
133-
v-model:expanded-keys="expandedKeys"
134-
:sections="renderedSections"
135-
:fill-node-info="fillNodeInfo"
136-
:sort-order="sortOrder"
137-
@node-click="handleNodeClick"
138-
/>
139-
<BlueprintsPanel
140-
v-if="selectedTab === 'blueprints'"
141-
v-model:expanded-keys="expandedKeys"
142-
:sections="renderedBlueprintsSections"
143-
@node-click="handleNodeClick"
144-
/>
112+
<TabsRoot v-model="selectedTab" class="flex h-full flex-col">
113+
<!-- Tab list in header (fixed) -->
114+
<div class="border-b border-comfy-input px-2 pt-2 pb-1 2xl:px-4">
115+
<TabsList class="flex w-full items-center gap-2">
116+
<Tab v-for="tab in tabs" :key="tab.value" :value="tab.value">
117+
{{ tab.label }}
118+
</Tab>
119+
</TabsList>
120+
</div>
121+
<!-- Tab content (scrollable) -->
122+
<div class="min-h-0 flex-1 overflow-y-auto py-2">
123+
<EssentialNodesPanel
124+
v-if="
125+
flags.nodeLibraryEssentialsEnabled && selectedTab === 'essentials'
126+
"
127+
v-model:expanded-keys="expandedKeys"
128+
:root="renderedEssentialRoot"
129+
:flat-nodes="essentialFlatNodes"
130+
@node-click="handleNodeClick"
131+
/>
132+
<AllNodesPanel
133+
v-if="selectedTab === 'all'"
134+
v-model:expanded-keys="expandedKeys"
135+
:sections="renderedSections"
136+
:fill-node-info="fillNodeInfo"
137+
:sort-order="sortOrder"
138+
@node-click="handleNodeClick"
139+
/>
140+
<BlueprintsPanel
141+
v-if="selectedTab === 'blueprints'"
142+
v-model:expanded-keys="expandedKeys"
143+
:sections="renderedBlueprintsSections"
144+
@node-click="handleNodeClick"
145+
/>
146+
</div>
145147
</TabsRoot>
146148
</template>
147149
</SidebarTabTemplate>
@@ -158,14 +160,14 @@ import {
158160
DropdownMenuRadioItem,
159161
DropdownMenuRoot,
160162
DropdownMenuTrigger,
163+
TabsList,
161164
TabsRoot
162165
} from 'reka-ui'
163166
import { computed, nextTick, onMounted, ref, watchEffect } from 'vue'
164167
import { useI18n } from 'vue-i18n'
165168
166169
import { resolveEssentialsDisplayName } from '@/constants/essentialsDisplayNames'
167170
import Tab from '@/components/tab/Tab.vue'
168-
import TabList from '@/components/tab/TabList.vue'
169171
import SearchInput from '@/components/ui/search-input/SearchInput.vue'
170172
import Button from '@/components/ui/button/Button.vue'
171173
import SidebarTopArea from '@/components/sidebar/tabs/SidebarTopArea.vue'

src/components/tab/Tab.vue

Lines changed: 18 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,30 @@
11
<template>
2-
<button
3-
:id="tabId"
4-
:class="tabClasses"
5-
role="tab"
6-
:aria-selected="isActive"
7-
:aria-controls="panelId"
8-
:tabindex="0"
9-
@click="handleClick"
2+
<TabsTrigger
3+
:value="value"
4+
:class="
5+
cn(
6+
'flex shrink-0 items-center justify-center',
7+
'cursor-pointer rounded-lg px-2.5 py-2 text-sm transition-all duration-200',
8+
'border-none outline-hidden',
9+
'data-[state=active]:text-bold data-[state=active]:bg-interface-menu-component-surface-hovered data-[state=active]:text-text-primary',
10+
'data-[state=inactive]:bg-transparent data-[state=inactive]:text-text-secondary data-[state=inactive]:hover:bg-button-hover-surface data-[state=inactive]:focus:bg-button-hover-surface',
11+
props.class
12+
)
13+
"
1014
>
1115
<slot />
12-
</button>
16+
</TabsTrigger>
1317
</template>
1418

1519
<script setup lang="ts" generic="T extends string = string">
16-
import type { Ref } from 'vue'
17-
import { computed, inject } from 'vue'
20+
import type { HTMLAttributes } from 'vue'
21+
22+
import { TabsTrigger } from 'reka-ui'
1823
1924
import { cn } from '@/utils/tailwindUtil'
2025
21-
const { value, panelId } = defineProps<{
26+
const props = defineProps<{
2227
value: T
23-
panelId?: string
28+
class?: HTMLAttributes['class']
2429
}>()
25-
26-
const currentValue = inject<Ref<T>>('tabs-value')
27-
const updateValue = inject<(value: T) => void>('tabs-update')
28-
29-
const tabId = computed(() => `tab-${value}`)
30-
const isActive = computed(() => currentValue?.value === value)
31-
32-
const tabClasses = computed(() => {
33-
return cn(
34-
// Base styles from TextButton
35-
'flex shrink-0 items-center justify-center',
36-
'cursor-pointer rounded-lg px-2.5 py-2 text-sm transition-all duration-200',
37-
'border-none outline-hidden',
38-
// State styles with semantic tokens
39-
isActive.value
40-
? 'text-bold bg-interface-menu-component-surface-hovered text-text-primary'
41-
: 'bg-transparent text-text-secondary hover:bg-button-hover-surface focus:bg-button-hover-surface'
42-
)
43-
})
44-
45-
const handleClick = () => {
46-
updateValue?.(value)
47-
}
4830
</script>

src/components/tab/TabList.vue

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
<template>
2-
<div role="tablist" class="flex w-full items-center gap-2 pb-1">
3-
<slot />
4-
</div>
2+
<TabsRoot v-model="modelValue">
3+
<TabsList as-child>
4+
<div role="tablist" class="flex w-full items-center gap-2">
5+
<slot />
6+
</div>
7+
</TabsList>
8+
</TabsRoot>
59
</template>
610

711
<script setup lang="ts" generic="T extends string = string">
8-
import { provide } from 'vue'
12+
import { TabsList, TabsRoot } from 'reka-ui'
913
1014
const modelValue = defineModel<T>({ required: true })
11-
12-
// Provide for child Tab components
13-
provide('tabs-value', modelValue)
14-
provide('tabs-update', (value: T) => {
15-
modelValue.value = value
16-
})
1715
</script>

0 commit comments

Comments
 (0)