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/dist/components/flex/src/flex.vue.d.ts +1 -1
- package/dist/components/nav-tabs/index.d.ts +3 -1
- package/dist/components/nav-tabs/src/nav-tabs.vue.d.ts +18 -44
- package/dist/components/nav-tabs/src/type.d.ts +14 -5
- package/dist/gi.css +1 -1
- package/dist/hooks/useNavTabs.d.ts +21 -1
- package/dist/index.es.js +250 -45
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/package.json +1 -1
- package/packages/components/button/src/button.vue +1 -1
- package/packages/components/nav-tabs/index.ts +4 -1
- package/packages/components/nav-tabs/src/nav-tabs.vue +96 -25
- package/packages/components/nav-tabs/src/type.ts +15 -4
- package/packages/components/page-layout/src/page-layout.vue +1 -1
- package/packages/components/tag/src/tag.vue +2 -2
- package/packages/hooks/useNavTabs.ts +239 -17
package/package.json
CHANGED
|
@@ -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 =
|
|
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
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
28
|
-
import type {
|
|
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:
|
|
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:
|
|
106
|
+
// margin-right: 8px;
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
&__right {
|
|
95
110
|
flex-shrink: 0;
|
|
96
|
-
margin-left:
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
+
}
|
|
@@ -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:
|
|
174
|
-
$tag-size-large-height:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
() => [
|
|
466
|
+
() => [
|
|
467
|
+
toValue(tabEl),
|
|
468
|
+
toValue(tabScrollEl),
|
|
469
|
+
toValue(tabLeftScrollBtnEl),
|
|
470
|
+
toValue(tabRightScrollBtnEl)
|
|
471
|
+
] as const,
|
|
250
472
|
() => {
|
|
251
|
-
|
|
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
|
-
|
|
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
|
}
|