fds-vue-core 8.4.0 → 8.4.1

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.
@@ -7,7 +7,9 @@ declare function __VLS_template(): {
7
7
  }> & {
8
8
  default?: Slot;
9
9
  };
10
- refs: {};
10
+ refs: {
11
+ drawerInnerRef: HTMLDivElement;
12
+ };
11
13
  rootEl: any;
12
14
  };
13
15
  type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
@@ -25,7 +27,9 @@ declare const __VLS_component: import('vue').DefineComponent<FdsDrawerProps, {},
25
27
  strict: boolean;
26
28
  lockScroll: boolean;
27
29
  info: "valid" | "invalid" | "warning";
28
- }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
30
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
31
+ drawerInnerRef: HTMLDivElement;
32
+ }, any>;
29
33
  declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
30
34
  export default _default;
31
35
  type __VLS_WithTemplateSlots<T, S> = T & {
@@ -0,0 +1,8 @@
1
+ export declare const getFocusableElements: (container: Element) => HTMLElement[];
2
+ /**
3
+ * Focus trap for dialogs (modal, drawer). Activates on open, cleans up on close/unmount.
4
+ */
5
+ export declare function useFocusTrap(): {
6
+ activate: (getContainer: () => HTMLElement | null) => void;
7
+ deactivate: () => void;
8
+ };
@@ -881,7 +881,7 @@ const _sfc_main$M = /* @__PURE__ */ vue.defineComponent({
881
881
  const { t } = useFdsI18n();
882
882
  const emit = __emit;
883
883
  const isExpanded = vue.ref(props.expanded);
884
- const isVisible = vue.ref(props.visible);
884
+ const isVisible2 = vue.ref(props.visible);
885
885
  const expandLabel = vue.computed(() => t("FdsBlockAlert.showMore"));
886
886
  const collapseLabel = vue.computed(() => t("FdsBlockAlert.showLess"));
887
887
  const closeLabel = vue.computed(() => t("FdsBlockAlert.close"));
@@ -889,7 +889,7 @@ const _sfc_main$M = /* @__PURE__ */ vue.defineComponent({
889
889
  vue.watch(
890
890
  () => props.visible,
891
891
  (v) => {
892
- isVisible.value = v;
892
+ isVisible2.value = v;
893
893
  }
894
894
  );
895
895
  const alertClasses = vue.computed(() => [
@@ -903,7 +903,7 @@ const _sfc_main$M = /* @__PURE__ */ vue.defineComponent({
903
903
  ...props.dataTestid && { "data-testid": props.dataTestid }
904
904
  }));
905
905
  function handleClose() {
906
- isVisible.value = false;
906
+ isVisible2.value = false;
907
907
  emit("update:visible", false);
908
908
  emit("close");
909
909
  }
@@ -912,7 +912,7 @@ const _sfc_main$M = /* @__PURE__ */ vue.defineComponent({
912
912
  emit("toggle");
913
913
  }
914
914
  return (_ctx, _cache) => {
915
- return isVisible.value ? (vue.openBlock(), vue.createElementBlock("div", vue.mergeProps({ key: 0 }, rootAttrs.value, { class: alertClasses.value }), [
915
+ return isVisible2.value ? (vue.openBlock(), vue.createElementBlock("div", vue.mergeProps({ key: 0 }, rootAttrs.value, { class: alertClasses.value }), [
916
916
  vue.createElementVNode("div", null, [
917
917
  vue.createElementVNode("div", null, [
918
918
  __props.heading ? (vue.openBlock(), vue.createElementBlock("div", {
@@ -6342,6 +6342,56 @@ const _sfc_main$w = /* @__PURE__ */ vue.defineComponent({
6342
6342
  };
6343
6343
  }
6344
6344
  });
6345
+ const FOCUSABLE_SELECTOR = [
6346
+ "a[href]:not([disabled])",
6347
+ "button:not([disabled])",
6348
+ "textarea:not([disabled])",
6349
+ 'input:not([disabled]):not([type="hidden"])',
6350
+ "select:not([disabled])",
6351
+ '[tabindex]:not([tabindex="-1"])'
6352
+ ].join(", ");
6353
+ const isVisible = (el) => !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
6354
+ const getFocusableElements = (container) => Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter(
6355
+ (el) => el.tabIndex >= 0 && !el.hasAttribute("disabled") && isVisible(el)
6356
+ );
6357
+ function useFocusTrap() {
6358
+ let cleanup = null;
6359
+ const deactivate = () => {
6360
+ cleanup?.();
6361
+ cleanup = null;
6362
+ };
6363
+ const activate = (getContainer) => {
6364
+ deactivate();
6365
+ vue.nextTick(() => {
6366
+ const container = getContainer();
6367
+ if (!container) return;
6368
+ const focusable = getFocusableElements(container);
6369
+ if (focusable.length > 0) {
6370
+ focusable[0].focus();
6371
+ }
6372
+ const handleTabKey = (e) => {
6373
+ if (e.key !== "Tab") return;
6374
+ const current = getFocusableElements(container);
6375
+ if (current.length === 0) return;
6376
+ const first = current[0];
6377
+ const last = current[current.length - 1];
6378
+ if (e.shiftKey && document.activeElement === first) {
6379
+ last.focus();
6380
+ e.preventDefault();
6381
+ } else if (!e.shiftKey && document.activeElement === last) {
6382
+ first.focus();
6383
+ e.preventDefault();
6384
+ }
6385
+ };
6386
+ container.addEventListener("keydown", handleTabKey, true);
6387
+ cleanup = () => {
6388
+ container.removeEventListener("keydown", handleTabKey, true);
6389
+ };
6390
+ });
6391
+ };
6392
+ vue.onBeforeUnmount(deactivate);
6393
+ return { activate, deactivate };
6394
+ }
6345
6395
  const _hoisted_1$m = { class: "flex justify-between mb-4" };
6346
6396
  const _hoisted_2$g = { class: "flex items-center gap-2" };
6347
6397
  const _hoisted_3$g = { class: "mb-0-last-child" };
@@ -6367,8 +6417,8 @@ const _sfc_main$v = /* @__PURE__ */ vue.defineComponent({
6367
6417
  const props = __props;
6368
6418
  const emit = __emit;
6369
6419
  const isOpen = vue.ref(props.open);
6370
- let cleanupFocusTrap = null;
6371
6420
  const previouslyFocusedElement = vue.ref(null);
6421
+ const { activate: activateFocusTrap, deactivate: deactivateFocusTrap } = useFocusTrap();
6372
6422
  const hasFooterSlot = useHasSlot("modal-footer");
6373
6423
  const { t } = useFdsI18n();
6374
6424
  const modalInnerRef = vue.ref(null);
@@ -6464,39 +6514,6 @@ const _sfc_main$v = /* @__PURE__ */ vue.defineComponent({
6464
6514
  handleClose();
6465
6515
  }
6466
6516
  };
6467
- const getTabList = (modal) => modal.querySelectorAll(
6468
- 'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])'
6469
- );
6470
- const trapFocus = () => {
6471
- if (!isOpen.value) return;
6472
- vue.nextTick(() => {
6473
- const modal = document.querySelector(".fds-modal-inner");
6474
- if (!modal) return;
6475
- const tabList = getTabList(modal);
6476
- if (tabList.length > 0) {
6477
- tabList[0].focus();
6478
- }
6479
- const handleTabKey = (e) => {
6480
- if (e.key !== "Tab") return;
6481
- const currentTabList = getTabList(modal);
6482
- if (currentTabList.length === 0) return;
6483
- const firstFocusableEl = currentTabList[0];
6484
- const lastFocusableEl = currentTabList[currentTabList.length - 1];
6485
- const isTabOrderReversed = e.shiftKey;
6486
- if (isTabOrderReversed && document.activeElement === firstFocusableEl) {
6487
- lastFocusableEl.focus();
6488
- e.preventDefault();
6489
- } else if (!isTabOrderReversed && document.activeElement === lastFocusableEl) {
6490
- firstFocusableEl.focus();
6491
- e.preventDefault();
6492
- }
6493
- };
6494
- modal.addEventListener("keydown", handleTabKey, true);
6495
- cleanupFocusTrap = () => {
6496
- modal.removeEventListener("keydown", handleTabKey, true);
6497
- };
6498
- });
6499
- };
6500
6517
  vue.watch(
6501
6518
  () => props.open,
6502
6519
  (newValue) => {
@@ -6507,14 +6524,11 @@ const _sfc_main$v = /* @__PURE__ */ vue.defineComponent({
6507
6524
  if (props.lockScroll) {
6508
6525
  setHtmlOverflow("hidden");
6509
6526
  }
6510
- trapFocus();
6527
+ activateFocusTrap(() => modalInnerRef.value);
6511
6528
  } else {
6512
6529
  setHtmlOverflow("auto");
6513
6530
  resetDragResize();
6514
- if (cleanupFocusTrap) {
6515
- cleanupFocusTrap();
6516
- cleanupFocusTrap = null;
6517
- }
6531
+ deactivateFocusTrap();
6518
6532
  vue.nextTick(() => {
6519
6533
  if (previouslyFocusedElement.value) {
6520
6534
  previouslyFocusedElement.value.focus();
@@ -6532,16 +6546,14 @@ const _sfc_main$v = /* @__PURE__ */ vue.defineComponent({
6532
6546
  if (props.lockScroll) {
6533
6547
  setHtmlOverflow("hidden");
6534
6548
  }
6535
- trapFocus();
6549
+ activateFocusTrap(() => modalInnerRef.value);
6536
6550
  }
6537
6551
  });
6538
6552
  vue.onBeforeUnmount(() => {
6539
6553
  document.removeEventListener("keydown", handleKeyDown, true);
6540
6554
  cleanupDragResize();
6541
6555
  setHtmlOverflow("auto");
6542
- if (cleanupFocusTrap) {
6543
- cleanupFocusTrap();
6544
- }
6556
+ deactivateFocusTrap();
6545
6557
  });
6546
6558
  const modalAttrs = vue.computed(() => {
6547
6559
  const attrs = {};
@@ -11280,8 +11292,9 @@ const _sfc_main$n = /* @__PURE__ */ vue.defineComponent({
11280
11292
  const props = __props;
11281
11293
  const emit = __emit;
11282
11294
  const isOpen = vue.ref(props.open);
11283
- let cleanupFocusTrap = null;
11295
+ const drawerInnerRef = vue.ref(null);
11284
11296
  const previouslyFocusedElement = vue.ref(null);
11297
+ const { activate: activateFocusTrap, deactivate: deactivateFocusTrap } = useFocusTrap();
11285
11298
  const { t } = useFdsI18n();
11286
11299
  const drawerOuterClasses = vue.computed(() => ["fixed top-0 left-0 w-full h-full z-100"]);
11287
11300
  const drawerInnerClasses = vue.computed(() => [
@@ -11343,39 +11356,6 @@ const _sfc_main$n = /* @__PURE__ */ vue.defineComponent({
11343
11356
  handleClose();
11344
11357
  }
11345
11358
  };
11346
- const getTabList = (drawer) => drawer.querySelectorAll(
11347
- 'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])'
11348
- );
11349
- const trapFocus = () => {
11350
- if (!isOpen.value) return;
11351
- vue.nextTick(() => {
11352
- const drawer = document.querySelector(".fds-drawer-inner");
11353
- if (!drawer) return;
11354
- const tabList = getTabList(drawer);
11355
- if (tabList.length > 0) {
11356
- tabList[0].focus();
11357
- }
11358
- const handleTabKey = (e) => {
11359
- if (e.key !== "Tab") return;
11360
- const currentTabList = getTabList(drawer);
11361
- if (currentTabList.length === 0) return;
11362
- const firstFocusableEl = currentTabList[0];
11363
- const lastFocusableEl = currentTabList[currentTabList.length - 1];
11364
- const isTabOrderReversed = e.shiftKey;
11365
- if (isTabOrderReversed && document.activeElement === firstFocusableEl) {
11366
- lastFocusableEl.focus();
11367
- e.preventDefault();
11368
- } else if (!isTabOrderReversed && document.activeElement === lastFocusableEl) {
11369
- firstFocusableEl.focus();
11370
- e.preventDefault();
11371
- }
11372
- };
11373
- drawer.addEventListener("keydown", handleTabKey, true);
11374
- cleanupFocusTrap = () => {
11375
- drawer.removeEventListener("keydown", handleTabKey, true);
11376
- };
11377
- });
11378
- };
11379
11359
  vue.watch(
11380
11360
  () => props.open,
11381
11361
  (newValue) => {
@@ -11385,13 +11365,10 @@ const _sfc_main$n = /* @__PURE__ */ vue.defineComponent({
11385
11365
  if (props.lockScroll) {
11386
11366
  setHtmlOverflow("hidden");
11387
11367
  }
11388
- trapFocus();
11368
+ activateFocusTrap(() => drawerInnerRef.value);
11389
11369
  } else {
11390
11370
  setHtmlOverflow("auto");
11391
- if (cleanupFocusTrap) {
11392
- cleanupFocusTrap();
11393
- cleanupFocusTrap = null;
11394
- }
11371
+ deactivateFocusTrap();
11395
11372
  }
11396
11373
  }
11397
11374
  );
@@ -11402,15 +11379,13 @@ const _sfc_main$n = /* @__PURE__ */ vue.defineComponent({
11402
11379
  if (props.lockScroll) {
11403
11380
  setHtmlOverflow("hidden");
11404
11381
  }
11405
- trapFocus();
11382
+ activateFocusTrap(() => drawerInnerRef.value);
11406
11383
  }
11407
11384
  });
11408
11385
  vue.onBeforeUnmount(() => {
11409
11386
  document.removeEventListener("keydown", handleKeyDown, true);
11410
11387
  setHtmlOverflow("auto");
11411
- if (cleanupFocusTrap) {
11412
- cleanupFocusTrap();
11413
- }
11388
+ deactivateFocusTrap();
11414
11389
  });
11415
11390
  const drawerAttrs = vue.computed(() => {
11416
11391
  const attrs = {};
@@ -11434,6 +11409,8 @@ const _sfc_main$n = /* @__PURE__ */ vue.defineComponent({
11434
11409
  }, [
11435
11410
  _cache[2] || (_cache[2] = vue.createElementVNode("div", { class: "fds-drawer-backdrop fixed top-0 left-0 w-full h-full bg-black/34 z-99" }, null, -1)),
11436
11411
  vue.createElementVNode("div", vue.mergeProps({
11412
+ ref_key: "drawerInnerRef",
11413
+ ref: drawerInnerRef,
11437
11414
  class: ["fds-drawer-inner", drawerInnerClasses.value],
11438
11415
  role: "dialog",
11439
11416
  "aria-modal": "true"
@@ -11486,7 +11463,7 @@ const _export_sfc = (sfc, props) => {
11486
11463
  }
11487
11464
  return target;
11488
11465
  };
11489
- const FdsDrawer = /* @__PURE__ */ _export_sfc(_sfc_main$n, [["__scopeId", "data-v-0a576ce5"]]);
11466
+ const FdsDrawer = /* @__PURE__ */ _export_sfc(_sfc_main$n, [["__scopeId", "data-v-afc45d1a"]]);
11490
11467
  function debounce(fn, delayMs) {
11491
11468
  let timeoutId;
11492
11469
  const debounced = (...args) => {