mirror of
https://github.com/Fabio286/antares.git
synced 2025-02-17 04:00:48 +01:00
fix: focus goes outside modals with tab key navigation
This commit is contained in:
parent
0a3a4827dd
commit
e42c424a13
@ -7,13 +7,7 @@ export interface TableParams {
|
||||
|
||||
export interface ExportOptions {
|
||||
schema: string;
|
||||
includes: {
|
||||
functions: boolean;
|
||||
views: boolean;
|
||||
triggers: boolean;
|
||||
routines: boolean;
|
||||
schedulers: boolean;
|
||||
};
|
||||
includes: {[key: string]: boolean};
|
||||
outputFormat: 'sql' | 'sql.zip';
|
||||
outputFile: string;
|
||||
sqlInsertAfter: number;
|
||||
|
@ -3,7 +3,7 @@
|
||||
<Teleport to="#window-content">
|
||||
<div class="modal active" :class="modalSizeClass">
|
||||
<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 class="modal-title h6">
|
||||
<slot name="header" />
|
||||
@ -47,6 +47,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFocusTrap } from '@/composables/useFocusTrap';
|
||||
import { computed, onBeforeUnmount, PropType, useSlots } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
@ -65,6 +66,8 @@ const props = defineProps({
|
||||
const emit = defineEmits(['confirm', 'hide']);
|
||||
const slots = useSlots();
|
||||
|
||||
const { trapRef } = useFocusTrap();
|
||||
|
||||
const hasHeader = computed(() => !!slots.header);
|
||||
const hasBody = computed(() => !!slots.body);
|
||||
const hasDefault = computed(() => !!slots.default);
|
||||
|
@ -377,6 +377,7 @@ export default defineComponent({
|
||||
// fix position when the component is created and opened at the same time
|
||||
if (isOpen.value) {
|
||||
setTimeout(() => {
|
||||
deactivate();
|
||||
adjustListPosition();
|
||||
}, 50);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Teleport to="#window-content">
|
||||
<div class="modal active modal-sm">
|
||||
<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-title h6">
|
||||
<div class="d-flex">
|
||||
@ -57,6 +57,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Ref, ref } from 'vue';
|
||||
import { useFocusTrap } from '@/composables/useFocusTrap';
|
||||
|
||||
const { trapRef } = useFocusTrap();
|
||||
|
||||
const credentials = ref({
|
||||
user: '',
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Teleport to="#window-content">
|
||||
<div class="modal active">
|
||||
<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-title h6">
|
||||
<div class="d-flex">
|
||||
@ -67,6 +67,7 @@ import { computed, onBeforeUnmount, Ref, ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useNotificationsStore } from '@/stores/notifications';
|
||||
import { useWorkspacesStore } from '@/stores/workspaces';
|
||||
import { useFocusTrap } from '@/composables/useFocusTrap';
|
||||
import Schema from '@/ipc-api/Schema';
|
||||
import BaseSelect from '@/components/BaseSelect.vue';
|
||||
|
||||
@ -83,6 +84,8 @@ const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
|
||||
|
||||
const { getWorkspace, getDatabaseVariable } = workspacesStore;
|
||||
|
||||
const { trapRef } = useFocusTrap();
|
||||
|
||||
const firstInput: Ref<HTMLInputElement> = ref(null);
|
||||
const database = ref({
|
||||
name: '',
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Teleport to="#window-content">
|
||||
<div class="modal active">
|
||||
<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-title h6">
|
||||
<div class="d-flex">
|
||||
@ -270,9 +270,10 @@ import { ipcRenderer } from 'electron';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
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 { useWorkspacesStore } from '@/stores/workspaces';
|
||||
import { useFocusTrap } from '@/composables/useFocusTrap';
|
||||
import Application from '@/ipc-api/Application';
|
||||
import Schema from '@/ipc-api/Schema';
|
||||
import { Customizations } from 'common/interfaces/customizations';
|
||||
@ -290,6 +291,8 @@ const workspacesStore = useWorkspacesStore();
|
||||
|
||||
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
|
||||
|
||||
const { trapRef } = useFocusTrap();
|
||||
|
||||
const {
|
||||
getWorkspace,
|
||||
refreshSchema
|
||||
@ -300,11 +303,13 @@ const isRefreshing = ref(false);
|
||||
const progressPercentage = ref(0);
|
||||
const progressStatus = ref('');
|
||||
const tables: Ref<TableParams[]> = ref([]);
|
||||
const options = ref({
|
||||
const options: Ref<ExportOptions> = ref({
|
||||
schema: props.selectedSchema,
|
||||
includes: {} as {[key: string]: boolean},
|
||||
outputFormat: 'sql',
|
||||
outputFile: '',
|
||||
outputFormat: 'sql' as 'sql' | 'sql.zip',
|
||||
sqlInsertAfter: 250,
|
||||
sqlInsertDivider: 'bytes'
|
||||
sqlInsertDivider: 'bytes' as 'bytes' | 'rows'
|
||||
});
|
||||
const basePath = ref('');
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Teleport to="#window-content">
|
||||
<div class="modal active">
|
||||
<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-title h6">
|
||||
<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 { useNotificationsStore } from '@/stores/notifications';
|
||||
import { useWorkspacesStore } from '@/stores/workspaces';
|
||||
import { useFocusTrap } from '@/composables/useFocusTrap';
|
||||
import Tables from '@/ipc-api/Tables';
|
||||
import FakerSelect from '@/components/FakerSelect.vue';
|
||||
import BaseSelect from '@/components/BaseSelect.vue';
|
||||
@ -124,6 +125,8 @@ const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
|
||||
|
||||
const { getWorkspace } = workspacesStore;
|
||||
|
||||
const { trapRef } = useFocusTrap();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const localRow: Ref<{[key: string]: any}> = ref({});
|
||||
const fieldsToExclude = ref([]);
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Teleport to="#window-content">
|
||||
<div class="modal active">
|
||||
<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-title h6">
|
||||
<div class="d-flex">
|
||||
@ -104,6 +104,7 @@ import * as moment from 'moment';
|
||||
import { ConnectionParams } from 'common/interfaces/antares';
|
||||
import { HistoryRecord, useHistoryStore } from '@/stores/history';
|
||||
import { useConnectionsStore } from '@/stores/connections';
|
||||
import { useFocusTrap } from '@/composables/useFocusTrap';
|
||||
import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
@ -111,6 +112,8 @@ const { t } = useI18n();
|
||||
const { getHistoryByWorkspace, deleteQueryFromHistory } = useHistoryStore();
|
||||
const { getConnectionName } = useConnectionsStore();
|
||||
|
||||
const { trapRef } = useFocusTrap();
|
||||
|
||||
const props = defineProps({
|
||||
connection: Object as Prop<ConnectionParams>
|
||||
});
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Teleport to="#window-content">
|
||||
<div class="modal active">
|
||||
<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-title h6">
|
||||
<div class="d-flex">
|
||||
@ -70,6 +70,7 @@ import { computed, onBeforeUnmount, Ref, ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useNotificationsStore } from '@/stores/notifications';
|
||||
import { useWorkspacesStore } from '@/stores/workspaces';
|
||||
import { useFocusTrap } from '@/composables/useFocusTrap';
|
||||
import Schema from '@/ipc-api/Schema';
|
||||
import BaseSelect from '@/components/BaseSelect.vue';
|
||||
|
||||
@ -80,6 +81,8 @@ const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
|
||||
|
||||
const { getWorkspace, getDatabaseVariable } = workspacesStore;
|
||||
|
||||
const { trapRef } = useFocusTrap();
|
||||
|
||||
const emit = defineEmits(['reload', 'close']);
|
||||
|
||||
const firstInput: Ref<HTMLInputElement> = ref(null);
|
||||
|
@ -12,7 +12,7 @@
|
||||
@close-context="closeContext"
|
||||
/>
|
||||
<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-title h6">
|
||||
<div class="d-flex">
|
||||
@ -138,6 +138,7 @@ import { Component, computed, onBeforeUnmount, onMounted, onUpdated, Prop, Ref,
|
||||
import { ConnectionParams } from 'common/interfaces/antares';
|
||||
import { arrayToFile } from '../libs/arrayToFile';
|
||||
import { useNotificationsStore } from '@/stores/notifications';
|
||||
import { useFocusTrap } from '@/composables/useFocusTrap';
|
||||
import Schema from '@/ipc-api/Schema';
|
||||
import { useConnectionsStore } from '@/stores/connections';
|
||||
import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
|
||||
@ -147,6 +148,8 @@ import ModalProcessesListContext from '@/components/ModalProcessesListContext.vu
|
||||
const { addNotification } = useNotificationsStore();
|
||||
const { getConnectionName } = useConnectionsStore();
|
||||
|
||||
const { trapRef } = useFocusTrap();
|
||||
|
||||
const props = defineProps({
|
||||
connection: Object as Prop<ConnectionParams>
|
||||
});
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Teleport to="#window-content">
|
||||
<div id="settings" class="modal active">
|
||||
<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-title h6">
|
||||
<div class="d-flex">
|
||||
@ -309,6 +309,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import { useApplicationStore } from '@/stores/application';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { useWorkspacesStore } from '@/stores/workspaces';
|
||||
import { useFocusTrap } from '@/composables/useFocusTrap';
|
||||
import { localesNames } from '@/i18n/supported-locales';
|
||||
import ModalSettingsUpdate from '@/components/ModalSettingsUpdate.vue';
|
||||
import ModalSettingsChangelog from '@/components/ModalSettingsChangelog.vue';
|
||||
@ -322,6 +323,8 @@ const applicationStore = useApplicationStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const workspacesStore = useWorkspacesStore();
|
||||
|
||||
const { trapRef } = useFocusTrap({ disableAutofocus: true });
|
||||
|
||||
const {
|
||||
selectedSettingTab,
|
||||
updateStatus
|
||||
|
@ -5,6 +5,7 @@
|
||||
tabindex="0"
|
||||
:style="{'height': resultsSize+'px'}"
|
||||
@blur="deselectRows"
|
||||
@focus="hasFocus = true"
|
||||
@keyup.delete="showDeleteConfirmModal"
|
||||
@keydown.esc="deselectRows"
|
||||
>
|
||||
@ -152,6 +153,7 @@ const resultsSize = ref(0);
|
||||
const localResults: Ref<QueryResult<any>[]> = ref([]);
|
||||
const isContext = ref(false);
|
||||
const isDeleteConfirmModal = ref(false);
|
||||
const hasFocus = ref(false);
|
||||
const contextEvent = ref(null);
|
||||
const selectedCell = ref(null);
|
||||
const selectedRows = ref([]);
|
||||
@ -455,6 +457,7 @@ const deselectRows = () => {
|
||||
if (!isEditingRow.value) {
|
||||
selectedRows.value = [];
|
||||
selectedField.value = null;
|
||||
hasFocus.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@ -519,6 +522,9 @@ const onKey = async (e: KeyboardEvent) => {
|
||||
if (!props.isSelected)
|
||||
return;
|
||||
|
||||
if (!hasFocus.value)
|
||||
return;
|
||||
|
||||
if (isEditingRow.value)
|
||||
return;
|
||||
|
||||
@ -626,7 +632,13 @@ const scrollToCell = (el: HTMLElement) => {
|
||||
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, () => {
|
||||
setLocalResults();
|
||||
|
81
src/renderer/composables/useFocusTrap.ts
Normal file
81
src/renderer/composables/useFocusTrap.ts
Normal 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 };
|
@ -1,6 +1,6 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
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 { ImportOptions } from 'common/interfaces/importer';
|
||||
|
||||
@ -110,7 +110,7 @@ export default class {
|
||||
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));
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user