focus-trap.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. 'use strict';
  2. Object.defineProperty(exports, '__esModule', { value: true });
  3. var vue = require('vue');
  4. var lodashUnified = require('lodash-unified');
  5. var utils = require('./utils.js');
  6. var tokens = require('./tokens.js');
  7. var pluginVue_exportHelper = require('../../../_virtual/plugin-vue_export-helper.js');
  8. var index = require('../../../hooks/use-escape-keydown/index.js');
  9. var aria = require('../../../constants/aria.js');
  10. var shared = require('@vue/shared');
  11. const _sfc_main = vue.defineComponent({
  12. name: "ElFocusTrap",
  13. inheritAttrs: false,
  14. props: {
  15. loop: Boolean,
  16. trapped: Boolean,
  17. focusTrapEl: Object,
  18. focusStartEl: {
  19. type: [Object, String],
  20. default: "first"
  21. }
  22. },
  23. emits: [
  24. tokens.ON_TRAP_FOCUS_EVT,
  25. tokens.ON_RELEASE_FOCUS_EVT,
  26. "focusin",
  27. "focusout",
  28. "focusout-prevented",
  29. "release-requested"
  30. ],
  31. setup(props, { emit }) {
  32. const forwardRef = vue.ref();
  33. let lastFocusBeforeTrapped;
  34. let lastFocusAfterTrapped;
  35. const { focusReason } = utils.useFocusReason();
  36. index.useEscapeKeydown((event) => {
  37. if (props.trapped && !focusLayer.paused) {
  38. emit("release-requested", event);
  39. }
  40. });
  41. const focusLayer = {
  42. paused: false,
  43. pause() {
  44. this.paused = true;
  45. },
  46. resume() {
  47. this.paused = false;
  48. }
  49. };
  50. const onKeydown = (e) => {
  51. if (!props.loop && !props.trapped)
  52. return;
  53. if (focusLayer.paused)
  54. return;
  55. const { code, altKey, ctrlKey, metaKey, currentTarget, shiftKey } = e;
  56. const { loop } = props;
  57. const isTabbing = code === aria.EVENT_CODE.tab && !altKey && !ctrlKey && !metaKey;
  58. const currentFocusingEl = document.activeElement;
  59. if (isTabbing && currentFocusingEl) {
  60. const container = currentTarget;
  61. const [first, last] = utils.getEdges(container);
  62. const isTabbable = first && last;
  63. if (!isTabbable) {
  64. if (currentFocusingEl === container) {
  65. const focusoutPreventedEvent = utils.createFocusOutPreventedEvent({
  66. focusReason: focusReason.value
  67. });
  68. emit("focusout-prevented", focusoutPreventedEvent);
  69. if (!focusoutPreventedEvent.defaultPrevented) {
  70. e.preventDefault();
  71. }
  72. }
  73. } else {
  74. if (!shiftKey && currentFocusingEl === last) {
  75. const focusoutPreventedEvent = utils.createFocusOutPreventedEvent({
  76. focusReason: focusReason.value
  77. });
  78. emit("focusout-prevented", focusoutPreventedEvent);
  79. if (!focusoutPreventedEvent.defaultPrevented) {
  80. e.preventDefault();
  81. if (loop)
  82. utils.tryFocus(first, true);
  83. }
  84. } else if (shiftKey && [first, container].includes(currentFocusingEl)) {
  85. const focusoutPreventedEvent = utils.createFocusOutPreventedEvent({
  86. focusReason: focusReason.value
  87. });
  88. emit("focusout-prevented", focusoutPreventedEvent);
  89. if (!focusoutPreventedEvent.defaultPrevented) {
  90. e.preventDefault();
  91. if (loop)
  92. utils.tryFocus(last, true);
  93. }
  94. }
  95. }
  96. }
  97. };
  98. vue.provide(tokens.FOCUS_TRAP_INJECTION_KEY, {
  99. focusTrapRef: forwardRef,
  100. onKeydown
  101. });
  102. vue.watch(() => props.focusTrapEl, (focusTrapEl) => {
  103. if (focusTrapEl) {
  104. forwardRef.value = focusTrapEl;
  105. }
  106. }, { immediate: true });
  107. vue.watch([forwardRef], ([forwardRef2], [oldForwardRef]) => {
  108. if (forwardRef2) {
  109. forwardRef2.addEventListener("keydown", onKeydown);
  110. forwardRef2.addEventListener("focusin", onFocusIn);
  111. forwardRef2.addEventListener("focusout", onFocusOut);
  112. }
  113. if (oldForwardRef) {
  114. oldForwardRef.removeEventListener("keydown", onKeydown);
  115. oldForwardRef.removeEventListener("focusin", onFocusIn);
  116. oldForwardRef.removeEventListener("focusout", onFocusOut);
  117. }
  118. });
  119. const trapOnFocus = (e) => {
  120. emit(tokens.ON_TRAP_FOCUS_EVT, e);
  121. };
  122. const releaseOnFocus = (e) => emit(tokens.ON_RELEASE_FOCUS_EVT, e);
  123. const onFocusIn = (e) => {
  124. const trapContainer = vue.unref(forwardRef);
  125. if (!trapContainer)
  126. return;
  127. const target = e.target;
  128. const relatedTarget = e.relatedTarget;
  129. const isFocusedInTrap = target && trapContainer.contains(target);
  130. if (!props.trapped) {
  131. const isPrevFocusedInTrap = relatedTarget && trapContainer.contains(relatedTarget);
  132. if (!isPrevFocusedInTrap) {
  133. lastFocusBeforeTrapped = relatedTarget;
  134. }
  135. }
  136. if (isFocusedInTrap)
  137. emit("focusin", e);
  138. if (focusLayer.paused)
  139. return;
  140. if (props.trapped) {
  141. if (isFocusedInTrap) {
  142. lastFocusAfterTrapped = target;
  143. } else {
  144. utils.tryFocus(lastFocusAfterTrapped, true);
  145. }
  146. }
  147. };
  148. const onFocusOut = (e) => {
  149. const trapContainer = vue.unref(forwardRef);
  150. if (focusLayer.paused || !trapContainer)
  151. return;
  152. if (props.trapped) {
  153. const relatedTarget = e.relatedTarget;
  154. if (!lodashUnified.isNil(relatedTarget) && !trapContainer.contains(relatedTarget)) {
  155. setTimeout(() => {
  156. if (!focusLayer.paused && props.trapped) {
  157. const focusoutPreventedEvent = utils.createFocusOutPreventedEvent({
  158. focusReason: focusReason.value
  159. });
  160. emit("focusout-prevented", focusoutPreventedEvent);
  161. if (!focusoutPreventedEvent.defaultPrevented) {
  162. utils.tryFocus(lastFocusAfterTrapped, true);
  163. }
  164. }
  165. }, 0);
  166. }
  167. } else {
  168. const target = e.target;
  169. const isFocusedInTrap = target && trapContainer.contains(target);
  170. if (!isFocusedInTrap)
  171. emit("focusout", e);
  172. }
  173. };
  174. async function startTrap() {
  175. await vue.nextTick();
  176. const trapContainer = vue.unref(forwardRef);
  177. if (trapContainer) {
  178. utils.focusableStack.push(focusLayer);
  179. const prevFocusedElement = trapContainer.contains(document.activeElement) ? lastFocusBeforeTrapped : document.activeElement;
  180. lastFocusBeforeTrapped = prevFocusedElement;
  181. const isPrevFocusContained = trapContainer.contains(prevFocusedElement);
  182. if (!isPrevFocusContained) {
  183. const focusEvent = new Event(tokens.FOCUS_AFTER_TRAPPED, tokens.FOCUS_AFTER_TRAPPED_OPTS);
  184. trapContainer.addEventListener(tokens.FOCUS_AFTER_TRAPPED, trapOnFocus);
  185. trapContainer.dispatchEvent(focusEvent);
  186. if (!focusEvent.defaultPrevented) {
  187. vue.nextTick(() => {
  188. let focusStartEl = props.focusStartEl;
  189. if (!shared.isString(focusStartEl)) {
  190. utils.tryFocus(focusStartEl);
  191. if (document.activeElement !== focusStartEl) {
  192. focusStartEl = "first";
  193. }
  194. }
  195. if (focusStartEl === "first") {
  196. utils.focusFirstDescendant(utils.obtainAllFocusableElements(trapContainer), true);
  197. }
  198. if (document.activeElement === prevFocusedElement || focusStartEl === "container") {
  199. utils.tryFocus(trapContainer);
  200. }
  201. });
  202. }
  203. }
  204. }
  205. }
  206. function stopTrap() {
  207. const trapContainer = vue.unref(forwardRef);
  208. if (trapContainer) {
  209. trapContainer.removeEventListener(tokens.FOCUS_AFTER_TRAPPED, trapOnFocus);
  210. const releasedEvent = new CustomEvent(tokens.FOCUS_AFTER_RELEASED, {
  211. ...tokens.FOCUS_AFTER_TRAPPED_OPTS,
  212. detail: {
  213. focusReason: focusReason.value
  214. }
  215. });
  216. trapContainer.addEventListener(tokens.FOCUS_AFTER_RELEASED, releaseOnFocus);
  217. trapContainer.dispatchEvent(releasedEvent);
  218. if (!releasedEvent.defaultPrevented && (focusReason.value == "keyboard" || !utils.isFocusCausedByUserEvent() || trapContainer.contains(document.activeElement))) {
  219. utils.tryFocus(lastFocusBeforeTrapped != null ? lastFocusBeforeTrapped : document.body);
  220. }
  221. trapContainer.removeEventListener(tokens.FOCUS_AFTER_RELEASED, releaseOnFocus);
  222. utils.focusableStack.remove(focusLayer);
  223. }
  224. }
  225. vue.onMounted(() => {
  226. if (props.trapped) {
  227. startTrap();
  228. }
  229. vue.watch(() => props.trapped, (trapped) => {
  230. if (trapped) {
  231. startTrap();
  232. } else {
  233. stopTrap();
  234. }
  235. });
  236. });
  237. vue.onBeforeUnmount(() => {
  238. if (props.trapped) {
  239. stopTrap();
  240. }
  241. if (forwardRef.value) {
  242. forwardRef.value.removeEventListener("keydown", onKeydown);
  243. forwardRef.value.removeEventListener("focusin", onFocusIn);
  244. forwardRef.value.removeEventListener("focusout", onFocusOut);
  245. forwardRef.value = void 0;
  246. }
  247. });
  248. return {
  249. onKeydown
  250. };
  251. }
  252. });
  253. function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  254. return vue.renderSlot(_ctx.$slots, "default", { handleKeydown: _ctx.onKeydown });
  255. }
  256. var ElFocusTrap = /* @__PURE__ */ pluginVue_exportHelper["default"](_sfc_main, [["render", _sfc_render], ["__file", "focus-trap.vue"]]);
  257. exports["default"] = ElFocusTrap;
  258. //# sourceMappingURL=focus-trap.js.map