mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
chore(ui): harden surface Tab cycling
Rebased onto current main with review follow-ups folded in. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
0265ad4c19
commit
12ea0219d1
4 files changed
+67
-1
No files matched your search
@@ -33,6 +33,8 @@
|
|||||||
|
|
||||||
- Director timeline seeks cross the dateline along the short longitude arc.
|
- Director timeline seeks cross the dateline along the short longitude arc.
|
||||||
|
|
||||||
|
- Surface Tab cycling also skips negative-tabindex, fieldset-disabled and inert controls.
|
||||||
|
|
||||||
- Treat explicitly current, complete empty vessel coverage as a successful update,
|
- Treat explicitly current, complete empty vessel coverage as a successful update,
|
||||||
clearing obsolete contacts while retaining stale or incomplete snapshots.
|
clearing obsolete contacts while retaining stale or incomplete snapshots.
|
||||||
|
|
||||||
|
|||||||
@@ -1060,6 +1060,9 @@ collapse can still be undone.
|
|||||||
|
|
||||||
## Surface keyboard lifecycle
|
## Surface keyboard lifecycle
|
||||||
|
|
||||||
|
Surface Tab cycling excludes controls with a negative tabindex, native disabled
|
||||||
|
state or inert ancestry; ordinary visible enabled controls retain their order.
|
||||||
|
|
||||||
`ui/surfaces` owns the capture-phase keyboard listener, Tab cycling and return
|
`ui/surfaces` owns the capture-phase keyboard listener, Tab cycling and return
|
||||||
focus shared by the first-run launcher and Provider Settings. Each caller
|
focus shared by the first-run launcher and Provider Settings. Each caller
|
||||||
activates it while open and deactivates it on dismissal; destruction releases
|
activates it while open and deactivates it on dismissal; destruction releases
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createSurfaceKeyboard } from './surfaceKeyboard.js';
|
||||||
|
test('surface cycling never targets deliberately excluded controls', () => {
|
||||||
|
let handler;
|
||||||
|
const documentRef = {
|
||||||
|
activeElement: {},
|
||||||
|
addEventListener: (_, fn) => (handler = fn),
|
||||||
|
removeEventListener() {},
|
||||||
|
};
|
||||||
|
const node = ({
|
||||||
|
attributes = {},
|
||||||
|
fieldsetDisabled = false,
|
||||||
|
inert = false,
|
||||||
|
} = {}) => ({
|
||||||
|
hasAttribute: (name) => Object.hasOwn(attributes, name),
|
||||||
|
getClientRects: () => [{}],
|
||||||
|
getAttribute: (name) => attributes[name] ?? null,
|
||||||
|
matches: (selector) => selector === ':disabled' && fieldsetDisabled,
|
||||||
|
closest: (selector) =>
|
||||||
|
selector === '[inert]' && inert ? { inert: true } : null,
|
||||||
|
focus() {
|
||||||
|
documentRef.activeElement = this;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const ignored = [
|
||||||
|
node({ attributes: { tabindex: '-1' } }),
|
||||||
|
node({ fieldsetDisabled: true }),
|
||||||
|
node({ inert: true }),
|
||||||
|
];
|
||||||
|
const first = node({}),
|
||||||
|
last = node({});
|
||||||
|
const controls = [...ignored, first, last];
|
||||||
|
const root = {
|
||||||
|
ownerDocument: documentRef,
|
||||||
|
querySelectorAll: () => controls,
|
||||||
|
contains: (n) => controls.includes(n),
|
||||||
|
};
|
||||||
|
const surface = createSurfaceKeyboard({
|
||||||
|
root,
|
||||||
|
isActive: () => true,
|
||||||
|
onEscape() {},
|
||||||
|
});
|
||||||
|
surface.activate();
|
||||||
|
const press = (shiftKey) =>
|
||||||
|
handler({
|
||||||
|
key: 'Tab',
|
||||||
|
shiftKey,
|
||||||
|
preventDefault() {},
|
||||||
|
stopPropagation() {},
|
||||||
|
});
|
||||||
|
press(false);
|
||||||
|
assert.equal(documentRef.activeElement, first);
|
||||||
|
press(true);
|
||||||
|
assert.equal(documentRef.activeElement, last);
|
||||||
|
surface.destroy();
|
||||||
|
});
|
||||||
@@ -39,7 +39,11 @@ export function createSurfaceKeyboard({
|
|||||||
),
|
),
|
||||||
].filter(
|
].filter(
|
||||||
(node) =>
|
(node) =>
|
||||||
!node.hasAttribute('disabled') && node.getClientRects().length > 0,
|
!node.hasAttribute('disabled') &&
|
||||||
|
!node.matches?.(':disabled') &&
|
||||||
|
!(Number(node.getAttribute?.('tabindex')) < 0) &&
|
||||||
|
!node.closest?.('[inert]') &&
|
||||||
|
node.getClientRects().length > 0,
|
||||||
);
|
);
|
||||||
if (!order.length) return;
|
if (!order.length) return;
|
||||||
const first = order[0];
|
const first = order[0];
|
||||||
|
|||||||
Reference in new issue
Block a user