import { defineComponent } from 'vue';
+import { DEFAULT_FOCUS_TRAP_OPTS, useBasicSetupFocusTrap, getFirstFocusableElement } from '@shell/composables/focusTrap';
+
+export const DEFAULT_ITERABLE_NODE_SELECTOR = 'body;';
export default defineComponent({
name: 'AppModal',
@@ -56,6 +59,27 @@ export default defineComponent({
name: {
type: String,
default: '',
+ },
+ /**
+ * trigger focus trap
+ */
+ triggerFocusTrap: {
+ type: Boolean,
+ default: false,
+ },
+ /**
+ * forcefully set return focus element based on this selector
+ */
+ returnFocusSelector: {
+ type: String,
+ default: '',
+ },
+ /**
+ * will return focus to the first iterable node of this container select
+ */
+ returnFocusFirstIterableNodeSelector: {
+ type: String,
+ default: DEFAULT_ITERABLE_NODE_SELECTOR,
}
},
computed: {
@@ -85,6 +109,31 @@ export default defineComponent({
};
}
},
+ setup(props) {
+ if (props.triggerFocusTrap) {
+ let opts:any = DEFAULT_FOCUS_TRAP_OPTS;
+
+ // if we have a "returnFocusFirstIterableNodeSelector" on top of "returnFocusSelector"
+ // then we will use "returnFocusFirstIterableNodeSelector" as a fallback of "returnFocusSelector"
+ if (props.returnFocusFirstIterableNodeSelector && props.returnFocusFirstIterableNodeSelector !== DEFAULT_ITERABLE_NODE_SELECTOR && props.returnFocusSelector) {
+ opts = {
+ ...DEFAULT_FOCUS_TRAP_OPTS,
+ setReturnFocus: () => {
+ return document.querySelector(props.returnFocusSelector) ? props.returnFocusSelector : getFirstFocusableElement(document.querySelector(props.returnFocusFirstIterableNodeSelector));
+ }
+ };
+ // otherwise, if we are sure of permanent existance of "returnFocusSelector"
+ // we just return to that element
+ } else if (props.returnFocusSelector) {
+ opts = {
+ ...DEFAULT_FOCUS_TRAP_OPTS,
+ setReturnFocus: props.returnFocusSelector
+ };
+ }
+
+ useBasicSetupFocusTrap('#modal-container-element', opts);
+ }
+ },
mounted() {
document.addEventListener('keydown', this.handleEscapeKey);
},
@@ -134,6 +183,7 @@ export default defineComponent({
>
import AsyncButton from '@shell/components/AsyncButton';
-import AppModal from '@shell/components/AppModal.vue';
+import AppModal, { DEFAULT_ITERABLE_NODE_SELECTOR } from '@shell/components/AppModal.vue';
export default {
emits: ['okay', 'closed'],
@@ -21,6 +21,22 @@ export default {
mode: {
type: String,
default: '',
+ },
+
+ /**
+ * forcefully set return focus element based on this selector
+ */
+ returnFocusSelector: {
+ type: String,
+ default: '',
+ },
+
+ /**
+ * will return focus to the first iterable node of this container select
+ */
+ returnFocusFirstIterableNodeSelector: {
+ type: String,
+ default: DEFAULT_ITERABLE_NODE_SELECTOR,
}
},
@@ -60,6 +76,9 @@ export default {
:name="name"
height="auto"
:scrollable="true"
+ :trigger-focus-trap="true"
+ :return-focus-selector="returnFocusSelector"
+ :return-focus-first-iterable-node-selector="returnFocusFirstIterableNodeSelector"
@close="closeDialog(false)"
@before-open="beforeOpen"
>
diff --git a/shell/components/Tabbed/index.vue b/shell/components/Tabbed/index.vue
index 25eb101fe3f..b0ee2d9f4e0 100644
--- a/shell/components/Tabbed/index.vue
+++ b/shell/components/Tabbed/index.vue
@@ -276,12 +276,11 @@ export default {
:data-testid="`btn-${tab.name}`"
:aria-controls="'#' + tab.name"
:aria-selected="tab.active"
- :aria-label="tab.labelDisplay"
+ :aria-label="tab.labelDisplay || ''"
role="tab"
tabindex="0"
@click.prevent="select(tab.name, $event)"
- @keyup.enter="select(tab.name, $event)"
- @keyup.space="select(tab.name, $event)"
+ @keyup.enter.space="select(tab.name, $event)"
>
{{ tab.labelDisplay }}
{
+ if (!el.hasAttribute('disabled')) {
+ filteredFocusableElements.push(el);
+ }
+ });
+
+ return filteredFocusableElements.length ? filteredFocusableElements[0] : document.body;
+}
+
+export const DEFAULT_FOCUS_TRAP_OPTS = {
+ escapeDeactivates: true,
+ allowOutsideClick: true
+};
+
+export function useBasicSetupFocusTrap(focusElement: string | HTMLElement, opts:any = DEFAULT_FOCUS_TRAP_OPTS) {
+ let focusTrapInstance: FocusTrap;
+ let focusEl;
+
+ onMounted(() => {
+ focusEl = typeof focusElement === 'string' ? document.querySelector(focusElement) as HTMLElement : focusElement;
+
+ focusTrapInstance = createFocusTrap(focusEl, opts);
+
+ nextTick(() => {
+ focusTrapInstance.activate();
+ });
+ });
+
+ onBeforeUnmount(() => {
+ if (Object.keys(focusTrapInstance).length) {
+ focusTrapInstance.deactivate();
+ }
+ });
+}
+
+export function useWatcherBasedSetupFocusTrapWithDestroyIncluded(watchVar:any, focusElement: string | HTMLElement, opts:any = DEFAULT_FOCUS_TRAP_OPTS) {
+ let focusTrapInstance: FocusTrap;
+ let focusEl;
+
+ watch(watchVar, (neu) => {
+ if (neu) {
+ nextTick(() => {
+ focusEl = typeof focusElement === 'string' ? document.querySelector(focusElement) as HTMLElement : focusElement;
+
+ focusTrapInstance = createFocusTrap(focusEl, opts);
+
+ nextTick(() => {
+ focusTrapInstance.activate();
+ });
+ });
+ } else if (!neu && Object.keys(focusTrapInstance).length) {
+ focusTrapInstance.deactivate();
+ }
+ });
+}
diff --git a/shell/pages/c/_cluster/explorer/ConfigBadge.vue b/shell/pages/c/_cluster/explorer/ConfigBadge.vue
index 100d1b1bbc2..ad8fc81ccfb 100644
--- a/shell/pages/c/_cluster/explorer/ConfigBadge.vue
+++ b/shell/pages/c/_cluster/explorer/ConfigBadge.vue
@@ -24,9 +24,7 @@ export default {
-
+