tab-nav.js 12 KB

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