diff --git a/common/changes/office-ui-fabric-react/magellan-detailsListRowFocus_2018-01-31-23-40.json b/common/changes/office-ui-fabric-react/magellan-detailsListRowFocus_2018-01-31-23-40.json new file mode 100644 index 00000000000000..f40b21e935a3b4 --- /dev/null +++ b/common/changes/office-ui-fabric-react/magellan-detailsListRowFocus_2018-01-31-23-40.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "[Focus] Enable focus forceIntoFirstElement parameter", + "type": "patch" + } + ], + "packageName": "office-ui-fabric-react", + "email": "law@microsoft.com" +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.test.tsx b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.test.tsx new file mode 100644 index 00000000000000..ff04576a4f4e89 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.test.tsx @@ -0,0 +1,114 @@ +import * as React from 'react'; +import * as renderer from 'react-test-renderer'; +import * as ReactTestUtils from 'react-dom/test-utils'; +import { mount } from 'enzyme'; + +import { + DetailsList +} from './DetailsList'; + +import { + IDetailsList, + IColumn +} from './DetailsList.types'; + +// Populate mock items for testing +function mockItems(count: number): any { + const items = []; + + for (let i = 0; i < count; i++) { + items.push({ + key: i, + name: 'Item ' + i, + value: i + }); + } + + return items; +} + +describe('DetailsList', () => { + it('renders List correctly', () => { + DetailsList.prototype.componentDidMount = jest.fn(); + + const component = renderer.create( + null } + skipViewportMeasures={ true } + // tslint:disable-next-line:jsx-no-lambda + onShouldVirtualize={ () => false } + /> + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('focuses row by index', () => { + jest.useFakeTimers(); + + let component: any; + const wrapper = mount( + component = ref } + skipViewportMeasures={ true } + // tslint:disable-next-line:jsx-no-lambda + onShouldVirtualize={ () => false } + />); + + expect(component).toBeDefined(); + (component as IDetailsList).focusIndex(2); + setTimeout(() => { + expect(document.activeElement.className.split(' ')).toContain('ms-DetailsRow'); + expect(document.activeElement.textContent).toEqual('2'); + }, 0); + jest.runOnlyPendingTimers(); + }); + + it('focuses into row element', () => { + const onRenderColumn = (item: any, index: number, column: IColumn) => { + let value = (item && column && column.fieldName) ? item[column.fieldName] : ''; + if (value === null || value === undefined) { + value = ''; + } + console.log('Rendered column'); + return ( +
+ { value } +
+ ); + }; + + jest.useFakeTimers(); + + let component: any; + const wrapper = mount( + component = ref } + skipViewportMeasures={ true } + // tslint:disable-next-line:jsx-no-lambda + onShouldVirtualize={ () => false } + onRenderItemColumn={ onRenderColumn } + />); + + expect(component).toBeDefined(); + (component as IDetailsList).focusIndex(2); + setTimeout(() => { + expect(document.activeElement.className.split(' ')).toContain('ms-DetailsRow'); + expect(document.activeElement.textContent).toEqual('2'); + }, 0); + jest.runOnlyPendingTimers(); + + (component as IDetailsList).focusIndex(2, true); + setTimeout(() => { + expect(document.activeElement.className.split(' ')).toContain('test-column'); + expect(document.activeElement.textContent).toEqual('2'); + }, 0); + jest.runOnlyPendingTimers(); + }); +}); \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.tsx b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.tsx index df241e22f95cfc..7e0dc127b7b714 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.tsx +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.tsx @@ -131,6 +131,23 @@ export class DetailsList extends BaseComponent number): void { + + const item = this.props.items[index]; + if (item) { + this.scrollToIndex(index, measureItem); + + const itemKey = this._getItemKey(item, index); + const row = this._activeRows[itemKey]; + if (row) { + this._setFocusToRow(row, forceIntoFirstElement); + } + } + } + public componentWillUnmount() { if (this._dragDropHelper) { this._dragDropHelper.dispose(); @@ -537,12 +554,12 @@ export class DetailsList extends BaseComponent { - row.focus(); + row.focus(forceIntoFirstElement); }, 0); } @@ -679,7 +696,7 @@ export class DetailsList extends BaseComponent { - const newColumn = assign( + const newColumn = assign( {}, column, { diff --git a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.types.ts b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.types.ts index f370f567e8a309..9ae30253e17580 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.types.ts +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.types.ts @@ -27,6 +27,16 @@ export interface IDetailsList extends IList { * call this to force a re-evaluation. Be aware that this can be an expensive operation and should be done sparingly. */ forceUpdate: () => void; + + /** + * Scroll to and focus the item at the given index. focusIndex will call scrollToIndex on the specified index. + * + * @param index Index of item to scroll to + * @param forceIntoFirstElement If true, focus will be set to the first focusable child element of the item rather + * than the item itself. + * @param measureItem Optional callback to measure the height of an individual item + */ + focusIndex: (index: number, forceIntoFirstElement?: boolean, measureItem?: (itemIndex: number) => number) => void; } export interface IDetailsListProps extends React.Props, IWithViewportProps { diff --git a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsRow.tsx b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsRow.tsx index 064c80759e8a49..05e097ffd328a0 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsRow.tsx +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsRow.tsx @@ -309,8 +309,8 @@ export class DetailsRow extends BaseComponent +
+
+
+
+
+ + + +
+
+ + + + key + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/packages/office-ui-fabric-react/src/components/DetailsList/examples/DetailsList.Grouped.Example.scss b/packages/office-ui-fabric-react/src/components/DetailsList/examples/DetailsList.Grouped.Example.scss new file mode 100644 index 00000000000000..45b51547ae8026 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/DetailsList/examples/DetailsList.Grouped.Example.scss @@ -0,0 +1,18 @@ +@import '../../../common/common'; + +:global { + .DetailsList-grouped-example { + .ms-DetailsList { + max-height: 500px; + overflow-y: scroll; + + .ms-DetailsRow-cell { + padding: 0px; + } + + .grouped-example-column { + padding: 11px 8px; + } + } + } +} diff --git a/packages/office-ui-fabric-react/src/components/DetailsList/examples/DetailsList.Grouped.Example.tsx b/packages/office-ui-fabric-react/src/components/DetailsList/examples/DetailsList.Grouped.Example.tsx index 4bd594fa936f14..7ecbe0e9dd3d80 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/examples/DetailsList.Grouped.Example.tsx +++ b/packages/office-ui-fabric-react/src/components/DetailsList/examples/DetailsList.Grouped.Example.tsx @@ -1,9 +1,14 @@ /* tslint:disable:no-unused-variable */ import * as React from 'react'; /* tslint:enable:no-unused-variable */ +import { + BaseComponent, + autobind +} from 'office-ui-fabric-react/lib/Utilities'; import { DefaultButton } from 'office-ui-fabric-react/lib/Button'; import { Fabric } from 'office-ui-fabric-react/lib/Fabric'; -import { DetailsList } from 'office-ui-fabric-react/lib/DetailsList'; +import { DetailsList, IColumn } from 'office-ui-fabric-react/lib/DetailsList'; +import './DetailsList.Grouped.Example.scss'; const _columns = [ { @@ -50,9 +55,11 @@ const _items = [ } ]; -export class DetailsListGroupedExample extends React.Component<{}, { +export class DetailsListGroupedExample extends BaseComponent<{}, { items: {}[]; }> { + private _root: DetailsList; + constructor(props: {}) { super(props); @@ -65,12 +72,13 @@ export class DetailsListGroupedExample extends React.Component<{}, { const { items } = this.state; return ( - + ); } + @autobind private _addItem() { const items = this.state.items; @@ -112,7 +122,25 @@ export class DetailsListGroupedExample extends React.Component<{}, { name: 'New item ' + items.length, color: 'blue' }]) + }, () => { + this._root.focusIndex(items.length, true); }); } + private _onRenderColumn(item: any, index: number, column: IColumn) { + let value = (item && column && column.fieldName) ? item[column.fieldName] : ''; + + if (value === null || value === undefined) { + value = ''; + } + + return ( +
+ { value } +
+ ); + } } diff --git a/packages/office-ui-fabric-react/src/components/List/List.tsx b/packages/office-ui-fabric-react/src/components/List/List.tsx index e09da24fb18831..bbf4170f2f07ac 100644 --- a/packages/office-ui-fabric-react/src/components/List/List.tsx +++ b/packages/office-ui-fabric-react/src/components/List/List.tsx @@ -266,7 +266,9 @@ export class List extends BaseComponent implements IList this._scrollElement = findScrollableParent(this._root) as HTMLElement; this._events.on(window, 'resize', this._onAsyncResize); - this._events.on(this._root, 'focus', this._onFocus, true); + if (this._root) { + this._events.on(this._root, 'focus', this._onFocus, true); + } if (this._scrollElement) { this._events.on(this._scrollElement, 'scroll', this._onScroll); this._events.on(this._scrollElement, 'scroll', this._onAsyncScroll);