Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add comments to tabbable code #2823

Merged
merged 3 commits into from
Dec 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/tabbable/src/TabbableEffects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function TabbableEffects() {
if (!editorDOMNode) return;

const handler = (event: KeyboardEvent) => {
// Check if the keydown is a tab key that should be handled
if (
event.key !== 'Tab' ||
event.defaultPrevented ||
Expand All @@ -37,11 +38,20 @@ export function TabbableEffects() {
return;
}

/**
* Get the list of additional tabbable entries specified in the plugin
* options
*/
const insertedTabbableEntries = insertTabbableEntries?.(
editor,
event
) as TabbableEntry[];

/**
* Global event listener only. Do not handle the tab event if the keydown
* was sent to an element other than the editor or one of the additional
* tabbable elements.
*/
if (
globalEventListener &&
event.target &&
Expand All @@ -53,8 +63,13 @@ export function TabbableEffects() {
return;
}

// Get all tabbable DOM nodes in the editor
const tabbableDOMNodes = tabbable(editorDOMNode) as HTMLElement[];

/**
* Construct a tabbable entry for each tabbable Slate node, filtered by
* the `isTabbable` option (defaulting to only void nodes).
*/
const defaultTabbableEntries = tabbableDOMNodes
.map((domNode) => {
const slateNode = toSlateNode(editor, domNode);
Expand All @@ -69,17 +84,28 @@ export function TabbableEffects() {
(entry) => entry && isTabbable?.(editor, entry)
) as TabbableEntry[];

/**
* The list of all tabbable entries. Sorting by path ensures a consistent
* tab order.
*/
const tabbableEntries = [
...insertedTabbableEntries,
...defaultTabbableEntries,
].sort((a, b) => Path.compare(a.path, b.path));

/**
* TODO: Refactor everything ABOVE this line into a util function and
* test separately
*/

// Check if any tabbable entry is the active element
const { activeElement } = document;
const activeTabbableEntry =
(activeElement &&
tabbableEntries.find((entry) => entry.domNode === activeElement)) ??
null;

// Find the next Slate node or DOM node to focus
const tabDestination = findTabDestination(editor, {
tabbableEntries,
activeTabbableEntry,
Expand Down Expand Up @@ -107,6 +133,12 @@ export function TabbableEffects() {
return;
}

/**
* There was no tab destination, so let the browser handle the tab event.
* We don't want the browser to focus anything that could have been
* focused by us, so we make make all tabbable DOM nodes in the editor
* unfocusable. This ensures that the focus exits the editor cleanly.
*/
tabbableDOMNodes.forEach((domNode) => {
const oldTabIndex = domNode.getAttribute('tabindex');
domNode.setAttribute('tabindex', '-1');
Expand Down
29 changes: 27 additions & 2 deletions packages/tabbable/src/findTabDestination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const findTabDestination = <V extends Value = Value>(
editor: PlateEditor<V>,
{ tabbableEntries, activeTabbableEntry, direction }: FindTabDestinationOptions
): TabDestination | null => {
// Case 1: A tabbable entry was active before tab was pressed
if (activeTabbableEntry) {
// Find the next tabbable entry after the active one
const activeTabbableEntryIndex =
Expand All @@ -26,7 +27,23 @@ export const findTabDestination = <V extends Value = Value>(
activeTabbableEntryIndex + (direction === 'forward' ? 1 : -1);
const nextTabbableEntry = tabbableEntries[nextTabbableEntryIndex];

// If the next tabbable entry is in the same void, focus it
/**
* If the next tabbable entry originated from the same path as the active
* tabbable entry, focus it.
*
* Examples of when this is true:
* - We're inside a void node and there is an additional tabbable inside
* the same void node.
* - We're inside a popover containing multiple tabbable elements all
* anchored to the same slate node, and there is an additional tabbable
* inside the same popover.
*
* Examples of when this is false:
* - We're inside a void node and the next tabbable is outside the void
* node.
* - We're in the last tabbable element of a popover.
* - There is no next tabbable element.
*/
if (
nextTabbableEntry &&
Path.equals(activeTabbableEntry.path, nextTabbableEntry.path)
Expand All @@ -37,7 +54,13 @@ export const findTabDestination = <V extends Value = Value>(
};
}

// Otherwise, focus the first path after the void
/**
* Otherwise, return the focus to the editor. If we're moving forward,
* focus the first point after the active tabbable's path. If we're moving
* backward, focus the point of the active tabbable's path.
* TODO: Let a tabbable entry specify custom before and after points.
*/

if (direction === 'forward') {
const pointAfter = getPointAfter(editor, activeTabbableEntry.path);
if (!pointAfter) return null;
Expand All @@ -53,6 +76,8 @@ export const findTabDestination = <V extends Value = Value>(
};
}

// Case 2: No tabbable entry was active before tab was pressed

const selectionPath = editor.selection?.anchor?.path || [];

// Find the first tabbable entry after the selection
Expand Down