@ngutil/aria 0.0.78 → 0.0.80
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/fesm2022/ngutil-aria.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { DOCUMENT } from '@angular/common';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { inject, NgZone, Injectable,
|
|
4
|
-
import { merge, fromEvent, map, share, filter, shareReplay, startWith, throttleTime, switchMap, timer, take, distinctUntilChanged,
|
|
3
|
+
import { inject, NgZone, Injectable, ElementRef, effect, Directive, input, computed, signal, untracked, NgModule } from '@angular/core';
|
|
4
|
+
import { merge, fromEvent, map, share, filter, shareReplay, startWith, throttleTime, switchMap, timer, take, distinctUntilChanged, BehaviorSubject, connect, Subject, tap, finalize, combineLatest, Observable, of, debounceTime, EMPTY, from, scan, takeWhile, animationFrames } from 'rxjs';
|
|
5
5
|
import { FocusTrapFactory } from '@angular/cdk/a11y';
|
|
6
6
|
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
7
|
+
import { isEqual, clamp } from 'lodash-es';
|
|
7
8
|
import { focusable, isFocusable } from 'tabbable';
|
|
8
9
|
import { coerceElement, Destructible, isElementInput, __zone_symbol__, isFalsy, isTruthy, deepClone, coerceBoolAttr } from '@ngutil/common';
|
|
9
|
-
import { clamp } from 'lodash-es';
|
|
10
10
|
|
|
11
11
|
const EVENT_OPTIONS$1 = {
|
|
12
12
|
capture: true,
|
|
@@ -58,50 +58,75 @@ const EVENT_OPTIONS = {
|
|
|
58
58
|
passive: true
|
|
59
59
|
};
|
|
60
60
|
class FocusService {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
constructor() {
|
|
62
|
+
this.#activity = inject(ActivityService);
|
|
63
|
+
this.#focusTrap = inject(FocusTrapFactory);
|
|
64
|
+
this.#zone = inject(NgZone);
|
|
65
|
+
this.#document = inject(DOCUMENT);
|
|
66
|
+
this.#originOverrides = new BehaviorSubject(new Map());
|
|
67
|
+
this.#focus = listener(this.#document, this.#zone, "focus", EVENT_OPTIONS).pipe(connect(focus => this.#zone.runOutsideAngular(() => {
|
|
68
|
+
let lastFocused = null;
|
|
69
|
+
const sideEffect = new Subject();
|
|
70
|
+
// if element removed form document, emit blur & focus
|
|
71
|
+
const mutation = new MutationObserver(mutations => {
|
|
72
|
+
for (const mutation of mutations) {
|
|
73
|
+
if (mutation.type === "childList" && mutation.removedNodes.length > 0) {
|
|
74
|
+
for (const removed of Array.from(mutation.removedNodes)) {
|
|
75
|
+
if (removed === lastFocused || removed.contains(lastFocused)) {
|
|
76
|
+
this.#blurSide.next(lastFocused);
|
|
77
|
+
sideEffect.next(this.#document.activeElement);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
74
81
|
}
|
|
75
|
-
})),
|
|
76
|
-
focus: focus,
|
|
77
|
-
blur: blur
|
|
78
|
-
}).pipe(takeUntilDestroyed(), map(({ activity, focus, blur }) => {
|
|
79
|
-
// console.log({ activity, focus, blur })
|
|
80
|
-
// If focus in with alt+tab
|
|
81
|
-
if (blur === document && activity.origin === "keyboard") {
|
|
82
|
-
return { origin: "program", element: focus };
|
|
83
|
-
}
|
|
84
|
-
if (focus === document) {
|
|
85
|
-
return { origin: "program", element: focus };
|
|
86
|
-
}
|
|
87
|
-
// If press tab button, first fire the event in the currently focused element
|
|
88
|
-
if (focus === blur) {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
// When press tab, the activity is on the current fucesd element,
|
|
92
|
-
// so when blur is changed to it, the focus change is completed
|
|
93
|
-
if (activity.origin === "keyboard" && isActivityElement(activity.node, blur)) {
|
|
94
|
-
return { origin: "keyboard", element: focus };
|
|
95
82
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
83
|
+
});
|
|
84
|
+
mutation.observe(this.#document, { subtree: true, childList: true });
|
|
85
|
+
return merge(focus.pipe(tap(node => (lastFocused = node)), finalize(() => mutation.disconnect())), sideEffect);
|
|
86
|
+
})), distinctUntilChanged(strictEq));
|
|
87
|
+
this.#blurEvent = listener(this.#document, this.#zone, "blur", EVENT_OPTIONS).pipe(startWith(null));
|
|
88
|
+
this.#blurSide = new Subject();
|
|
89
|
+
this.#blur = merge(this.#blurEvent, this.#blurSide).pipe(distinctUntilChanged(strictEq));
|
|
90
|
+
this.events = this.#zone.runOutsideAngular(() => combineLatest({
|
|
91
|
+
activity: this.#activity.events$.pipe(filter(event => event.type !== "mousemove")),
|
|
92
|
+
focus: this.#focus,
|
|
93
|
+
blur: this.#blur.pipe(tap(el => this.#delOrigin(el))),
|
|
94
|
+
overrides: this.#originOverrides
|
|
95
|
+
}).pipe(takeUntilDestroyed(), map(({ activity, focus, blur, overrides }) => {
|
|
96
|
+
const override = overrides.get(focus);
|
|
97
|
+
// If focus in with alt+tab
|
|
98
|
+
if (blur === document && activity.origin === "keyboard") {
|
|
99
|
+
return { origin: override || "program", element: focus };
|
|
100
|
+
}
|
|
101
|
+
if (focus === document) {
|
|
102
|
+
return { origin: override || "program", element: focus };
|
|
103
|
+
}
|
|
104
|
+
// If press tab button, first fire the event in the currently focused element
|
|
105
|
+
if (focus === blur) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
// When press tab, the activity is on the current fucesd element,
|
|
109
|
+
// so when blur is changed to it, the focus change is completed
|
|
110
|
+
if (activity.origin === "keyboard" && isActivityElement(activity.node, blur)) {
|
|
111
|
+
return { origin: override || "keyboard", element: focus };
|
|
112
|
+
}
|
|
113
|
+
if (isActivityElement(activity.node, focus)) {
|
|
114
|
+
return { origin: override || activity.origin, element: focus };
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
return { origin: override || "program", element: focus };
|
|
118
|
+
}
|
|
119
|
+
}), filter(v => !!v), distinctUntilChanged(isEqual), shareReplay({ bufferSize: 1, refCount: true })));
|
|
104
120
|
}
|
|
121
|
+
#activity;
|
|
122
|
+
#focusTrap;
|
|
123
|
+
#zone;
|
|
124
|
+
#document;
|
|
125
|
+
#originOverrides;
|
|
126
|
+
#focus;
|
|
127
|
+
#blurEvent;
|
|
128
|
+
#blurSide;
|
|
129
|
+
#blur;
|
|
105
130
|
watch(element) {
|
|
106
131
|
const el = coerceElement(element);
|
|
107
132
|
return this.events.pipe(map(event => {
|
|
@@ -112,38 +137,56 @@ class FocusService {
|
|
|
112
137
|
return { element: el, origin: null };
|
|
113
138
|
}));
|
|
114
139
|
}
|
|
115
|
-
focus(node,
|
|
116
|
-
|
|
117
|
-
node.focus();
|
|
140
|
+
focus(node, origin) {
|
|
141
|
+
this.#setOrigin(node, origin);
|
|
142
|
+
coerceElement(node).focus();
|
|
118
143
|
}
|
|
119
144
|
queryFocusable(inside) {
|
|
120
|
-
return focusable(inside, { includeContainer: false });
|
|
145
|
+
return focusable(coerceElement(inside), { includeContainer: false });
|
|
121
146
|
}
|
|
122
147
|
getFirstFocusable(inside) {
|
|
123
148
|
return this.queryFocusable(inside)[0];
|
|
124
149
|
}
|
|
125
150
|
isFocusable(node) {
|
|
126
|
-
return isFocusable(node);
|
|
151
|
+
return isFocusable(coerceElement(node));
|
|
127
152
|
}
|
|
128
153
|
focusTrap(inside, deferCaptureElements = false) {
|
|
129
|
-
return this.#focusTrap.create(inside, deferCaptureElements);
|
|
154
|
+
return this.#focusTrap.create(coerceElement(inside), deferCaptureElements);
|
|
155
|
+
}
|
|
156
|
+
#setOrigin(el, origin) {
|
|
157
|
+
const target = coerceElement(el);
|
|
158
|
+
const map = this.#originOverrides.value;
|
|
159
|
+
map.set(target, origin);
|
|
160
|
+
this.#originOverrides.next(map);
|
|
130
161
|
}
|
|
131
|
-
|
|
162
|
+
#delOrigin(el) {
|
|
163
|
+
const target = coerceElement(el);
|
|
164
|
+
const map = this.#originOverrides.value;
|
|
165
|
+
map.delete(target);
|
|
166
|
+
this.#originOverrides.next(map);
|
|
167
|
+
}
|
|
168
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: FocusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
132
169
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: FocusService, providedIn: "root" }); }
|
|
133
170
|
}
|
|
134
171
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: FocusService, decorators: [{
|
|
135
172
|
type: Injectable,
|
|
136
173
|
args: [{ providedIn: "root" }]
|
|
137
|
-
}]
|
|
138
|
-
type: Inject,
|
|
139
|
-
args: [DOCUMENT]
|
|
140
|
-
}] }, { type: i0.NgZone, decorators: [{
|
|
141
|
-
type: Inject,
|
|
142
|
-
args: [NgZone]
|
|
143
|
-
}] }] });
|
|
174
|
+
}] });
|
|
144
175
|
function isActivityElement(activityEl, focused) {
|
|
145
176
|
return activityEl != null && focused != null && (activityEl === focused || focused.contains(activityEl));
|
|
146
177
|
}
|
|
178
|
+
function listener(doc, zone, type, options) {
|
|
179
|
+
return new Observable((dst) => zone.runOutsideAngular(() => {
|
|
180
|
+
const handler = (e) => {
|
|
181
|
+
dst.next(e.target);
|
|
182
|
+
};
|
|
183
|
+
document.addEventListener(type, handler, options);
|
|
184
|
+
return () => document.removeEventListener(type, handler, options);
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
function strictEq(a, b) {
|
|
188
|
+
return a === b;
|
|
189
|
+
}
|
|
147
190
|
|
|
148
191
|
// TODO: implement
|
|
149
192
|
class FocusTrap extends Destructible {
|
|
@@ -642,13 +685,14 @@ class GestureService {
|
|
|
642
685
|
this.#enableTouchAction();
|
|
643
686
|
}), filter(() => false)))),
|
|
644
687
|
// Select active watchers
|
|
645
|
-
map(
|
|
646
|
-
updatePointers(
|
|
647
|
-
const eventTarget =
|
|
688
|
+
map((detail) => {
|
|
689
|
+
updatePointers(detail);
|
|
690
|
+
const eventTarget = detail.origin.target;
|
|
691
|
+
detail.target = eventTarget;
|
|
648
692
|
let includeScrollDistance = false;
|
|
649
693
|
const gestures = this.#watchers
|
|
650
694
|
.filter(({ gesture, el }) => (el === eventTarget || ("contains" in el && el.contains(eventTarget))) &&
|
|
651
|
-
gesture.shouldCapture(
|
|
695
|
+
gesture.shouldCapture(detail))
|
|
652
696
|
.reduce((gestures, { gesture }) => {
|
|
653
697
|
if (!gestures.has(gesture)) {
|
|
654
698
|
includeScrollDistance = includeScrollDistance || gesture.includeScrollDistance;
|
|
@@ -656,7 +700,7 @@ class GestureService {
|
|
|
656
700
|
}
|
|
657
701
|
return gestures;
|
|
658
702
|
}, new Map());
|
|
659
|
-
return { detail:
|
|
703
|
+
return { detail: detail, gestures, includeScrollDistance };
|
|
660
704
|
}), filter(({ gestures }) => gestures.size > 0), tap(this.#disableTouchAction)
|
|
661
705
|
// finalize(() => console.log("FINALIZE BEGIN"))
|
|
662
706
|
)
|