【问题标题】:Office UI Fabric TextField focus/cursor issuesOffice UI Fabric TextField 焦点/光标问题
【发布时间】:2025-12-02 14:45:02
【问题描述】:

我有一个在这里说明问题的 CodePen:https://codepen.io/elegault/pen/QzZwLO

场景:一个 DetailsList 组件和一个搜索框(TextField 组件)。当用户在搜索框中键入时,可以过滤列表项。如果在搜索结果中,任何选定的项目仍将在搜索结果中被选中。如果它不在搜索结果中,并且后续搜索确实包含该选择,则会重新选择它。 (注意:Office UI Fabric 团队似乎意识到这应该在本地处理,但我不确定添加此功能的计划,正如GitHub issue)。

问题:每次按键后焦点都会丢失,这使得输入和编辑搜索条件变得困难,因为用户每次都必须重新插入光标。

什么不起作用:在 TextField 已经获得焦点时调用 focus() (isFocused = true) 什么都不做。调用 focus() 仅在 isFocused = false 时有效。但这仅在筛选列表中恢复选择后调用 DetailsList.focusIndex() 时才成立。

伪代码:

componentDidUpdate(previousProps: any, previousState: AppProjectListState) {
  //Toggle off the current selection
  this._selection.toggleIndexSelected(currentIdx);
  //Set the new selection
  this._selection.toggleIndexSelected(newIdx);
  //Scroll the selection into view
  this._detailsListRef.current.focusIndex(newIdx, false);
}

这是 TextField 或 DetailsList 组件中的某种错误吗?或者我在 React 组件生命周期中这样做的方式?或者有没有办法确保在用户键入和重新计算列表项以及修改所选索引时不会丢失 TextField 的焦点?

【问题讨论】:

    标签: reactjs typescript office-ui-fabric


    【解决方案1】:

    我最近偶然发现了一个类似的功能请求,并提出了以下解决方案,该解决方案允许在过滤数据时保留DetailsList 中的选择

    首先引入了一个单独的组件,它实现了保留选择的逻辑:

    export interface IViewSelection {}
    
    export interface IViewSelectionProps
      extends React.HTMLAttributes<HTMLDivElement> {
      componentRef?: IRefObject<IViewSelection>;
    
      /**
       * The selection object to interact with when updating selection changes.
       */
      selection: ISelection;
    
      items: any[];
    }
    
    export interface IViewSelectionState {}
    
    export class ViewSelection extends BaseComponent<
      IViewSelectionProps,
      IViewSelectionState
    > {
      private items: any[];
      private selectedIndices: any[];
      constructor(props: IViewSelectionProps) {
        super(props);
        this.state = {};
        this.items = this.props.items;
        this.selectedIndices = [];
      }
    
      public render() {
        const { children } = this.props;
        return <div>{children}</div>;
      }
    
      public componentWillUpdate(
        nextProps: IViewSelectionProps,
        nextState: IViewSelectionState
      ) {
        this.saveSelection();
      }
    
      public componentDidUpdate(
        prevProps: IViewSelectionProps,
        prevState: IViewSelectionState
      ) {
        this.restoreSelection();
      }
    
      private toListIndex(index: number) {
        const viewItems = this.props.selection.getItems();
        const viewItem = viewItems[index];
        return this.items.findIndex(listItem => listItem === viewItem);
      }
    
      private toViewIndex(index: number) {
        const listItem = this.items[index];
        const viewIndex = this.props.selection
          .getItems()
          .findIndex(viewItem => viewItem === listItem);
        return viewIndex;
      }
    
      private saveSelection(): void {
        const newIndices = this.props.selection
          .getSelectedIndices()
          .map(index => this.toListIndex(index))
          .filter(index => this.selectedIndices.indexOf(index) === -1);
    
        const unselectedIndices = this.props.selection
          .getItems()
          .map((item, index) => index)
          .filter(index => this.props.selection.isIndexSelected(index) === false)
          .map(index => this.toListIndex(index));
    
        this.selectedIndices = this.selectedIndices.filter(
          index => unselectedIndices.indexOf(index) === -1
        );
        this.selectedIndices = [...this.selectedIndices, ...newIndices];
      }
    
      private restoreSelection(): void {
        const indices = this.selectedIndices
          .map(index => this.toViewIndex(index))
          .filter(index => index !== -1);
        for (const index of indices) {
          this.props.selection.setIndexSelected(index, true, false);
        }
      }
    }
    

    现在DetailsList 组件需要用ViewSelection 组件包裹以在应用过滤时保存和恢复选择

    const items = generateItems(20);
    
    export default class DetailsListBasicExample extends React.Component<
      {},
      {
        viewItems: any[];
      }
    > {
      private selection: Selection;
      private detailsList = React.createRef<IDetailsList>();
    
      constructor(props: {}) {
        super(props);
    
        this.selection = new Selection({
        });
        this.state = {
          viewItems: items
        };
        this.handleChange = this.handleChange.bind(this);
      }
    
      public render(): JSX.Element {
        return (
          <div>
            <TextField label="Filter by name:" onChange={this.handleChange} />
            <ViewSelection selection={this.selection} items={this.state.viewItems} >
              <DetailsList
                componentRef={this.detailsList}
                items={this.state.viewItems}
                columns={columns}
                setKey="set"
                layoutMode={DetailsListLayoutMode.fixedColumns}
                selection={this.selection}
                selectionMode={SelectionMode.multiple}
                selectionPreservedOnEmptyClick={true}
              />
            </ViewSelection>
          </div>
        );
      }
    
      private handleChange = (
        ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
        text: string
      ): void => {
        const viewItems = text
          ? items.filter(item => item.name.toLowerCase().indexOf(text.toLocaleLowerCase()) > -1)
          : items;
        this.setState({ viewItems });
      };
    }
    

    这里是a demo

    【讨论】:

    • 这太棒了 - 感谢 Vadim!但是我的核心问题仍然存在:如何将恢复的选择滚动到视图中(如果它在列表的下方)并保持对 TextField 的关注,以便保持光标位置以允许用户继续修改过滤器。我在这里分叉了你的演示:stackblitz.com/edit/office-ui-fabric-detailslist-selectionstate。顺便说一句,感谢您让我开始使用 StackBlitz!
    • 我已经在我的主项目中实现了你的解决方案,并且出现了另一个奇怪的问题:当 ViewSelection.restoreSelection() 调用 this.props.selection.setIndexSelected() 时,它会触发 onSelectionChanged 事件再次,并且错误地恢复了两个索引而不是一个(selectionMode 是单一的)。知道为什么会发生这种情况吗?
    • Eric,如果您可以创建一个示例来演示该问题并创建一个单独的问题,那就太好了,我认为这会更好,因为它不会使当前问题复杂化
    • 我希望我能 - 这是一个复杂的应用程序,不容易重新创建以进行测试。
    • 仅供参考:这是@Vadim Gremyachev 上面示例的更新版本。这个使用 React v17、Fluent UI 8 和一个功能组件(不使用 componentWillUpdate 或 componentDidUpdate):stackblitz.com/edit/react-ts-3shcme