Skip to content

Commit

Permalink
Merge pull request #4149 from SherpasGroup/fix-selection-scrolling
Browse files Browse the repository at this point in the history
Implement handling of scrolling after selection
  • Loading branch information
wobba authored Dec 14, 2024
2 parents da50df4 + 2d44874 commit 87047b8
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 14 deletions.
103 changes: 90 additions & 13 deletions search-parts/src/components/DetailsListComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Fabric, ShimmeredDetailsList, IShimmeredDetailsListProps, Checkbox } from '@fluentui/react';
import { Fabric, ShimmeredDetailsList, IShimmeredDetailsListProps, Checkbox, ScrollToMode } from '@fluentui/react';
import { ITooltipHostProps, TooltipHost, ITooltipStyles, Shimmer, ShimmerElementsGroup, ShimmerElementType, IShimmerElement, mergeStyleSets, ITheme, Selection } from '@fluentui/react';
import * as Handlebars from 'handlebars';
import { IReadonlyTheme } from '@microsoft/sp-component-base';
import { BaseWebComponent, BuiltinTemplateSlots, ExtensibilityConstants, ISortInfo, SortFieldDirection } from '@pnp/modern-search-extensibility';
import { groupBy, sortBy, findIndex, isEmpty } from "@microsoft/sp-lodash-subset";
import { FileIcon } from '../components/FileIconComponent';
import { DetailsListLayoutMode, SelectionMode, IColumn, IGroup, IDetailsRowProps, DetailsRow, IDetailsHeaderProps, CheckboxVisibility, IDetailsRowCheckProps, DetailsRowCheck, IDetailsCheckboxProps, IDetailsListStyles, ConstrainMode, ISelectionZoneProps } from '@fluentui/react/lib/DetailsList';
import { DetailsListLayoutMode, SelectionMode, IColumn, IGroup, IDetailsRowProps, DetailsRow, IDetailsHeaderProps, CheckboxVisibility, IDetailsRowCheckProps, DetailsRowCheck, IDetailsCheckboxProps, IDetailsListStyles, ConstrainMode, ISelectionZoneProps, IDetailsList } from '@fluentui/react/lib/DetailsList';
import { DEFAULT_CELL_STYLE_PROPS, DEFAULT_ROW_HEIGHTS } from '@fluentui/react/lib/components/DetailsList/DetailsRow.styles';
import { ISearchResultsTemplateContext } from '../models/common/ITemplateContext';
import { ObjectHelper } from '../helpers/ObjectHelper';
Expand Down Expand Up @@ -242,11 +242,13 @@ export interface IDetailsListComponentState {

export class DetailsListComponent extends React.Component<IDetailsListComponentProps, IDetailsListComponentState> {

private scrollPositionKey = 'scrollPosition';
private _allItems: any[];
private _templateContext: ISearchResultsTemplateContext;
private _domPurify: any;
private _selection: Selection;
private _selectionMode: SelectionMode = SelectionMode.none;
private _detailsListRef: React.RefObject<IDetailsList>;

constructor(props: IDetailsListComponentProps) {
super(props);
Expand All @@ -261,6 +263,8 @@ export class DetailsListComponent extends React.Component<IDetailsListComponentP
this._domPurify.addHook('uponSanitizeElement', DomPurifyHelper.allowCustomComponentsHook);
this._domPurify.addHook('uponSanitizeAttribute', DomPurifyHelper.allowCustomAttributesHook);

this._detailsListRef = React.createRef<IDetailsList>();

// Build the intitial groups
if (this.props.groupBy) {
// Because groups are determined by a start index and a count, we need to sort items to regroup them in the collection before processing.
Expand Down Expand Up @@ -432,6 +436,7 @@ export class DetailsListComponent extends React.Component<IDetailsListComponentP
const { columns, items } = this.state;

let shimmeredDetailsListProps: IShimmeredDetailsListProps = {
componentRef: this._detailsListRef,
theme: this.props.themeVariant as ITheme,
items: items,
compact: this.props.isCompact,
Expand All @@ -444,9 +449,14 @@ export class DetailsListComponent extends React.Component<IDetailsListComponentP
selectionPreservedOnEmptyClick: true,
enterModalSelectionOnTouch: true,
disableSelectionZone: true,
isSelectedOnFocus: false,
focusZoneProps: {
shouldRaiseClicks: false,
},
onRenderCustomPlaceholder: this._onRenderCustomPlaceholder,
onRenderRow: this._onRenderRow,
onRenderDetailsHeader: this._onRenderDetailsHeader,
onShouldVirtualize: () => { return false; },
};

if (this.props.enableStickyHeader) {
Expand Down Expand Up @@ -483,15 +493,19 @@ export class DetailsListComponent extends React.Component<IDetailsListComponentP
const selectionZoneProps: ISelectionZoneProps = {
className: classNames.selectionZone,
selection: this._selection,
selectionMode: this._selectionMode,
};

shimmeredDetailsListProps.detailsListStyles = gridStyles;
shimmeredDetailsListProps.focusZoneProps = focusZoneProps;
shimmeredDetailsListProps.focusZoneProps = {
...focusZoneProps,
...shimmeredDetailsListProps.focusZoneProps,
};
shimmeredDetailsListProps.selectionZoneProps = selectionZoneProps;
shimmeredDetailsListProps.constrainMode = ConstrainMode.unconstrained;
shimmeredDetailsListProps.layoutMode = DetailsListLayoutMode.fixedColumns;
}

if (this.state.groups.length > 0) {
shimmeredDetailsListProps.groups = this.state.groups;
shimmeredDetailsListProps.groupProps = {
Expand All @@ -513,18 +527,73 @@ export class DetailsListComponent extends React.Component<IDetailsListComponentP
const additionalGroupBy = this.props.additionalGroupBy ? this.props.additionalGroupBy.map((field) => field.value) : [];
const groups = this._buildGroups(this.state.items, [this.props.groupBy, ...additionalGroupBy], 0, 0);

this.setState({groups: groups});
this.setState({groups: groups}, () => {
this.updateSelection();
});
}
else {
this.updateSelection();
}
}

// Manually select the items in the list
if (this.props.selectedKeys) {
private updateSelection = () => {
if (this.props.selectedKeys) {
this.forceUpdate(() => {
this._selection.setAllSelected(false);
this.props.selectedKeys.forEach(key => {
this._selection.setKeySelected(key, true, true);
});
const savedScrollPosition = sessionStorage.getItem(`${this.scrollPositionKey}${this._templateContext?.instanceId}`);
if (savedScrollPosition) {
const { top, left, itemIndex } = JSON.parse(savedScrollPosition);
const webpartElement = document.querySelector(`[data-instance-id="${this._templateContext?.instanceId}"]`);
const itemElement = webpartElement?.querySelector(`[data-item-index="${itemIndex}"]`)?.parentElement;
if (itemElement) {
this._detailsListRef.current?.focusIndex(itemIndex, false, () => itemElement.getBoundingClientRect().height, ScrollToMode.auto);
const scrollableContainer = itemElement.closest('[data-is-scrollable="true"]');
if (scrollableContainer) {
const scrollToOptions = {
top: top,
left: left,
behavior: 'instant'
}
scrollableContainer.scrollTo(scrollToOptions as unknown as ScrollToOptions);
}
}
sessionStorage.removeItem(`${this.scrollPositionKey}${this._templateContext?.instanceId}`);
}
});
}
}

this.props.selectedKeys.forEach(key => {
this._selection.setKeySelected(key, true, false);
});
private handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>, itemIndex: number) => {
if (event.key === ' ') {
this.setScrollPosition(event.currentTarget as HTMLElement);
}
};

private handleItemClick = (event: React.MouseEvent<HTMLDivElement>) => {
this.setScrollPosition(event.currentTarget as HTMLElement);
}

this.forceUpdate();
}
private setScrollPosition = (target: HTMLElement) => {
const itemIndex = this.getItemIndex(target);
const scrollableContainer = target.closest('[data-is-scrollable="true"]');
const scrollPosition = {
top: scrollableContainer.scrollTop,
left: scrollableContainer.scrollLeft,
itemIndex: itemIndex
};
sessionStorage.setItem(`${this.scrollPositionKey}${this._templateContext?.instanceId}`, JSON.stringify(scrollPosition));
}

private getItemIndex(target: HTMLElement): string | null {
if (target.hasAttribute('data-item-index')) {
return target.getAttribute('data-item-index');
}
const closestElement = target.closest('[data-item-index]');
const closestChild = target.querySelector('[data-item-index]');
return closestElement ? closestElement.getAttribute('data-item-index') : closestChild ? closestChild.getAttribute('data-item-index') : null;
}

private _onRenderCustomPlaceholder(rowProps: IDetailsRowProps): JSX.Element {
Expand Down Expand Up @@ -628,7 +697,15 @@ export class DetailsListComponent extends React.Component<IDetailsListComponentP
return (<DetailsRowCheck {...props} theme={this.props.themeVariant as ITheme} />);
};

return <DetailsRow {...rowProps} theme={this.props.themeVariant as ITheme} />;
return <div
onPointerDown={this.props.allowItemSelection ? this.handleItemClick : undefined}
onKeyDown={this.props.allowItemSelection ? (event) => this.handleKeyDown(event, rowProps.itemIndex) : undefined}
>
<DetailsRow
{...rowProps}
theme={this.props.themeVariant as ITheme}
/>
</div>;
}

private _onRenderDetailsHeader(props: IDetailsHeaderProps, defaultRender): JSX.Element {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ export default class SearchResultsContainer extends React.Component<ISearchResul

renderTemplate = <SelectionZone
selection={this._selection}
selectionMode={selectionMode}>
selectionMode={selectionMode}
isSelectedOnFocus={false}>
<TemplateRenderer
templateContent={templateContent}
templateContext={templateContext}
Expand Down

0 comments on commit 87047b8

Please sign in to comment.