From 59caaef827915ab766ec3875fbac141addb30d2f Mon Sep 17 00:00:00 2001 From: Lambert W Date: Wed, 31 Jan 2018 15:39:47 -0800 Subject: [PATCH 1/5] [Focus] Enable focus forceIntoFirstElement parameter --- .../components/DetailsList/DetailsList.tsx | 21 +++++++++-- .../src/components/DetailsList/DetailsRow.tsx | 6 ++-- .../DetailsList/DetailsRowFields.tsx | 1 - .../examples/DetailsList.Grouped.Example.scss | 18 ++++++++++ .../examples/DetailsList.Grouped.Example.tsx | 35 ++++++++++++++++--- .../components/FocusZone/FocusZone.types.ts | 3 +- 6 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 packages/office-ui-fabric-react/src/components/DetailsList/examples/DetailsList.Grouped.Example.scss 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 c5751ebc41a24..8b7dde2b0dc4d 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.tsx +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.tsx @@ -130,6 +130,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(); @@ -532,12 +549,12 @@ export class DetailsList extends BaseComponent { - row.focus(); + row.focus(forceIntoFirstElement); }, 0); } 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 6e53c0ddba93c..949342332cf1c 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsRow.tsx +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsRow.tsx @@ -202,7 +202,7 @@ export class DetailsRow extends BaseComponent { + private _root: DetailsList; + constructor(props: {}) { super(props); @@ -65,12 +72,13 @@ export class DetailsListGroupedExample extends React.Component<{}, { let { items } = this.state; return ( - + ); } + @autobind private _addItem() { let items = this.state.items; @@ -112,7 +122,24 @@ 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/FocusZone/FocusZone.types.ts b/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.types.ts index d8417e1699c65..be728aa4ca891 100644 --- a/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.types.ts +++ b/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.types.ts @@ -7,9 +7,10 @@ import { FocusZone } from './FocusZone'; export interface IFocusZone { /** * Sets focus to the first tabbable item in the zone. + * @param forceIntoFirstElement If the item itself contains tabbable elements, focus into the first tabbable child element. * @returns True if focus could be set to an active element, false if no operation was taken. */ - focus(): boolean; + focus(forceIntoFirstElement?: boolean): boolean; /** * Sets focus to a specific child element within the zone. This can be used in conjunction with From edf211d5d89dd1a4803a73e0cc96e1b67cc7d02f Mon Sep 17 00:00:00 2001 From: Lambert W Date: Wed, 31 Jan 2018 15:40:15 -0800 Subject: [PATCH 2/5] Change file --- ...magellan-detailsListRowFocus_2018-01-31-23-40.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/office-ui-fabric-react/magellan-detailsListRowFocus_2018-01-31-23-40.json 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 0000000000000..f40b21e935a3b --- /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 From 04459406119eb62997d28b412bf7ef477e300faa Mon Sep 17 00:00:00 2001 From: Lambert W Date: Wed, 31 Jan 2018 16:04:51 -0800 Subject: [PATCH 3/5] Fixed formatting --- .../DetailsList/examples/DetailsList.Grouped.Example.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 1535886f604e0..6dd7e0ee99e00 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 @@ -137,7 +137,8 @@ export class DetailsListGroupedExample extends BaseComponent<{}, { return (
+ data-is-focusable={ true } + > { value }
); From db3c4c1a6ee713f38908b5e1b7e1738f066e0200 Mon Sep 17 00:00:00 2001 From: Lambert W Date: Thu, 22 Feb 2018 11:12:09 -0800 Subject: [PATCH 4/5] Added DetailsList test and public api --- .../DetailsList/DetailsList.test.tsx | 108 +++++++++ .../components/DetailsList/DetailsList.tsx | 2 +- .../DetailsList/DetailsList.types.ts | 10 + .../__snapshots__/DetailsList.test.tsx.snap | 224 ++++++++++++++++++ .../src/components/List/List.tsx | 4 +- 5 files changed, 346 insertions(+), 2 deletions(-) create mode 100644 packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.test.tsx create mode 100644 packages/office-ui-fabric-react/src/components/DetailsList/__snapshots__/DetailsList.test.tsx.snap 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 0000000000000..3711772985232 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.test.tsx @@ -0,0 +1,108 @@ +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( + // tslint:disable-next-line:jsx-no-lambda + null } + skipViewportMeasures={ true } + 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 } + 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', () => { + let onRenderColumn = (item: any, index: number, column: IColumn) => { + let value = (item && column && column.fieldName) ? item[column.fieldName] : ''; + if (value === null || value === undefined) { + value = ''; + } + return ( +
+ { value } +
+ ); + } + + jest.useFakeTimers(); + + let component: any; + const wrapper = mount( + component = ref } + skipViewportMeasures={ true } + 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 7e632e6668ddb..7e0dc127b7b71 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.tsx +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.tsx @@ -696,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 f370f567e8a30..9ae30253e1758 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/__snapshots__/DetailsList.test.tsx.snap b/packages/office-ui-fabric-react/src/components/DetailsList/__snapshots__/DetailsList.test.tsx.snap new file mode 100644 index 0000000000000..73c2e236ca0bc --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/DetailsList/__snapshots__/DetailsList.test.tsx.snap @@ -0,0 +1,224 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DetailsList renders List correctly 1`] = ` +
+
+
+
+
+
+ + + +
+
+ + + + key + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; 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 9ab99cc74aed8..fe1846f04ee7a 100644 --- a/packages/office-ui-fabric-react/src/components/List/List.tsx +++ b/packages/office-ui-fabric-react/src/components/List/List.tsx @@ -265,7 +265,9 @@ export class List extends BaseComponent implements IList this._scrollElement = findScrollableParent(this.refs.root) as HTMLElement; this._events.on(window, 'resize', this._onAsyncResize); - this._events.on(this.refs.root, 'focus', this._onFocus, true); + if (this.refs.root) { + this._events.on(this.refs.root, 'focus', this._onFocus, true); + } if (this._scrollElement) { this._events.on(this._scrollElement, 'scroll', this._onScroll); this._events.on(this._scrollElement, 'scroll', this._onAsyncScroll); From 74255a8d73ec75f399531c261b6cc333375e7ff0 Mon Sep 17 00:00:00 2001 From: Lambert W Date: Thu, 22 Feb 2018 11:45:35 -0800 Subject: [PATCH 5/5] Fixed build errors --- .../src/components/DetailsList/DetailsList.test.tsx | 12 +++++++++--- .../__snapshots__/DetailsList.test.tsx.snap | 2 -- 2 files changed, 9 insertions(+), 5 deletions(-) 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 index 3711772985232..ff04576a4f4e8 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.test.tsx +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.test.tsx @@ -32,11 +32,12 @@ describe('DetailsList', () => { DetailsList.prototype.componentDidMount = jest.fn(); const component = renderer.create( - // tslint:disable-next-line:jsx-no-lambda null } skipViewportMeasures={ true } + // tslint:disable-next-line:jsx-no-lambda onShouldVirtualize={ () => false } /> ); @@ -51,8 +52,10 @@ describe('DetailsList', () => { const wrapper = mount( component = ref } skipViewportMeasures={ true } + // tslint:disable-next-line:jsx-no-lambda onShouldVirtualize={ () => false } />); @@ -66,17 +69,18 @@ describe('DetailsList', () => { }); it('focuses into row element', () => { - let onRenderColumn = (item: any, index: number, column: IColumn) => { + 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(); @@ -84,8 +88,10 @@ describe('DetailsList', () => { const wrapper = mount( component = ref } skipViewportMeasures={ true } + // tslint:disable-next-line:jsx-no-lambda onShouldVirtualize={ () => false } onRenderItemColumn={ onRenderColumn } />); diff --git a/packages/office-ui-fabric-react/src/components/DetailsList/__snapshots__/DetailsList.test.tsx.snap b/packages/office-ui-fabric-react/src/components/DetailsList/__snapshots__/DetailsList.test.tsx.snap index 73c2e236ca0bc..3781b2dbc69d2 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/__snapshots__/DetailsList.test.tsx.snap +++ b/packages/office-ui-fabric-react/src/components/DetailsList/__snapshots__/DetailsList.test.tsx.snap @@ -66,7 +66,6 @@ exports[`DetailsList renders List correctly 1`] = ` >