Skip to content
Draft
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
66 changes: 49 additions & 17 deletions src/Core/Components/AnchoredRegion/FluentAnchoredRegion.razor.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,50 @@ export function goToNextFocusableElement(forContainer, toOriginal, delay) {
// ---------------------------------------------------------------------------

const keyboardNavigationState = new Map();
const focusableSelectors = "input, select, textarea, button, object, a[href], area[href], [tabindex]";

function getAnchorFocusTarget(anchorElement) {
if (isFocusableElement(anchorElement)) {
return anchorElement;
}

return new FocusableElement(anchorElement).findNextFocusableElement() ?? anchorElement;
}

function getAnchorTraversalStart(anchorElement) {
const focusTarget = getAnchorFocusTarget(anchorElement);
return getFluentShadowFocusTarget(focusTarget) ?? focusTarget;
}

function getFluentShadowFocusTarget(element) {
if (!element?.tagName?.startsWith("FLUENT-")) {
return null;
}

return Array.from(element.shadowRoot?.children ?? [])
.find(child => child.tabIndex !== -1 && child.checkVisibility()) ?? null;
}
Comment on lines +62 to +69

function findNextFocusableElementAfterAnchor(anchorElement, popupElement) {
const startFrom = getAnchorTraversalStart(anchorElement);
const focusableElements = new FocusableElement(anchorElement.getRootNode())
.getFocusableElements()
.filter(element => !popupElement.contains(element));
const currentIndex = focusableElements.indexOf(startFrom);

return currentIndex === -1 ? null : focusableElements[currentIndex + 1] ?? null;
}
Comment on lines +71 to +79

function isFocusableElement(element) {
if (!element) {
return false;
}

const isFocusableCandidate = element.matches(focusableSelectors)
|| element.tagName.toLowerCase().startsWith("fluent-");

return isFocusableCandidate && element.tabIndex !== -1 && element.checkVisibility();
}

/**
* Attaches keyboard navigation listeners to an anchor+popup pair.
Expand Down Expand Up @@ -85,16 +129,10 @@ export function initializeKeyboardNavigation(anchorId, popupId, dotNetHelper, cl
ev.stopPropagation();
if (!ev.shiftKey) {
// Case 3: move to element after anchor in page
let startFrom;
if (anchorElement.tagName.startsWith("FLUENT-") && anchorElement.shadowRoot?.children.length > 0) {
startFrom = anchorElement.shadowRoot.children[0];
} else {
startFrom = anchorElement;
}
new FocusableElement(anchorElement.getRootNode()).findNextFocusableElement(startFrom)?.focus();
findNextFocusableElementAfterAnchor(anchorElement, popupElement)?.focus();
} else {
// Case 2: Shift+Tab → focus anchor
anchorElement.focus();
getAnchorFocusTarget(anchorElement).focus();
}
Comment on lines 129 to 136
dotNetHelper.invokeMethodAsync('CloseAsync');
} else {
Expand All @@ -107,19 +145,13 @@ export function initializeKeyboardNavigation(anchorId, popupId, dotNetHelper, cl
// Case 3: Tab on last element → next page element after anchor, close
ev.preventDefault();
ev.stopPropagation();
let startFrom;
if (anchorElement.tagName.startsWith("FLUENT-") && anchorElement.shadowRoot?.children.length > 0) {
startFrom = anchorElement.shadowRoot.children[0];
} else {
startFrom = anchorElement;
}
new FocusableElement(anchorElement.getRootNode()).findNextFocusableElement(startFrom)?.focus();
findNextFocusableElementAfterAnchor(anchorElement, popupElement)?.focus();
dotNetHelper.invokeMethodAsync('CloseAsync');
} else if (ev.shiftKey && (focusables.length === 0 || activeIndex === 0)) {
// Case 2: Shift+Tab on first element → focus anchor, close
ev.preventDefault();
ev.stopPropagation();
anchorElement.focus();
getAnchorFocusTarget(anchorElement).focus();
dotNetHelper.invokeMethodAsync('CloseAsync');
}
// Otherwise: middle element — let browser handle Tab/Shift+Tab naturally
Expand Down Expand Up @@ -174,7 +206,7 @@ export function disposeKeyboardNavigation(anchorId) {
* Focusable Element
*/
export class FocusableElement {
FOCUSABLE_SELECTORS = "input, select, textarea, button, object, a[href], area[href], [tabindex]";
FOCUSABLE_SELECTORS = focusableSelectors;
_originalActiveElement;
_container;

Expand Down
56 changes: 56 additions & 0 deletions src/Core/Components/DataGrid/FluentDataGrid.razor.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
let grids = [];

const focusableSelectors = "input, select, textarea, button, object, a[href], area[href], [tabindex]";

function getFocusableElements(container) {
const queriedElements = Array.from(container.querySelectorAll("*")).filter(el => {
return el.matches(focusableSelectors) || el.tagName.toLowerCase().startsWith("fluent-");
});

const focusableElements = [];
queriedElements.forEach(el => {
if (el.tagName.toLowerCase().startsWith("fluent-") && el.tabIndex === -1 && !!el.shadowRoot) {
Array.from(el.shadowRoot.children).forEach(child => {
if (child.tabIndex !== -1 && child.checkVisibility()) {
focusableElements.push(child);
}
});
} else {
focusableElements.push(el);
}
});

return focusableElements.filter(el => !!el && el.tabIndex !== -1 && el.checkVisibility());
}

function findAdjacentFocusableElement(currentElement, reverse = false) {
const focusableElements = getFocusableElements(document.body);
const currentIndex = focusableElements.indexOf(currentElement);
if (currentIndex === -1) {
return null;
}

return focusableElements[currentIndex + (reverse ? -1 : 1)] ?? null;
}

function closeColumnResizeAndFocus(gridElement, columnResizeElement, focusTarget) {
gridElement.dispatchEvent(new CustomEvent('closecolumnresize', { bubbles: true }));

const columnHeaderButton = columnResizeElement.closest('.column-header')?.querySelector('.col-sort-button');
(focusTarget ?? columnHeaderButton ?? gridElement).focus();
}

export function init(gridElement, autoFocus) {
if (gridElement === undefined || gridElement === null) {
return;
Expand Down Expand Up @@ -62,6 +102,22 @@ export function init(gridElement, autoFocus) {
event.stopPropagation();
return;
}

if (event.key === "Tab") {
const resizeFocusables = getFocusableElements(columnResizeElement);
const activeIndex = resizeFocusables.indexOf(document.activeElement);
const isFirstElement = activeIndex === 0;
const isLastElement = activeIndex === resizeFocusables.length - 1;

if ((event.shiftKey && isFirstElement) || (!event.shiftKey && isLastElement)) {
const focusTarget = findAdjacentFocusableElement(document.activeElement, event.shiftKey);
Comment on lines +107 to +113
event.preventDefault();
event.stopPropagation();
closeColumnResizeAndFocus(gridElement, columnResizeElement, focusTarget);
}

return;
}
}

if (document.activeElement.tagName.toLowerCase() != 'table' && document.activeElement.tagName.toLowerCase() != 'td' && document.activeElement.tagName.toLowerCase() != 'th') {
Expand Down
Loading