@flux-ui/internals 3.0.0-next.9 → 3.1.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/README.md +24 -3
- package/dist/composable/index.d.ts +2 -17
- package/dist/composable/index.js +1 -0
- package/dist/composable-DPolUL3R.js +2 -0
- package/dist/composable-DPolUL3R.js.map +1 -0
- package/dist/data/index.d.ts +246 -1
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +2 -0
- package/dist/data/index.js.map +1 -0
- package/dist/directive/index.d.ts +2 -3
- package/dist/directive/index.js +1 -0
- package/dist/directive-Bti3pKwZ.js +2 -0
- package/dist/directive-Bti3pKwZ.js.map +1 -0
- package/dist/index-BXp0Gr5f.d.ts +138 -0
- package/dist/index-BXp0Gr5f.d.ts.map +1 -0
- package/dist/index-Bf3XnnIf.d.ts +73 -0
- package/dist/index-Bf3XnnIf.d.ts.map +1 -0
- package/dist/index-ZutYFtKs.d.ts +14 -0
- package/dist/index-ZutYFtKs.d.ts.map +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1 -4
- package/dist/util/index.d.ts +2 -13
- package/dist/util/index.js +1 -0
- package/dist/util-BGzD1ED0.js +2 -0
- package/dist/util-BGzD1ED0.js.map +1 -0
- package/package.json +30 -13
- package/src/composable/index.ts +3 -5
- package/src/composable/useCalendar.ts +1 -1
- package/src/composable/useCalendarMonthSwitcher.ts +1 -2
- package/src/composable/useCalendarTimeGrid.ts +103 -0
- package/src/composable/useCalendarYearSwitcher.ts +1 -2
- package/src/composable/useEventListener.ts +6 -4
- package/src/composable/useFocusTrap.ts +28 -24
- package/src/composable/useFocusTrapLock.ts +1 -2
- package/src/composable/useFocusTrapReturn.ts +11 -7
- package/src/composable/useFocusTrapSubscription.ts +1 -2
- package/src/composable/useFocusZone.ts +13 -12
- package/src/composable/useInView.ts +2 -4
- package/src/composable/useKeyboardGrab.ts +156 -0
- package/src/composable/useRemembered.ts +21 -7
- package/src/composable/useScrollEdges.ts +76 -0
- package/src/composable/useScrollPosition.ts +16 -8
- package/src/directive/focusTrap.ts +4 -0
- package/src/directive/heightTransition.ts +6 -2
- package/src/directive/index.ts +1 -1
- package/src/util/animationFrameDebounce.ts +15 -0
- package/src/util/flattenVNodeTree.ts +3 -4
- package/src/util/focusTrap.ts +9 -5
- package/src/util/getBidirectionalFocusElement.ts +1 -1
- package/src/util/getComponentName.ts +1 -1
- package/src/util/getFocusableElement.ts +2 -1
- package/src/util/getFocusableElements.ts +5 -4
- package/src/util/getKeyboardFocusableElements.ts +1 -1
- package/src/util/index.ts +3 -1
- package/src/util/isActiveElement.ts +5 -0
- package/src/util/unrefTemplateElement.ts +1 -1
- package/src/util/wrapFocus.ts +1 -1
- package/dist/composable/useCalendar.d.ts +0 -20
- package/dist/composable/useCalendarMonthSwitcher.d.ts +0 -10
- package/dist/composable/useCalendarYearSwitcher.d.ts +0 -8
- package/dist/composable/useClickOutside.d.ts +0 -4
- package/dist/composable/useComponentId.d.ts +0 -2
- package/dist/composable/useDebouncedRef.d.ts +0 -2
- package/dist/composable/useEventListener.d.ts +0 -2
- package/dist/composable/useFocusTrap.d.ts +0 -8
- package/dist/composable/useFocusTrapLock.d.ts +0 -2
- package/dist/composable/useFocusTrapReturn.d.ts +0 -2
- package/dist/composable/useFocusTrapSubscription.d.ts +0 -2
- package/dist/composable/useFocusZone.d.ts +0 -6
- package/dist/composable/useInView.d.ts +0 -6
- package/dist/composable/useInterval.d.ts +0 -2
- package/dist/composable/useMutationObserver.d.ts +0 -2
- package/dist/composable/useRemembered.d.ts +0 -2
- package/dist/composable/useScrollPosition.d.ts +0 -7
- package/dist/data/color.d.ts +0 -242
- package/dist/directive/focusTrap.d.ts +0 -5
- package/dist/directive/heightTransition.d.ts +0 -5
- package/dist/index.js.map +0 -42
- package/dist/util/flattenVNodeTree.d.ts +0 -2
- package/dist/util/focusTrap.d.ts +0 -8
- package/dist/util/getBidirectionalFocusElement.d.ts +0 -1
- package/dist/util/getComponentName.d.ts +0 -7
- package/dist/util/getComponentProps.d.ts +0 -1
- package/dist/util/getExposedRef.d.ts +0 -2
- package/dist/util/getFocusableElement.d.ts +0 -1
- package/dist/util/getFocusableElements.d.ts +0 -1
- package/dist/util/getKeyboardFocusableElements.d.ts +0 -1
- package/dist/util/unrefTemplateElement.d.ts +0 -4
- package/dist/util/warn.d.ts +0 -1
- package/dist/util/wrapFocus.d.ts +0 -1
- package/src/composable/useClickOutside.ts +0 -38
- package/src/composable/useComponentId.ts +0 -8
- package/src/composable/useDebouncedRef.ts +0 -38
- package/src/composable/useInterval.ts +0 -23
- package/src/composable/useMutationObserver.ts +0 -38
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { DateTime } from 'luxon';
|
|
2
|
+
import { computed, type ComputedRef, type MaybeRefOrGetter, ref, type Ref, toValue, unref, watch } from 'vue';
|
|
3
|
+
|
|
4
|
+
export type UseCalendarTimeGridDayCount = 1 | 2 | 7;
|
|
5
|
+
|
|
6
|
+
export type UseCalendarTimeGridReturn = {
|
|
7
|
+
readonly isTransitioningToPast: Ref<boolean>;
|
|
8
|
+
readonly viewDate: Ref<DateTime>;
|
|
9
|
+
readonly viewDates: ComputedRef<DateTime[]>;
|
|
10
|
+
readonly rangeLabel: ComputedRef<string>;
|
|
11
|
+
|
|
12
|
+
setViewDate(date: DateTime): void;
|
|
13
|
+
next(): void;
|
|
14
|
+
previous(): void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function getAnchor(date: DateTime, dayCount: UseCalendarTimeGridDayCount): DateTime {
|
|
18
|
+
return dayCount === 7 ? date.startOf('week') : date.startOf('day');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function stepDuration(dayCount: UseCalendarTimeGridDayCount): { day?: number; week?: number } {
|
|
22
|
+
if (dayCount === 7) {
|
|
23
|
+
return {week: 1};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (dayCount === 2) {
|
|
27
|
+
return {day: 2};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {day: 1};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default function (
|
|
34
|
+
initialDate: DateTime,
|
|
35
|
+
dayCount: MaybeRefOrGetter<UseCalendarTimeGridDayCount>
|
|
36
|
+
): UseCalendarTimeGridReturn {
|
|
37
|
+
const isTransitioningToPast = ref(false);
|
|
38
|
+
const viewDate = ref<DateTime>(getAnchor(initialDate, toValue(dayCount)));
|
|
39
|
+
|
|
40
|
+
// Re-anchor on dayCount change (e.g. switching from week to two-days view).
|
|
41
|
+
watch(() => toValue(dayCount), (count) => {
|
|
42
|
+
viewDate.value = getAnchor(unref(viewDate), count);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const viewDates = computed<DateTime[]>(() => {
|
|
46
|
+
const anchor = unref(viewDate);
|
|
47
|
+
const count = toValue(dayCount);
|
|
48
|
+
const out: DateTime[] = [];
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < count; ++i) {
|
|
51
|
+
out.push(anchor.plus({day: i}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return out;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const rangeLabel = computed<string>(() => {
|
|
58
|
+
const dates = unref(viewDates);
|
|
59
|
+
const first = dates[0];
|
|
60
|
+
const last = dates[dates.length - 1];
|
|
61
|
+
const count = toValue(dayCount);
|
|
62
|
+
|
|
63
|
+
if (count === 1) {
|
|
64
|
+
return first.toLocaleString({weekday: 'long', day: 'numeric', month: 'long', year: 'numeric'});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const sameMonth = first.month === last.month;
|
|
68
|
+
const sameYear = first.year === last.year;
|
|
69
|
+
|
|
70
|
+
const firstFmt = first.toLocaleString(
|
|
71
|
+
sameMonth && sameYear
|
|
72
|
+
? {weekday: 'short', day: 'numeric'}
|
|
73
|
+
: {weekday: 'short', day: 'numeric', month: 'short'}
|
|
74
|
+
);
|
|
75
|
+
const lastFmt = last.toLocaleString({weekday: 'short', day: 'numeric', month: 'short', year: sameYear ? undefined : 'numeric'});
|
|
76
|
+
|
|
77
|
+
return `${firstFmt} – ${lastFmt}`;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function setViewDate(date: DateTime): void {
|
|
81
|
+
const anchor = getAnchor(date, toValue(dayCount));
|
|
82
|
+
isTransitioningToPast.value = unref(viewDate) > anchor;
|
|
83
|
+
viewDate.value = anchor;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function next(): void {
|
|
87
|
+
setViewDate(unref(viewDate).plus(stepDuration(toValue(dayCount))));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function previous(): void {
|
|
91
|
+
setViewDate(unref(viewDate).minus(stepDuration(toValue(dayCount))));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
isTransitioningToPast,
|
|
96
|
+
viewDate,
|
|
97
|
+
viewDates,
|
|
98
|
+
rangeLabel,
|
|
99
|
+
setViewDate,
|
|
100
|
+
next,
|
|
101
|
+
previous
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { DateTime } from 'luxon';
|
|
2
|
-
import type
|
|
3
|
-
import { computed, ref, unref, watch } from 'vue';
|
|
2
|
+
import { computed, ref, type Ref, unref, watch } from 'vue';
|
|
4
3
|
|
|
5
4
|
export default function (currentDate: Ref<DateTime>, limit: number = 10): UseCalendarYearSwitcherReturn {
|
|
6
5
|
const index = ref(0);
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { watch } from 'vue';
|
|
2
|
-
import type
|
|
2
|
+
import { type TemplateRef, unrefTemplateElement } from '../util';
|
|
3
3
|
|
|
4
4
|
export default function <K extends keyof HTMLElementEventMap>(elementRef: TemplateRef<HTMLElement>, eventName: K, listener: (evt: HTMLElementEventMap[K]) => any, options: AddEventListenerOptions = {passive: true}): void {
|
|
5
|
-
watch(elementRef, (
|
|
5
|
+
watch(elementRef, (value, _, onCleanup) => {
|
|
6
|
+
const element: EventTarget | null = unrefTemplateElement(elementRef) ?? (value instanceof EventTarget ? value : null);
|
|
7
|
+
|
|
6
8
|
if (!element) {
|
|
7
9
|
return;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
element.addEventListener(eventName, listener, options);
|
|
12
|
+
element.addEventListener(eventName, listener as EventListener, options);
|
|
11
13
|
|
|
12
|
-
onCleanup(() => element.removeEventListener(eventName, listener));
|
|
14
|
+
onCleanup(() => element.removeEventListener(eventName, listener as EventListener, options));
|
|
13
15
|
}, {immediate: true});
|
|
14
16
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
3
|
-
import type { TemplateRef } from '../util';
|
|
4
|
-
import { getFocusableElements, isSSR, unrefTemplateElement, wrapFocus } from '../util';
|
|
1
|
+
import { ref, type Ref, unref, watch } from 'vue';
|
|
2
|
+
import { getFocusableElements, isActiveElement, isSSR, type TemplateRef, unrefTemplateElement, wrapFocus } from '../util';
|
|
5
3
|
import useFocusTrapLock from './useFocusTrapLock';
|
|
6
4
|
import useFocusTrapReturn from './useFocusTrapReturn';
|
|
7
5
|
|
|
@@ -11,9 +9,9 @@ export default function (containerRef: TemplateRef<HTMLElement>, options: UseFoc
|
|
|
11
9
|
}
|
|
12
10
|
|
|
13
11
|
const {disable = ref(false), disableReturn = ref(false), attachTo = null} = options;
|
|
14
|
-
const enabled = useFocusTrapLock(!disable);
|
|
12
|
+
const enabled = useFocusTrapLock(!unref(disable));
|
|
15
13
|
|
|
16
|
-
useFocusTrapReturn(disableReturn);
|
|
14
|
+
useFocusTrapReturn(containerRef, disableReturn);
|
|
17
15
|
|
|
18
16
|
watch(containerRef, (_, __, onCleanup) => {
|
|
19
17
|
const container = unrefTemplateElement(containerRef);
|
|
@@ -63,21 +61,27 @@ export default function (containerRef: TemplateRef<HTMLElement>, options: UseFoc
|
|
|
63
61
|
attach.addEventListener('focusout', onFocusOut as EventListener, {capture: true});
|
|
64
62
|
|
|
65
63
|
if (container) {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
64
|
+
const autofocusElement = container.querySelector<HTMLElement>('[autofocus]');
|
|
65
|
+
|
|
66
|
+
if (autofocusElement) {
|
|
67
|
+
autofocusElement.focus();
|
|
68
|
+
} else {
|
|
69
|
+
const elements = getFocusableElements(container);
|
|
70
|
+
const isActiveIndex = elements.findIndex(elm => isActiveElement(elm) && !elm.hasAttribute('aria-disabled'));
|
|
71
|
+
const notDisabledIndex = elements.findIndex(elm => !elm.hasAttribute('aria-disabled'));
|
|
72
|
+
let element = elements[0];
|
|
73
|
+
|
|
74
|
+
if (isActiveIndex > -1) {
|
|
75
|
+
element = elements[isActiveIndex];
|
|
76
|
+
} else if (notDisabledIndex > -1) {
|
|
77
|
+
element = elements[notDisabledIndex];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (element) {
|
|
81
|
+
element.focus();
|
|
82
|
+
} else {
|
|
83
|
+
container.focus();
|
|
84
|
+
}
|
|
81
85
|
}
|
|
82
86
|
}
|
|
83
87
|
|
|
@@ -87,11 +91,11 @@ export default function (containerRef: TemplateRef<HTMLElement>, options: UseFoc
|
|
|
87
91
|
});
|
|
88
92
|
}, {immediate: true});
|
|
89
93
|
|
|
90
|
-
watch(
|
|
94
|
+
watch(disable, disabled => {
|
|
91
95
|
const container = unrefTemplateElement(containerRef);
|
|
92
|
-
enabled.value = !
|
|
96
|
+
enabled.value = !disabled;
|
|
93
97
|
|
|
94
|
-
if (
|
|
98
|
+
if (disabled || !container) {
|
|
95
99
|
return;
|
|
96
100
|
}
|
|
97
101
|
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
1
|
+
import { type Ref, unref, watch } from 'vue';
|
|
2
|
+
import { type TemplateRef, unrefTemplateElement } from '../util';
|
|
3
3
|
|
|
4
|
-
export default function (disabled: Ref<boolean>): void {
|
|
5
|
-
|
|
4
|
+
export default function (containerRef: TemplateRef<HTMLElement>, disabled: Ref<boolean>): void {
|
|
5
|
+
watch(containerRef, (_, __, onCleanup) => {
|
|
6
|
+
const container = unrefTemplateElement(containerRef);
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
if (unref(disabled)) {
|
|
8
|
+
if (!container || unref(disabled)) {
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
const previousTarget = document.activeElement as HTMLElement | null;
|
|
13
|
+
|
|
14
|
+
onCleanup(() => {
|
|
15
|
+
requestAnimationFrame(() => previousTarget?.focus());
|
|
16
|
+
});
|
|
13
17
|
});
|
|
14
18
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { onMounted, onUnmounted, ref } from 'vue';
|
|
2
|
-
import type
|
|
3
|
-
import { FOCUS_TRAP_LOCKS } from '../util';
|
|
2
|
+
import { FOCUS_TRAP_LOCKS, type FocusTrapListener } from '../util';
|
|
4
3
|
|
|
5
4
|
export default function (listener: FocusTrapListener): void {
|
|
6
5
|
const unsubscribe = ref<Function | null>(null);
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type
|
|
3
|
-
import { getBidirectionalFocusElement, getFocusableElement, getFocusableElements,
|
|
4
|
-
import useMutationObserver from './useMutationObserver';
|
|
1
|
+
import { unwrapElement, useMutationObserver } from '@basmilius/common';
|
|
2
|
+
import { type ComponentPublicInstance, type Ref, watch } from 'vue';
|
|
3
|
+
import { getBidirectionalFocusElement, getFocusableElement, getFocusableElements, isActiveElement } from '../util';
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
type EligibleElement = ComponentPublicInstance | HTMLElement;
|
|
6
|
+
|
|
7
|
+
export default function <TElement extends EligibleElement>(containerRef: Ref<TElement>, {cycle = true, direction = 'bidirectional'}: UseFocusZoneOptions = {}): void {
|
|
8
|
+
useMutationObserver(containerRef, () => updateFocus(findInitialIndex(), false), {childList: true, subtree: true});
|
|
8
9
|
|
|
9
10
|
function findInitialIndex(): number {
|
|
10
|
-
const container =
|
|
11
|
+
const container = unwrapElement(containerRef);
|
|
11
12
|
const elements = getFocusableElements(container);
|
|
12
|
-
const isActiveIndex = elements.findIndex(
|
|
13
|
-
const notDisabledIndex = elements.findIndex(
|
|
13
|
+
const isActiveIndex = elements.findIndex(elm => isActiveElement(elm) && !elm.hasAttribute('aria-disabled'));
|
|
14
|
+
const notDisabledIndex = elements.findIndex(elm => !elm.hasAttribute('aria-disabled'));
|
|
14
15
|
|
|
15
16
|
if (isActiveIndex > -1) {
|
|
16
17
|
return isActiveIndex;
|
|
@@ -24,7 +25,7 @@ export default function <TElement extends HTMLElement>(containerRef: TemplateRef
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
function updateFocus(elementIndex: number, doFocus: boolean = true): void {
|
|
27
|
-
const container =
|
|
28
|
+
const container = unwrapElement(containerRef)!;
|
|
28
29
|
const elements = getFocusableElements(container);
|
|
29
30
|
elements.forEach((elm, index) => elm.tabIndex = index === elementIndex ? 0 : -1);
|
|
30
31
|
|
|
@@ -32,7 +33,7 @@ export default function <TElement extends HTMLElement>(containerRef: TemplateRef
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function onKeyDown(evt: KeyboardEvent): void {
|
|
35
|
-
const container =
|
|
36
|
+
const container = unwrapElement(containerRef)!;
|
|
36
37
|
const elements = getFocusableElements(container);
|
|
37
38
|
|
|
38
39
|
if (['Enter', ' '].includes(evt.key)) {
|
|
@@ -52,7 +53,7 @@ export default function <TElement extends HTMLElement>(containerRef: TemplateRef
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
watch(containerRef, (_, __, onCleanup) => {
|
|
55
|
-
const container =
|
|
56
|
+
const container = unwrapElement(containerRef);
|
|
56
57
|
|
|
57
58
|
if (!container) {
|
|
58
59
|
return;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
3
|
-
import type { TemplateRef } from '../util';
|
|
4
|
-
import { unrefTemplateElement } from '../util';
|
|
1
|
+
import { ref, type Ref, watch } from 'vue';
|
|
2
|
+
import { type TemplateRef, unrefTemplateElement } from '../util';
|
|
5
3
|
|
|
6
4
|
export default function <TElement extends HTMLElement>(containerRef: TemplateRef<TElement>, options: UseInViewOptions = {}): Ref<boolean> {
|
|
7
5
|
const inView = ref(options.initial ?? false);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { computed, type ComputedRef, ref, type Ref, unref } from 'vue';
|
|
2
|
+
|
|
3
|
+
export type KeyboardGrabDirection = 'up' | 'down' | 'left' | 'right';
|
|
4
|
+
|
|
5
|
+
export type UseKeyboardGrabOptions<TPos> = {
|
|
6
|
+
readonly isDraggable: Ref<boolean>;
|
|
7
|
+
readonly itemId: Ref<string | number | null | undefined>;
|
|
8
|
+
readonly grabbedId: Ref<string | number | null>;
|
|
9
|
+
onGrab(): TPos;
|
|
10
|
+
onMove(direction: KeyboardGrabDirection): void;
|
|
11
|
+
onCommit(origin: TPos): void;
|
|
12
|
+
onCancel(origin: TPos): void;
|
|
13
|
+
announce?(message: string): void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type UseKeyboardGrabReturn = {
|
|
17
|
+
readonly isGrabbed: ComputedRef<boolean>;
|
|
18
|
+
handleKeyDown(evt: KeyboardEvent): void;
|
|
19
|
+
release(): void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let liveRegion: HTMLElement | null = null;
|
|
23
|
+
|
|
24
|
+
function ensureLiveRegion(): HTMLElement {
|
|
25
|
+
if (liveRegion) {
|
|
26
|
+
return liveRegion;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof document === 'undefined') {
|
|
30
|
+
return null as unknown as HTMLElement;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const region = document.createElement('div');
|
|
34
|
+
region.setAttribute('role', 'status');
|
|
35
|
+
region.setAttribute('aria-live', 'polite');
|
|
36
|
+
region.setAttribute('aria-atomic', 'true');
|
|
37
|
+
region.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0';
|
|
38
|
+
document.body.appendChild(region);
|
|
39
|
+
liveRegion = region;
|
|
40
|
+
return region;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function defaultAnnounce(message: string): void {
|
|
44
|
+
const region = ensureLiveRegion();
|
|
45
|
+
|
|
46
|
+
if (!region) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
region.textContent = '';
|
|
51
|
+
requestAnimationFrame(() => {
|
|
52
|
+
region.textContent = message;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generic keyboard-grab state machine. Maps Space/Enter/Escape/Arrow keys
|
|
58
|
+
* onto grab/commit/cancel/move callbacks. The actual movement logic is
|
|
59
|
+
* delegated to `onMove` since it depends on the host component's topology.
|
|
60
|
+
*/
|
|
61
|
+
export default function useKeyboardGrab<TPos>(options: UseKeyboardGrabOptions<TPos>): UseKeyboardGrabReturn {
|
|
62
|
+
const origin = ref<TPos | null>(null);
|
|
63
|
+
const announce = options.announce ?? defaultAnnounce;
|
|
64
|
+
|
|
65
|
+
const isGrabbed = computed<boolean>(() => {
|
|
66
|
+
const id = unref(options.itemId);
|
|
67
|
+
const grabbed = unref(options.grabbedId);
|
|
68
|
+
|
|
69
|
+
return id != null && grabbed === id;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function release(): void {
|
|
73
|
+
origin.value = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function handleKeyDown(evt: KeyboardEvent): void {
|
|
77
|
+
if (!unref(options.isDraggable)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const id = unref(options.itemId);
|
|
82
|
+
|
|
83
|
+
if (id == null) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!isGrabbed.value) {
|
|
88
|
+
if (evt.key === ' ' || evt.key === 'Enter') {
|
|
89
|
+
evt.preventDefault();
|
|
90
|
+
origin.value = options.onGrab() as TPos;
|
|
91
|
+
announce('Grabbed, use arrow keys to move');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
switch (evt.key) {
|
|
98
|
+
case 'ArrowUp':
|
|
99
|
+
evt.preventDefault();
|
|
100
|
+
options.onMove('up');
|
|
101
|
+
announce('Moved up');
|
|
102
|
+
break;
|
|
103
|
+
|
|
104
|
+
case 'ArrowDown':
|
|
105
|
+
evt.preventDefault();
|
|
106
|
+
options.onMove('down');
|
|
107
|
+
announce('Moved down');
|
|
108
|
+
break;
|
|
109
|
+
|
|
110
|
+
case 'ArrowLeft':
|
|
111
|
+
evt.preventDefault();
|
|
112
|
+
options.onMove('left');
|
|
113
|
+
announce('Moved left');
|
|
114
|
+
break;
|
|
115
|
+
|
|
116
|
+
case 'ArrowRight':
|
|
117
|
+
evt.preventDefault();
|
|
118
|
+
options.onMove('right');
|
|
119
|
+
announce('Moved right');
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
case ' ':
|
|
123
|
+
case 'Enter': {
|
|
124
|
+
evt.preventDefault();
|
|
125
|
+
const originValue = origin.value;
|
|
126
|
+
origin.value = null;
|
|
127
|
+
|
|
128
|
+
if (originValue !== null) {
|
|
129
|
+
options.onCommit(originValue);
|
|
130
|
+
announce('Dropped');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case 'Escape': {
|
|
137
|
+
evt.preventDefault();
|
|
138
|
+
const originValue = origin.value;
|
|
139
|
+
origin.value = null;
|
|
140
|
+
|
|
141
|
+
if (originValue !== null) {
|
|
142
|
+
options.onCancel(originValue);
|
|
143
|
+
announce('Cancelled');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
isGrabbed,
|
|
153
|
+
handleKeyDown,
|
|
154
|
+
release
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
import { DateTime } from 'luxon';
|
|
2
|
-
import type
|
|
3
|
-
import {
|
|
2
|
+
import { ref, type Ref, watch } from 'vue';
|
|
3
|
+
import { isSSR } from '../util';
|
|
4
4
|
|
|
5
5
|
export default function <T>(key: string, initialValue: T): Ref<T> {
|
|
6
|
+
if (isSSR) {
|
|
7
|
+
return ref(initialValue) as Ref<T>;
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
const realKey = `flux/${key}`;
|
|
7
11
|
const value = ref<T>(get() ?? initialValue);
|
|
8
12
|
|
|
9
13
|
function get(): T | null {
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
const storedValue = localStorage.getItem(realKey);
|
|
15
|
+
|
|
16
|
+
if (storedValue === null) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
let storageValue = JSON.parse(storedValue);
|
|
12
22
|
|
|
13
23
|
if (Array.isArray(storageValue) && storageValue[0] === 'DateTime') {
|
|
14
24
|
storageValue = DateTime.fromISO(storageValue[1]);
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
return storageValue;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
18
30
|
}
|
|
19
|
-
|
|
20
|
-
return null;
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
watch(value, value => {
|
|
@@ -30,7 +40,11 @@ export default function <T>(key: string, initialValue: T): Ref<T> {
|
|
|
30
40
|
})] as unknown as T;
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
|
|
43
|
+
try {
|
|
44
|
+
localStorage.setItem(realKey, JSON.stringify(_value));
|
|
45
|
+
} catch {
|
|
46
|
+
// Storage can be unavailable or full (e.g. private browsing); remembering is best-effort.
|
|
47
|
+
}
|
|
34
48
|
});
|
|
35
49
|
|
|
36
50
|
return value as Ref<T>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ref, type Ref, watch } from 'vue';
|
|
2
|
+
import { type TemplateRef, unrefTemplateElement } from '../util';
|
|
3
|
+
|
|
4
|
+
export default function <TElement extends HTMLElement>(elementRef: TemplateRef<TElement>): UseScrollEdgesReturn {
|
|
5
|
+
const isAtStart = ref(true);
|
|
6
|
+
const isAtEnd = ref(true);
|
|
7
|
+
const isAtLeft = ref(true);
|
|
8
|
+
const isAtRight = ref(true);
|
|
9
|
+
|
|
10
|
+
watch(elementRef, (_, __, onCleanup) => {
|
|
11
|
+
const element = unrefTemplateElement(elementRef);
|
|
12
|
+
|
|
13
|
+
if (!element) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const update = (): void => {
|
|
18
|
+
const {scrollTop, scrollHeight, clientHeight, scrollLeft, scrollWidth, clientWidth} = element;
|
|
19
|
+
|
|
20
|
+
isAtStart.value = scrollTop <= 0;
|
|
21
|
+
isAtEnd.value = Math.ceil(scrollTop + clientHeight) >= scrollHeight;
|
|
22
|
+
isAtLeft.value = scrollLeft <= 0;
|
|
23
|
+
isAtRight.value = Math.ceil(scrollLeft + clientWidth) >= scrollWidth;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const resizeObserver = new ResizeObserver(update);
|
|
27
|
+
resizeObserver.observe(element);
|
|
28
|
+
|
|
29
|
+
for (const child of Array.from(element.children)) {
|
|
30
|
+
resizeObserver.observe(child);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const mutationObserver = new MutationObserver(mutations => {
|
|
34
|
+
for (const mutation of mutations) {
|
|
35
|
+
for (const node of Array.from(mutation.addedNodes)) {
|
|
36
|
+
if (node instanceof Element) {
|
|
37
|
+
resizeObserver.observe(node);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const node of Array.from(mutation.removedNodes)) {
|
|
42
|
+
if (node instanceof Element) {
|
|
43
|
+
resizeObserver.unobserve(node);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
update();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
mutationObserver.observe(element, {childList: true});
|
|
52
|
+
element.addEventListener('scroll', update, {passive: true});
|
|
53
|
+
|
|
54
|
+
update();
|
|
55
|
+
|
|
56
|
+
onCleanup(() => {
|
|
57
|
+
element.removeEventListener('scroll', update);
|
|
58
|
+
resizeObserver.disconnect();
|
|
59
|
+
mutationObserver.disconnect();
|
|
60
|
+
});
|
|
61
|
+
}, {immediate: true});
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
isAtStart,
|
|
65
|
+
isAtEnd,
|
|
66
|
+
isAtLeft,
|
|
67
|
+
isAtRight
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type UseScrollEdgesReturn = {
|
|
72
|
+
readonly isAtStart: Ref<boolean>;
|
|
73
|
+
readonly isAtEnd: Ref<boolean>;
|
|
74
|
+
readonly isAtLeft: Ref<boolean>;
|
|
75
|
+
readonly isAtRight: Ref<boolean>;
|
|
76
|
+
};
|
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
3
|
-
import type { TemplateRef } from '../util';
|
|
1
|
+
import { ref, type Ref, unref, watch } from 'vue';
|
|
2
|
+
import { isSSR, type TemplateRef } from '../util';
|
|
4
3
|
import useEventListener from './useEventListener';
|
|
5
4
|
|
|
6
5
|
export default function <TElement extends HTMLElement>(elementRef?: TemplateRef<TElement>): UseScrollPositionReturn {
|
|
7
6
|
const x = ref(0);
|
|
8
7
|
const y = ref(0);
|
|
9
8
|
|
|
10
|
-
if (
|
|
11
|
-
|
|
9
|
+
if (isSSR) {
|
|
10
|
+
return {
|
|
11
|
+
x,
|
|
12
|
+
y
|
|
13
|
+
};
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
const targetRef = elementRef ?? ref(document);
|
|
17
|
+
|
|
18
|
+
const update = (): void => {
|
|
19
|
+
let element = unref(targetRef);
|
|
16
20
|
|
|
17
21
|
if (element instanceof Document) {
|
|
18
22
|
element = element.scrollingElement;
|
|
@@ -20,7 +24,11 @@ export default function <TElement extends HTMLElement>(elementRef?: TemplateRef<
|
|
|
20
24
|
|
|
21
25
|
x.value = element?.scrollLeft ?? 0;
|
|
22
26
|
y.value = element?.scrollTop ?? 0;
|
|
23
|
-
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
useEventListener(targetRef, 'scroll', update);
|
|
30
|
+
|
|
31
|
+
watch(targetRef, () => update(), {immediate: true});
|
|
24
32
|
|
|
25
33
|
return {
|
|
26
34
|
x,
|