b0cbc9f4428bbbc70b87c8910678fc0560cd4e69d1e7542a9ff9521274fcbcc469c93cb9a136bf6b2da699a55c95249d8892cd93dd494117856bf40826f976 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import { defineComponent, inject, ref, shallowRef, computed, watch, onMounted, onUpdated, triggerRef, createVNode, nextTick } from 'vue';
  2. import { useDocumentVisibility, useWindowFocus, useElementSize, useResizeObserver } from '@vueuse/core';
  3. import { ElIcon } from '../../icon/index.mjs';
  4. import { ArrowLeft, ArrowRight, Close } from '@element-plus/icons-vue';
  5. import useWheel from '../../virtual-list/src/hooks/use-wheel.mjs';
  6. import { clamp } from 'lodash-unified';
  7. import TabBar from './tab-bar2.mjs';
  8. import { tabsRootContextKey } from './constants.mjs';
  9. import { buildProps, definePropType } from '../../../utils/vue/props/runtime.mjs';
  10. import { mutable } from '../../../utils/typescript.mjs';
  11. import { throwError } from '../../../utils/error.mjs';
  12. import { useNamespace } from '../../../hooks/use-namespace/index.mjs';
  13. import { EVENT_CODE } from '../../../constants/aria.mjs';
  14. import { capitalize } from '../../../utils/strings.mjs';
  15. const tabNavProps = buildProps({
  16. panes: {
  17. type: definePropType(Array),
  18. default: () => mutable([])
  19. },
  20. currentName: {
  21. type: [String, Number],
  22. default: ""
  23. },
  24. editable: Boolean,
  25. type: {
  26. type: String,
  27. values: ["card", "border-card", ""],
  28. default: ""
  29. },
  30. stretch: Boolean
  31. });
  32. const tabNavEmits = {
  33. tabClick: (tab, tabName, ev) => ev instanceof Event,
  34. tabRemove: (tab, ev) => ev instanceof Event
  35. };
  36. const COMPONENT_NAME = "ElTabNav";
  37. const TabNav = defineComponent({
  38. name: COMPONENT_NAME,
  39. props: tabNavProps,
  40. emits: tabNavEmits,
  41. setup(props, {
  42. expose,
  43. emit
  44. }) {
  45. const rootTabs = inject(tabsRootContextKey);
  46. if (!rootTabs)
  47. throwError(COMPONENT_NAME, `<el-tabs><tab-nav /></el-tabs>`);
  48. const ns = useNamespace("tabs");
  49. const visibility = useDocumentVisibility();
  50. const focused = useWindowFocus();
  51. const navScroll$ = ref();
  52. const nav$ = ref();
  53. const el$ = ref();
  54. const tabRefsMap = ref({});
  55. const tabBarRef = ref();
  56. const scrollable = ref(false);
  57. const navOffset = ref(0);
  58. const isFocus = ref(false);
  59. const focusable = ref(true);
  60. const tracker = shallowRef();
  61. const isHorizontal = computed(() => ["top", "bottom"].includes(rootTabs.props.tabPosition));
  62. const sizeName = computed(() => isHorizontal.value ? "width" : "height");
  63. const navStyle = computed(() => {
  64. const dir = sizeName.value === "width" ? "X" : "Y";
  65. return {
  66. transform: `translate${dir}(-${navOffset.value}px)`
  67. };
  68. });
  69. const {
  70. width: navContainerWidth,
  71. height: navContainerHeight
  72. } = useElementSize(navScroll$);
  73. const {
  74. width: navWidth,
  75. height: navHeight
  76. } = useElementSize(nav$, {
  77. width: 0,
  78. height: 0
  79. }, {
  80. box: "border-box"
  81. });
  82. const navContainerSize = computed(() => isHorizontal.value ? navContainerWidth.value : navContainerHeight.value);
  83. const navSize = computed(() => isHorizontal.value ? navWidth.value : navHeight.value);
  84. const {
  85. onWheel
  86. } = useWheel({
  87. atStartEdge: computed(() => navOffset.value <= 0),
  88. atEndEdge: computed(() => navSize.value - navOffset.value <= navContainerSize.value),
  89. layout: computed(() => isHorizontal.value ? "horizontal" : "vertical")
  90. }, (offset) => {
  91. navOffset.value = clamp(navOffset.value + offset, 0, navSize.value - navContainerSize.value);
  92. });
  93. const scrollPrev = () => {
  94. if (!navScroll$.value)
  95. return;
  96. const containerSize = navScroll$.value[`offset${capitalize(sizeName.value)}`];
  97. const currentOffset = navOffset.value;
  98. if (!currentOffset)
  99. return;
  100. const newOffset = currentOffset > containerSize ? currentOffset - containerSize : 0;
  101. navOffset.value = newOffset;
  102. };
  103. const scrollNext = () => {
  104. if (!navScroll$.value || !nav$.value)
  105. return;
  106. const navSize2 = nav$.value[`offset${capitalize(sizeName.value)}`];
  107. const containerSize = navScroll$.value[`offset${capitalize(sizeName.value)}`];
  108. const currentOffset = navOffset.value;
  109. if (navSize2 - currentOffset <= containerSize)
  110. return;
  111. const newOffset = navSize2 - currentOffset > containerSize * 2 ? currentOffset + containerSize : navSize2 - containerSize;
  112. navOffset.value = newOffset;
  113. };
  114. const scrollToActiveTab = async () => {
  115. const nav = nav$.value;
  116. if (!scrollable.value || !el$.value || !navScroll$.value || !nav)
  117. return;
  118. await nextTick();
  119. const activeTab = tabRefsMap.value[props.currentName];
  120. if (!activeTab)
  121. return;
  122. const navScroll = navScroll$.value;
  123. const activeTabBounding = activeTab.getBoundingClientRect();
  124. const navScrollBounding = navScroll.getBoundingClientRect();
  125. const maxOffset = isHorizontal.value ? nav.offsetWidth - navScrollBounding.width : nav.offsetHeight - navScrollBounding.height;
  126. const currentOffset = navOffset.value;
  127. let newOffset = currentOffset;
  128. if (isHorizontal.value) {
  129. if (activeTabBounding.left < navScrollBounding.left) {
  130. newOffset = currentOffset - (navScrollBounding.left - activeTabBounding.left);
  131. }
  132. if (activeTabBounding.right > navScrollBounding.right) {
  133. newOffset = currentOffset + activeTabBounding.right - navScrollBounding.right;
  134. }
  135. } else {
  136. if (activeTabBounding.top < navScrollBounding.top) {
  137. newOffset = currentOffset - (navScrollBounding.top - activeTabBounding.top);
  138. }
  139. if (activeTabBounding.bottom > navScrollBounding.bottom) {
  140. newOffset = currentOffset + (activeTabBounding.bottom - navScrollBounding.bottom);
  141. }
  142. }
  143. newOffset = Math.max(newOffset, 0);
  144. navOffset.value = Math.min(newOffset, maxOffset);
  145. };
  146. const update = () => {
  147. var _a;
  148. if (!nav$.value || !navScroll$.value)
  149. return;
  150. props.stretch && ((_a = tabBarRef.value) == null ? void 0 : _a.update());
  151. const navSize2 = nav$.value[`offset${capitalize(sizeName.value)}`];
  152. const containerSize = navScroll$.value[`offset${capitalize(sizeName.value)}`];
  153. const currentOffset = navOffset.value;
  154. if (containerSize < navSize2) {
  155. scrollable.value = scrollable.value || {};
  156. scrollable.value.prev = currentOffset;
  157. scrollable.value.next = currentOffset + containerSize < navSize2;
  158. if (navSize2 - currentOffset < containerSize) {
  159. navOffset.value = navSize2 - containerSize;
  160. }
  161. } else {
  162. scrollable.value = false;
  163. if (currentOffset > 0) {
  164. navOffset.value = 0;
  165. }
  166. }
  167. };
  168. const changeTab = (event) => {
  169. let step = 0;
  170. switch (event.code) {
  171. case EVENT_CODE.left:
  172. case EVENT_CODE.up:
  173. step = -1;
  174. break;
  175. case EVENT_CODE.right:
  176. case EVENT_CODE.down:
  177. step = 1;
  178. break;
  179. default:
  180. return;
  181. }
  182. const tabList = Array.from(event.currentTarget.querySelectorAll("[role=tab]:not(.is-disabled)"));
  183. const currentIndex = tabList.indexOf(event.target);
  184. let nextIndex = currentIndex + step;
  185. if (nextIndex < 0) {
  186. nextIndex = tabList.length - 1;
  187. } else if (nextIndex >= tabList.length) {
  188. nextIndex = 0;
  189. }
  190. tabList[nextIndex].focus({
  191. preventScroll: true
  192. });
  193. tabList[nextIndex].click();
  194. setFocus();
  195. };
  196. const setFocus = () => {
  197. if (focusable.value)
  198. isFocus.value = true;
  199. };
  200. const removeFocus = () => isFocus.value = false;
  201. const setRefs = (el, key) => {
  202. tabRefsMap.value[key] = el;
  203. };
  204. const focusActiveTab = async () => {
  205. await nextTick();
  206. const activeTab = tabRefsMap.value[props.currentName];
  207. activeTab == null ? void 0 : activeTab.focus({
  208. preventScroll: true
  209. });
  210. };
  211. watch(visibility, (visibility2) => {
  212. if (visibility2 === "hidden") {
  213. focusable.value = false;
  214. } else if (visibility2 === "visible") {
  215. setTimeout(() => focusable.value = true, 50);
  216. }
  217. });
  218. watch(focused, (focused2) => {
  219. if (focused2) {
  220. setTimeout(() => focusable.value = true, 50);
  221. } else {
  222. focusable.value = false;
  223. }
  224. });
  225. useResizeObserver(el$, update);
  226. onMounted(() => setTimeout(() => scrollToActiveTab(), 0));
  227. onUpdated(() => update());
  228. expose({
  229. scrollToActiveTab,
  230. removeFocus,
  231. focusActiveTab,
  232. tabListRef: nav$,
  233. tabBarRef,
  234. scheduleRender: () => triggerRef(tracker)
  235. });
  236. return () => {
  237. const scrollBtn = scrollable.value ? [createVNode("span", {
  238. "class": [ns.e("nav-prev"), ns.is("disabled", !scrollable.value.prev)],
  239. "onClick": scrollPrev
  240. }, [createVNode(ElIcon, null, {
  241. default: () => [createVNode(ArrowLeft, null, null)]
  242. })]), createVNode("span", {
  243. "class": [ns.e("nav-next"), ns.is("disabled", !scrollable.value.next)],
  244. "onClick": scrollNext
  245. }, [createVNode(ElIcon, null, {
  246. default: () => [createVNode(ArrowRight, null, null)]
  247. })])] : null;
  248. const tabs = props.panes.map((pane, index) => {
  249. var _a, _b, _c, _d;
  250. const uid = pane.uid;
  251. const disabled = pane.props.disabled;
  252. const tabName = (_b = (_a = pane.props.name) != null ? _a : pane.index) != null ? _b : `${index}`;
  253. const closable = !disabled && (pane.isClosable || pane.props.closable !== false && props.editable);
  254. pane.index = `${index}`;
  255. const btnClose = closable ? createVNode(ElIcon, {
  256. "class": "is-icon-close",
  257. "onClick": (ev) => emit("tabRemove", pane, ev)
  258. }, {
  259. default: () => [createVNode(Close, null, null)]
  260. }) : null;
  261. const tabLabelContent = ((_d = (_c = pane.slots).label) == null ? void 0 : _d.call(_c)) || pane.props.label;
  262. const tabindex = !disabled && pane.active ? 0 : -1;
  263. return createVNode("div", {
  264. "ref": (el) => setRefs(el, tabName),
  265. "class": [ns.e("item"), ns.is(rootTabs.props.tabPosition), ns.is("active", pane.active), ns.is("disabled", disabled), ns.is("closable", closable), ns.is("focus", isFocus.value)],
  266. "id": `tab-${tabName}`,
  267. "key": `tab-${uid}`,
  268. "aria-controls": `pane-${tabName}`,
  269. "role": "tab",
  270. "aria-selected": pane.active,
  271. "tabindex": tabindex,
  272. "onFocus": () => setFocus(),
  273. "onBlur": () => removeFocus(),
  274. "onClick": (ev) => {
  275. removeFocus();
  276. emit("tabClick", pane, tabName, ev);
  277. },
  278. "onKeydown": (ev) => {
  279. if (closable && (ev.code === EVENT_CODE.delete || ev.code === EVENT_CODE.backspace)) {
  280. emit("tabRemove", pane, ev);
  281. }
  282. }
  283. }, [...[tabLabelContent, btnClose]]);
  284. });
  285. tracker.value;
  286. return createVNode("div", {
  287. "ref": el$,
  288. "class": [ns.e("nav-wrap"), ns.is("scrollable", !!scrollable.value), ns.is(rootTabs.props.tabPosition)]
  289. }, [scrollBtn, createVNode("div", {
  290. "class": ns.e("nav-scroll"),
  291. "ref": navScroll$
  292. }, [props.panes.length > 0 ? createVNode("div", {
  293. "class": [ns.e("nav"), ns.is(rootTabs.props.tabPosition), ns.is("stretch", props.stretch && ["top", "bottom"].includes(rootTabs.props.tabPosition))],
  294. "ref": nav$,
  295. "style": navStyle.value,
  296. "role": "tablist",
  297. "onKeydown": changeTab,
  298. "onWheel": onWheel
  299. }, [...[!props.type ? createVNode(TabBar, {
  300. "ref": tabBarRef,
  301. "tabs": [...props.panes],
  302. "tabRefs": tabRefsMap.value
  303. }, null) : null, tabs]]) : null])]);
  304. };
  305. }
  306. });
  307. export { TabNav as default, tabNavEmits, tabNavProps };
  308. //# sourceMappingURL=tab-nav.mjs.map