@autoafleveren/ui 1.3.3 → 1.4.2
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/types/components/AppActionBar/AppActionBarItem.vue.d.ts +4 -0
- package/dist/types/composables/index.d.ts +1 -0
- package/dist/types/composables/useFocusTrap/index.d.ts +10 -0
- package/dist/ui-storybook.css +1 -1
- package/dist/ui.cjs +47 -40
- package/dist/ui.js +13974 -13286
- package/package.json +3 -1
- package/src/modules/components/AppActionBar/AppActionBarItem.vue +40 -12
- package/src/modules/components/AppActionBar/__tests__/app-action-bar-item.spec.ts +2 -2
- package/src/modules/components/AppActionBar/index.d.ts +1 -0
- package/src/modules/components/AppActionBar/index.ts +1 -1
- package/src/modules/components/AppContextMenu/AppContextMenu.vue +18 -9
- package/src/modules/components/AppContextMenu/ShortcutItem.vue +2 -1
- package/src/modules/components/AppDefinitionList/AppDefinitionList.vue +1 -0
- package/src/modules/composables/index.ts +1 -0
- package/src/modules/composables/useFocusTrap/index.d.ts +3 -0
- package/src/modules/composables/useFocusTrap/index.ts +98 -0
- package/src/modules/plugins/Sentry/index.ts +1 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autoafleveren/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/*",
|
|
@@ -86,7 +86,9 @@
|
|
|
86
86
|
"@vuepic/vue-datepicker": "^11.0.2",
|
|
87
87
|
"@vueuse/components": "^13.5.0",
|
|
88
88
|
"@vueuse/core": "^13.5.0",
|
|
89
|
+
"@vueuse/integrations": "^13.7.0",
|
|
89
90
|
"date-fns": "^4.1.0",
|
|
91
|
+
"focus-trap": "^7",
|
|
90
92
|
"js-cookie": "^3.0.5",
|
|
91
93
|
"laravel-echo": "^2.1.7",
|
|
92
94
|
"mini-svg-data-uri": "^1.4.4",
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import { computed, onMounted, ref, toValue, useTemplateRef } from 'vue';
|
|
3
|
-
import { onClickOutside } from '@vueuse/core';
|
|
2
|
+
import { computed, nextTick, onMounted, ref, toValue, useTemplateRef } from 'vue';
|
|
4
3
|
import { WarningIcon } from '~icons';
|
|
5
4
|
import { AppLoader } from '~components';
|
|
6
|
-
import { useActionBar, useContextMenu } from '~composables';
|
|
5
|
+
import { useActionBar, useContextMenu, useFocusTrap } from '~composables';
|
|
7
6
|
import AppActionBarSubMenu from './AppActionBarSubMenu.vue';
|
|
8
7
|
import { domClassesPerMenu, domClassesPerType } from '.';
|
|
9
8
|
|
|
@@ -16,11 +15,16 @@
|
|
|
16
15
|
confirmed: false,
|
|
17
16
|
});
|
|
18
17
|
|
|
19
|
-
const emit = defineEmits<{
|
|
18
|
+
const emit = defineEmits<{
|
|
19
|
+
(event: 'close'): void;
|
|
20
|
+
(event: 'confirm'): void;
|
|
21
|
+
(event: 'confirmed'): void;
|
|
22
|
+
}>();
|
|
20
23
|
|
|
21
24
|
const actionBar = useActionBar();
|
|
22
25
|
const contextMenu = useContextMenu();
|
|
23
|
-
const
|
|
26
|
+
const submenuElement = useTemplateRef<HTMLDivElement>('submenuElement');
|
|
27
|
+
const { activate, deactivate, onEscape, onBack } = useFocusTrap(submenuElement);
|
|
24
28
|
const confirm = ref(false);
|
|
25
29
|
const loading = ref(false);
|
|
26
30
|
const modelValueSubMenu = ref<number[]>([]);
|
|
@@ -61,6 +65,19 @@
|
|
|
61
65
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
62
66
|
if ((props.action.confirm || props.action.component) && !confirm.value) {
|
|
63
67
|
confirm.value = true;
|
|
68
|
+
|
|
69
|
+
if (props.action.component) {
|
|
70
|
+
await nextTick();
|
|
71
|
+
|
|
72
|
+
emit('confirm');
|
|
73
|
+
|
|
74
|
+
await nextTick();
|
|
75
|
+
|
|
76
|
+
if (props.action.focusTrap !== false) {
|
|
77
|
+
setTimeout(activate, 300);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
64
81
|
return;
|
|
65
82
|
}
|
|
66
83
|
|
|
@@ -81,27 +98,37 @@
|
|
|
81
98
|
}
|
|
82
99
|
|
|
83
100
|
function subMenuClose(): void {
|
|
101
|
+
deactivate();
|
|
102
|
+
|
|
84
103
|
confirm.value = false;
|
|
85
104
|
|
|
105
|
+
emit('confirmed');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function close(): void {
|
|
109
|
+
subMenuClose();
|
|
110
|
+
|
|
86
111
|
emit('close');
|
|
87
112
|
}
|
|
88
113
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
});
|
|
114
|
+
onEscape(close);
|
|
115
|
+
onBack(subMenuClose);
|
|
92
116
|
</script>
|
|
93
117
|
|
|
94
118
|
<template>
|
|
95
119
|
<div
|
|
96
120
|
v-if="itemHidden !== true"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
121
|
+
:class="[
|
|
122
|
+
...domClassesPerType[type],
|
|
123
|
+
...domClassesPerMenu[props.context ? 'contextMenu' : 'actionBar'],
|
|
124
|
+
props.action.component ? 'has-submenu' : '',
|
|
125
|
+
]"
|
|
126
|
+
class="relative cursor-pointer rounded-md transition-colors duration-200"
|
|
100
127
|
>
|
|
101
128
|
<div
|
|
102
129
|
v-tippy="context ? undefined : action.name"
|
|
103
130
|
:class="{ 'px-2.5 py-4': !context }"
|
|
104
|
-
class="flex size-full items-center space-x-2 whitespace-nowrap"
|
|
131
|
+
class="app-action-bar-item flex size-full items-center space-x-2 whitespace-nowrap"
|
|
105
132
|
data-test-action-bar-icon
|
|
106
133
|
@click="onClickAction"
|
|
107
134
|
>
|
|
@@ -115,6 +142,7 @@
|
|
|
115
142
|
|
|
116
143
|
<AppActionBarSubMenu
|
|
117
144
|
v-if="props.action.component && confirm"
|
|
145
|
+
ref="submenuElement"
|
|
118
146
|
v-model="modelValueSubMenu"
|
|
119
147
|
v-bind="props"
|
|
120
148
|
@close="subMenuClose"
|
|
@@ -125,10 +125,10 @@ describe('the AppActionBarItem component', () => {
|
|
|
125
125
|
|
|
126
126
|
expect(subMenu.exists()).toBe(true);
|
|
127
127
|
|
|
128
|
-
expect(wrapper.emitted('
|
|
128
|
+
expect(wrapper.emitted('confirmed')).toBeUndefined();
|
|
129
129
|
|
|
130
130
|
subMenu.vm.$emit('close');
|
|
131
131
|
|
|
132
|
-
expect(wrapper.emitted('
|
|
132
|
+
expect(wrapper.emitted('confirmed')).toBeDefined();
|
|
133
133
|
});
|
|
134
134
|
});
|
|
@@ -8,7 +8,7 @@ export const domClassesPerType: Record<Type, string[]> = {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
export const domClassesPerMenu: Record<'contextMenu' | 'actionBar', string[]> = {
|
|
11
|
-
contextMenu: ['
|
|
11
|
+
contextMenu: ['focus:bg-zinc-600', 'px-2', 'py-1'],
|
|
12
12
|
actionBar: ['hover:bg-black-100/20'],
|
|
13
13
|
};
|
|
14
14
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import { computed, ref } from 'vue';
|
|
3
|
-
import { onClickOutside
|
|
4
|
-
import { useActionBar, useClientPoint, useContextMenu } from '~composables';
|
|
2
|
+
import { computed, nextTick, onUnmounted, ref } from 'vue';
|
|
3
|
+
import { onClickOutside } from '@vueuse/core';
|
|
4
|
+
import { useActionBar, useClientPoint, useContextMenu, useFocusTrap } from '~composables';
|
|
5
5
|
import AppActionBarItem from '~components/AppActionBar/AppActionBarItem.vue';
|
|
6
6
|
import ShortcutItem from './ShortcutItem.vue';
|
|
7
7
|
|
|
@@ -22,9 +22,12 @@
|
|
|
22
22
|
const contextMenu = useContextMenu();
|
|
23
23
|
const contextMenuElement = ref<HTMLDivElement>();
|
|
24
24
|
const clientComputedPosition = useClientPoint(contextMenuElement);
|
|
25
|
+
const { activate, deactivate, onEscape } = useFocusTrap(contextMenuElement);
|
|
25
26
|
|
|
26
27
|
const actionsWithFallback = computed((): Action[] => props.actions ?? actionbar.actions.value);
|
|
27
28
|
|
|
29
|
+
onUnmounted(deactivate);
|
|
30
|
+
|
|
28
31
|
async function open(): Promise<void> {
|
|
29
32
|
isOpen.value = true;
|
|
30
33
|
|
|
@@ -33,11 +36,17 @@
|
|
|
33
36
|
document.querySelector('#app')?.classList.add('context-menu-open');
|
|
34
37
|
|
|
35
38
|
clientComputedPosition.setPosition(props.event.x, props.event.y);
|
|
39
|
+
|
|
40
|
+
await nextTick();
|
|
41
|
+
|
|
42
|
+
activate();
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
async function close(): Promise<void> {
|
|
39
46
|
isOpen.value = false;
|
|
40
47
|
|
|
48
|
+
deactivate();
|
|
49
|
+
|
|
41
50
|
emit('close');
|
|
42
51
|
|
|
43
52
|
document.querySelector('#app')?.classList.remove('context-menu-open');
|
|
@@ -55,11 +64,7 @@
|
|
|
55
64
|
|
|
56
65
|
defineExpose({ isOpen, open, close, submit });
|
|
57
66
|
onClickOutside(contextMenuElement, close);
|
|
58
|
-
|
|
59
|
-
event.preventDefault();
|
|
60
|
-
|
|
61
|
-
close();
|
|
62
|
-
});
|
|
67
|
+
onEscape(close);
|
|
63
68
|
</script>
|
|
64
69
|
|
|
65
70
|
<template>
|
|
@@ -91,8 +96,12 @@
|
|
|
91
96
|
:context="true"
|
|
92
97
|
:item="item"
|
|
93
98
|
:confirmed="confirmed && confirmed === action.key"
|
|
94
|
-
|
|
99
|
+
:tabindex="`100${index}`"
|
|
100
|
+
class="flex h-10 items-center space-x-2 first:rounded-t-lg last:rounded-b-lg focus:outline-none"
|
|
101
|
+
@confirm="deactivate"
|
|
102
|
+
@confirmed="activate"
|
|
95
103
|
@close="close"
|
|
104
|
+
@mouseover="($event) => ($event.target as HTMLDivElement | undefined)?.focus()"
|
|
96
105
|
>
|
|
97
106
|
<span class="text-base">{{ action.name }}</span>
|
|
98
107
|
</AppActionBarItem>
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
<template>
|
|
30
30
|
<button
|
|
31
31
|
type="button"
|
|
32
|
-
|
|
32
|
+
tabindex="-1"
|
|
33
|
+
class="w-full p-1.5 text-xxs text-white cursor-pointer hover:bg-zinc-600 focus:outline-0"
|
|
33
34
|
@click="click"
|
|
34
35
|
>
|
|
35
36
|
{{ shortcut.copied ? name : shortcut.name }}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { ref } from 'vue';
|
|
2
|
+
import { onKeyStroke, useActiveElement } from '@vueuse/core';
|
|
3
|
+
import { useFocusTrap as useFocusTrapVueUse } from '@vueuse/integrations/useFocusTrap';
|
|
4
|
+
|
|
5
|
+
import type { MaybeRefOrGetter } from 'vue';
|
|
6
|
+
import type { Arrayable, MaybeComputedElementRef } from '@vueuse/core';
|
|
7
|
+
import type { Options } from './index.d';
|
|
8
|
+
|
|
9
|
+
export function useFocusTrap(
|
|
10
|
+
target: MaybeRefOrGetter<Arrayable<MaybeRefOrGetter<string> | MaybeComputedElementRef>>,
|
|
11
|
+
options: Options = {},
|
|
12
|
+
) {
|
|
13
|
+
const isActive = ref<boolean>(false);
|
|
14
|
+
const { activate: activateTrap, deactivate: deactivateTrap } = useFocusTrapVueUse(target);
|
|
15
|
+
const activeElement = useActiveElement();
|
|
16
|
+
const onEscapeCallback = ref<() => void>();
|
|
17
|
+
const onBackCallback = ref<() => void>();
|
|
18
|
+
|
|
19
|
+
function onEscape(callback: () => void): void {
|
|
20
|
+
onEscapeCallback.value = callback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function onBack(callback: () => void): void {
|
|
24
|
+
onBackCallback.value = callback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function activate(): void {
|
|
28
|
+
isActive.value = true;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
activateTrap();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.warn(error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function deactivate(): void {
|
|
38
|
+
isActive.value = false;
|
|
39
|
+
|
|
40
|
+
deactivateTrap();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
onKeyStroke(['Escape'], (event: KeyboardEvent) => {
|
|
44
|
+
if (!isActive.value) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
|
|
50
|
+
onEscapeCallback.value?.();
|
|
51
|
+
});
|
|
52
|
+
onKeyStroke(['Enter', 'ArrowRight'], (event: KeyboardEvent) => {
|
|
53
|
+
if (!isActive.value) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
|
|
59
|
+
if (event.key === 'Enter' || (event.key === 'ArrowRight' && activeElement.value?.classList.contains('has-submenu'))) {
|
|
60
|
+
if (options.childTargetClass) {
|
|
61
|
+
(activeElement.value?.querySelector(options?.childTargetClass ?? '.app-action-bar-item') as HTMLDivElement | undefined)?.click();
|
|
62
|
+
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
(activeElement.value?.firstElementChild as HTMLDivElement | undefined)?.click();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
onKeyStroke(['ArrowLeft'], () => {
|
|
70
|
+
if (!isActive.value) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
onBackCallback.value?.();
|
|
75
|
+
});
|
|
76
|
+
onKeyStroke(['ArrowDown', 'ArrowUp'], (event: KeyboardEvent) => {
|
|
77
|
+
if (!isActive.value) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
|
|
83
|
+
if (event.key === 'ArrowUp') {
|
|
84
|
+
(activeElement.value?.previousElementSibling as HTMLDivElement | undefined)?.focus();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
(activeElement.value?.nextElementSibling as HTMLDivElement | undefined)?.focus();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
isActive,
|
|
93
|
+
activate,
|
|
94
|
+
deactivate,
|
|
95
|
+
onEscape,
|
|
96
|
+
onBack,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as Sentry from '@sentry/vue';
|
|
2
|
-
import nlTranslation from './language/nl';
|
|
3
2
|
|
|
4
3
|
import type { App } from 'vue';
|
|
5
4
|
import type { Router } from 'vue-router';
|
|
@@ -22,23 +21,7 @@ export default function registerSentry(app: App, router: Router, options: Option
|
|
|
22
21
|
},
|
|
23
22
|
|
|
24
23
|
beforeSend(beforeEvent: ErrorEvent, hint: EventHint): ErrorEvent {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// Check if it is an exception, and if so, show the report dialog
|
|
28
|
-
if (event.exception && options.reportDialog !== false) {
|
|
29
|
-
Sentry.showReportDialog({
|
|
30
|
-
eventId: event.event_id,
|
|
31
|
-
...nlTranslation,
|
|
32
|
-
user: event?.user,
|
|
33
|
-
onLoad: () => {
|
|
34
|
-
document.querySelector('.sentry-error-embed-wrapper #id_name')?.setAttribute('placeholder', '');
|
|
35
|
-
document.querySelector('.sentry-error-embed-wrapper #id_email')?.setAttribute('placeholder', '');
|
|
36
|
-
document.querySelector('.sentry-error-embed-wrapper #id_comments')?.setAttribute('placeholder', '');
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return event;
|
|
24
|
+
return options?.beforeSend ? options.beforeSend(beforeEvent, hint) : beforeEvent;
|
|
42
25
|
},
|
|
43
26
|
|
|
44
27
|
// Alternatively, use `process.env.npm_package_version` for a dynamic release version
|