gi-component 0.0.46 → 0.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gi-component",
3
3
  "type": "module",
4
- "version": "0.0.46",
4
+ "version": "0.0.48",
5
5
  "description": "Vue3中基于Element Plus二次封装基础组件库",
6
6
  "author": "lin",
7
7
  "license": "MIT",
@@ -53,7 +53,7 @@ const bindProps = computed(() => {
53
53
  })
54
54
 
55
55
  const btnText = computed(() => {
56
- return obj[props.type].btnText
56
+ return obj?.[props.type]?.btnText || ''
57
57
  })
58
58
  </script>
59
59
 
@@ -1,5 +1,8 @@
1
+ import type { DefineComponent } from 'vue'
2
+ import type { NavTabBase, NavTabsProps } from './src/type'
1
3
  import NavTabs from './src/nav-tabs.vue'
2
4
 
3
- export type NavTabsInstance = InstanceType<typeof NavTabs>
5
+ export type NavTabsInstance<T extends NavTabBase = NavTabBase> = DefineComponent<NavTabsProps<T>>
6
+
4
7
  export * from './src/type'
5
8
  export default NavTabs
@@ -3,20 +3,32 @@
3
3
  <div v-if="slots['left-extra']" :class="b('nav-tabs__left')">
4
4
  <slot name="left-extra" />
5
5
  </div>
6
- <div ref="scrollRef" :class="b('nav-tabs__scroll')">
7
- <div v-for="item in props.data" :key="item.value" :class="[
8
- b('nav-tabs-item'),
9
- props.custom
10
- ? b('nav-tabs-item--custom')
11
- : {
12
- [b('nav-tabs-item--active')]: model === item.value,
13
- [b('nav-tabs-item--disabled')]: item.disabled,
14
- },
15
- ]" :data-value="item.value" @click="handleItemClick(item)">
16
- <slot :item="item" :active="model === item.value" :disabled="!!item.disabled">
17
- {{ item.label }}
18
- </slot>
6
+ <div :class="b('nav-tabs__scroll-wrap')">
7
+ <button ref="leftBtnRef" type="button" :class="[b('nav-tabs__nav-btn'), b('nav-tabs__nav-btn--prev')]">
8
+ <ElIcon>
9
+ <ArrowLeft />
10
+ </ElIcon>
11
+ </button>
12
+ <div ref="scrollRef" :class="b('nav-tabs__scroll')">
13
+ <div v-for="item in props.data" :key="item.value" :class="[
14
+ b('nav-tabs-item'),
15
+ props.custom
16
+ ? b('nav-tabs-item--custom')
17
+ : {
18
+ [b('nav-tabs-item--active')]: model === item.value,
19
+ [b('nav-tabs-item--disabled')]: item.disabled,
20
+ },
21
+ ]" :data-value="item.value" @click="handleItemClick(item)">
22
+ <slot :item="item" :active="model === item.value" :disabled="!!item.disabled">
23
+ {{ item.label }}
24
+ </slot>
25
+ </div>
19
26
  </div>
27
+ <button ref="rightBtnRef" type="button" :class="[b('nav-tabs__nav-btn'), b('nav-tabs__nav-btn--next')]">
28
+ <ElIcon>
29
+ <ArrowRight />
30
+ </ElIcon>
31
+ </button>
20
32
  </div>
21
33
  <div v-if="slots['right-extra']" :class="b('nav-tabs__right')">
22
34
  <slot name="right-extra" />
@@ -24,14 +36,16 @@
24
36
  </div>
25
37
  </template>
26
38
 
27
- <script setup lang="ts">
28
- import type { NavTabItem, NavTabsProps } from './type.ts'
39
+ <script lang="ts" setup generic="T extends NavTabBase">
40
+ import type { NavTabBase, NavTabSlotProps, NavTabsProps } from './type.ts'
41
+ import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
42
+ import { ElIcon } from 'element-plus'
29
43
  import { ref, useSlots } from 'vue'
30
44
  import { useBemClass, useNavTabs } from '../../../hooks'
31
45
 
32
46
  const model = defineModel<string | number>()
