@flux-ui/internals 3.0.0-next.8 → 3.0.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.
Files changed (95) hide show
  1. package/README.md +35 -0
  2. package/dist/composable/index.d.ts +2 -17
  3. package/dist/composable/index.js +1 -0
  4. package/dist/composable-DPolUL3R.js +2 -0
  5. package/dist/composable-DPolUL3R.js.map +1 -0
  6. package/dist/data/index.d.ts +246 -1
  7. package/dist/data/index.d.ts.map +1 -0
  8. package/dist/data/index.js +2 -0
  9. package/dist/data/index.js.map +1 -0
  10. package/dist/directive/index.d.ts +2 -3
  11. package/dist/directive/index.js +1 -0
  12. package/dist/directive-Bti3pKwZ.js +2 -0
  13. package/dist/directive-Bti3pKwZ.js.map +1 -0
  14. package/dist/index-BXp0Gr5f.d.ts +138 -0
  15. package/dist/index-BXp0Gr5f.d.ts.map +1 -0
  16. package/dist/index-Bf3XnnIf.d.ts +73 -0
  17. package/dist/index-Bf3XnnIf.d.ts.map +1 -0
  18. package/dist/index-ZutYFtKs.d.ts +14 -0
  19. package/dist/index-ZutYFtKs.d.ts.map +1 -0
  20. package/dist/index.d.ts +5 -4
  21. package/dist/index.js +1 -4
  22. package/dist/util/index.d.ts +2 -13
  23. package/dist/util/index.js +1 -0
  24. package/dist/util-BGzD1ED0.js +2 -0
  25. package/dist/util-BGzD1ED0.js.map +1 -0
  26. package/package.json +30 -13
  27. package/src/composable/index.ts +3 -5
  28. package/src/composable/useCalendar.ts +1 -1
  29. package/src/composable/useCalendarMonthSwitcher.ts +1 -2
  30. package/src/composable/useCalendarTimeGrid.ts +103 -0
  31. package/src/composable/useCalendarYearSwitcher.ts +1 -2
  32. package/src/composable/useEventListener.ts +6 -4
  33. package/src/composable/useFocusTrap.ts +28 -24
  34. package/src/composable/useFocusTrapLock.ts +1 -2
  35. package/src/composable/useFocusTrapReturn.ts +11 -7
  36. package/src/composable/useFocusTrapSubscription.ts +1 -2
  37. package/src/composable/useFocusZone.ts +13 -12
  38. package/src/composable/useInView.ts +2 -4
  39. package/src/composable/useKeyboardGrab.ts +156 -0
  40. package/src/composable/useRemembered.ts +21 -7
  41. package/src/composable/useScrollEdges.ts +76 -0
  42. package/src/composable/useScrollPosition.ts +16 -8
  43. package/src/directive/focusTrap.ts +4 -0
  44. package/src/directive/heightTransition.ts +6 -2
  45. package/src/directive/index.ts +1 -1
  46. package/src/util/animationFrameDebounce.ts +15 -0
  47. package/src/util/flattenVNodeTree.ts +3 -4
  48. package/src/util/focusTrap.ts +9 -5
  49. package/src/util/getBidirectionalFocusElement.ts +1 -1
  50. package/src/util/getComponentName.ts +1 -1
  51. package/src/util/getFocusableElement.ts +2 -1
  52. package/src/util/getFocusableElements.ts +5 -4
  53. package/src/util/getKeyboardFocusableElements.ts +1 -1
  54. package/src/util/index.ts +3 -1
  55. package/src/util/isActiveElement.ts +5 -0
  56. package/src/util/unrefTemplateElement.ts +1 -1
  57. package/src/util/wrapFocus.ts +1 -1
  58. package/dist/composable/useCalendar.d.ts +0 -20
  59. package/dist/composable/useCalendarMonthSwitcher.d.ts +0 -10
  60. package/dist/composable/useCalendarYearSwitcher.d.ts +0 -8
  61. package/dist/composable/useClickOutside.d.ts +0 -4
  62. package/dist/composable/useComponentId.d.ts +0 -2
  63. package/dist/composable/useDebouncedRef.d.ts +0 -2
  64. package/dist/composable/useEventListener.d.ts +0 -2
  65. package/dist/composable/useFocusTrap.d.ts +0 -8
  66. package/dist/composable/useFocusTrapLock.d.ts +0 -2
  67. package/dist/composable/useFocusTrapReturn.d.ts +0 -2
  68. package/dist/composable/useFocusTrapSubscription.d.ts +0 -2
  69. package/dist/composable/useFocusZone.d.ts +0 -6
  70. package/dist/composable/useInView.d.ts +0 -6
  71. package/dist/composable/useInterval.d.ts +0 -2
  72. package/dist/composable/useMutationObserver.d.ts +0 -2
  73. package/dist/composable/useRemembered.d.ts +0 -2
  74. package/dist/composable/useScrollPosition.d.ts +0 -7
  75. package/dist/data/color.d.ts +0 -242
  76. package/dist/directive/focusTrap.d.ts +0 -5
  77. package/dist/directive/heightTransition.d.ts +0 -5
  78. package/dist/index.js.map +0 -42
  79. package/dist/util/flattenVNodeTree.d.ts +0 -2
  80. package/dist/util/focusTrap.d.ts +0 -8
  81. package/dist/util/getBidirectionalFocusElement.d.ts +0 -1
  82. package/dist/util/getComponentName.d.ts +0 -7
  83. package/dist/util/getComponentProps.d.ts +0 -1
  84. package/dist/util/getExposedRef.d.ts +0 -2
  85. package/dist/util/getFocusableElement.d.ts +0 -1
  86. package/dist/util/getFocusableElements.d.ts +0 -1
  87. package/dist/util/getKeyboardFocusableElements.d.ts +0 -1
  88. package/dist/util/unrefTemplateElement.d.ts +0 -4
  89. package/dist/util/warn.d.ts +0 -1
  90. package/dist/util/wrapFocus.d.ts +0 -1
  91. package/src/composable/useClickOutside.ts +0 -38
  92. package/src/composable/useComponentId.ts +0 -8
  93. package/src/composable/useDebouncedRef.ts +0 -38
  94. package/src/composable/useInterval.ts +0 -23
  95. 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 { Ref } from 'vue';
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 { TemplateRef } from '../util';
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, (element: HTMLElement, _, onCleanup) => {
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 { Ref } from 'vue';
2
- import { ref, watch } from 'vue';
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 elements = getFocusableElements(container);
67
- const isActiveIndex = elements.findIndex(e => e.classList.contains('is-active'));
68
- const notDisabledIndex = elements.findIndex(e => !e.hasAttribute('aria-disabled'));
69
- let element = elements[0];
70
-
71
- if (isActiveIndex > -1) {
72
- element = elements[isActiveIndex];
73
- }
74
-
75
- if (notDisabledIndex > -1) {
76
- element = elements[notDisabledIndex];
77
- }
78
-
79
- if (element) {
80
- element.focus();
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(() => disable, () => {
94
+ watch(disable, disabled => {
91
95
  const container = unrefTemplateElement(containerRef);
92
- enabled.value = !disable;
96
+ enabled.value = !disabled;
93
97
 
94
- if (disable || !container) {
98
+ if (disabled || !container) {
95
99
  return;
96
100
  }
97
101
 
@@ -1,5 +1,4 @@
1
- import type { Ref } from 'vue';
2
- import { onMounted, onUnmounted, ref, unref } from 'vue';
1
+ import { onMounted, onUnmounted, ref, type Ref, unref } from 'vue';
3
2
  import { FOCUS_TRAP_LOCKS } from '../util';
4
3
 
5
4
  let lockId = 0;
@@ -1,14 +1,18 @@
1
- import type { Ref } from 'vue';
2
- import { onUnmounted, ref, unref } from 'vue';
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
- const target = ref<HTMLElement | null>(document.activeElement as HTMLElement | null);
4
+ export default function (containerRef: TemplateRef<HTMLElement>, disabled: Ref<boolean>): void {
5
+ watch(containerRef, (_, __, onCleanup) => {
6
+ const container = unrefTemplateElement(containerRef);
6
7
 
7
- onUnmounted(() => {
8
- if (unref(disabled)) {
8
+ if (!container || unref(disabled)) {
9
9
  return;
10
10
  }
11
11
 
12
- requestAnimationFrame(() => unref(target)?.focus());
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 { FocusTrapListener } from '../util';
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 { watch } from 'vue';
2
- import type { TemplateRef } from '../util';
3
- import { getBidirectionalFocusElement, getFocusableElement, getFocusableElements, unrefTemplateElement } from '../util';
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
- export default function <TElement extends HTMLElement>(containerRef: TemplateRef<TElement>, {cycle = true, direction = 'bidirectional'}: UseFocusZoneOptions = {}): void {
7
- useMutationObserver(containerRef, () => updateFocus(findInitialIndex(), false));
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 = unrefTemplateElement(containerRef)!;
11
+ const container = unwrapElement(containerRef);
11
12
  const elements = getFocusableElements(container);
12
- const isActiveIndex = elements.findIndex(e => e.classList.contains('is-active'));
13
- const notDisabledIndex = elements.findIndex(e => !e.hasAttribute('aria-disabled'));
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 = unrefTemplateElement(containerRef)!;
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 = unrefTemplateElement(containerRef)!;
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 = unrefTemplateElement(containerRef);
56
+ const container = unwrapElement(containerRef);
56
57
 
57
58
  if (!container) {
58
59
  return;
@@ -1,7 +1,5 @@
1
- import type { Ref } from 'vue';
2
- import { ref, watch } from 'vue';
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 { Ref } from 'vue';
3
- import { ref, watch } from 'vue';
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
- if (realKey in localStorage) {
11
- let storageValue = JSON.parse(localStorage.getItem(realKey)!);
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
- localStorage.setItem(realKey, JSON.stringify(_value));
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 { Ref } from 'vue';
2
- import { ref, unref } from 'vue';
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 (!elementRef) {
11
- elementRef = ref(document);
9
+ if (isSSR) {
10
+ return {
11
+ x,
12
+ y
13
+ };
12
14
  }
13
15
 
14
- useEventListener(elementRef, 'scroll', () => {
15
- let element = unref(elementRef);
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,
@@ -71,6 +71,10 @@ export default {
71
71
  const focusTrap = new FocusTrap(elm);
72
72
  focusTrap.register();
73
73
  focusTraps.set(elm, focusTrap);
74
+ },
75
+
76
+ getSSRProps(): Record<string, unknown> {
77
+ return {};
74
78
  }
75
79
  } satisfies Directive;
76
80