diff --git a/CHANGELOG.md b/CHANGELOG.md index f8eb670190..0f5e592597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Added sourcemaps to the dist output to simplify debugging @miroslavstastny ([#2329](https://github.com/microsoft/fluent-ui-react/pull/2329)) - Adding 'expand', 'collapse', 'companion', 'share-to' and 'settings-audio' icons @TanelVari ([#2343](https://github.com/microsoft/fluent-ui-react/pull/2343)) - Add support for Children API in `List` component @layershifter ([#2207](https://github.com/microsoft/fluent-ui-react/pull/2207)) +- Added virtualized table prototype using `react-virtualized` and `Table` components @pompomon ([#2339](https://github.com/microsoft/fluent-ui-react/pull/2339)) ### Performance - Add styles caching when there aren't inline overrides defined @mnajdova ([#2309](https://github.com/microsoft/fluent-ui-react/pull/2309)) diff --git a/docs/package.json b/docs/package.json index 60c39bdf94..ecc1d34f20 100644 --- a/docs/package.json +++ b/docs/package.json @@ -28,6 +28,7 @@ "@types/node": "^10.3.2", "@types/react-custom-scrollbars": "^4.0.5", "@types/react-router-dom": "^4.3.4", + "@types/react-virtualized": "^9.21.8", "@types/webpack-env": "^1.14.1", "gulp": "^4.0.2" }, diff --git a/docs/src/components/Sidebar/Sidebar.tsx b/docs/src/components/Sidebar/Sidebar.tsx index a034b6e1cc..e01f0297f4 100644 --- a/docs/src/components/Sidebar/Sidebar.tsx +++ b/docs/src/components/Sidebar/Sidebar.tsx @@ -414,6 +414,11 @@ class Sidebar extends React.Component { }, public: true, }, + { + key: 'virtualized-table', + title: { content: 'VirtualizedTable', as: NavLink, to: '/virtualized-table' }, + public: true, + }, ] const componentTreeSection = { diff --git a/docs/src/prototypes/VirtualizedTable/VirtualizedTable.tsx b/docs/src/prototypes/VirtualizedTable/VirtualizedTable.tsx new file mode 100644 index 0000000000..cdb246c37b --- /dev/null +++ b/docs/src/prototypes/VirtualizedTable/VirtualizedTable.tsx @@ -0,0 +1,74 @@ +import { + gridCellBehavior, + gridHeaderCellBehavior, + gridNestedBehavior, + gridRowBehavior, + Table, +} from '@fluentui/react' +import * as React from 'react' +import { + AutoSizer, + List as ReactVirtualizedList, + ListRowRenderer, + ListProps, +} from 'react-virtualized' +import getItems from './itemsGenerator' + +const { rows } = getItems() +const rowGetter = ({ index }) => { + return rows[index] +} + +// Overrides ARIA attributes assigned by default, which break accessibility +const accessibilityListProperties: Partial = { + 'aria-label': '', + 'aria-readonly': undefined, + containerRole: 'presentation', + role: 'presentation', +} + +const rowRenderer: ListRowRenderer = ({ index, style }) => { + const row = rows[index] + return ( + + + + + + + ) +} + +const VirtualizedTablePrototype = () => ( + + {({ width }) => ( + + + + + + + + + +
+ )} +
+) + +export default VirtualizedTablePrototype diff --git a/docs/src/prototypes/VirtualizedTable/VirtualizedTables.tsx b/docs/src/prototypes/VirtualizedTable/VirtualizedTables.tsx new file mode 100644 index 0000000000..f5f1bd0e17 --- /dev/null +++ b/docs/src/prototypes/VirtualizedTable/VirtualizedTables.tsx @@ -0,0 +1,147 @@ +import { + Accordion, + gridCellBehavior, + gridHeaderCellBehavior, + gridNestedBehavior, + gridRowBehavior, + Table, +} from '@fluentui/react' +import * as React from 'react' +import { + AutoSizer, + List as ReactVirtualizedList, + WindowScroller, + ListProps, + ListRowRenderer, +} from 'react-virtualized' +import getItems from './itemsGenerator' + +// Magic offset for native scrollbar in Edge on Mac +const scrollbarOffset = 10 + +function VirtualizedTablesPrototype() { + const [ref, setRef] = React.useState(null) + + const tables = [ + { + key: 'table1', + title:
Table one
, + content: , + }, + { + key: 'table2', + title:
Custom table title
, + content: , + }, + ] + + return ( +
+ {ref && } +
+ ) +} + +interface VirtualizedTableProps { + scrollElementRef: HTMLDivElement + label: string +} + +const accessibilityListProperties: Partial = { + 'aria-label': '', + 'aria-readonly': undefined, + containerRole: 'presentation', + role: 'presentation', + tabIndex: null, +} + +const accessibilityWrapperProperties: React.HTMLAttributes = { + 'aria-label': '', + 'aria-readonly': undefined, + role: 'presentation', +} + +function VirtualizedTable(props: VirtualizedTableProps) { + const { header, rows } = getItems(20, 50) + const renderedItems = [header, ...rows] + const itemsCount = renderedItems.length + + const rowGetter = ({ index }) => { + return renderedItems[index] + } + + const rowRenderer: ListRowRenderer = ({ index, style }) => { + const row = renderedItems[index] + const header = row.key === 'header' + return ( + + + + + + + ) + } + + return ( + + {({ height, isScrolling, registerChild, onChildScroll, scrollTop }) => ( + + {({ width }) => { + return height ? ( + +
registerChild(el)} {...accessibilityWrapperProperties}> + +
+
+ ) : null + }} +
+ )} +
+ ) +} + +export default VirtualizedTablesPrototype diff --git a/docs/src/prototypes/VirtualizedTable/index.tsx b/docs/src/prototypes/VirtualizedTable/index.tsx new file mode 100644 index 0000000000..47bd676815 --- /dev/null +++ b/docs/src/prototypes/VirtualizedTable/index.tsx @@ -0,0 +1,48 @@ +import * as React from 'react' +import VirtualizedTable from './VirtualizedTable' +import VirtualizedTables from './VirtualizedTables' +import { PrototypeSection, ComponentPrototype } from '../Prototypes' + +export default () => ( + + + + + + Notes: +
+ + Prototype is using fixed row height, for dynamic height please check{' '} + + CellMeasurer component. + + +
+ Known issues: +
+ Integration with React-custom-scrollbars. + + React-virtualized has{' '} + + an opened feature request + {' '} + to support React-custom-scrollbars and there are a couple of ways to add custom + scrollbars to List component (see{' '} + issue one and{' '} + issue two). + Unfortunately, suggested solutions do not seem to work with two lists wrapped with + WindowScroller elements. + + + } + > + +
+
+) diff --git a/docs/src/prototypes/VirtualizedTable/itemsGenerator.ts b/docs/src/prototypes/VirtualizedTable/itemsGenerator.ts new file mode 100644 index 0000000000..c44498be96 --- /dev/null +++ b/docs/src/prototypes/VirtualizedTable/itemsGenerator.ts @@ -0,0 +1,47 @@ +import * as _ from 'lodash' + +function getItems(minItems = 20, maxItems = 40) { + function getRandomNumber(minItems, maxItems) { + return _.random(minItems, maxItems) + } + + function getRandomName() { + const names = ['Roman', 'Alex', 'Ali', 'Bo', 'Timbuktu', 'Daria', 'E.T.'] + const addLongName = Math.random() > 0.5 + return ( + names[Math.floor(Math.random() * names.length)] + + (addLongName ? ' van von der Longername' : '') + ) + } + + function generateRows() { + const header = { + key: 'header', + items: [ + { content: 'id', key: 'id' }, + { content: 'Name', key: 'name' }, + { content: 'Picture', key: 'pic' }, + { content: 'Age', key: 'action' }, + ], + } + const rowsPlain = _.times(getRandomNumber(minItems, maxItems), index => ({ + key: `${index}`, + items: [ + { content: `${index}`, key: `${index}-1` }, + { + content: getRandomName(), + truncateContent: true, + key: `${index}-2`, + }, + { content: 'None', key: `${index}-3` }, + { content: `${getRandomNumber(10, 1000)} years`, key: `${index}-4` }, + ], + })) + + return { header, rows: rowsPlain } + } + + return generateRows() +} + +export default getItems diff --git a/docs/src/routes.tsx b/docs/src/routes.tsx index 7c4c84abd1..77992c142e 100644 --- a/docs/src/routes.tsx +++ b/docs/src/routes.tsx @@ -51,6 +51,7 @@ import CustomScrollbarPrototype from './prototypes/customScrollbar' import EditorToolbarPrototype from './prototypes/EditorToolbar' import HexagonalAvatarPrototype from './prototypes/hexagonalAvatar' import TablePrototype from './prototypes/table' +import VirtualizedTablePrototype from './prototypes/VirtualizedTable' const Routes = () => ( @@ -90,6 +91,7 @@ const Routes = () => ( component={NestedPopupsAndDialogsPrototype} /> + diff --git a/packages/react/src/components/Table/Table.tsx b/packages/react/src/components/Table/Table.tsx index 60c5a24695..7919ca7c40 100644 --- a/packages/react/src/components/Table/Table.tsx +++ b/packages/react/src/components/Table/Table.tsx @@ -16,7 +16,7 @@ import { import { ComponentVariablesObject, mergeComponentVariables } from '@fluentui/styles' import TableRow, { TableRowProps } from './TableRow' import TableCell from './TableCell' -import { WithAsProp, ShorthandCollection, ShorthandValue } from '../../types' +import { WithAsProp, ShorthandCollection, ShorthandValue, withSafeTypeForAs } from '../../types' export interface TableSlotClassNames { header: string @@ -46,23 +46,6 @@ const handleVariablesOverrides = variables => predefinedProps => ({ variables: mergeComponentVariables(variables, predefinedProps.variables), }) -/** - * A Table is used to display data in tabular layout - * * @accessibility - * Implements ARIA [Data Grid](https://www.w3.org/TR/wai-aria-practices/#dataGrid) design pattern for presenting tabular information. - * When gridcell contains actionable element, use [gridCellWithFocusableElementBehavior](/components/table/accessibility#grid-cell-with-focusable-element-behavior-ts). [More information available in aria documentation.](https://www.w3.org/TR/wai-aria-practices/#gridNav_focus) - * Use [gridCellMultipleFocusableBehavior](/components/table/accessibility#gridCellMultipleFocusableBehavior), when gridcell contains: - * \- editable content - * \- multiple actionable elements - * \- component that utilizes arrow keys in its navigation, like menu button, dropdown, radio group, slider, etc. - * [More information available in aria documentation.](https://www.w3.org/TR/wai-aria-practices/#gridNav_inside) - * @accessibilityIssues - * [NVDA narrate table title(aria-label) twice](https://github.com/nvaccess/nvda/issues/10548) - * [Accessibility DOM > Table > gridcell > when gridcell is focused, then selected state is send to reader](https://bugs.chromium.org/p/chromium/issues/detail?id=1030378) - * [JAWS narrate grid name twice, once as table and second time as grid](https://github.com/FreedomScientific/VFO-standards-support/issues/346) - * [JAWS doesn't narrate grid column name, when focus is on actionable element in the cell] (https://github.com/FreedomScientific/VFO-standards-support/issues/348) - * [aria-sort is not output at child elements](https://github.com/FreedomScientific/VFO-standards-support/issues/319) - */ class Table extends UIComponent> { static displayName = 'Table' static className = 'ui-table' @@ -170,4 +153,23 @@ class Table extends UIComponent> { } } -export default Table +/** + * A Table is used to display data in tabular layout + * * @accessibility + * Implements ARIA [Data Grid](https://www.w3.org/TR/wai-aria-practices/#dataGrid) design pattern for presenting tabular information. + * When gridcell contains actionable element, use [gridCellWithFocusableElementBehavior](/components/table/accessibility#grid-cell-with-focusable-element-behavior-ts). [More information available in aria documentation.](https://www.w3.org/TR/wai-aria-practices/#gridNav_focus) + * Use [gridCellMultipleFocusableBehavior](/components/table/accessibility#gridCellMultipleFocusableBehavior), when gridcell contains: + * \- editable content + * \- multiple actionable elements + * \- component that utilizes arrow keys in its navigation, like menu button, dropdown, radio group, slider, etc. + * [More information available in aria documentation.](https://www.w3.org/TR/wai-aria-practices/#gridNav_inside) + * @accessibilityIssues + * [NVDA narrate table title(aria-label) twice](https://github.com/nvaccess/nvda/issues/10548) + * [Accessibility DOM > Table > gridcell > when gridcell is focused, then selected state is send to reader](https://bugs.chromium.org/p/chromium/issues/detail?id=1030378) + * [JAWS narrate grid name twice, once as table and second time as grid](https://github.com/FreedomScientific/VFO-standards-support/issues/346) + * [JAWS doesn't narrate grid column name, when focus is on actionable element in the cell] (https://github.com/FreedomScientific/VFO-standards-support/issues/348) + * [aria-sort is not output at child elements](https://github.com/FreedomScientific/VFO-standards-support/issues/319) + * [VoiceOver not announcing rows correctly for a grid with presentation elements inside](https://bugs.chromium.org/p/chromium/issues/detail?id=1054424) + * VoiceOver doesn't narrate aria-rowcount value in table or grid + */ +export default withSafeTypeForAs(Table) diff --git a/packages/react/src/components/Table/TableCell.tsx b/packages/react/src/components/Table/TableCell.tsx index f0a8a91ac3..ae4ef803c0 100644 --- a/packages/react/src/components/Table/TableCell.tsx +++ b/packages/react/src/components/Table/TableCell.tsx @@ -17,7 +17,7 @@ import { applyAccessibilityKeyHandlers, } from '../../utils' import Box, { BoxProps } from '../Box/Box' -import { WithAsProp, ShorthandValue } from '../../types' +import { WithAsProp, ShorthandValue, withSafeTypeForAs } from '../../types' export interface TableCellProps extends UIComponentProps, @@ -39,11 +39,7 @@ export interface TableCellSlotClassNames { content: string } -/** - * Component represents a table cell - * - */ -class TableCell extends UIComponent, any> { +class TableCell extends UIComponent> { static displayName = 'TableCell' static className = 'ui-table__cell' @@ -69,7 +65,6 @@ class TableCell extends UIComponent, any> { } static defaultProps = { - as: 'div', accessibility: tableCellBehavior as Accessibility, } @@ -123,4 +118,7 @@ class TableCell extends UIComponent, any> { TableCell.create = createShorthandFactory({ Component: TableCell, mappedProp: 'content' }) -export default TableCell +/** + * Component represents a table cell + */ +export default withSafeTypeForAs(TableCell) diff --git a/packages/react/src/components/Table/TableRow.tsx b/packages/react/src/components/Table/TableRow.tsx index 89f9ed7288..bfb0795f4a 100644 --- a/packages/react/src/components/Table/TableRow.tsx +++ b/packages/react/src/components/Table/TableRow.tsx @@ -15,7 +15,7 @@ import { applyAccessibilityKeyHandlers, childrenExist, } from '../../utils' -import { ShorthandCollection, WithAsProp } from '../../types' +import { ShorthandCollection, WithAsProp, withSafeTypeForAs } from '../../types' import { Accessibility, tableRowBehavior } from '@fluentui/accessibility' import { ComponentVariablesObject, mergeComponentVariables } from '@fluentui/styles' @@ -45,10 +45,7 @@ const handleVariablesOverrides = variables => predefinedProps => ({ variables: mergeComponentVariables(variables, predefinedProps.variables), }) -/** - * Component represents a single row in a tabular structure - */ -class TableRow extends UIComponent, any> { +class TableRow extends UIComponent> { static displayName = 'TableRow' static className = 'ui-table__row' @@ -72,7 +69,6 @@ class TableRow extends UIComponent, any> { } static defaultProps = { - as: 'div', accessibility: tableRowBehavior as Accessibility, } @@ -144,4 +140,7 @@ class TableRow extends UIComponent, any> { TableRow.create = createShorthandFactory({ Component: TableRow, mappedArrayProp: 'items' }) -export default TableRow +/** + * Component represents a single row in a tabular structure + */ +export default withSafeTypeForAs(TableRow) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 326c33203a..940ac58af6 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -383,6 +383,7 @@ Tree.create = createShorthandFactory({ * Implements [ARIA TreeView](https://www.w3.org/TR/wai-aria-practices-1.1/#TreeView) design pattern. * @accessibilityIssues * [Treeview - JAWS doesn't narrate position for each tree item](https://github.com/FreedomScientific/VFO-standards-support/issues/338) + * [Aria compliant trees are read as empty tables](https://bugs.chromium.org/p/chromium/issues/detail?id=1048770) */ export default withSafeTypeForAs(Tree) diff --git a/yarn.lock b/yarn.lock index a2ec702472..c96f8a8560 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5202,6 +5202,14 @@ dependencies: "@types/react" "*" +"@types/react-virtualized@^9.21.8": + version "9.21.8" + resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.8.tgz#dc0150a75fd6e42f33729886463ece04d03367ea" + integrity sha512-7fZoA0Azd2jLIE9XC37fMZgMqaJe3o3pfzGjvrzphoKjBCdT4oNl6wikvo4dDMESDnpkZ8DvVTc7aSe4DW86Ew== + dependencies: + "@types/prop-types" "*" + "@types/react" "*" + "@types/react@*", "@types/react@^16.8.10": version "16.8.10" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.10.tgz#1ccb6fde17f71a62ef055382ec68bdc379d4d8d9"