@autoafleveren/ui 1.5.5 → 1.6.0
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/icons.cjs +1 -1
- package/dist/icons.css +1 -1
- package/dist/icons.js +112 -112
- package/dist/types/composables/useContextMenu/index.d.ts +4 -0
- package/dist/ui-storybook.css +1 -1
- package/dist/ui.cjs +43 -43
- package/dist/ui.css +1 -1
- package/dist/ui.js +3952 -3942
- package/package.json +1 -1
- package/src/modules/components/AppActionBar/AppActionBar.vue +1 -1
- package/src/modules/components/AppActionBar/AppActionBarItem.vue +1 -1
- package/src/modules/components/AppActionBar/AppActionBarSubMenu.vue +1 -1
- package/src/modules/components/AppContextMenu/AppContextMenu.vue +2 -2
- package/src/modules/components/AppContextMenu/index.d.ts +1 -0
- package/src/modules/components/AppDataTable/AppDataTable.vue +2 -1
- package/src/modules/composables/useContextMenu/__tests__/index.spec.ts +98 -0
- package/src/modules/composables/useContextMenu/index.ts +18 -1
- package/src/modules/composables/useFocusTrap/index.d.ts +2 -0
- package/src/modules/composables/useFocusTrap/index.ts +2 -2
package/package.json
CHANGED
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
<div
|
|
63
63
|
ref="actionBar"
|
|
64
64
|
:class="{ 'translate-y-0! max-md:mt-0!': selection.length > 0 && isOpen }"
|
|
65
|
-
class="fixed inset-x-0 top-0 z-100 flex h-14 md:-mt-1 -translate-y-24 items-center justify-between bg-secondary
|
|
65
|
+
class="app-actionbar fixed inset-x-0 top-0 z-100 flex h-14 md:-mt-1 -translate-y-24 items-center justify-between bg-secondary
|
|
66
66
|
px-4 text-white transition-[translate] ease-in-out max-md:transform-none! max-md:translate-none! max-md:-mt-10"
|
|
67
67
|
data-test-action-bar-wrapper
|
|
68
68
|
>
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
<div
|
|
53
53
|
ref="subMenu"
|
|
54
54
|
:class="{ '-top-full left-full': props.context }"
|
|
55
|
-
class="absolute w-fit rounded-lg bg-secondary p-2"
|
|
55
|
+
class="absolute w-fit rounded-lg bg-secondary p-2 [.app-actionbar:not(.translate-y-0\!)_&]:hidden"
|
|
56
56
|
>
|
|
57
57
|
<div
|
|
58
58
|
ref="submenuArrow"
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
searchActionsQuery.value = value;
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
const actionsWithFallback = computed((): Action[] => props.actions ?? actionbar.actions.value);
|
|
33
|
+
const actionsWithFallback = computed((): Action[] => props.actions ?? contextMenu.actions.value ?? actionbar.actions.value);
|
|
34
34
|
const normalizedSearchQuery = computed((): string => searchActionsQuery.value?.toLowerCase().trim() || '');
|
|
35
35
|
const isSearching = computed((): boolean => normalizedSearchQuery.value?.length > 0);
|
|
36
36
|
const filteredActions = computed((): Action[] => {
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
v-if="isOpen && (actionsWithFallback.length > 0 || contextMenu.shortcuts.value.length > 0)"
|
|
99
99
|
ref="contextMenuElement"
|
|
100
100
|
:style="`left: ${event.x}px; top: ${event.y}px;`"
|
|
101
|
-
class="app-context-menu fixed z-
|
|
101
|
+
class="app-context-menu fixed z-100 flex w-64 flex-col rounded-lg bg-secondary p-2 drop-shadow-card empty:hidden"
|
|
102
102
|
data-test-context-menu
|
|
103
103
|
>
|
|
104
104
|
<div v-if="enableSearch && actionsWithFallback.length > 0">
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { flushPromises } from '@vue/test-utils';
|
|
3
|
+
import { activeRowClass, useContextMenu } from '../index';
|
|
4
|
+
import ResizeObserverMock from '~/tests/mocks/resize-observer';
|
|
5
|
+
|
|
6
|
+
function createRowWithCell(): { row: HTMLTableRowElement; cell: HTMLTableCellElement } {
|
|
7
|
+
const table = document.createElement('table');
|
|
8
|
+
const tbody = document.createElement('tbody');
|
|
9
|
+
const row = document.createElement('tr');
|
|
10
|
+
const cell = document.createElement('td');
|
|
11
|
+
|
|
12
|
+
row.append(cell);
|
|
13
|
+
tbody.append(row);
|
|
14
|
+
table.append(tbody);
|
|
15
|
+
document.body.append(table);
|
|
16
|
+
|
|
17
|
+
return { row, cell };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('the useContextMenu composable', () => {
|
|
21
|
+
const contextMenu = useContextMenu();
|
|
22
|
+
|
|
23
|
+
beforeAll(() => {
|
|
24
|
+
global.ResizeObserver = ResizeObserverMock;
|
|
25
|
+
|
|
26
|
+
window.scrollTo = vi.fn();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
contextMenu.closeAll();
|
|
31
|
+
|
|
32
|
+
await new Promise(resolve => setTimeout(resolve, 600));
|
|
33
|
+
|
|
34
|
+
document.body.innerHTML = '';
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('adds the active row class to the closest table row when opened from a cell event', async () => {
|
|
38
|
+
expect.assertions(3);
|
|
39
|
+
|
|
40
|
+
const { row, cell } = createRowWithCell();
|
|
41
|
+
const event = new MouseEvent('contextmenu', { bubbles: true });
|
|
42
|
+
|
|
43
|
+
Object.defineProperty(event, 'target', { value: cell, configurable: true });
|
|
44
|
+
|
|
45
|
+
contextMenu.open({ id: 1 }, event);
|
|
46
|
+
|
|
47
|
+
await flushPromises();
|
|
48
|
+
|
|
49
|
+
expect(row.classList.contains(activeRowClass)).toBe(true);
|
|
50
|
+
expect(document.querySelectorAll(`tr.${activeRowClass}`)).toHaveLength(1);
|
|
51
|
+
|
|
52
|
+
contextMenu.close();
|
|
53
|
+
|
|
54
|
+
await new Promise(resolve => setTimeout(resolve, 350));
|
|
55
|
+
|
|
56
|
+
expect(row.classList.contains(activeRowClass)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('does not throw and adds no class when the event target is not inside table row', async () => {
|
|
60
|
+
expect.assertions(1);
|
|
61
|
+
|
|
62
|
+
const target = document.createElement('div');
|
|
63
|
+
document.body.append(target);
|
|
64
|
+
|
|
65
|
+
const event = new MouseEvent('contextmenu', { bubbles: true });
|
|
66
|
+
Object.defineProperty(event, 'target', { value: target, configurable: true });
|
|
67
|
+
|
|
68
|
+
contextMenu.open({ id: 1 }, event);
|
|
69
|
+
|
|
70
|
+
await flushPromises();
|
|
71
|
+
|
|
72
|
+
expect(document.querySelectorAll(`.${activeRowClass}`)).toHaveLength(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('removes the active row class from the previous row when a new context menu is opened', async () => {
|
|
76
|
+
expect.assertions(3);
|
|
77
|
+
|
|
78
|
+
const { row: firstRow, cell: firstCell } = createRowWithCell();
|
|
79
|
+
const firstEvent = new MouseEvent('contextmenu', { bubbles: true });
|
|
80
|
+
Object.defineProperty(firstEvent, 'target', { value: firstCell, configurable: true });
|
|
81
|
+
|
|
82
|
+
contextMenu.open({ id: 1 }, firstEvent);
|
|
83
|
+
await flushPromises();
|
|
84
|
+
|
|
85
|
+
expect(firstRow.classList.contains(activeRowClass)).toBe(true);
|
|
86
|
+
|
|
87
|
+
const { row: secondRow, cell: secondCell } = createRowWithCell();
|
|
88
|
+
const secondEvent = new MouseEvent('contextmenu', { bubbles: true });
|
|
89
|
+
Object.defineProperty(secondEvent, 'target', { value: secondCell, configurable: true });
|
|
90
|
+
|
|
91
|
+
contextMenu.open({ id: 2 }, secondEvent);
|
|
92
|
+
await flushPromises();
|
|
93
|
+
|
|
94
|
+
expect(firstRow.classList.contains(activeRowClass)).toBe(false);
|
|
95
|
+
expect(secondRow.classList.contains(activeRowClass)).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
import { createApp, inject, ref } from 'vue';
|
|
1
|
+
import { createApp, inject, ref, shallowRef } from 'vue';
|
|
2
2
|
import { get } from 'radash';
|
|
3
3
|
import { AppContextMenu } from '~components';
|
|
4
4
|
|
|
5
5
|
import type { App, Ref } from 'vue';
|
|
6
6
|
import type { ContextMenuInstance, Shortcut, Props } from '~components/AppContextMenu/index.d';
|
|
7
|
+
import type { Action } from '~components/AppActionBar/index.d';
|
|
8
|
+
|
|
9
|
+
export const activeRowClass = 'app-context-menu-active-row';
|
|
7
10
|
|
|
8
11
|
const state = {
|
|
9
12
|
instances: ref<ContextMenuInstance[]>([]),
|
|
10
13
|
shortcuts: ref<Shortcut[]>([]),
|
|
11
14
|
item: ref<unknown>(),
|
|
15
|
+
actions: shallowRef<Action[]>([]),
|
|
12
16
|
};
|
|
13
17
|
|
|
14
18
|
function closeContextMenu(contextInstance: ContextMenuInstance): void {
|
|
19
|
+
contextInstance?.activeRow?.classList.remove(activeRowClass);
|
|
15
20
|
contextInstance?.instance?.unmount();
|
|
16
21
|
contextInstance?.element?.remove();
|
|
17
22
|
}
|
|
@@ -22,6 +27,8 @@ interface UseContextMenu<T> {
|
|
|
22
27
|
close: () => Promise<void>;
|
|
23
28
|
closeAll: () => void;
|
|
24
29
|
shortcuts: Ref<Shortcut[]>;
|
|
30
|
+
actions: Ref<Action[]>;
|
|
31
|
+
setActions: (actions: Action[]) => void;
|
|
25
32
|
instance: Ref<ContextMenuInstance | undefined>;
|
|
26
33
|
instances: Ref<ContextMenuInstance[]>;
|
|
27
34
|
}
|
|
@@ -42,6 +49,9 @@ export function useContextMenu<T>(): UseContextMenu<T> {
|
|
|
42
49
|
const contextMenuRootElement = document.createElement('div');
|
|
43
50
|
document.body.append(contextMenuRootElement);
|
|
44
51
|
|
|
52
|
+
const activeRow = (event.target as HTMLElement | null)?.closest('tr') ?? null;
|
|
53
|
+
activeRow?.classList.add(activeRowClass);
|
|
54
|
+
|
|
45
55
|
let contextInstance = null as null | App;
|
|
46
56
|
let contextMenuRef = null as null | typeof AppContextMenu;
|
|
47
57
|
|
|
@@ -73,6 +83,7 @@ export function useContextMenu<T>(): UseContextMenu<T> {
|
|
|
73
83
|
ref: contextMenuRef,
|
|
74
84
|
instance: contextInstance,
|
|
75
85
|
element: contextMenuRootElement,
|
|
86
|
+
activeRow,
|
|
76
87
|
};
|
|
77
88
|
|
|
78
89
|
// @ts-expect-error contextInstance wrong type
|
|
@@ -85,6 +96,7 @@ export function useContextMenu<T>(): UseContextMenu<T> {
|
|
|
85
96
|
// @ts-expect-error contextInstance can not be null
|
|
86
97
|
instance: contextInstance,
|
|
87
98
|
element: contextMenuRootElement,
|
|
99
|
+
activeRow,
|
|
88
100
|
});
|
|
89
101
|
}, 500);
|
|
90
102
|
}
|
|
@@ -103,6 +115,10 @@ export function useContextMenu<T>(): UseContextMenu<T> {
|
|
|
103
115
|
return Promise.resolve();
|
|
104
116
|
}
|
|
105
117
|
|
|
118
|
+
function setActions(actions: Action[]): void {
|
|
119
|
+
state.actions.value = actions;
|
|
120
|
+
}
|
|
121
|
+
|
|
106
122
|
function closeAll(): void {
|
|
107
123
|
state.instances.value.forEach(stateInstance => {
|
|
108
124
|
stateInstance?.ref.close();
|
|
@@ -123,5 +139,6 @@ export function useContextMenu<T>(): UseContextMenu<T> {
|
|
|
123
139
|
close,
|
|
124
140
|
closeAll,
|
|
125
141
|
setShortcuts,
|
|
142
|
+
setActions,
|
|
126
143
|
} as unknown as UseContextMenu<T>;
|
|
127
144
|
}
|
|
@@ -8,10 +8,10 @@ import type { Options } from './index.d';
|
|
|
8
8
|
|
|
9
9
|
export function useFocusTrap(
|
|
10
10
|
target: MaybeRefOrGetter<Arrayable<MaybeRefOrGetter<string> | MaybeComputedElementRef>>,
|
|
11
|
-
options: Options = {},
|
|
11
|
+
options: Options = { allowOutsideClick: true, preventScroll: false },
|
|
12
12
|
) {
|
|
13
13
|
const isActive = ref<boolean>(false);
|
|
14
|
-
const { activate: activateTrap, deactivate: deactivateTrap } = useFocusTrapVueUse(target);
|
|
14
|
+
const { activate: activateTrap, deactivate: deactivateTrap } = useFocusTrapVueUse(target, options);
|
|
15
15
|
const activeElement = useActiveElement();
|
|
16
16
|
const onEscapeCallback = ref<() => void>();
|
|
17
17
|
const onBackCallback = ref<() => void>();
|