tab-nav.mjs 9.9 KB

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