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.
- package/dist/components/FdsDrawer/FdsDrawer.vue.d.ts +6 -2
- package/dist/composables/useFocusTrap.d.ts +8 -0
- package/dist/fds-vue-core.cjs.js +68 -91
- package/dist/fds-vue-core.cjs.js.map +1 -1
- package/dist/fds-vue-core.css +1 -1
- package/dist/fds-vue-core.es.js +68 -91
- package/dist/fds-vue-core.es.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/components/FdsDrawer/FdsDrawer.vue +9 -56
- package/src/components/FdsModal/FdsModal.vue +6 -57
- package/src/composables/useFocusTrap.spec.ts +57 -0
- package/src/composables/useFocusTrap.ts +72 -0
|
@@ -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, {
|
|
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
|
+
};
|
package/dist/fds-vue-core.cjs.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6527
|
+
activateFocusTrap(() => modalInnerRef.value);
|
|
6511
6528
|
} else {
|
|
6512
6529
|
setHtmlOverflow("auto");
|
|
6513
6530
|
resetDragResize();
|
|
6514
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11368
|
+
activateFocusTrap(() => drawerInnerRef.value);
|
|
11389
11369
|
} else {
|
|
11390
11370
|
setHtmlOverflow("auto");
|
|
11391
|
-
|
|
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
|
-
|
|
11382
|
+
activateFocusTrap(() => drawerInnerRef.value);
|
|
11406
11383
|
}
|
|
11407
11384
|
});
|
|
11408
11385
|
vue.onBeforeUnmount(() => {
|
|
11409
11386
|
document.removeEventListener("keydown", handleKeyDown, true);
|
|
11410
11387
|
setHtmlOverflow("auto");
|
|
11411
|
-
|
|
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-
|
|
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) => {
|