@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.
@@ -1,12 +1,12 @@
1
1
  import { DOCUMENT } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { inject, NgZone, Injectable, Inject, 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, combineLatest, BehaviorSubject, of, debounceTime, Observable, EMPTY, from, tap, Subject, connect, scan, takeWhile, finalize, animationFrames } from 'rxjs';
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
- #activity = inject(ActivityService);
62
- #focusTrap = inject(FocusTrapFactory);
63
- constructor(document, zone) {
64
- this.events = zone.runOutsideAngular(() => {
65
- const focus = fromEvent(document, "focus", EVENT_OPTIONS).pipe(map(e => e.target));
66
- const blur = fromEvent(document, "blur", EVENT_OPTIONS).pipe(startWith(null), map(e => e?.target || null));
67
- return combineLatest({
68
- activity: this.#activity.events$.pipe(filter(event => event.type !== "mousemove"), distinctUntilChanged((prev, curr) => {
69
- if (prev && curr) {
70
- return prev.origin === curr.origin && prev.node === curr.node;
71
- }
72
- else {
73
- return false;
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
- if (isActivityElement(activity.node, focus)) {
97
- return { origin: activity.origin, element: focus };
98
- }
99
- else {
100
- return { origin: "program", element: focus };
101
- }
102
- }), filter(v => !!v), shareReplay(1));
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, _origin) {
116
- // TODO: focus origin
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
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: FocusService, deps: [{ token: DOCUMENT }, { token: NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); }
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
- }], ctorParameters: () => [{ type: Document, decorators: [{
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(event => {
646
- updatePointers(event);
647
- const eventTarget = event.origin.target;
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(event))
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: event, gestures, includeScrollDistance };
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
  )