Skip to content

Commit

Permalink
Merge branch 'main' into sync-shadcn
Browse files Browse the repository at this point in the history
  • Loading branch information
zbeyens authored Dec 23, 2023
2 parents 0d6ede3 + 90bd461 commit 77bff62
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 2 deletions.
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

0 comments on commit 77bff62

Please sign in to comment.