@react-aria/landmark 3.0.0-alpha.4 → 3.0.0-alpha.6
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/import.mjs +407 -0
- package/dist/main.js +232 -101
- package/dist/main.js.map +1 -1
- package/dist/module.js +229 -102
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +12 -6
- package/src/index.ts +2 -2
- package/src/useLandmark.ts +261 -88
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-aria/landmark",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.6",
|
|
4
4
|
"description": "Spectrum UI components in React",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/main.js",
|
|
7
7
|
"module": "dist/module.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
"types": "./dist/types.d.ts",
|
|
10
|
+
"import": "./dist/import.mjs",
|
|
11
|
+
"require": "./dist/main.js"
|
|
12
|
+
},
|
|
8
13
|
"types": "dist/types.d.ts",
|
|
9
14
|
"source": "src/index.ts",
|
|
10
15
|
"files": [
|
|
@@ -17,10 +22,11 @@
|
|
|
17
22
|
"url": "https://github.com/adobe/react-spectrum"
|
|
18
23
|
},
|
|
19
24
|
"dependencies": {
|
|
20
|
-
"@
|
|
21
|
-
"@react-aria/
|
|
22
|
-
"@react-
|
|
23
|
-
"@
|
|
25
|
+
"@react-aria/focus": "^3.11.0",
|
|
26
|
+
"@react-aria/utils": "^3.15.0",
|
|
27
|
+
"@react-types/shared": "^3.17.0",
|
|
28
|
+
"@swc/helpers": "^0.4.14",
|
|
29
|
+
"use-sync-external-store": "^1.2.0"
|
|
24
30
|
},
|
|
25
31
|
"peerDependencies": {
|
|
26
32
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
|
|
@@ -28,5 +34,5 @@
|
|
|
28
34
|
"publishConfig": {
|
|
29
35
|
"access": "public"
|
|
30
36
|
},
|
|
31
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "a0efee84aa178cb1a202951dfd6d8de02b292307"
|
|
32
38
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,5 +10,5 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
export type {AriaLandmarkRole, AriaLandmarkProps, LandmarkAria} from './useLandmark';
|
|
14
|
-
export {useLandmark} from './useLandmark';
|
|
13
|
+
export type {AriaLandmarkRole, AriaLandmarkProps, LandmarkAria, LandmarkController} from './useLandmark';
|
|
14
|
+
export {useLandmark, createLandmarkController} from './useLandmark';
|
package/src/useLandmark.ts
CHANGED
|
@@ -13,81 +13,149 @@
|
|
|
13
13
|
import {AriaLabelingProps, DOMAttributes, FocusableElement} from '@react-types/shared';
|
|
14
14
|
import {MutableRefObject, useCallback, useEffect, useState} from 'react';
|
|
15
15
|
import {useLayoutEffect} from '@react-aria/utils';
|
|
16
|
+
import {useSyncExternalStore} from 'use-sync-external-store/shim';
|
|
16
17
|
|
|
17
18
|
export type AriaLandmarkRole = 'main' | 'region' | 'search' | 'navigation' | 'form' | 'banner' | 'contentinfo' | 'complementary';
|
|
18
19
|
|
|
19
20
|
export interface AriaLandmarkProps extends AriaLabelingProps {
|
|
20
|
-
role: AriaLandmarkRole
|
|
21
|
+
role: AriaLandmarkRole,
|
|
22
|
+
focus?: (direction: 'forward' | 'backward') => void
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export interface LandmarkAria {
|
|
24
26
|
landmarkProps: DOMAttributes
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
// Increment this version number whenever the
|
|
30
|
+
// LandmarkManagerApi or Landmark interfaces change.
|
|
31
|
+
const LANDMARK_API_VERSION = 1;
|
|
32
|
+
|
|
33
|
+
// Minimal API for LandmarkManager that must continue to work between versions.
|
|
34
|
+
// Changes to this interface are considered breaking. New methods/properties are
|
|
35
|
+
// safe to add, but changes or removals are not allowed (same as public APIs).
|
|
36
|
+
interface LandmarkManagerApi {
|
|
37
|
+
version: number,
|
|
38
|
+
createLandmarkController(): LandmarkController,
|
|
39
|
+
registerLandmark(landmark: Landmark): () => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Changes to this interface are considered breaking.
|
|
43
|
+
// New properties MUST be optional so that registering a landmark
|
|
44
|
+
// from an older version of useLandmark against a newer version of
|
|
45
|
+
// LandmarkManager does not crash.
|
|
46
|
+
interface Landmark {
|
|
28
47
|
ref: MutableRefObject<Element>,
|
|
29
48
|
role: AriaLandmarkRole,
|
|
30
49
|
label?: string,
|
|
31
50
|
lastFocused?: FocusableElement,
|
|
32
|
-
focus: () => void,
|
|
51
|
+
focus: (direction: 'forward' | 'backward') => void,
|
|
33
52
|
blur: () => void
|
|
34
|
-
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface LandmarkControllerOptions {
|
|
56
|
+
/**
|
|
57
|
+
* The element from which to start navigating.
|
|
58
|
+
* @default document.activeElement
|
|
59
|
+
*/
|
|
60
|
+
from?: Element
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A LandmarkController allows programmatic navigation of landmarks. */
|
|
64
|
+
export interface LandmarkController {
|
|
65
|
+
/** Moves focus to the next landmark. */
|
|
66
|
+
focusNext(opts?: LandmarkControllerOptions): boolean,
|
|
67
|
+
/** Moves focus to the previous landmark. */
|
|
68
|
+
focusPrevious(opts?: LandmarkControllerOptions): boolean,
|
|
69
|
+
/** Moves focus to the main landmark. */
|
|
70
|
+
focusMain(): boolean,
|
|
71
|
+
/** Moves focus either forward or backward in the landmark sequence. */
|
|
72
|
+
navigate(direction: 'forward' | 'backward', opts?: LandmarkControllerOptions): boolean,
|
|
73
|
+
/**
|
|
74
|
+
* Disposes the landmark controller. When no landmarks are registered, and no
|
|
75
|
+
* controllers are active, the landmark keyboard listeners are removed from the page.
|
|
76
|
+
*/
|
|
77
|
+
dispose(): void
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Symbol under which the singleton landmark manager instance is attached to the document.
|
|
81
|
+
const landmarkSymbol = Symbol.for('react-aria-landmark-manager');
|
|
82
|
+
|
|
83
|
+
function subscribe(fn: () => void) {
|
|
84
|
+
document.addEventListener('react-aria-landmark-manager-change', fn);
|
|
85
|
+
return () => document.removeEventListener('react-aria-landmark-manager-change', fn);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getLandmarkManager(): LandmarkManagerApi {
|
|
89
|
+
// Reuse an existing instance if it has the same or greater version.
|
|
90
|
+
let instance = document[landmarkSymbol];
|
|
91
|
+
if (instance && instance.version >= LANDMARK_API_VERSION) {
|
|
92
|
+
return instance;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Otherwise, create a new instance and dispatch an event so anything using the existing
|
|
96
|
+
// instance updates and re-registers their landmarks with the new one.
|
|
97
|
+
document[landmarkSymbol] = new LandmarkManager();
|
|
98
|
+
document.dispatchEvent(new CustomEvent('react-aria-landmark-manager-change'));
|
|
99
|
+
return document[landmarkSymbol];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Subscribes a React component to the current landmark manager instance.
|
|
103
|
+
function useLandmarkManager(): LandmarkManagerApi {
|
|
104
|
+
return useSyncExternalStore(subscribe, getLandmarkManager);
|
|
105
|
+
}
|
|
35
106
|
|
|
36
|
-
class LandmarkManager {
|
|
107
|
+
class LandmarkManager implements LandmarkManagerApi {
|
|
37
108
|
private landmarks: Array<Landmark> = [];
|
|
38
|
-
private static instance: LandmarkManager;
|
|
39
109
|
private isListening = false;
|
|
110
|
+
private refCount = 0;
|
|
111
|
+
public version = LANDMARK_API_VERSION;
|
|
40
112
|
|
|
41
|
-
|
|
113
|
+
constructor() {
|
|
42
114
|
this.f6Handler = this.f6Handler.bind(this);
|
|
43
115
|
this.focusinHandler = this.focusinHandler.bind(this);
|
|
44
116
|
this.focusoutHandler = this.focusoutHandler.bind(this);
|
|
45
117
|
}
|
|
46
118
|
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
|
|
119
|
+
private setupIfNeeded() {
|
|
120
|
+
if (this.isListening) {
|
|
121
|
+
return;
|
|
50
122
|
}
|
|
51
|
-
|
|
52
|
-
return LandmarkManager.instance;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
private setup() {
|
|
56
123
|
document.addEventListener('keydown', this.f6Handler, {capture: true});
|
|
57
124
|
document.addEventListener('focusin', this.focusinHandler, {capture: true});
|
|
58
125
|
document.addEventListener('focusout', this.focusoutHandler, {capture: true});
|
|
59
126
|
this.isListening = true;
|
|
60
127
|
}
|
|
61
128
|
|
|
62
|
-
private
|
|
129
|
+
private teardownIfNeeded() {
|
|
130
|
+
if (!this.isListening || this.landmarks.length > 0 || this.refCount > 0) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
63
133
|
document.removeEventListener('keydown', this.f6Handler, {capture: true});
|
|
64
134
|
document.removeEventListener('focusin', this.focusinHandler, {capture: true});
|
|
65
135
|
document.removeEventListener('focusout', this.focusoutHandler, {capture: true});
|
|
66
136
|
this.isListening = false;
|
|
67
137
|
}
|
|
68
138
|
|
|
69
|
-
private focusLandmark(landmark: Element) {
|
|
70
|
-
this.landmarks.find(l => l.ref.current === landmark)?.focus();
|
|
139
|
+
private focusLandmark(landmark: Element, direction: 'forward' | 'backward') {
|
|
140
|
+
this.landmarks.find(l => l.ref.current === landmark)?.focus(direction);
|
|
71
141
|
}
|
|
72
142
|
|
|
73
143
|
/**
|
|
74
144
|
* Return set of landmarks with a specific role.
|
|
75
145
|
*/
|
|
76
|
-
|
|
146
|
+
private getLandmarksByRole(role: AriaLandmarkRole) {
|
|
77
147
|
return new Set(this.landmarks.filter(l => l.role === role));
|
|
78
148
|
}
|
|
79
149
|
|
|
80
150
|
/**
|
|
81
151
|
* Return first landmark with a specific role.
|
|
82
152
|
*/
|
|
83
|
-
|
|
153
|
+
private getLandmarkByRole(role: AriaLandmarkRole) {
|
|
84
154
|
return this.landmarks.find(l => l.role === role);
|
|
85
155
|
}
|
|
86
156
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
this.setup();
|
|
90
|
-
}
|
|
157
|
+
private addLandmark(newLandmark: Landmark) {
|
|
158
|
+
this.setupIfNeeded();
|
|
91
159
|
if (this.landmarks.find(landmark => landmark.ref === newLandmark.ref)) {
|
|
92
160
|
return;
|
|
93
161
|
}
|
|
@@ -98,6 +166,7 @@ class LandmarkManager {
|
|
|
98
166
|
|
|
99
167
|
if (this.landmarks.length === 0) {
|
|
100
168
|
this.landmarks = [newLandmark];
|
|
169
|
+
this.checkLabels(newLandmark.role);
|
|
101
170
|
return;
|
|
102
171
|
}
|
|
103
172
|
|
|
@@ -119,9 +188,10 @@ class LandmarkManager {
|
|
|
119
188
|
}
|
|
120
189
|
|
|
121
190
|
this.landmarks.splice(start, 0, newLandmark);
|
|
191
|
+
this.checkLabels(newLandmark.role);
|
|
122
192
|
}
|
|
123
193
|
|
|
124
|
-
|
|
194
|
+
private updateLandmark(landmark: Pick<Landmark, 'ref'> & Partial<Landmark>) {
|
|
125
195
|
let index = this.landmarks.findIndex(l => l.ref === landmark.ref);
|
|
126
196
|
if (index >= 0) {
|
|
127
197
|
this.landmarks[index] = {...this.landmarks[index], ...landmark};
|
|
@@ -129,11 +199,9 @@ class LandmarkManager {
|
|
|
129
199
|
}
|
|
130
200
|
}
|
|
131
201
|
|
|
132
|
-
|
|
202
|
+
private removeLandmark(ref: MutableRefObject<Element>) {
|
|
133
203
|
this.landmarks = this.landmarks.filter(landmark => landmark.ref !== ref);
|
|
134
|
-
|
|
135
|
-
this.teardown();
|
|
136
|
-
}
|
|
204
|
+
this.teardownIfNeeded();
|
|
137
205
|
}
|
|
138
206
|
|
|
139
207
|
/**
|
|
@@ -172,7 +240,7 @@ class LandmarkManager {
|
|
|
172
240
|
private closestLandmark(element: Element) {
|
|
173
241
|
let landmarkMap = new Map(this.landmarks.map(l => [l.ref.current, l]));
|
|
174
242
|
let currentElement = element;
|
|
175
|
-
while (!landmarkMap.has(currentElement) && currentElement !== document.body) {
|
|
243
|
+
while (currentElement && !landmarkMap.has(currentElement) && currentElement !== document.body) {
|
|
176
244
|
currentElement = currentElement.parentElement;
|
|
177
245
|
}
|
|
178
246
|
return landmarkMap.get(currentElement);
|
|
@@ -184,22 +252,52 @@ class LandmarkManager {
|
|
|
184
252
|
* If not inside a landmark, will return first landmark.
|
|
185
253
|
* Returns undefined if there are no landmarks.
|
|
186
254
|
*/
|
|
187
|
-
|
|
188
|
-
if (this.landmarks.length === 0) {
|
|
189
|
-
return undefined;
|
|
190
|
-
}
|
|
191
|
-
|
|
255
|
+
private getNextLandmark(element: Element, {backward}: {backward?: boolean }) {
|
|
192
256
|
let currentLandmark = this.closestLandmark(element);
|
|
193
|
-
let nextLandmarkIndex = backward ? -1 : 0;
|
|
257
|
+
let nextLandmarkIndex = backward ? this.landmarks.length - 1 : 0;
|
|
194
258
|
if (currentLandmark) {
|
|
195
|
-
nextLandmarkIndex = this.landmarks.
|
|
259
|
+
nextLandmarkIndex = this.landmarks.indexOf(currentLandmark) + (backward ? -1 : 1);
|
|
196
260
|
}
|
|
197
261
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
262
|
+
let wrapIfNeeded = () => {
|
|
263
|
+
// When we reach the end of the landmark sequence, fire a custom event that can be listened for by applications.
|
|
264
|
+
// If this event is canceled, we return immediately. This can be used to implement landmark navigation across iframes.
|
|
265
|
+
if (nextLandmarkIndex < 0) {
|
|
266
|
+
if (!element.dispatchEvent(new CustomEvent('react-aria-landmark-navigation', {detail: {direction: 'backward'}, bubbles: true, cancelable: true}))) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
nextLandmarkIndex = this.landmarks.length - 1;
|
|
271
|
+
} else if (nextLandmarkIndex >= this.landmarks.length) {
|
|
272
|
+
if (!element.dispatchEvent(new CustomEvent('react-aria-landmark-navigation', {detail: {direction: 'forward'}, bubbles: true, cancelable: true}))) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
nextLandmarkIndex = 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (nextLandmarkIndex < 0 || nextLandmarkIndex >= this.landmarks.length) {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return false;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
if (wrapIfNeeded()) {
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Skip over hidden landmarks.
|
|
291
|
+
let i = nextLandmarkIndex;
|
|
292
|
+
while (this.landmarks[nextLandmarkIndex].ref.current.closest('[aria-hidden=true]')) {
|
|
293
|
+
nextLandmarkIndex += backward ? -1 : 1;
|
|
294
|
+
if (wrapIfNeeded()) {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (nextLandmarkIndex === i) {
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
203
301
|
}
|
|
204
302
|
|
|
205
303
|
return this.landmarks[nextLandmarkIndex];
|
|
@@ -210,49 +308,59 @@ class LandmarkManager {
|
|
|
210
308
|
* If not, focus the landmark itself.
|
|
211
309
|
* If no landmarks at all, or none with focusable elements, don't move focus.
|
|
212
310
|
*/
|
|
213
|
-
|
|
311
|
+
private f6Handler(e: KeyboardEvent) {
|
|
214
312
|
if (e.key === 'F6') {
|
|
215
|
-
|
|
216
|
-
e.
|
|
313
|
+
// If alt key pressed, focus main landmark, otherwise navigate forward or backward based on shift key.
|
|
314
|
+
let handled = e.altKey ? this.focusMain() : this.navigate(e.target as Element, e.shiftKey);
|
|
315
|
+
if (handled) {
|
|
316
|
+
e.preventDefault();
|
|
317
|
+
e.stopPropagation();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
217
321
|
|
|
218
|
-
|
|
219
|
-
|
|
322
|
+
private focusMain() {
|
|
323
|
+
let main = this.getLandmarkByRole('main');
|
|
324
|
+
if (main && document.contains(main.ref.current)) {
|
|
325
|
+
this.focusLandmark(main.ref.current, 'forward');
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
220
328
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
225
331
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
this.focusLandmark(main.ref.current);
|
|
231
|
-
}
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
332
|
+
private navigate(from: Element, backward: boolean) {
|
|
333
|
+
let nextLandmark = this.getNextLandmark(from, {
|
|
334
|
+
backward
|
|
335
|
+
});
|
|
234
336
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (document.body.contains(lastFocused)) {
|
|
239
|
-
lastFocused.focus();
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
337
|
+
if (!nextLandmark) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
243
340
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
341
|
+
// If something was previously focused in the next landmark, then return focus to it
|
|
342
|
+
if (nextLandmark.lastFocused) {
|
|
343
|
+
let lastFocused = nextLandmark.lastFocused;
|
|
344
|
+
if (document.body.contains(lastFocused)) {
|
|
345
|
+
lastFocused.focus();
|
|
346
|
+
return true;
|
|
247
347
|
}
|
|
248
348
|
}
|
|
349
|
+
|
|
350
|
+
// Otherwise, focus the landmark itself
|
|
351
|
+
if (document.contains(nextLandmark.ref.current)) {
|
|
352
|
+
this.focusLandmark(nextLandmark.ref.current, backward ? 'backward' : 'forward');
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return false;
|
|
249
357
|
}
|
|
250
358
|
|
|
251
359
|
/**
|
|
252
360
|
* Sets lastFocused for a landmark, if focus is moved within that landmark.
|
|
253
361
|
* Lets the last focused landmark know it was blurred if something else is focused.
|
|
254
362
|
*/
|
|
255
|
-
|
|
363
|
+
private focusinHandler(e: FocusEvent) {
|
|
256
364
|
let currentLandmark = this.closestLandmark(e.target as Element);
|
|
257
365
|
if (currentLandmark && currentLandmark.ref.current !== e.target) {
|
|
258
366
|
this.updateLandmark({ref: currentLandmark.ref, lastFocused: e.target as FocusableElement});
|
|
@@ -269,7 +377,7 @@ class LandmarkManager {
|
|
|
269
377
|
/**
|
|
270
378
|
* Track if the focus is lost to the body. If it is, do cleanup on the landmark that last had focus.
|
|
271
379
|
*/
|
|
272
|
-
|
|
380
|
+
private focusoutHandler(e: FocusEvent) {
|
|
273
381
|
let previousFocusedElement = e.target as Element;
|
|
274
382
|
let nextFocusedElement = e.relatedTarget;
|
|
275
383
|
// the === document seems to be a jest thing for focus to go there on generic blur event such as landmark.blur();
|
|
@@ -281,6 +389,78 @@ class LandmarkManager {
|
|
|
281
389
|
}
|
|
282
390
|
}
|
|
283
391
|
}
|
|
392
|
+
|
|
393
|
+
public createLandmarkController(): LandmarkController {
|
|
394
|
+
let instance = this;
|
|
395
|
+
instance.refCount++;
|
|
396
|
+
instance.setupIfNeeded();
|
|
397
|
+
return {
|
|
398
|
+
navigate(direction, opts) {
|
|
399
|
+
return instance.navigate(opts?.from || document.activeElement, direction === 'backward');
|
|
400
|
+
},
|
|
401
|
+
focusNext(opts) {
|
|
402
|
+
return instance.navigate(opts?.from || document.activeElement, false);
|
|
403
|
+
},
|
|
404
|
+
focusPrevious(opts) {
|
|
405
|
+
return instance.navigate(opts?.from || document.activeElement, true);
|
|
406
|
+
},
|
|
407
|
+
focusMain() {
|
|
408
|
+
return instance.focusMain();
|
|
409
|
+
},
|
|
410
|
+
dispose() {
|
|
411
|
+
instance.refCount--;
|
|
412
|
+
instance.teardownIfNeeded();
|
|
413
|
+
instance = null;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
public registerLandmark(landmark: Landmark): () => void {
|
|
419
|
+
if (this.landmarks.find(l => l.ref === landmark.ref)) {
|
|
420
|
+
this.updateLandmark(landmark);
|
|
421
|
+
} else {
|
|
422
|
+
this.addLandmark(landmark);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return () => this.removeLandmark(landmark.ref);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Creates a LandmarkController, which allows programmatic navigation of landmarks. */
|
|
430
|
+
export function createLandmarkController(): LandmarkController {
|
|
431
|
+
// Get the current landmark manager and create a controller using it.
|
|
432
|
+
let instance = getLandmarkManager();
|
|
433
|
+
let controller = instance.createLandmarkController();
|
|
434
|
+
|
|
435
|
+
let unsubscribe = subscribe(() => {
|
|
436
|
+
// If the landmark manager changes, dispose the old
|
|
437
|
+
// controller and create a new one.
|
|
438
|
+
controller.dispose();
|
|
439
|
+
instance = getLandmarkManager();
|
|
440
|
+
controller = instance.createLandmarkController();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Return a wrapper that proxies requests to the current controller instance.
|
|
444
|
+
return {
|
|
445
|
+
navigate(direction, opts) {
|
|
446
|
+
return controller.navigate(direction, opts);
|
|
447
|
+
},
|
|
448
|
+
focusNext(opts) {
|
|
449
|
+
return controller.focusNext(opts);
|
|
450
|
+
},
|
|
451
|
+
focusPrevious(opts) {
|
|
452
|
+
return controller.focusPrevious(opts);
|
|
453
|
+
},
|
|
454
|
+
focusMain() {
|
|
455
|
+
return controller.focusMain();
|
|
456
|
+
},
|
|
457
|
+
dispose() {
|
|
458
|
+
controller.dispose();
|
|
459
|
+
unsubscribe();
|
|
460
|
+
controller = null;
|
|
461
|
+
instance = null;
|
|
462
|
+
}
|
|
463
|
+
};
|
|
284
464
|
}
|
|
285
465
|
|
|
286
466
|
/**
|
|
@@ -292,13 +472,14 @@ export function useLandmark(props: AriaLandmarkProps, ref: MutableRefObject<Focu
|
|
|
292
472
|
const {
|
|
293
473
|
role,
|
|
294
474
|
'aria-label': ariaLabel,
|
|
295
|
-
'aria-labelledby': ariaLabelledby
|
|
475
|
+
'aria-labelledby': ariaLabelledby,
|
|
476
|
+
focus
|
|
296
477
|
} = props;
|
|
297
|
-
let manager =
|
|
478
|
+
let manager = useLandmarkManager();
|
|
298
479
|
let label = ariaLabel || ariaLabelledby;
|
|
299
480
|
let [isLandmarkFocused, setIsLandmarkFocused] = useState(false);
|
|
300
481
|
|
|
301
|
-
let
|
|
482
|
+
let defaultFocus = useCallback(() => {
|
|
302
483
|
setIsLandmarkFocused(true);
|
|
303
484
|
}, [setIsLandmarkFocused]);
|
|
304
485
|
|
|
@@ -307,18 +488,8 @@ export function useLandmark(props: AriaLandmarkProps, ref: MutableRefObject<Focu
|
|
|
307
488
|
}, [setIsLandmarkFocused]);
|
|
308
489
|
|
|
309
490
|
useLayoutEffect(() => {
|
|
310
|
-
manager.
|
|
311
|
-
|
|
312
|
-
return () => {
|
|
313
|
-
manager.removeLandmark(ref);
|
|
314
|
-
};
|
|
315
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
316
|
-
}, []);
|
|
317
|
-
|
|
318
|
-
useLayoutEffect(() => {
|
|
319
|
-
manager.updateLandmark({ref, label, role, focus, blur});
|
|
320
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
321
|
-
}, [label, ref, role]);
|
|
491
|
+
return manager.registerLandmark({ref, label, role, focus: focus || defaultFocus, blur});
|
|
492
|
+
}, [manager, label, ref, role, focus, defaultFocus, blur]);
|
|
322
493
|
|
|
323
494
|
useEffect(() => {
|
|
324
495
|
if (isLandmarkFocused) {
|
|
@@ -329,7 +500,9 @@ export function useLandmark(props: AriaLandmarkProps, ref: MutableRefObject<Focu
|
|
|
329
500
|
return {
|
|
330
501
|
landmarkProps: {
|
|
331
502
|
role,
|
|
332
|
-
tabIndex: isLandmarkFocused ? -1 : undefined
|
|
503
|
+
tabIndex: isLandmarkFocused ? -1 : undefined,
|
|
504
|
+
'aria-label': ariaLabel,
|
|
505
|
+
'aria-labelledby': ariaLabelledby
|
|
333
506
|
}
|
|
334
507
|
};
|
|
335
508
|
}
|