33
47
 
34
- const props = withDefaults(defineProps<NavTabsProps>(), {
48
+ const props = withDefaults(defineProps<NavTabsProps<T>>(), {
35
49
  data: () => [],
36
50
  wheelSpeed: 1,
37
51
  custom: false
@@ -42,11 +56,7 @@ const emits = defineEmits<{
42
56
  }>()
43
57
 
44
58
  defineSlots<{
45
- 'default': (props: {
46
- item: NavTabItem
47
- active: boolean
48
- disabled: boolean
49
- }) => void
59
+ 'default': (props: NavTabSlotProps<T>) => void
50
60
  'left-extra': () => void
51
61
  'right-extra': () => void
52
62
  }>()
@@ -56,6 +66,8 @@ const { b } = useBemClass()
56
66
 
57
67
  const rootRef = ref<HTMLElement | null>(null)
58
68
  const scrollRef = ref<HTMLElement | null>(null)
69
+ const leftBtnRef = ref<HTMLElement | null>(null)
70
+ const rightBtnRef = ref<HTMLElement | null>(null)
59
71
 
60
72
  const tabItemClassName = b('nav-tabs-item')
61
73
 
@@ -64,10 +76,13 @@ useNavTabs({
64
76
  tabScrollEl: scrollRef,
65
77
  tabItemClassName,
66
78
  activeValue: model,
67
- wheelSpeed: props.wheelSpeed
79
+ wheelSpeed: props.wheelSpeed,
80
+ tabLeftScrollBtnEl: leftBtnRef,
81
+ tabRightScrollBtnEl: rightBtnRef,
82
+ navBtnDisabledClassName: b('nav-tabs__nav-btn--disabled')
68
83
  })
69
84
 
70
- function handleItemClick(item: NavTabItem) {
85
+ function handleItemClick(item: T) {
71
86
  if (item.disabled) {
72
87
  return
73
88
  }
@@ -88,16 +103,26 @@ function handleItemClick(item: NavTabItem) {
88
103
 
89
104
  &__left {
90
105
  flex-shrink: 0;
91
- margin-right: 10px;
106
+ // margin-right: 8px;
92
107
  }
93
108
 
94
109
  &__right {
95
110
  flex-shrink: 0;
96
- margin-left: 10px;
111
+ //margin-left: 8px;
112
+ }
113
+
114
+ &__scroll-wrap {
115
+ flex: 1;
116
+ display: flex;
117
+ align-items: center;
118
+ min-width: 0;
119
+ overflow: hidden;
120
+ height: 100%;
97
121
  }
98
122
 
99
123
  &__scroll {
100
124
  flex: 1;
125
+ min-width: 0;
101
126
  display: flex;
102
127
  overflow-x: auto;
103
128
  overflow-y: hidden;
@@ -109,6 +134,52 @@ function handleItemClick(item: NavTabItem) {
109
134
  }
110
135
  }
111
136
 
137
+ &__nav-btn {
138
+ flex-shrink: 0;
139
+ display: none;
140
+ align-items: center;
141
+ justify-content: center;
142
+ width: 20px;
143
+ height: 20px;
144
+ border-radius: 50%;
145
+ padding: 0;
146
+ border: none;
147
+ background: transparent;
148
+ color: var(--el-text-color-secondary);
149
+ cursor: pointer;
150
+ outline: none;
151
+ margin: 0 4px;
152
+
153
+ &--prev {
154
+ margin-left: 6px;
155
+ }
156
+
157
+ &--next {
158
+ margin-right: 6px;
159
+ }
160
+
161
+ &:hover {
162
+ color: var(--el-color-primary);
163
+ background-color: var(--el-fill-color-light);
164
+ }
165
+
166
+ &:active {
167
+ background-color: var(--el-fill-color);
168
+ }
169
+
170
+ &--disabled {
171
+ color: var(--el-text-color-disabled);
172
+ cursor: not-allowed;
173
+ pointer-events: none;
174
+
175
+ &:hover,
176
+ &:active {
177
+ color: var(--el-text-color-disabled);
178
+ background-color: transparent;
179
+ }
180
+ }
181
+ }
182
+
112
183
  &:not(.#{a.$prefix}-nav-tabs--custom) {
113
184
  .#{a.$prefix}-nav-tabs-item {
114
185
  padding: 0 16px;
@@ -1,13 +1,24 @@
1
- export type NavTabItem = {
1
+ /** 页签项基础字段(必填 + 可选 disabled) */
2
+ export interface NavTabBase {
2
3
  label: string
3
4
  value: string | number
4
5
  disabled?: boolean
5
- [key: string]: unknown
6
6
  }
7
7
 
8
- export interface NavTabsProps {
9
- data?: NavTabItem[]
8
+ /** 兼容旧用法:无扩展字段时的默认项类型 */
9
+ export type NavTabItem = NavTabBase
10
+
11
+ /** 组件 Props,T 由 data 数组元素类型推导 */
12
+ export interface NavTabsProps<T extends NavTabBase = NavTabBase> {
13
+ data?: T[]
10
14
  wheelSpeed?: number
11
15
  /** 自定义项样式:无 padding,不应用 --active / --disabled 修饰类 */
12
16
  custom?: boolean
13
17
  }
18
+
19
+ /** 默认插槽作用域 */
20
+ export type NavTabSlotProps<T extends NavTabBase = NavTabBase> = {
21
+ item: T
22
+ active: boolean
23
+ disabled: boolean
24
+ }
@@ -92,7 +92,7 @@ function handleClick() {
92
92
 
93
93
  .#{a.$prefix}-page-layout {
94
94
  flex: 1;
95
- width: 100%;
95
+ width: auto;
96
96
  height: 100%;
97
97
  display: flex;
98
98
  overflow: hidden;
@@ -170,8 +170,8 @@ function handleClose(event: MouseEvent) {
170
170
  $theme-colors: primary, success, warning, danger, info;
171
171
 
172
172
  $tag-size-small-height: 20px;
173
- $tag-size-default-height: 22px;
174
- $tag-size-large-height: 24px;
173
+ $tag-size-default-height: 24px;
174
+ $tag-size-large-height: 26px;
175
175
 
176
176
  $tag-size-small-padding: 0 6px;
177
177
  $tag-size-default-padding: 0 8px;
@@ -1,9 +1,10 @@
1
- import type { MaybeRefOrGetter } from 'vue'
1
+ import type { MaybeRefOrGetter, Ref } from 'vue'
2
2
  import {
3
3
  getCurrentInstance,
4
4
  nextTick,
5
5
  onMounted,
6
6
  onUnmounted,
7
+ ref,
7
8
  toRef,
8
9
  toValue,
9
10
  watch
@@ -20,6 +21,16 @@ export interface UseNavTabsOptions {
20
21
  activeValue?: MaybeRefOrGetter<string | number | undefined>
21
22
  /** 滚轮换算系数,默认 1 */
22
23
  wheelSpeed?: number
24
+ /** 左侧滚动按钮元素 */
25
+ tabLeftScrollBtnEl?: MaybeRefOrGetter<string | HTMLElement | null>
26
+ /** 右侧滚动按钮元素 */
27
+ tabRightScrollBtnEl?: MaybeRefOrGetter<string | HTMLElement | null>
28
+ /** 按钮滚动步长占可视宽度比例,默认 0.6 */
29
+ scrollBtnStepRatio?: number
30
+ /** 按钮滚动最小步长(px),默认 120 */
31
+ scrollBtnMinStep?: number
32
+ /** 按钮不可滚动时添加的 class(如 nav-tabs__nav-btn--disabled) */
33
+ navBtnDisabledClassName?: string
23
34
  }
24
35
 
25
36
  export interface UseNavTabsReturn {
@@ -27,6 +38,16 @@ export interface UseNavTabsReturn {
27
38
  scrollToActive: (behavior?: ScrollBehavior) => void
28
39
  /** 获取解析后的滚动容器 */
29
40
  getScrollEl: () => HTMLElement | null
41
+ /** 停止滚轮插值动画,避免与 scrollTo 冲突 */
42
+ cancelWheelScroll: () => void
43
+ /** 按步滚动(供外部调用,传入按钮时内部已绑定) */
44
+ scrollByStep: (direction: -1 | 1) => void
45
+ /** 内容是否溢出(可选,与按钮显隐同步更新) */
46
+ showNavBtn: Ref<boolean>
47
+ /** 是否可向左滚动 */
48
+ canScrollLeft: Ref<boolean>
49
+ /** 是否可向右滚动 */
50
+ canScrollRight: Ref<boolean>
30
51
  }
31
52
 
32
53
  function normalizeSelector(value: string): string {
@@ -50,6 +71,21 @@ function resolveElement(
50
71
  return scope.querySelector(normalizeSelector(target))
51
72
  }
52
73
 
74
+ function getMaxScrollLeft(scrollEl: HTMLElement) {
75
+ return Math.max(0, scrollEl.scrollWidth - scrollEl.clientWidth)
76
+ }
77
+
78
+ /** 边界容差,避免浮点误差导致禁用态抖动 */
79
+ const SCROLL_EDGE_EPSILON = 1
80
+
81
+ function canScrollToLeft(scrollEl: HTMLElement) {
82
+ return scrollEl.scrollLeft > SCROLL_EDGE_EPSILON
83
+ }
84
+
85
+ function canScrollToRight(scrollEl: HTMLElement) {
86
+ return scrollEl.scrollLeft < getMaxScrollLeft(scrollEl) - SCROLL_EDGE_EPSILON
87
+ }
88
+
53
89
  /** 滚轮平滑插值系数,越大跟手越快 */
54
90
  const WHEEL_SCROLL_LERP = 0.4
55
91
  const WHEEL_LINE_HEIGHT = 16
@@ -69,8 +105,7 @@ function getWheelPixelDelta(event: WheelEvent, scrollEl: HTMLElement): number {
69
105
  }
70
106
 
71
107
  function clampScrollLeft(scrollEl: HTMLElement, left: number): number {
72
- const maxScroll = Math.max(0, scrollEl.scrollWidth - scrollEl.clientWidth)
73
- return Math.max(0, Math.min(left, maxScroll))
108
+ return Math.max(0, Math.min(left, getMaxScrollLeft(scrollEl)))
74
109
  }
75
110
 
76
111
  function scrollItemToCenter(
@@ -78,12 +113,11 @@ function scrollItemToCenter(
78
113
  activeEl: HTMLElement,
79
114
  behavior: ScrollBehavior = 'smooth'
80
115
  ) {
81
- const maxScroll = scrollEl.scrollWidth - scrollEl.clientWidth
116
+ const maxScroll = getMaxScrollLeft(scrollEl)
82
117
  if (maxScroll <= 0) {
83
118
  return
84
119
  }
85
120
 
86
- // 使用视口坐标计算,兼容 flex gap / margin 等间距,避免 offsetLeft 偏差
87
121
  const scrollRect = scrollEl.getBoundingClientRect()
88
122
  const activeRect = activeEl.getBoundingClientRect()
89
123
  const activeLeftInContent = activeRect.left - scrollRect.left + scrollEl.scrollLeft
@@ -95,23 +129,62 @@ function scrollItemToCenter(
95
129
  })
96
130
  }
97
131
 
132
+ function setElementVisible(el: HTMLElement | null, visible: boolean) {
133
+ if (!el) {
134
+ return
135
+ }
136
+ el.style.display = visible ? 'flex' : 'none'
137
+ }
138
+
139
+ function setBtnDisabled(
140
+ el: HTMLElement | null,
141
+ disabled: boolean,
142
+ disabledClassName?: string
143
+ ) {
144
+ if (!el) {
145
+ return
146
+ }
147
+ if (disabledClassName) {
148
+ el.classList.toggle(disabledClassName, disabled)
149
+ }
150
+ if (disabled) {
151
+ el.setAttribute('aria-disabled', 'true')
152
+ } else {
153
+ el.removeAttribute('aria-disabled')
154
+ }
155
+ }
156
+
98
157
  export function useNavTabs(options: UseNavTabsOptions): UseNavTabsReturn {
99
158
  const {
100
159
  tabEl,
101
160
  tabScrollEl,
102
161
  tabItemClassName,
103
162
  activeValue,
104
- wheelSpeed = 1
163
+ wheelSpeed = 1,
164
+ tabLeftScrollBtnEl,
165
+ tabRightScrollBtnEl,
166
+ scrollBtnStepRatio = 0.6,
167
+ scrollBtnMinStep = 120,
168
+ navBtnDisabledClassName
105
169
  } = options
106
170
 
107
171
  const activeClassName = `${tabItemClassName}--active`
108
172
  const itemSelector = normalizeSelector(tabItemClassName)
173
+ const showNavBtn = ref(false)
174
+ const canScrollLeft = ref(false)
175
+ const canScrollRight = ref(false)
109
176
 
110
177
  let rootEl: HTMLElement | null = null
111
178
  let scrollEl: HTMLElement | null = null
179
+ let leftBtnEl: HTMLElement | null = null
180
+ let rightBtnEl: HTMLElement | null = null
112
181
  let resizeObserver: ResizeObserver | null = null
113
182
  let wheelRafId: number | null = null
114
183
  let wheelTargetScrollLeft = 0
184
+ let navBtnRafId: number | null = null
185
+
186
+ let onPrevClick: ((event: MouseEvent) => void) | null = null
187
+ let onNextClick: ((event: MouseEvent) => void) | null = null
115
188
 
116
189
  const cancelWheelAnimation = () => {
117
190
  if (wheelRafId !== null) {
@@ -120,6 +193,13 @@ export function useNavTabs(options: UseNavTabsOptions): UseNavTabsReturn {
120
193
  }
121
194
  }
122
195
 
196
+ const cancelWheelScroll = () => {
197
+ if (scrollEl) {
198
+ wheelTargetScrollLeft = scrollEl.scrollLeft
199
+ }
200
+ cancelWheelAnimation()
201
+ }
202
+
123
203
  const runWheelAnimation = () => {
124
204
  if (!scrollEl) {
125
205
  cancelWheelAnimation()
@@ -132,6 +212,7 @@ export function useNavTabs(options: UseNavTabsOptions): UseNavTabsReturn {
132
212
  if (Math.abs(diff) < 0.5) {
133
213
  scrollEl.scrollLeft = wheelTargetScrollLeft
134
214
  cancelWheelAnimation()
215
+ scheduleUpdateNavBtnState()
135
216
  return
136
217
  }
137
218
 
@@ -139,12 +220,87 @@ export function useNavTabs(options: UseNavTabsOptions): UseNavTabsReturn {
139
220
  wheelRafId = requestAnimationFrame(runWheelAnimation)
140
221
  }
141
222
 
223
+ const updateNavBtnState = () => {
224
+ if (!scrollEl) {
225
+ showNavBtn.value = false
226
+ canScrollLeft.value = false
227
+ canScrollRight.value = false
228
+ setElementVisible(leftBtnEl, false)
229
+ setElementVisible(rightBtnEl, false)
230
+ setBtnDisabled(leftBtnEl, false, navBtnDisabledClassName)
231
+ setBtnDisabled(rightBtnEl, false, navBtnDisabledClassName)
232
+ return
233
+ }
234
+
235
+ const overflow = getMaxScrollLeft(scrollEl) > 1
236
+ const scrollLeftEnabled = canScrollToLeft(scrollEl)
237
+ const scrollRightEnabled = canScrollToRight(scrollEl)
238
+
239
+ showNavBtn.value = overflow
240
+ canScrollLeft.value = scrollLeftEnabled
241
+ canScrollRight.value = scrollRightEnabled
242
+
243
+ setElementVisible(leftBtnEl, overflow)
244
+ setElementVisible(rightBtnEl, overflow)
245
+ setBtnDisabled(leftBtnEl, overflow && !scrollLeftEnabled, navBtnDisabledClassName)
246
+ setBtnDisabled(rightBtnEl, overflow && !scrollRightEnabled, navBtnDisabledClassName)
247
+ }
248
+
249
+ const scheduleUpdateNavBtnState = () => {
250
+ if (navBtnRafId !== null) {
251
+ return
252
+ }
253
+ navBtnRafId = requestAnimationFrame(() => {
254
+ navBtnRafId = null
255
+ updateNavBtnState()
256
+ })
257
+ }
258
+
259
+ const cancelNavBtnRaf = () => {
260
+ if (navBtnRafId !== null) {
261
+ cancelAnimationFrame(navBtnRafId)
262
+ navBtnRafId = null
263
+ }
264
+ }
265
+
266
+ const scrollByStep = (direction: -1 | 1) => {
267
+ if (!scrollEl) {
268
+ return
269
+ }
270
+
271
+ if (direction === -1 && !canScrollToLeft(scrollEl)) {
272
+ return
273
+ }
274
+ if (direction === 1 && !canScrollToRight(scrollEl)) {
275
+ return
276
+ }
277
+
278
+ cancelWheelScroll()
279
+
280
+ const maxScroll = getMaxScrollLeft(scrollEl)
281
+ if (maxScroll <= 0) {
282
+ return
283
+ }
284
+
285
+ const step = Math.max(scrollBtnMinStep, scrollEl.clientWidth * scrollBtnStepRatio)
286
+ const target = clampScrollLeft(scrollEl, scrollEl.scrollLeft + direction * step)
287
+
288
+ if (Math.abs(target - scrollEl.scrollLeft) < 1) {
289
+ return
290
+ }
291
+
292
+ scrollEl.scrollTo({ left: target, behavior: 'smooth' })
293
+ }
294
+
142
295
  const resolveElements = () => {
143
296
  const instanceRoot = getCurrentInstance()?.proxy?.$el as HTMLElement | undefined
144
297
  const fallbackRoot = instanceRoot instanceof HTMLElement ? instanceRoot : document
145
298
 
146
299
  rootEl = resolveElement(toValue(tabEl), fallbackRoot)
147
- scrollEl = resolveElement(toValue(tabScrollEl), rootEl ?? fallbackRoot)
300
+ const scope = rootEl ?? fallbackRoot
301
+ scrollEl = resolveElement(toValue(tabScrollEl), scope)
302
+ leftBtnEl = resolveElement(toValue(tabLeftScrollBtnEl), scope)
303
+ rightBtnEl = resolveElement(toValue(tabRightScrollBtnEl), scope)
148
304
 
149
305
  return Boolean(rootEl && scrollEl)
150
306
  }
@@ -173,11 +329,12 @@ export function useNavTabs(options: UseNavTabsOptions): UseNavTabsReturn {
173
329
  if (!scrollEl) {
174
330
  return
175
331
  }
176
- cancelWheelAnimation()
332
+ cancelWheelScroll()
177
333
  const activeItem = findActiveItem()
178
334
  if (activeItem) {
179
335
  scrollItemToCenter(scrollEl, activeItem, behavior)
180
336
  }
337
+ scheduleUpdateNavBtnState()
181
338
  }
182
339
 
183
340
  const handleWheel = (event: WheelEvent) => {
@@ -204,23 +361,69 @@ export function useNavTabs(options: UseNavTabsOptions): UseNavTabsReturn {
204
361
  if (wheelRafId === null) {
205
362
  wheelRafId = requestAnimationFrame(runWheelAnimation)
206
363
  }
364
+
365
+ scheduleUpdateNavBtnState()
366
+ }
367
+
368
+ const handleScroll = () => {
369
+ scheduleUpdateNavBtnState()
207
370
  }
208
371
 
209
372
  const bindWheel = () => {
210
373
  scrollEl?.addEventListener('wheel', handleWheel, { passive: false })
374
+ scrollEl?.addEventListener('scroll', handleScroll, { passive: true })
211
375
  }
212
376
 
213
377
  const unbindWheel = () => {
214
378
  scrollEl?.removeEventListener('wheel', handleWheel)
379
+ scrollEl?.removeEventListener('scroll', handleScroll)
215
380
  cancelWheelAnimation()
216
381
  }
217
382
 
383
+ const bindNavButtons = () => {
384
+ unbindNavButtons()
385
+
386
+ if (leftBtnEl) {
387
+ onPrevClick = (event: MouseEvent) => {
388
+ if (leftBtnEl?.getAttribute('aria-disabled') === 'true') {
389
+ event.preventDefault()
390
+ return
391
+ }
392
+ scrollByStep(-1)
393
+ }
394
+ leftBtnEl.addEventListener('click', onPrevClick)
395
+ }
396
+
397
+ if (rightBtnEl) {
398
+ onNextClick = (event: MouseEvent) => {
399
+ if (rightBtnEl?.getAttribute('aria-disabled') === 'true') {
400
+ event.preventDefault()
401
+ return
402
+ }
403
+ scrollByStep(1)
404
+ }
405
+ rightBtnEl.addEventListener('click', onNextClick)
406
+ }
407
+ }
408
+
409
+ const unbindNavButtons = () => {
410
+ if (leftBtnEl && onPrevClick) {
411
+ leftBtnEl.removeEventListener('click', onPrevClick)
412
+ }
413
+ if (rightBtnEl && onNextClick) {
414
+ rightBtnEl.removeEventListener('click', onNextClick)
415
+ }
416
+ onPrevClick = null
417
+ onNextClick = null
418
+ }
419
+
218
420
  const bindResizeObserver = () => {
219
421
  if (!scrollEl || typeof ResizeObserver === 'undefined') {
220
422
  return
221
423
  }
222
424
 
223
425
  resizeObserver = new ResizeObserver(() => {
426
+ scheduleUpdateNavBtnState()
224
427
  scrollToActive('auto')
225
428
  })
226
429
  resizeObserver.observe(scrollEl)
@@ -231,13 +434,27 @@ export function useNavTabs(options: UseNavTabsOptions): UseNavTabsReturn {
231
434
  resizeObserver = null
232
435
  }
233
436
 
437
+ const teardown = () => {
438
+ unbindWheel()
439
+ unbindNavButtons()
440
+ unbindResizeObserver()
441
+ cancelWheelAnimation()
442
+ cancelNavBtnRaf()
443
+ rootEl = null
444
+ scrollEl = null
445
+ leftBtnEl = null
446
+ rightBtnEl = null
447
+ }
448
+
234
449
  const setup = async () => {
235
450
  await nextTick()
236
451
  if (!resolveElements()) {
237
452
  return
238
453
  }
239
454
  bindWheel()
455
+ bindNavButtons()
240
456
  bindResizeObserver()
457
+ updateNavBtnState()
241
458
  scrollToActive('auto')
242
459
  }
243
460
 
@@ -246,10 +463,14 @@ export function useNavTabs(options: UseNavTabsOptions): UseNavTabsReturn {
246
463
  })
247
464
 
248
465
  watch(
249
- () => [toValue(tabEl), toValue(tabScrollEl)] as const,
466
+ () => [
467
+ toValue(tabEl),
468
+ toValue(tabScrollEl),
469
+ toValue(tabLeftScrollBtnEl),
470
+ toValue(tabRightScrollBtnEl)
471
+ ] as const,
250
472
  () => {
251
- unbindWheel()
252
- unbindResizeObserver()
473
+ teardown()
253
474
  setup()
254
475
  }
255
476
  )
@@ -261,15 +482,16 @@ export function useNavTabs(options: UseNavTabsOptions): UseNavTabsReturn {
261
482
  }
262
483
 
263
484
  onUnmounted(() => {
264
- unbindWheel()
265
- unbindResizeObserver()
266
- cancelWheelAnimation()
267
- rootEl = null
268
- scrollEl = null
485
+ teardown()
269
486
  })
270
487
 
271
488
  return {
272
489
  scrollToActive,
273
- getScrollEl: () => scrollEl
490
+ getScrollEl: () => scrollEl,
491
+ cancelWheelScroll,
492
+ scrollByStep,
493
+ showNavBtn,
494
+ canScrollLeft,
495
+ canScrollRight
274
496
  }
275
497
  }