@proyecto-viviana/solidaria 0.2.4 → 0.2.8
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/LICENSE +21 -0
- package/dist/actiongroup/createActionGroup.d.ts +29 -0
- package/dist/actiongroup/createActionGroup.d.ts.map +1 -0
- package/dist/actiongroup/index.d.ts +2 -0
- package/dist/actiongroup/index.d.ts.map +1 -0
- package/dist/autocomplete/createAutocomplete.d.ts +6 -2
- package/dist/autocomplete/createAutocomplete.d.ts.map +1 -1
- package/dist/breadcrumbs/createBreadcrumbs.d.ts +2 -0
- package/dist/breadcrumbs/createBreadcrumbs.d.ts.map +1 -1
- package/dist/button/createToggleButtonGroup.d.ts +32 -0
- package/dist/button/createToggleButtonGroup.d.ts.map +1 -0
- package/dist/button/index.d.ts +2 -0
- package/dist/button/index.d.ts.map +1 -1
- package/dist/calendar/createCalendarCell.d.ts +2 -0
- package/dist/calendar/createCalendarCell.d.ts.map +1 -1
- package/dist/calendar/createCalendarGrid.d.ts.map +1 -1
- package/dist/calendar/createRangeCalendarCell.d.ts +3 -1
- package/dist/calendar/createRangeCalendarCell.d.ts.map +1 -1
- package/dist/checkbox/createCheckboxGroup.d.ts +5 -1
- package/dist/checkbox/createCheckboxGroup.d.ts.map +1 -1
- package/dist/collections/index.d.ts +56 -0
- package/dist/collections/index.d.ts.map +1 -0
- package/dist/color/createColorArea.d.ts.map +1 -1
- package/dist/color/createColorSlider.d.ts.map +1 -1
- package/dist/color/createColorWheel.d.ts.map +1 -1
- package/dist/combobox/createComboBox.d.ts +6 -0
- package/dist/combobox/createComboBox.d.ts.map +1 -1
- package/dist/datepicker/createDatePicker.d.ts +6 -0
- package/dist/datepicker/createDatePicker.d.ts.map +1 -1
- package/dist/datepicker/createDateRangePicker.d.ts +40 -0
- package/dist/datepicker/createDateRangePicker.d.ts.map +1 -0
- package/dist/datepicker/createDateSegment.d.ts +1 -1
- package/dist/datepicker/createDateSegment.d.ts.map +1 -1
- package/dist/datepicker/createTimeSegment.d.ts +29 -0
- package/dist/datepicker/createTimeSegment.d.ts.map +1 -0
- package/dist/datepicker/index.d.ts +2 -0
- package/dist/datepicker/index.d.ts.map +1 -1
- package/dist/disclosure/createDisclosureGroup.d.ts +2 -1
- package/dist/disclosure/createDisclosureGroup.d.ts.map +1 -1
- package/dist/dnd/createDrag.d.ts.map +1 -1
- package/dist/dnd/createDraggableCollection.d.ts +4 -0
- package/dist/dnd/createDraggableCollection.d.ts.map +1 -1
- package/dist/dnd/createDraggableItem.d.ts.map +1 -1
- package/dist/dnd/createDrop.d.ts.map +1 -1
- package/dist/dnd/createDroppableCollection.d.ts +32 -1
- package/dist/dnd/createDroppableCollection.d.ts.map +1 -1
- package/dist/dnd/createDroppableItem.d.ts.map +1 -1
- package/dist/dnd/index.d.ts +1 -1
- package/dist/dnd/index.d.ts.map +1 -1
- package/dist/grid/createGrid.d.ts.map +1 -1
- package/dist/gridlist/createGridList.d.ts.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4659 -3452
- package/dist/index.js.map +1 -7
- package/dist/index.ssr.js +4659 -3452
- package/dist/index.ssr.js.map +1 -7
- package/dist/interactions/createFocus.d.ts.map +1 -1
- package/dist/interactions/createFocusWithin.d.ts.map +1 -1
- package/dist/link/createLink.d.ts +10 -0
- package/dist/link/createLink.d.ts.map +1 -1
- package/dist/listbox/createListBox.d.ts +1 -0
- package/dist/listbox/createListBox.d.ts.map +1 -1
- package/dist/listbox/createOption.d.ts.map +1 -1
- package/dist/menu/createMenu.d.ts +1 -0
- package/dist/menu/createMenu.d.ts.map +1 -1
- package/dist/meter/createMeter.d.ts.map +1 -1
- package/dist/numberfield/createNumberField.d.ts +18 -0
- package/dist/numberfield/createNumberField.d.ts.map +1 -1
- package/dist/overlays/createModal.d.ts +16 -0
- package/dist/overlays/createModal.d.ts.map +1 -1
- package/dist/overlays/createOverlay.d.ts.map +1 -1
- package/dist/overlays/index.d.ts +1 -1
- package/dist/overlays/index.d.ts.map +1 -1
- package/dist/popover/createOverlayPosition.d.ts.map +1 -1
- package/dist/popover/createPopover.d.ts.map +1 -1
- package/dist/progress/createProgressBar.d.ts.map +1 -1
- package/dist/radio/createRadioGroup.d.ts +2 -2
- package/dist/radio/createRadioGroup.d.ts.map +1 -1
- package/dist/searchfield/createSearchField.d.ts.map +1 -1
- package/dist/select/createHiddenSelect.d.ts.map +1 -1
- package/dist/select/createSelect.d.ts.map +1 -1
- package/dist/slider/createSlider.d.ts.map +1 -1
- package/dist/table/createTable.d.ts.map +1 -1
- package/dist/tabs/createTabs.d.ts +1 -1
- package/dist/tabs/createTabs.d.ts.map +1 -1
- package/dist/tag/createTag.d.ts.map +1 -1
- package/dist/tag/createTagGroup.d.ts.map +1 -1
- package/dist/toast/createToast.d.ts +4 -0
- package/dist/toast/createToast.d.ts.map +1 -1
- package/dist/toast/createToastRegion.d.ts.map +1 -1
- package/dist/toolbar/createToolbar.d.ts.map +1 -1
- package/dist/tooltip/createTooltipTrigger.d.ts.map +1 -1
- package/dist/tree/createTree.d.ts.map +1 -1
- package/dist/tree/createTreeItem.d.ts.map +1 -1
- package/dist/tree/types.d.ts +4 -0
- package/dist/tree/types.d.ts.map +1 -1
- package/dist/utils/env.d.ts +1 -1
- package/dist/utils/env.d.ts.map +1 -1
- package/dist/utils/platform.d.ts.map +1 -1
- package/dist/visually-hidden/createVisuallyHidden.d.ts.map +1 -1
- package/package.json +8 -6
- package/src/actiongroup/createActionGroup.ts +324 -0
- package/src/actiongroup/index.ts +8 -0
- package/src/autocomplete/createAutocomplete.ts +32 -9
- package/src/breadcrumbs/createBreadcrumbs.ts +10 -15
- package/src/button/createButton.ts +1 -1
- package/src/button/createToggleButtonGroup.ts +128 -0
- package/src/button/index.ts +9 -0
- package/src/calendar/createCalendarCell.ts +6 -4
- package/src/calendar/createCalendarGrid.ts +27 -18
- package/src/calendar/createRangeCalendarCell.ts +26 -9
- package/src/checkbox/createCheckboxGroup.ts +21 -4
- package/src/collections/index.ts +242 -0
- package/src/color/createColorArea.ts +380 -314
- package/src/color/createColorField.ts +137 -137
- package/src/color/createColorSlider.ts +286 -197
- package/src/color/createColorSwatch.ts +40 -40
- package/src/color/createColorWheel.ts +218 -208
- package/src/color/index.ts +24 -24
- package/src/color/types.ts +116 -116
- package/src/combobox/createComboBox.ts +670 -647
- package/src/combobox/index.ts +6 -6
- package/src/datepicker/createDatePicker.ts +54 -16
- package/src/datepicker/createDateRangePicker.ts +246 -0
- package/src/datepicker/createDateSegment.ts +185 -31
- package/src/datepicker/createTimeSegment.ts +370 -0
- package/src/datepicker/index.ts +14 -0
- package/src/dialog/createDialog.ts +120 -120
- package/src/dialog/index.ts +2 -2
- package/src/dialog/types.ts +19 -19
- package/src/disclosure/createDisclosureGroup.ts +5 -2
- package/src/dnd/createDrag.ts +224 -209
- package/src/dnd/createDraggableCollection.ts +96 -63
- package/src/dnd/createDraggableItem.ts +259 -243
- package/src/dnd/createDrop.ts +322 -321
- package/src/dnd/createDroppableCollection.ts +682 -293
- package/src/dnd/createDroppableItem.ts +215 -213
- package/src/dnd/index.ts +55 -47
- package/src/dnd/types.ts +89 -89
- package/src/dnd/utils.ts +294 -294
- package/src/focus/createAutoFocus.ts +321 -321
- package/src/focus/createFocusRestore.ts +313 -313
- package/src/focus/createVirtualFocus.ts +396 -396
- package/src/form/createFormValidation.ts +224 -224
- package/src/form/index.ts +11 -11
- package/src/grid/createGrid.ts +3 -1
- package/src/gridlist/createGridList.ts +16 -0
- package/src/gridlist/createGridListItem.ts +1 -1
- package/src/i18n/NumberFormatter.ts +266 -266
- package/src/i18n/createCollator.ts +79 -79
- package/src/i18n/createDateFormatter.ts +83 -83
- package/src/i18n/createFilter.ts +131 -131
- package/src/i18n/createNumberFormatter.ts +52 -52
- package/src/i18n/index.ts +40 -40
- package/src/i18n/locale.tsx +188 -188
- package/src/i18n/utils.ts +99 -99
- package/src/index.ts +51 -0
- package/src/interactions/createFocus.ts +6 -5
- package/src/interactions/createFocusWithin.ts +6 -5
- package/src/interactions/createLongPress.ts +174 -174
- package/src/interactions/createMove.ts +289 -289
- package/src/interactions/createPress.ts +5 -5
- package/src/landmark/createLandmark.ts +377 -377
- package/src/landmark/index.ts +8 -8
- package/src/link/createLink.ts +23 -8
- package/src/listbox/createListBox.ts +308 -269
- package/src/listbox/createOption.ts +162 -151
- package/src/listbox/index.ts +12 -12
- package/src/live-announcer/announce.ts +322 -322
- package/src/live-announcer/index.ts +9 -9
- package/src/menu/createMenu.ts +405 -396
- package/src/menu/createMenuItem.ts +149 -149
- package/src/menu/createMenuTrigger.ts +88 -88
- package/src/menu/index.ts +18 -18
- package/src/meter/createMeter.ts +1 -6
- package/src/numberfield/createNumberField.ts +311 -268
- package/src/numberfield/index.ts +5 -5
- package/src/overlays/ariaHideOutside.ts +219 -219
- package/src/overlays/createInteractOutside.ts +149 -149
- package/src/overlays/createModal.tsx +238 -202
- package/src/overlays/createOverlay.ts +165 -155
- package/src/overlays/createOverlayTrigger.ts +85 -85
- package/src/overlays/createPreventScroll.ts +266 -266
- package/src/overlays/index.ts +48 -44
- package/src/popover/calculatePosition.ts +6 -6
- package/src/popover/createOverlayPosition.ts +7 -4
- package/src/popover/createPopover.ts +21 -7
- package/src/progress/createProgressBar.ts +6 -1
- package/src/radio/createRadioGroup.ts +88 -14
- package/src/searchfield/createSearchField.ts +241 -186
- package/src/searchfield/index.ts +2 -2
- package/src/select/createHiddenSelect.tsx +263 -236
- package/src/select/createSelect.ts +373 -395
- package/src/select/index.ts +14 -14
- package/src/slider/createSlider.ts +364 -349
- package/src/slider/index.ts +2 -2
- package/src/ssr/index.tsx +370 -370
- package/src/table/createTable.ts +3 -1
- package/src/table/createTableColumnHeader.ts +1 -1
- package/src/table/createTableRow.ts +1 -1
- package/src/tabs/createTabs.ts +80 -51
- package/src/tag/createTag.ts +135 -6
- package/src/tag/createTagGroup.ts +7 -2
- package/src/toast/createToast.ts +8 -2
- package/src/toast/createToastRegion.ts +0 -1
- package/src/toolbar/createToolbar.ts +75 -1
- package/src/tooltip/createTooltip.ts +79 -79
- package/src/tooltip/createTooltipTrigger.ts +226 -222
- package/src/tooltip/index.ts +6 -6
- package/src/tree/createTree.ts +261 -246
- package/src/tree/createTreeItem.ts +282 -233
- package/src/tree/createTreeSelectionCheckbox.ts +68 -68
- package/src/tree/index.ts +16 -16
- package/src/tree/types.ts +91 -87
- package/src/utils/env.ts +55 -54
- package/src/utils/platform.ts +16 -6
- package/src/visually-hidden/createVisuallyHidden.ts +139 -124
- package/src/visually-hidden/index.ts +6 -6
|
@@ -1,377 +1,377 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* createLandmark - SolidJS implementation of React Aria's useLandmark
|
|
3
|
-
*
|
|
4
|
-
* Provides landmark navigation in an application. Call this with a role and label
|
|
5
|
-
* to register a landmark navigable with the F6 key.
|
|
6
|
-
*
|
|
7
|
-
* ARIA landmarks help screen reader users navigate between major sections of a page.
|
|
8
|
-
* The F6 key (or Shift+F6) cycles through all registered landmarks.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { JSX, Accessor } from 'solid-js';
|
|
12
|
-
import { createEffect, onCleanup } from 'solid-js';
|
|
13
|
-
import { access, type MaybeAccessor } from '../utils';
|
|
14
|
-
import { filterDOMProps } from '../utils';
|
|
15
|
-
|
|
16
|
-
// ============================================
|
|
17
|
-
// TYPES
|
|
18
|
-
// ============================================
|
|
19
|
-
|
|
20
|
-
/** ARIA landmark roles */
|
|
21
|
-
export type AriaLandmarkRole =
|
|
22
|
-
| 'main'
|
|
23
|
-
| 'region'
|
|
24
|
-
| 'search'
|
|
25
|
-
| 'navigation'
|
|
26
|
-
| 'form'
|
|
27
|
-
| 'banner'
|
|
28
|
-
| 'contentinfo'
|
|
29
|
-
| 'complementary';
|
|
30
|
-
|
|
31
|
-
export interface AriaLandmarkProps {
|
|
32
|
-
/** The ARIA landmark role. */
|
|
33
|
-
role: AriaLandmarkRole;
|
|
34
|
-
/**
|
|
35
|
-
* A human-readable label for the landmark.
|
|
36
|
-
* Required when multiple landmarks with the same role exist on a page.
|
|
37
|
-
*/
|
|
38
|
-
'aria-label'?: string;
|
|
39
|
-
/** Identifies the element(s) that labels the landmark. */
|
|
40
|
-
'aria-labelledby'?: string;
|
|
41
|
-
/** The element's unique identifier. */
|
|
42
|
-
id?: string;
|
|
43
|
-
/**
|
|
44
|
-
* A custom focus handler called when this landmark receives focus via F6 navigation.
|
|
45
|
-
* Use this to focus a specific element within the landmark instead of the container.
|
|
46
|
-
*/
|
|
47
|
-
focus?: () => void;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface LandmarkAria<T extends HTMLElement = HTMLElement> {
|
|
51
|
-
/** Props to spread on the landmark element. */
|
|
52
|
-
landmarkProps: JSX.HTMLAttributes<T>;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface LandmarkController {
|
|
56
|
-
/** Focus the next landmark in DOM order. */
|
|
57
|
-
focusNext: () => void;
|
|
58
|
-
/** Focus the previous landmark in DOM order. */
|
|
59
|
-
focusPrevious: () => void;
|
|
60
|
-
/** Focus the main landmark. */
|
|
61
|
-
focusMain: () => void;
|
|
62
|
-
/** Navigate to a specific landmark by role. If multiple exist, the first one is focused. */
|
|
63
|
-
navigate: (role: AriaLandmarkRole) => void;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ============================================
|
|
67
|
-
// INTERNAL: Landmark Entry
|
|
68
|
-
// ============================================
|
|
69
|
-
|
|
70
|
-
interface LandmarkEntry {
|
|
71
|
-
ref: HTMLElement;
|
|
72
|
-
role: AriaLandmarkRole;
|
|
73
|
-
label?: string;
|
|
74
|
-
focus?: () => void;
|
|
75
|
-
lastFocused?: HTMLElement;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ============================================
|
|
79
|
-
// LANDMARK MANAGER (Singleton)
|
|
80
|
-
// ============================================
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Manages all registered landmarks and handles F6 keyboard navigation.
|
|
84
|
-
*/
|
|
85
|
-
class LandmarkManager {
|
|
86
|
-
private landmarks: LandmarkEntry[] = [];
|
|
87
|
-
private currentIndex = -1;
|
|
88
|
-
private listening = false;
|
|
89
|
-
|
|
90
|
-
constructor() {
|
|
91
|
-
if (typeof window !== 'undefined') {
|
|
92
|
-
this.startListening();
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
private startListening() {
|
|
97
|
-
if (this.listening) return;
|
|
98
|
-
this.listening = true;
|
|
99
|
-
|
|
100
|
-
window.addEventListener('keydown', this.handleKeyDown.bind(this), true);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
private handleKeyDown(event: KeyboardEvent) {
|
|
104
|
-
// F6 to navigate landmarks
|
|
105
|
-
if (event.key === 'F6') {
|
|
106
|
-
event.preventDefault();
|
|
107
|
-
if (event.shiftKey) {
|
|
108
|
-
this.focusPrevious();
|
|
109
|
-
} else {
|
|
110
|
-
this.focusNext();
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
register(entry: LandmarkEntry): void {
|
|
116
|
-
// Insert in DOM order using compareDocumentPosition
|
|
117
|
-
const index = this.findInsertionIndex(entry.ref);
|
|
118
|
-
this.landmarks.splice(index, 0, entry);
|
|
119
|
-
|
|
120
|
-
// Validate: if multiple landmarks have the same role, they should have different labels
|
|
121
|
-
this.validateLabels();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
unregister(ref: HTMLElement): void {
|
|
125
|
-
const index = this.landmarks.findIndex((l) => l.ref === ref);
|
|
126
|
-
if (index !== -1) {
|
|
127
|
-
this.landmarks.splice(index, 1);
|
|
128
|
-
// Adjust currentIndex if needed
|
|
129
|
-
if (this.currentIndex >= this.landmarks.length) {
|
|
130
|
-
this.currentIndex = this.landmarks.length - 1;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private findInsertionIndex(ref: HTMLElement): number {
|
|
136
|
-
// Binary search for insertion point based on DOM order
|
|
137
|
-
let low = 0;
|
|
138
|
-
let high = this.landmarks.length;
|
|
139
|
-
|
|
140
|
-
while (low < high) {
|
|
141
|
-
const mid = Math.floor((low + high) / 2);
|
|
142
|
-
const comparison = this.landmarks[mid].ref.compareDocumentPosition(ref);
|
|
143
|
-
|
|
144
|
-
// Node.DOCUMENT_POSITION_FOLLOWING = 4
|
|
145
|
-
if (comparison & Node.DOCUMENT_POSITION_FOLLOWING) {
|
|
146
|
-
low = mid + 1;
|
|
147
|
-
} else {
|
|
148
|
-
high = mid;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return low;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private validateLabels(): void {
|
|
156
|
-
// Group landmarks by role
|
|
157
|
-
const roleGroups = new Map<AriaLandmarkRole, LandmarkEntry[]>();
|
|
158
|
-
for (const landmark of this.landmarks) {
|
|
159
|
-
const group = roleGroups.get(landmark.role) || [];
|
|
160
|
-
group.push(landmark);
|
|
161
|
-
roleGroups.set(landmark.role, group);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Warn if multiple landmarks with the same role lack unique labels
|
|
165
|
-
for (const [role, group] of roleGroups) {
|
|
166
|
-
if (group.length > 1) {
|
|
167
|
-
const labels = group.map((l) => l.label);
|
|
168
|
-
const uniqueLabels = new Set(labels.filter(Boolean));
|
|
169
|
-
if (uniqueLabels.size < group.length) {
|
|
170
|
-
console.warn(
|
|
171
|
-
`Multiple landmarks with role "${role}" exist. Each should have a unique aria-label or aria-labelledby.`
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
focusNext(): void {
|
|
179
|
-
if (this.landmarks.length === 0) return;
|
|
180
|
-
|
|
181
|
-
// Find the currently focused landmark
|
|
182
|
-
const activeElement = document.activeElement;
|
|
183
|
-
this.currentIndex = this.findCurrentLandmarkIndex(activeElement);
|
|
184
|
-
|
|
185
|
-
// Move to next
|
|
186
|
-
this.currentIndex = (this.currentIndex + 1) % this.landmarks.length;
|
|
187
|
-
this.focusLandmark(this.landmarks[this.currentIndex]);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
focusPrevious(): void {
|
|
191
|
-
if (this.landmarks.length === 0) return;
|
|
192
|
-
|
|
193
|
-
// Find the currently focused landmark
|
|
194
|
-
const activeElement = document.activeElement;
|
|
195
|
-
this.currentIndex = this.findCurrentLandmarkIndex(activeElement);
|
|
196
|
-
|
|
197
|
-
// Move to previous
|
|
198
|
-
this.currentIndex =
|
|
199
|
-
(this.currentIndex - 1 + this.landmarks.length) % this.landmarks.length;
|
|
200
|
-
this.focusLandmark(this.landmarks[this.currentIndex]);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
focusMain(): void {
|
|
204
|
-
const main = this.landmarks.find((l) => l.role === 'main');
|
|
205
|
-
if (main) {
|
|
206
|
-
this.focusLandmark(main);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
navigate(role: AriaLandmarkRole): void {
|
|
211
|
-
const landmark = this.landmarks.find((l) => l.role === role);
|
|
212
|
-
if (landmark) {
|
|
213
|
-
this.focusLandmark(landmark);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private findCurrentLandmarkIndex(activeElement: Element | null): number {
|
|
218
|
-
if (!activeElement) return -1;
|
|
219
|
-
|
|
220
|
-
// Check if active element is within any landmark
|
|
221
|
-
for (let i = 0; i < this.landmarks.length; i++) {
|
|
222
|
-
if (this.landmarks[i].ref.contains(activeElement)) {
|
|
223
|
-
// Store the last focused element for this landmark
|
|
224
|
-
if (activeElement instanceof HTMLElement) {
|
|
225
|
-
this.landmarks[i].lastFocused = activeElement;
|
|
226
|
-
}
|
|
227
|
-
return i;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return -1;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
private focusLandmark(landmark: LandmarkEntry): void {
|
|
235
|
-
// If a custom focus handler is provided, use it
|
|
236
|
-
if (landmark.focus) {
|
|
237
|
-
landmark.focus();
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// If we previously focused an element in this landmark, try to restore it
|
|
242
|
-
if (landmark.lastFocused && landmark.ref.contains(landmark.lastFocused)) {
|
|
243
|
-
landmark.lastFocused.focus();
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Try to find the first focusable element
|
|
248
|
-
const focusable = this.findFirstFocusable(landmark.ref);
|
|
249
|
-
if (focusable) {
|
|
250
|
-
focusable.focus();
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Fallback: make the landmark itself focusable and focus it
|
|
255
|
-
if (!landmark.ref.hasAttribute('tabindex')) {
|
|
256
|
-
landmark.ref.setAttribute('tabindex', '-1');
|
|
257
|
-
}
|
|
258
|
-
landmark.ref.focus();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
private findFirstFocusable(container: HTMLElement): HTMLElement | null {
|
|
262
|
-
const focusableSelectors = [
|
|
263
|
-
'a[href]',
|
|
264
|
-
'button:not([disabled])',
|
|
265
|
-
'input:not([disabled])',
|
|
266
|
-
'select:not([disabled])',
|
|
267
|
-
'textarea:not([disabled])',
|
|
268
|
-
'[tabindex]:not([tabindex="-1"])',
|
|
269
|
-
].join(', ');
|
|
270
|
-
|
|
271
|
-
return container.querySelector<HTMLElement>(focusableSelectors);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
getController(): LandmarkController {
|
|
275
|
-
return {
|
|
276
|
-
focusNext: () => this.focusNext(),
|
|
277
|
-
focusPrevious: () => this.focusPrevious(),
|
|
278
|
-
focusMain: () => this.focusMain(),
|
|
279
|
-
navigate: (role) => this.navigate(role),
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Global singleton instance
|
|
285
|
-
let landmarkManager: LandmarkManager | null = null;
|
|
286
|
-
|
|
287
|
-
function getLandmarkManager(): LandmarkManager {
|
|
288
|
-
if (!landmarkManager) {
|
|
289
|
-
landmarkManager = new LandmarkManager();
|
|
290
|
-
}
|
|
291
|
-
return landmarkManager;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// ============================================
|
|
295
|
-
// CREATE LANDMARK
|
|
296
|
-
// ============================================
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Provides landmark navigation in an application.
|
|
300
|
-
* Call this with a role and label to register a landmark navigable with F6.
|
|
301
|
-
*
|
|
302
|
-
* @example
|
|
303
|
-
* ```tsx
|
|
304
|
-
* function Navigation(props) {
|
|
305
|
-
* let ref: HTMLElement;
|
|
306
|
-
* const { landmarkProps } = createLandmark({
|
|
307
|
-
* role: 'navigation',
|
|
308
|
-
* 'aria-label': 'Main navigation'
|
|
309
|
-
* });
|
|
310
|
-
*
|
|
311
|
-
* return (
|
|
312
|
-
* <nav {...landmarkProps} ref={ref}>
|
|
313
|
-
* {props.children}
|
|
314
|
-
* </nav>
|
|
315
|
-
* );
|
|
316
|
-
* }
|
|
317
|
-
* ```
|
|
318
|
-
*/
|
|
319
|
-
export function createLandmark<T extends HTMLElement = HTMLElement>(
|
|
320
|
-
props: MaybeAccessor<AriaLandmarkProps>,
|
|
321
|
-
ref: Accessor<T | undefined>
|
|
322
|
-
): LandmarkAria<T> {
|
|
323
|
-
// Register with the landmark manager
|
|
324
|
-
createEffect(() => {
|
|
325
|
-
const element = ref();
|
|
326
|
-
if (!element) return;
|
|
327
|
-
|
|
328
|
-
const p = access(props);
|
|
329
|
-
const entry: LandmarkEntry = {
|
|
330
|
-
ref: element,
|
|
331
|
-
role: p.role,
|
|
332
|
-
label: p['aria-label'],
|
|
333
|
-
focus: p.focus,
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
const manager = getLandmarkManager();
|
|
337
|
-
manager.register(entry);
|
|
338
|
-
|
|
339
|
-
onCleanup(() => {
|
|
340
|
-
manager.unregister(element);
|
|
341
|
-
});
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
const getLandmarkProps = (): JSX.HTMLAttributes<T> => {
|
|
345
|
-
const p = access(props);
|
|
346
|
-
const domProps = filterDOMProps(p as unknown as Record<string, unknown>, { labelable: true });
|
|
347
|
-
|
|
348
|
-
return {
|
|
349
|
-
...domProps,
|
|
350
|
-
role: p.role,
|
|
351
|
-
};
|
|
352
|
-
};
|
|
353
|
-
|
|
354
|
-
return {
|
|
355
|
-
get landmarkProps() {
|
|
356
|
-
return getLandmarkProps();
|
|
357
|
-
},
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// ============================================
|
|
362
|
-
// LANDMARK CONTROLLER
|
|
363
|
-
// ============================================
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Returns a controller for programmatic landmark navigation.
|
|
367
|
-
*
|
|
368
|
-
* @example
|
|
369
|
-
* ```tsx
|
|
370
|
-
* const controller = getLandmarkController();
|
|
371
|
-
* controller.focusMain(); // Focus the main landmark
|
|
372
|
-
* controller.focusNext(); // Focus the next landmark
|
|
373
|
-
* ```
|
|
374
|
-
*/
|
|
375
|
-
export function getLandmarkController(): LandmarkController {
|
|
376
|
-
return getLandmarkManager().getController();
|
|
377
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* createLandmark - SolidJS implementation of React Aria's useLandmark
|
|
3
|
+
*
|
|
4
|
+
* Provides landmark navigation in an application. Call this with a role and label
|
|
5
|
+
* to register a landmark navigable with the F6 key.
|
|
6
|
+
*
|
|
7
|
+
* ARIA landmarks help screen reader users navigate between major sections of a page.
|
|
8
|
+
* The F6 key (or Shift+F6) cycles through all registered landmarks.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { JSX, Accessor } from 'solid-js';
|
|
12
|
+
import { createEffect, onCleanup } from 'solid-js';
|
|
13
|
+
import { access, type MaybeAccessor } from '../utils';
|
|
14
|
+
import { filterDOMProps } from '../utils';
|
|
15
|
+
|
|
16
|
+
// ============================================
|
|
17
|
+
// TYPES
|
|
18
|
+
// ============================================
|
|
19
|
+
|
|
20
|
+
/** ARIA landmark roles */
|
|
21
|
+
export type AriaLandmarkRole =
|
|
22
|
+
| 'main'
|
|
23
|
+
| 'region'
|
|
24
|
+
| 'search'
|
|
25
|
+
| 'navigation'
|
|
26
|
+
| 'form'
|
|
27
|
+
| 'banner'
|
|
28
|
+
| 'contentinfo'
|
|
29
|
+
| 'complementary';
|
|
30
|
+
|
|
31
|
+
export interface AriaLandmarkProps {
|
|
32
|
+
/** The ARIA landmark role. */
|
|
33
|
+
role: AriaLandmarkRole;
|
|
34
|
+
/**
|
|
35
|
+
* A human-readable label for the landmark.
|
|
36
|
+
* Required when multiple landmarks with the same role exist on a page.
|
|
37
|
+
*/
|
|
38
|
+
'aria-label'?: string;
|
|
39
|
+
/** Identifies the element(s) that labels the landmark. */
|
|
40
|
+
'aria-labelledby'?: string;
|
|
41
|
+
/** The element's unique identifier. */
|
|
42
|
+
id?: string;
|
|
43
|
+
/**
|
|
44
|
+
* A custom focus handler called when this landmark receives focus via F6 navigation.
|
|
45
|
+
* Use this to focus a specific element within the landmark instead of the container.
|
|
46
|
+
*/
|
|
47
|
+
focus?: () => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface LandmarkAria<T extends HTMLElement = HTMLElement> {
|
|
51
|
+
/** Props to spread on the landmark element. */
|
|
52
|
+
landmarkProps: JSX.HTMLAttributes<T>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface LandmarkController {
|
|
56
|
+
/** Focus the next landmark in DOM order. */
|
|
57
|
+
focusNext: () => void;
|
|
58
|
+
/** Focus the previous landmark in DOM order. */
|
|
59
|
+
focusPrevious: () => void;
|
|
60
|
+
/** Focus the main landmark. */
|
|
61
|
+
focusMain: () => void;
|
|
62
|
+
/** Navigate to a specific landmark by role. If multiple exist, the first one is focused. */
|
|
63
|
+
navigate: (role: AriaLandmarkRole) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================
|
|
67
|
+
// INTERNAL: Landmark Entry
|
|
68
|
+
// ============================================
|
|
69
|
+
|
|
70
|
+
interface LandmarkEntry {
|
|
71
|
+
ref: HTMLElement;
|
|
72
|
+
role: AriaLandmarkRole;
|
|
73
|
+
label?: string;
|
|
74
|
+
focus?: () => void;
|
|
75
|
+
lastFocused?: HTMLElement;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ============================================
|
|
79
|
+
// LANDMARK MANAGER (Singleton)
|
|
80
|
+
// ============================================
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Manages all registered landmarks and handles F6 keyboard navigation.
|
|
84
|
+
*/
|
|
85
|
+
class LandmarkManager {
|
|
86
|
+
private landmarks: LandmarkEntry[] = [];
|
|
87
|
+
private currentIndex = -1;
|
|
88
|
+
private listening = false;
|
|
89
|
+
|
|
90
|
+
constructor() {
|
|
91
|
+
if (typeof window !== 'undefined') {
|
|
92
|
+
this.startListening();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private startListening() {
|
|
97
|
+
if (this.listening) return;
|
|
98
|
+
this.listening = true;
|
|
99
|
+
|
|
100
|
+
window.addEventListener('keydown', this.handleKeyDown.bind(this), true);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private handleKeyDown(event: KeyboardEvent) {
|
|
104
|
+
// F6 to navigate landmarks
|
|
105
|
+
if (event.key === 'F6') {
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
if (event.shiftKey) {
|
|
108
|
+
this.focusPrevious();
|
|
109
|
+
} else {
|
|
110
|
+
this.focusNext();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
register(entry: LandmarkEntry): void {
|
|
116
|
+
// Insert in DOM order using compareDocumentPosition
|
|
117
|
+
const index = this.findInsertionIndex(entry.ref);
|
|
118
|
+
this.landmarks.splice(index, 0, entry);
|
|
119
|
+
|
|
120
|
+
// Validate: if multiple landmarks have the same role, they should have different labels
|
|
121
|
+
this.validateLabels();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
unregister(ref: HTMLElement): void {
|
|
125
|
+
const index = this.landmarks.findIndex((l) => l.ref === ref);
|
|
126
|
+
if (index !== -1) {
|
|
127
|
+
this.landmarks.splice(index, 1);
|
|
128
|
+
// Adjust currentIndex if needed
|
|
129
|
+
if (this.currentIndex >= this.landmarks.length) {
|
|
130
|
+
this.currentIndex = this.landmarks.length - 1;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private findInsertionIndex(ref: HTMLElement): number {
|
|
136
|
+
// Binary search for insertion point based on DOM order
|
|
137
|
+
let low = 0;
|
|
138
|
+
let high = this.landmarks.length;
|
|
139
|
+
|
|
140
|
+
while (low < high) {
|
|
141
|
+
const mid = Math.floor((low + high) / 2);
|
|
142
|
+
const comparison = this.landmarks[mid].ref.compareDocumentPosition(ref);
|
|
143
|
+
|
|
144
|
+
// Node.DOCUMENT_POSITION_FOLLOWING = 4
|
|
145
|
+
if (comparison & Node.DOCUMENT_POSITION_FOLLOWING) {
|
|
146
|
+
low = mid + 1;
|
|
147
|
+
} else {
|
|
148
|
+
high = mid;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return low;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private validateLabels(): void {
|
|
156
|
+
// Group landmarks by role
|
|
157
|
+
const roleGroups = new Map<AriaLandmarkRole, LandmarkEntry[]>();
|
|
158
|
+
for (const landmark of this.landmarks) {
|
|
159
|
+
const group = roleGroups.get(landmark.role) || [];
|
|
160
|
+
group.push(landmark);
|
|
161
|
+
roleGroups.set(landmark.role, group);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Warn if multiple landmarks with the same role lack unique labels
|
|
165
|
+
for (const [role, group] of roleGroups) {
|
|
166
|
+
if (group.length > 1) {
|
|
167
|
+
const labels = group.map((l) => l.label);
|
|
168
|
+
const uniqueLabels = new Set(labels.filter(Boolean));
|
|
169
|
+
if (uniqueLabels.size < group.length) {
|
|
170
|
+
console.warn(
|
|
171
|
+
`Multiple landmarks with role "${role}" exist. Each should have a unique aria-label or aria-labelledby.`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
focusNext(): void {
|
|
179
|
+
if (this.landmarks.length === 0) return;
|
|
180
|
+
|
|
181
|
+
// Find the currently focused landmark
|
|
182
|
+
const activeElement = document.activeElement;
|
|
183
|
+
this.currentIndex = this.findCurrentLandmarkIndex(activeElement);
|
|
184
|
+
|
|
185
|
+
// Move to next
|
|
186
|
+
this.currentIndex = (this.currentIndex + 1) % this.landmarks.length;
|
|
187
|
+
this.focusLandmark(this.landmarks[this.currentIndex]);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
focusPrevious(): void {
|
|
191
|
+
if (this.landmarks.length === 0) return;
|
|
192
|
+
|
|
193
|
+
// Find the currently focused landmark
|
|
194
|
+
const activeElement = document.activeElement;
|
|
195
|
+
this.currentIndex = this.findCurrentLandmarkIndex(activeElement);
|
|
196
|
+
|
|
197
|
+
// Move to previous
|
|
198
|
+
this.currentIndex =
|
|
199
|
+
(this.currentIndex - 1 + this.landmarks.length) % this.landmarks.length;
|
|
200
|
+
this.focusLandmark(this.landmarks[this.currentIndex]);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
focusMain(): void {
|
|
204
|
+
const main = this.landmarks.find((l) => l.role === 'main');
|
|
205
|
+
if (main) {
|
|
206
|
+
this.focusLandmark(main);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
navigate(role: AriaLandmarkRole): void {
|
|
211
|
+
const landmark = this.landmarks.find((l) => l.role === role);
|
|
212
|
+
if (landmark) {
|
|
213
|
+
this.focusLandmark(landmark);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private findCurrentLandmarkIndex(activeElement: Element | null): number {
|
|
218
|
+
if (!activeElement) return -1;
|
|
219
|
+
|
|
220
|
+
// Check if active element is within any landmark
|
|
221
|
+
for (let i = 0; i < this.landmarks.length; i++) {
|
|
222
|
+
if (this.landmarks[i].ref.contains(activeElement)) {
|
|
223
|
+
// Store the last focused element for this landmark
|
|
224
|
+
if (activeElement instanceof HTMLElement) {
|
|
225
|
+
this.landmarks[i].lastFocused = activeElement;
|
|
226
|
+
}
|
|
227
|
+
return i;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return -1;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private focusLandmark(landmark: LandmarkEntry): void {
|
|
235
|
+
// If a custom focus handler is provided, use it
|
|
236
|
+
if (landmark.focus) {
|
|
237
|
+
landmark.focus();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// If we previously focused an element in this landmark, try to restore it
|
|
242
|
+
if (landmark.lastFocused && landmark.ref.contains(landmark.lastFocused)) {
|
|
243
|
+
landmark.lastFocused.focus();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Try to find the first focusable element
|
|
248
|
+
const focusable = this.findFirstFocusable(landmark.ref);
|
|
249
|
+
if (focusable) {
|
|
250
|
+
focusable.focus();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Fallback: make the landmark itself focusable and focus it
|
|
255
|
+
if (!landmark.ref.hasAttribute('tabindex')) {
|
|
256
|
+
landmark.ref.setAttribute('tabindex', '-1');
|
|
257
|
+
}
|
|
258
|
+
landmark.ref.focus();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private findFirstFocusable(container: HTMLElement): HTMLElement | null {
|
|
262
|
+
const focusableSelectors = [
|
|
263
|
+
'a[href]',
|
|
264
|
+
'button:not([disabled])',
|
|
265
|
+
'input:not([disabled])',
|
|
266
|
+
'select:not([disabled])',
|
|
267
|
+
'textarea:not([disabled])',
|
|
268
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
269
|
+
].join(', ');
|
|
270
|
+
|
|
271
|
+
return container.querySelector<HTMLElement>(focusableSelectors);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
getController(): LandmarkController {
|
|
275
|
+
return {
|
|
276
|
+
focusNext: () => this.focusNext(),
|
|
277
|
+
focusPrevious: () => this.focusPrevious(),
|
|
278
|
+
focusMain: () => this.focusMain(),
|
|
279
|
+
navigate: (role) => this.navigate(role),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Global singleton instance
|
|
285
|
+
let landmarkManager: LandmarkManager | null = null;
|
|
286
|
+
|
|
287
|
+
function getLandmarkManager(): LandmarkManager {
|
|
288
|
+
if (!landmarkManager) {
|
|
289
|
+
landmarkManager = new LandmarkManager();
|
|
290
|
+
}
|
|
291
|
+
return landmarkManager;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ============================================
|
|
295
|
+
// CREATE LANDMARK
|
|
296
|
+
// ============================================
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Provides landmark navigation in an application.
|
|
300
|
+
* Call this with a role and label to register a landmark navigable with F6.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```tsx
|
|
304
|
+
* function Navigation(props) {
|
|
305
|
+
* let ref: HTMLElement;
|
|
306
|
+
* const { landmarkProps } = createLandmark({
|
|
307
|
+
* role: 'navigation',
|
|
308
|
+
* 'aria-label': 'Main navigation'
|
|
309
|
+
* });
|
|
310
|
+
*
|
|
311
|
+
* return (
|
|
312
|
+
* <nav {...landmarkProps} ref={ref}>
|
|
313
|
+
* {props.children}
|
|
314
|
+
* </nav>
|
|
315
|
+
* );
|
|
316
|
+
* }
|
|
317
|
+
* ```
|
|
318
|
+
*/
|
|
319
|
+
export function createLandmark<T extends HTMLElement = HTMLElement>(
|
|
320
|
+
props: MaybeAccessor<AriaLandmarkProps>,
|
|
321
|
+
ref: Accessor<T | undefined>
|
|
322
|
+
): LandmarkAria<T> {
|
|
323
|
+
// Register with the landmark manager
|
|
324
|
+
createEffect(() => {
|
|
325
|
+
const element = ref();
|
|
326
|
+
if (!element) return;
|
|
327
|
+
|
|
328
|
+
const p = access(props);
|
|
329
|
+
const entry: LandmarkEntry = {
|
|
330
|
+
ref: element,
|
|
331
|
+
role: p.role,
|
|
332
|
+
label: p['aria-label'],
|
|
333
|
+
focus: p.focus,
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const manager = getLandmarkManager();
|
|
337
|
+
manager.register(entry);
|
|
338
|
+
|
|
339
|
+
onCleanup(() => {
|
|
340
|
+
manager.unregister(element);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const getLandmarkProps = (): JSX.HTMLAttributes<T> => {
|
|
345
|
+
const p = access(props);
|
|
346
|
+
const domProps = filterDOMProps(p as unknown as Record<string, unknown>, { labelable: true });
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
...domProps,
|
|
350
|
+
role: p.role,
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
get landmarkProps() {
|
|
356
|
+
return getLandmarkProps();
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ============================================
|
|
362
|
+
// LANDMARK CONTROLLER
|
|
363
|
+
// ============================================
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Returns a controller for programmatic landmark navigation.
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* ```tsx
|
|
370
|
+
* const controller = getLandmarkController();
|
|
371
|
+
* controller.focusMain(); // Focus the main landmark
|
|
372
|
+
* controller.focusNext(); // Focus the next landmark
|
|
373
|
+
* ```
|
|
374
|
+
*/
|
|
375
|
+
export function getLandmarkController(): LandmarkController {
|
|
376
|
+
return getLandmarkManager().getController();
|
|
377
|
+
}
|