Skip to content

Commit

Permalink
feature(react-tree): preventScroll on navigation (microsoft#31577)
Browse files Browse the repository at this point in the history
  • Loading branch information
bsunderhus authored and miroslavstastny committed Jun 14, 2024
1 parent 684ee5a commit 2cf3309
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feature: preventScroll on navigation",
"packageName": "@fluentui/react-tree",
"email": "bernardo.sunderhus@gmail.com",
"dependentChangeType": "patch"
}
2 changes: 1 addition & 1 deletion packages/react-components/react-tree/etc/react-tree.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ export type TreeProps = ComponentProps<TreeSlots> & {
openItems?: Iterable<TreeItemValue>;
defaultOpenItems?: Iterable<TreeItemValue>;
onOpenChange?(event: TreeOpenChangeEvent, data: TreeOpenChangeData): void;
onNavigation?(event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable): void;
onNavigation?(event: TreeNavigationEvent_unstable, data: TreeNavigationDataParam): void;
selectionMode?: SelectionMode_2;
checkedItems?: Iterable<TreeItemValue | [TreeItemValue, TreeSelectionValue]>;
onCheckedChange?(event: TreeCheckedChangeEvent, data: TreeCheckedChangeData): void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,24 @@ describe('Tree', () => {
cy.get('[data-testid="item2__item1__item1"]').should('be.focused').realPress('{home}');
cy.get('[data-testid="item1"]').should('be.focused');
});
it('should prevent scrolling when `preventScroll()` is called in navigation', () => {
mount(
<TreeTest
onNavigation={(_event, data) => {
data.preventScroll();
}}
defaultOpenItems={['item1', 'item2', 'item2__item1']}
>
{Array.from({ length: 200 }, (_, index) => (
<TreeItem itemType="branch" value={`item${index}`} data-testid={`item${index}`}>
<TreeItemLayout>level 0, item {index + 1}</TreeItemLayout>
</TreeItem>
))}
</TreeTest>,
);
cy.get('[data-testid="item0"]').focus().realPress('{end}');
cy.get('[data-testid="item199"]').should('be.focused').isOutsideViewport();
});
});
});

Expand Down Expand Up @@ -362,6 +380,7 @@ describe('Tree', () => {
cy.get('[data-testid="tree-item-2-1-1"]').should('exist');
});
});

it('should ensure roving tab indexes when focusing programmatically', () => {
mount(
<>
Expand All @@ -377,3 +396,23 @@ describe('Tree', () => {
cy.get('[data-testid="item2__item1"]').should('be.focused');
});
});

declare global {
namespace Cypress {
interface Chainable<Subject> {
isOutsideViewport(): Chainable<Subject>;
}
}
}

Cypress.Commands.add('isOutsideViewport', { prevSubject: true }, subject => {
const windowInnerHeight = Cypress.config(`viewportHeight`);

const bounding = subject[0].getBoundingClientRect();

const bottomBoundOfWindow = windowInnerHeight;

expect(bounding.top).to.be.greaterThan(bottomBoundOfWindow);

return subject;
});
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ export type TreeOpenChangeData = {
| { event: React.KeyboardEvent<HTMLElement>; type: typeof ArrowLeft }
);

/**
* @internal
*
* To avoid breaking changes on TreeNavigationData
* we are creating a new type that extends the old one
* and adds the new methods, and this type will not be exported
*/
type TreeNavigationDataParam = TreeNavigationData_unstable & {
preventScroll(): void;
isScrollPrevented(): boolean;
};

export type TreeOpenChangeEvent = TreeOpenChangeData['event'];

export type TreeCheckedChangeData = {
Expand Down Expand Up @@ -121,7 +133,7 @@ export type TreeProps = ComponentProps<TreeSlots> & {
* @param event - a React's Synthetic event
* @param data - A data object with relevant information,
*/
onNavigation?(event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable): void;
onNavigation?(event: TreeNavigationEvent_unstable, data: TreeNavigationDataParam): void;

/**
* This refers to the selection mode of the tree.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ function useNestedRootTree(props: TreeProps, ref: React.Ref<HTMLElement>): TreeS
onNavigation: useEventCallback((event, data) => {
props.onNavigation?.(event, data);
if (!event.isDefaultPrevented()) {
navigation.navigate(data);
navigation.navigate(data, {
preventScroll: data.isScrollPrevented(),
});
}
}),
onCheckedChange: useEventCallback((event, data) => {
Expand All @@ -50,7 +52,7 @@ function useNestedRootTree(props: TreeProps, ref: React.Ref<HTMLElement>): TreeS
});
}),
},
useMergedRefs(ref, navigation.rootRef),
useMergedRefs(ref, navigation.treeRef),
),
{ treeType: 'nested' } as const,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ export function useRootTree(
};

const requestNavigation = (request: Extract<TreeItemRequest, { requestType: 'navigate' }>) => {
props.onNavigation?.(request.event, request);
let isScrollPrevented = false;
props.onNavigation?.(request.event, {
...request,
preventScroll: () => {
isScrollPrevented = true;
},
isScrollPrevented: () => isScrollPrevented,
});
switch (request.type) {
case treeDataTypes.ArrowDown:
case treeDataTypes.ArrowUp:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useFocusedElementChange } from '@fluentui/react-tabster';
import { elementContains } from '@fluentui/react-utilities';

/**
* @internal
* https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex
*/
export function useRovingTabIndex() {
Expand Down Expand Up @@ -37,13 +38,13 @@ export function useRovingTabIndex() {
nextElement.tabIndex = -1;
}
}, []);
const rove = React.useCallback((nextElement: HTMLElement) => {
const rove = React.useCallback((nextElement: HTMLElement, focusOptions?: FocusOptions) => {
if (!currentElementRef.current) {
return;
}
currentElementRef.current.tabIndex = -1;
nextElement.tabIndex = 0;
nextElement.focus();
nextElement.focus(focusOptions);
currentElementRef.current = nextElement;
}, []);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import * as React from 'react';
import { useHTMLElementWalkerRef } from './useHTMLElementWalkerRef';
import { useMergedRefs } from '@fluentui/react-utilities';

/**
* @internal
*/
export function useTreeNavigation() {
const { rove, initialize: initializeRovingTabIndex } = useRovingTabIndex();
const { walkerRef, rootRef: walkerRootRef } = useHTMLElementWalkerRef();
Expand Down Expand Up @@ -50,13 +53,16 @@ export function useTreeNavigation() {
return walkerRef.current.previousElement();
}
};
function navigate(data: TreeNavigationData_unstable) {
function navigate(data: TreeNavigationData_unstable, focusOptions?: FocusOptions) {
const nextElement = getNextElement(data);
if (nextElement) {
rove(nextElement);
rove(nextElement, focusOptions);
}
}
return { navigate, rootRef: useMergedRefs(walkerRootRef, rootRefCallback) } as const;
return {
navigate,
treeRef: useMergedRefs(walkerRootRef, rootRefCallback) as React.RefCallback<HTMLElement>,
} as const;
}

function lastChildRecursive(walker: HTMLElementWalker) {
Expand Down

0 comments on commit 2cf3309

Please sign in to comment.