1
1
mirror of https://github.com/Fabio286/antares.git synced 2025-06-05 21:59:22 +02:00

fix: focus goes outside modals with tab key navigation

This commit is contained in:
2022-06-26 15:07:37 +02:00
parent 0a3a4827dd
commit e42c424a13
14 changed files with 140 additions and 23 deletions

View File

@ -7,13 +7,7 @@ export interface TableParams {
export interface ExportOptions { export interface ExportOptions {
schema: string; schema: string;
includes: { includes: {[key: string]: boolean};
functions: boolean;
views: boolean;
triggers: boolean;
routines: boolean;
schedulers: boolean;
};
outputFormat: 'sql' | 'sql.zip'; outputFormat: 'sql' | 'sql.zip';
outputFile: string; outputFile: string;
sqlInsertAfter: number; sqlInsertAfter: number;

View File

@ -3,7 +3,7 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active" :class="modalSizeClass"> <div class="modal active" :class="modalSizeClass">
<a class="modal-overlay" @click="hideModal" /> <a class="modal-overlay" @click="hideModal" />
<div class="modal-container"> <div ref="trapRef" class="modal-container">
<div v-if="hasHeader" class="modal-header pl-2"> <div v-if="hasHeader" class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<slot name="header" /> <slot name="header" />
@ -47,6 +47,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useFocusTrap } from '@/composables/useFocusTrap';
import { computed, onBeforeUnmount, PropType, useSlots } from 'vue'; import { computed, onBeforeUnmount, PropType, useSlots } from 'vue';
const props = defineProps({ const props = defineProps({
@ -65,6 +66,8 @@ const props = defineProps({
const emit = defineEmits(['confirm', 'hide']); const emit = defineEmits(['confirm', 'hide']);
const slots = useSlots(); const slots = useSlots();
const { trapRef } = useFocusTrap();
const hasHeader = computed(() => !!slots.header); const hasHeader = computed(() => !!slots.header);
const hasBody = computed(() => !!slots.body); const hasBody = computed(() => !!slots.body);
const hasDefault = computed(() => !!slots.default); const hasDefault = computed(() => !!slots.default);

View File

@ -377,6 +377,7 @@ export default defineComponent({
// fix position when the component is created and opened at the same time // fix position when the component is created and opened at the same time
if (isOpen.value) { if (isOpen.value) {
setTimeout(() => { setTimeout(() => {
deactivate();
adjustListPosition(); adjustListPosition();
}, 50); }, 50);
} }

View File

@ -2,7 +2,7 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active modal-sm"> <div class="modal active modal-sm">
<a class="modal-overlay" /> <a class="modal-overlay" />
<div class="modal-container p-0"> <div ref="trapRef" class="modal-container p-0">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
@ -57,6 +57,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { Ref, ref } from 'vue'; import { Ref, ref } from 'vue';
import { useFocusTrap } from '@/composables/useFocusTrap';
const { trapRef } = useFocusTrap();
const credentials = ref({ const credentials = ref({
user: '', user: '',

View File

@ -2,7 +2,7 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active"> <div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" /> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0"> <div ref="trapRef" class="modal-container p-0">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
@ -67,6 +67,7 @@ import { computed, onBeforeUnmount, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { useFocusTrap } from '@/composables/useFocusTrap';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
@ -83,6 +84,8 @@ const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, getDatabaseVariable } = workspacesStore; const { getWorkspace, getDatabaseVariable } = workspacesStore;
const { trapRef } = useFocusTrap();
const firstInput: Ref<HTMLInputElement> = ref(null); const firstInput: Ref<HTMLInputElement> = ref(null);
const database = ref({ const database = ref({
name: '', name: '',

View File

@ -2,7 +2,7 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active"> <div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" /> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0"> <div ref="trapRef" class="modal-container p-0">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
@ -270,9 +270,10 @@ import { ipcRenderer } from 'electron';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { SchemaInfos } from 'common/interfaces/antares'; import { SchemaInfos } from 'common/interfaces/antares';
import { ExportState, TableParams } from 'common/interfaces/exporter'; import { ExportOptions, ExportState, TableParams } from 'common/interfaces/exporter';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { useFocusTrap } from '@/composables/useFocusTrap';
import Application from '@/ipc-api/Application'; import Application from '@/ipc-api/Application';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { Customizations } from 'common/interfaces/customizations'; import { Customizations } from 'common/interfaces/customizations';
@ -290,6 +291,8 @@ const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { trapRef } = useFocusTrap();
const { const {
getWorkspace, getWorkspace,
refreshSchema refreshSchema
@ -300,11 +303,13 @@ const isRefreshing = ref(false);
const progressPercentage = ref(0); const progressPercentage = ref(0);
const progressStatus = ref(''); const progressStatus = ref('');
const tables: Ref<TableParams[]> = ref([]); const tables: Ref<TableParams[]> = ref([]);
const options = ref({ const options: Ref<ExportOptions> = ref({
schema: props.selectedSchema,
includes: {} as {[key: string]: boolean}, includes: {} as {[key: string]: boolean},
outputFormat: 'sql', outputFile: '',
outputFormat: 'sql' as 'sql' | 'sql.zip',
sqlInsertAfter: 250, sqlInsertAfter: 250,
sqlInsertDivider: 'bytes' sqlInsertDivider: 'bytes' as 'bytes' | 'rows'
}); });
const basePath = ref(''); const basePath = ref('');

View File

@ -2,7 +2,7 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active"> <div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" /> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0"> <div ref="trapRef" class="modal-container p-0">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
@ -105,6 +105,7 @@ import { storeToRefs } from 'pinia';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { useFocusTrap } from '@/composables/useFocusTrap';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import FakerSelect from '@/components/FakerSelect.vue'; import FakerSelect from '@/components/FakerSelect.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
@ -124,6 +125,8 @@ const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace } = workspacesStore; const { getWorkspace } = workspacesStore;
const { trapRef } = useFocusTrap();
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const localRow: Ref<{[key: string]: any}> = ref({}); const localRow: Ref<{[key: string]: any}> = ref({});
const fieldsToExclude = ref([]); const fieldsToExclude = ref([]);

View File

@ -2,7 +2,7 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active"> <div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" /> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0 pb-4"> <div ref="trapRef" class="modal-container p-0 pb-4">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
@ -104,6 +104,7 @@ import * as moment from 'moment';
import { ConnectionParams } from 'common/interfaces/antares'; import { ConnectionParams } from 'common/interfaces/antares';
import { HistoryRecord, useHistoryStore } from '@/stores/history'; import { HistoryRecord, useHistoryStore } from '@/stores/history';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useFocusTrap } from '@/composables/useFocusTrap';
import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue'; import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
const { t } = useI18n(); const { t } = useI18n();
@ -111,6 +112,8 @@ const { t } = useI18n();
const { getHistoryByWorkspace, deleteQueryFromHistory } = useHistoryStore(); const { getHistoryByWorkspace, deleteQueryFromHistory } = useHistoryStore();
const { getConnectionName } = useConnectionsStore(); const { getConnectionName } = useConnectionsStore();
const { trapRef } = useFocusTrap();
const props = defineProps({ const props = defineProps({
connection: Object as Prop<ConnectionParams> connection: Object as Prop<ConnectionParams>
}); });

View File

@ -2,7 +2,7 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active"> <div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" /> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0"> <div ref="trapRef" class="modal-container p-0">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
@ -70,6 +70,7 @@ import { computed, onBeforeUnmount, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { useFocusTrap } from '@/composables/useFocusTrap';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
@ -80,6 +81,8 @@ const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, getDatabaseVariable } = workspacesStore; const { getWorkspace, getDatabaseVariable } = workspacesStore;
const { trapRef } = useFocusTrap();
const emit = defineEmits(['reload', 'close']); const emit = defineEmits(['reload', 'close']);
const firstInput: Ref<HTMLInputElement> = ref(null); const firstInput: Ref<HTMLInputElement> = ref(null);

View File

@ -12,7 +12,7 @@
@close-context="closeContext" @close-context="closeContext"
/> />
<a class="modal-overlay" @click.stop="closeModal" /> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0 pb-4"> <div ref="trapRef" class="modal-container p-0 pb-4">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
@ -138,6 +138,7 @@ import { Component, computed, onBeforeUnmount, onMounted, onUpdated, Prop, Ref,
import { ConnectionParams } from 'common/interfaces/antares'; import { ConnectionParams } from 'common/interfaces/antares';
import { arrayToFile } from '../libs/arrayToFile'; import { arrayToFile } from '../libs/arrayToFile';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useFocusTrap } from '@/composables/useFocusTrap';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue'; import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
@ -147,6 +148,8 @@ import ModalProcessesListContext from '@/components/ModalProcessesListContext.vu
const { addNotification } = useNotificationsStore(); const { addNotification } = useNotificationsStore();
const { getConnectionName } = useConnectionsStore(); const { getConnectionName } = useConnectionsStore();
const { trapRef } = useFocusTrap();
const props = defineProps({ const props = defineProps({
connection: Object as Prop<ConnectionParams> connection: Object as Prop<ConnectionParams>
}); });

View File

@ -2,7 +2,7 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div id="settings" class="modal active"> <div id="settings" class="modal active">
<a class="modal-overlay c-hand" @click="closeModal" /> <a class="modal-overlay c-hand" @click="closeModal" />
<div class="modal-container"> <div ref="trapRef" class="modal-container">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
@ -309,6 +309,7 @@ import { useI18n } from 'vue-i18n';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { useFocusTrap } from '@/composables/useFocusTrap';
import { localesNames } from '@/i18n/supported-locales'; import { localesNames } from '@/i18n/supported-locales';
import ModalSettingsUpdate from '@/components/ModalSettingsUpdate.vue'; import ModalSettingsUpdate from '@/components/ModalSettingsUpdate.vue';
import ModalSettingsChangelog from '@/components/ModalSettingsChangelog.vue'; import ModalSettingsChangelog from '@/components/ModalSettingsChangelog.vue';
@ -322,6 +323,8 @@ const applicationStore = useApplicationStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { trapRef } = useFocusTrap({ disableAutofocus: true });
const { const {
selectedSettingTab, selectedSettingTab,
updateStatus updateStatus

View File

@ -5,6 +5,7 @@
tabindex="0" tabindex="0"
:style="{'height': resultsSize+'px'}" :style="{'height': resultsSize+'px'}"
@blur="deselectRows" @blur="deselectRows"
@focus="hasFocus = true"
@keyup.delete="showDeleteConfirmModal" @keyup.delete="showDeleteConfirmModal"
@keydown.esc="deselectRows" @keydown.esc="deselectRows"
> >
@ -152,6 +153,7 @@ const resultsSize = ref(0);
const localResults: Ref<QueryResult<any>[]> = ref([]); const localResults: Ref<QueryResult<any>[]> = ref([]);
const isContext = ref(false); const isContext = ref(false);
const isDeleteConfirmModal = ref(false); const isDeleteConfirmModal = ref(false);
const hasFocus = ref(false);
const contextEvent = ref(null); const contextEvent = ref(null);
const selectedCell = ref(null); const selectedCell = ref(null);
const selectedRows = ref([]); const selectedRows = ref([]);
@ -455,6 +457,7 @@ const deselectRows = () => {
if (!isEditingRow.value) { if (!isEditingRow.value) {
selectedRows.value = []; selectedRows.value = [];
selectedField.value = null; selectedField.value = null;
hasFocus.value = false;
} }
}; };
@ -519,6 +522,9 @@ const onKey = async (e: KeyboardEvent) => {
if (!props.isSelected) if (!props.isSelected)
return; return;
if (!hasFocus.value)
return;
if (isEditingRow.value) if (isEditingRow.value)
return; return;
@ -626,7 +632,13 @@ const scrollToCell = (el: HTMLElement) => {
scrollElement.value.scrollLeft = el.offsetLeft - scrollElement.value.clientWidth + el.clientWidth; scrollElement.value.scrollLeft = el.offsetLeft - scrollElement.value.clientWidth + el.clientWidth;
}; };
defineExpose({ applyUpdate, refreshScroller, resetSort, resizeResults, downloadTable }); defineExpose({
applyUpdate,
refreshScroller,
resetSort,
resizeResults,
downloadTable
});
watch(() => props.results, () => { watch(() => props.results, () => {
setLocalResults(); setLocalResults();

View File

@ -0,0 +1,81 @@
import { customRef, ref } from 'vue';
const focusableElementsSelector =
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
const useFocusTrap = (args?: {disableAutofocus?: boolean}) => {
let localArgs = {
disableAutofocus: false
};
if (args) {
localArgs = {
...localArgs,
...args
};
}
let focusableElements: NodeListOf<HTMLInputElement>;
let $firstFocusable: HTMLElement;
let $lastFocusable: HTMLElement;
const isInitiated = ref(false);
const trapRef = customRef((track, trigger) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let $trapEl: any = null;
return {
get () {
track();
return $trapEl;
},
set (value) {
$trapEl = value;
value ? initFocusTrap() : clearFocusTrap();
trigger();
}
};
});
function keyHandler (e: KeyboardEvent) {
const isTabPressed = e.key === 'Tab';
if (!isTabPressed) return;
if (e.shiftKey) {
if (document.activeElement === $firstFocusable) {
$lastFocusable.focus();
e.preventDefault();
}
}
else {
if (document.activeElement === $lastFocusable) {
$firstFocusable.focus();
e.preventDefault();
}
}
}
function initFocusTrap () {
if (!trapRef.value || isInitiated.value) return;
focusableElements = (trapRef.value as HTMLElement).querySelectorAll(
focusableElementsSelector
);
$firstFocusable = focusableElements[0];
$lastFocusable = focusableElements[focusableElements.length - 1];
document.addEventListener('keydown', keyHandler);
isInitiated.value = true;
if (!localArgs.disableAutofocus) $firstFocusable.focus();
}
function clearFocusTrap () {
document.removeEventListener('keydown', keyHandler);
}
return {
trapRef,
initFocusTrap,
clearFocusTrap
};
};
export { useFocusTrap };

View File

@ -1,6 +1,6 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { unproxify } from '../libs/unproxify'; import { unproxify } from '../libs/unproxify';
import { IpcResponse/*, EventInfos, QueryResult, RoutineInfos, TableInfos, TriggerInfos */ } from 'common/interfaces/antares'; import { ClientCode, IpcResponse/*, EventInfos, QueryResult, RoutineInfos, TableInfos, TriggerInfos */ } from 'common/interfaces/antares';
import { ExportOptions } from 'common/interfaces/exporter'; import { ExportOptions } from 'common/interfaces/exporter';
import { ImportOptions } from 'common/interfaces/importer'; import { ImportOptions } from 'common/interfaces/importer';
@ -110,7 +110,7 @@ export default class {
return ipcRenderer.invoke('raw-query', unproxify(params)); return ipcRenderer.invoke('raw-query', unproxify(params));
} }
static export (params: { uid: string; type: string; tables: string; options: ExportOptions }): Promise<IpcResponse> { static export (params: ExportOptions & {uid: string; type: ClientCode}): Promise<IpcResponse> {
return ipcRenderer.invoke('export', unproxify(params)); return ipcRenderer.invoke('export', unproxify(params));
} }