diff --git a/search-parts/src/components/DetailsListComponent.tsx b/search-parts/src/components/DetailsListComponent.tsx index dc1a7e0f..fbdb75de 100644 --- a/search-parts/src/components/DetailsListComponent.tsx +++ b/search-parts/src/components/DetailsListComponent.tsx @@ -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'; @@ -242,11 +242,13 @@ export interface IDetailsListComponentState { export class DetailsListComponent extends React.Component { + private scrollPositionKey = 'scrollPosition'; private _allItems: any[]; private _templateContext: ISearchResultsTemplateContext; private _domPurify: any; private _selection: Selection; private _selectionMode: SelectionMode = SelectionMode.none; + private _detailsListRef: React.RefObject; constructor(props: IDetailsListComponentProps) { super(props); @@ -261,6 +263,8 @@ export class DetailsListComponent extends React.Component(); + // 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. @@ -432,6 +436,7 @@ export class DetailsListComponent extends React.Component { return false; }, }; if (this.props.enableStickyHeader) { @@ -483,15 +493,19 @@ export class DetailsListComponent extends React.Component 0) { shimmeredDetailsListProps.groups = this.state.groups; shimmeredDetailsListProps.groupProps = { @@ -513,18 +527,73 @@ export class DetailsListComponent extends React.Component 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, itemIndex: number) => { + if (event.key === ' ') { + this.setScrollPosition(event.currentTarget as HTMLElement); + } + }; + + private handleItemClick = (event: React.MouseEvent) => { + 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 { @@ -628,7 +697,15 @@ export class DetailsListComponent extends React.Component); }; - return ; + return
this.handleKeyDown(event, rowProps.itemIndex) : undefined} + > + +
; } private _onRenderDetailsHeader(props: IDetailsHeaderProps, defaultRender): JSX.Element { diff --git a/search-parts/src/webparts/searchResults/components/SearchResultsContainer.tsx b/search-parts/src/webparts/searchResults/components/SearchResultsContainer.tsx index 5fc28c19..8ea25ef7 100644 --- a/search-parts/src/webparts/searchResults/components/SearchResultsContainer.tsx +++ b/search-parts/src/webparts/searchResults/components/SearchResultsContainer.tsx @@ -113,7 +113,8 @@ export default class SearchResultsContainer extends React.Component + selectionMode={selectionMode} + isSelectedOnFocus={false}>