Skip to content

saving some work on focus trap #14119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions shell/components/AppModal.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { DEFAULT_FOCUS_TRAP_OPTS, useBasicSetupFocusTrap, getFirstFocusableElement } from '@shell/composables/focusTrap';
import {
DEFAULT_FOCUS_TRAP_OPTS,
useBasicSetupFocusTrap,

Check warning on line 5 in shell/components/AppModal.vue

View workflow job for this annotation

GitHub Actions / lint

'useBasicSetupFocusTrap' is defined but never used
getFirstFocusableElement,
useWatcherBasedSetupFocusTrapWithDestroyIncluded
} from '@shell/composables/focusTrap';

export const DEFAULT_ITERABLE_NODE_SELECTOR = 'body;';

Expand All @@ -10,7 +15,8 @@
emits: ['close'],

inheritAttrs: false,
props: {

props: {
/**
* If set to false, it will not be possible to close modal by clicking on
* the background or by pressing Esc key.
Expand Down Expand Up @@ -80,7 +86,25 @@
returnFocusFirstIterableNodeSelector: {
type: String,
default: DEFAULT_ITERABLE_NODE_SELECTOR,
}
},
/**
* trigger focus trap - but watcher based
*/
triggerFocusTrapWatcherBased: {
type: Boolean,
default: false,
},
/**
* watcher-based focus trap variable to watch
*/
focusTrapWatcherBasedVariable: {
type: Boolean,
default: false,
},
},

data() {
return { fakeWatchVar: true };
},
computed: {
modalWidth(): string {
Expand Down Expand Up @@ -131,9 +155,23 @@
};
}

useBasicSetupFocusTrap('#modal-container-element', opts);
useWatcherBasedSetupFocusTrapWithDestroyIncluded(() => props.focusTrapWatcherBasedVariable || 'fakeWatchVar', '#modal-container-element', opts, true);

// if (!props.triggerFocusTrapWatcherBased) {
// useBasicSetupFocusTrap('#modal-container-element', opts);
// }
}
},
// created() {
// // This usecase is to cover the PromptModal scenario where it's renders a generic component inside.
// // Due to the architecture of the PromptModal it needs to be handled with a watcher based focus trap
// // but with a dedicated unmount hook
// if (this.triggerFocusTrapWatcherBased) {
// const opts:any = DEFAULT_FOCUS_TRAP_OPTS;

// useWatcherBasedSetupFocusTrapWithDestroyIncluded(() => this.focusTrapWatcherBasedVariable, '#modal-container-element', opts, true);
// }
// },
mounted() {
document.addEventListener('keydown', this.handleEscapeKey);
},
Expand Down
2 changes: 2 additions & 0 deletions shell/components/Dialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
},

data() {
console.error('ON DIALOG COMPONENT....');

Check warning on line 44 in shell/components/Dialog.vue

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement

return { closed: false };
},

Expand Down
51 changes: 51 additions & 0 deletions shell/components/__tests__/PromptModal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { nextTick } from 'vue';

Check warning on line 1 in shell/components/__tests__/PromptModal.test.ts

View workflow job for this annotation

GitHub Actions / lint

'nextTick' is defined but never used
import { shallowMount, mount } from '@vue/test-utils';

Check warning on line 2 in shell/components/__tests__/PromptModal.test.ts

View workflow job for this annotation

GitHub Actions / lint

'shallowMount' is defined but never used
import PromptModal from '@shell/components/PromptModal.vue';
import GenericPrompt from '@shell/dialog/GenericPrompt.vue';
import { createStore } from 'vuex';
import { ExtendedVue, Vue } from 'vue/types/vue';

Check warning on line 6 in shell/components/__tests__/PromptModal.test.ts

View workflow job for this annotation

GitHub Actions / lint

'ExtendedVue' is defined but never used

Check warning on line 6 in shell/components/__tests__/PromptModal.test.ts

View workflow job for this annotation

GitHub Actions / lint

'Vue' is defined but never used
import { DefaultProps } from 'vue/types/options';

Check warning on line 7 in shell/components/__tests__/PromptModal.test.ts

View workflow job for this annotation

GitHub Actions / lint

'DefaultProps' is defined but never used
import { CAPI, NORMAN } from '@shell/config/types';

Check warning on line 8 in shell/components/__tests__/PromptModal.test.ts

View workflow job for this annotation

GitHub Actions / lint

'CAPI' is defined but never used

Check warning on line 8 in shell/components/__tests__/PromptModal.test.ts

View workflow job for this annotation

GitHub Actions / lint

'NORMAN' is defined but never used
import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';

Check warning on line 9 in shell/components/__tests__/PromptModal.test.ts

View workflow job for this annotation

GitHub Actions / lint

'STATES_ENUM' is defined but never used

describe('component: PromptModal', () => {
const store = createStore({
modules: {
'action-menu': {
namespaced: true,
state: {
showModal: true,
modalData: {
whatever: true,
closeOnClickOutside: true
}
},
},
},
getters: {
'type-map/importDialog': () => () => GenericPrompt,
'i18n/exists': () => jest.fn(),
'i18n/t': jest.fn()
},
// actions: { 'management/findAll': jest.fn().mockResolvedValue(snapShots), 'rancher/findAll': jest.fn().mockResolvedValue([]) }
});

it('should emit copied after click', async() => {
document.body.innerHTML = '<div id="modals"></div>';
const wrapper = mount(PromptModal,
{
attachTo: document.body,
data() {
return { opened: true };
},
global: { mocks: { $store: store } }
}
);

console.log(document.querySelector('#modals').innerHTML);

// await wrapper.find('code').trigger('click');

expect(wrapper.vm.opened).toBe(true);
});
});
18 changes: 14 additions & 4 deletions shell/composables/focusTrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,14 @@ export function useBasicSetupFocusTrap(focusElement: string | HTMLElement, opts:
});
}

export function useWatcherBasedSetupFocusTrapWithDestroyIncluded(watchVar:any, focusElement: string | HTMLElement, opts:any = DEFAULT_FOCUS_TRAP_OPTS) {
export function useWatcherBasedSetupFocusTrapWithDestroyIncluded(watchVar:any, focusElement: string | HTMLElement, opts:any = DEFAULT_FOCUS_TRAP_OPTS, useUnmountHook = false) {
let focusTrapInstance: FocusTrap;
let focusEl;

console.error('RUNNING WATCHER FOCUS TRAP');
watch(watchVar, (neu) => {
if (neu) {
console.error('inside watcher');
if (neu && !focusTrapInstance) {
nextTick(() => {
focusEl = typeof focusElement === 'string' ? document.querySelector(focusElement) as HTMLElement : focusElement;

Expand All @@ -61,8 +63,16 @@ export function useWatcherBasedSetupFocusTrapWithDestroyIncluded(watchVar:any, f
focusTrapInstance.activate();
});
});
} else if (!neu && Object.keys(focusTrapInstance).length) {
} else if (!neu && Object.keys(focusTrapInstance).length && !useUnmountHook) {
focusTrapInstance.deactivate();
}
});
}, { immediate: true });

if (useUnmountHook) {
onBeforeUnmount(() => {
if (Object.keys(focusTrapInstance).length) {
focusTrapInstance.deactivate();
}
});
}
}
1 change: 1 addition & 0 deletions shell/dialog/GenericPrompt.vue
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export default {
:label="err"
/>
<div class="buttons">
ALEX
<button
class="btn role-secondary mr-10"
@click="close"
Expand Down
Loading