-
Notifications
You must be signed in to change notification settings - Fork 2.9k
DetailsList: adding an optional column re-ordering feature #4857
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c73ff74
aa103d7
4503275
1865dbb
f999333
3b67d87
f78090e
b9cb878
da75d70
d9dba7a
1a756a0
fe970d6
3fa74f0
001cd1f
5bc464a
cc91cc4
cb94179
0451931
b568a27
3a15dbf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "changes": [ | ||
| { | ||
| "packageName": "office-ui-fabric-react", | ||
| "comment": "Added column reorder with DragAndDrop support in Details List. As part of this feature, a new component DetailsColumn has been added inside the header, for each column. An optional new prop(ColumnReorderOptions) has been added to DetailsList to handle the column reorder. Have added column level drag subscriptions to be able to drag the columns, and added one header level subscription, to handle the drops. All the drag drop events are being handled at header level.", | ||
| "type": "patch" | ||
| } | ||
| ], | ||
| "packageName": "office-ui-fabric-react", | ||
| "email": "laxmika@microsoft.com" | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,254 @@ | ||
| import * as React from 'react'; | ||
| import * as stylesImport from './DetailsHeader.scss'; | ||
| const styles: any = stylesImport; | ||
| import { Icon } from '../../Icon'; | ||
| import { BaseComponent, css, IRenderFunction, createRef, IDisposable } from '../../Utilities'; | ||
| import { IColumn, ColumnActionsMode } from './DetailsList.types'; | ||
|
|
||
| import { ITooltipHostProps } from '../../Tooltip'; | ||
| import { IDragDropHelper, IDragDropOptions } from './../../utilities/dragdrop/interfaces'; | ||
|
|
||
| const INNER_PADDING = 16; // Account for padding around the cell. | ||
| const ISPADDED_WIDTH = 24; | ||
| const MOUSEDOWN_PRIMARY_BUTTON = 0; // for mouse down event we are using ev.button property, 0 means left button | ||
|
|
||
| export interface IDetailsColumnProps extends React.Props<DetailsColumn> { | ||
| componentRef?: () => void; | ||
| column: IColumn; | ||
| columnIndex: number; | ||
| parentId?: string; | ||
| onRenderColumnHeaderTooltip?: IRenderFunction<ITooltipHostProps>; | ||
| onColumnClick?: (ev: React.MouseEvent<HTMLElement>, column: IColumn) => void; | ||
| onColumnContextMenu?: (column: IColumn, ev: React.MouseEvent<HTMLElement>) => void; | ||
| dragDropHelper?: IDragDropHelper | null; | ||
| isDraggable?: boolean; | ||
| setDraggedItemIndex?: (itemIndex: number) => void; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Rather than this, the notification should be simple - like 'onColumnDragStart' with no params. The params (index) can be handled inside the DetailsHeader, where this callback is binded to a function. #Closed |
||
| isDropped?: boolean; | ||
| } | ||
|
|
||
| export class DetailsColumn extends BaseComponent<IDetailsColumnProps> { | ||
| private _root: any; | ||
| private _dragDropSubscription: IDisposable; | ||
|
|
||
| constructor(props: IDetailsColumnProps) { | ||
| super(props); | ||
|
|
||
| this._root = createRef(); | ||
| this._onDragStart = this._onDragStart.bind(this); | ||
| this._onDragEnd = this._onDragEnd.bind(this); | ||
| this._onRootMouseDown = this._onRootMouseDown.bind(this); | ||
| } | ||
|
|
||
| public render() { | ||
| const { column, columnIndex, parentId, isDraggable } = this.props; | ||
| const { onRenderColumnHeaderTooltip = this._onRenderColumnHeaderTooltip } = this.props; | ||
|
|
||
| return [ | ||
| <div | ||
| key={column.key} | ||
| ref={this._root} | ||
| role={'columnheader'} | ||
| aria-sort={column.isSorted ? (column.isSortedDescending ? 'descending' : 'ascending') : 'none'} | ||
| aria-disabled={column.columnActionsMode === ColumnActionsMode.disabled} | ||
| aria-colindex={columnIndex} | ||
| className={css( | ||
| 'ms-DetailsHeader-cell', | ||
| styles.cell, | ||
| column.headerClassName, | ||
| column.columnActionsMode !== ColumnActionsMode.disabled && 'is-actionable ' + styles.cellIsActionable, | ||
| !column.name && 'is-empty ' + styles.cellIsEmpty, | ||
| (column.isSorted || column.isGrouped || column.isFiltered) && 'is-icon-visible', | ||
| column.isPadded && styles.cellWrapperPadded | ||
| )} | ||
| data-is-draggable={isDraggable} | ||
| draggable={isDraggable} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need both data-is-draggable and draggable attributes?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have followed DetailsRow, might have been kept both for legacy reasons,
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @yiminwu @ThomasMichon - do any of you recall why we need both these attributes? Seems sort of redundant for the new component.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| style={{ width: column.calculatedWidth! + INNER_PADDING + (column.isPadded ? ISPADDED_WIDTH : 0) }} | ||
| data-automationid={'ColumnsHeaderColumn'} | ||
| data-item-key={column.key} | ||
| > | ||
| {isDraggable && <Icon iconName={'GripperBarVertical'} className={css(styles.GripperBarVerticalStyle)} />} | ||
| {onRenderColumnHeaderTooltip( | ||
| { | ||
| hostClassName: css(styles.cellTooltip), | ||
| id: `${parentId}-${column.key}-tooltip`, | ||
| setAriaDescribedBy: false, | ||
| content: column.columnActionsMode !== ColumnActionsMode.disabled ? column.ariaLabel : '', | ||
| children: ( | ||
| <span | ||
| id={`${parentId}-${column.key}`} | ||
| aria-label={column.isIconOnly ? column.name : undefined} | ||
| aria-labelledby={column.isIconOnly ? undefined : `${parentId}-${column.key}-name `} | ||
| className={css('ms-DetailsHeader-cellTitle', styles.cellTitle)} | ||
| data-is-focusable={column.columnActionsMode !== ColumnActionsMode.disabled} | ||
| role={column.columnActionsMode !== ColumnActionsMode.disabled ? 'button' : undefined} | ||
| aria-describedby={ | ||
| this.props.onRenderColumnHeaderTooltip ? `${parentId}-${column.key}-tooltip` : undefined | ||
| } | ||
| onContextMenu={this._onColumnContextMenu.bind(this, column)} | ||
| onClick={this._onColumnClick.bind(this, column)} | ||
| aria-haspopup={column.columnActionsMode === ColumnActionsMode.hasDropdown} | ||
| > | ||
| <span | ||
| id={`${parentId}-${column.key}-name`} | ||
| className={css('ms-DetailsHeader-cellName', styles.cellName, { | ||
| [styles.iconOnlyHeader]: column.isIconOnly | ||
| })} | ||
| > | ||
| {(column.iconName || column.iconClassName) && ( | ||
| <Icon className={css(styles.nearIcon, column.iconClassName)} iconName={column.iconName} /> | ||
| )} | ||
|
|
||
| {!column.isIconOnly ? column.name : undefined} | ||
| </span> | ||
|
|
||
| {column.isFiltered && <Icon className={styles.nearIcon} iconName={'Filter'} />} | ||
|
|
||
| {column.isSorted && ( | ||
| <Icon | ||
| className={css(styles.nearIcon, styles.sortIcon)} | ||
| iconName={column.isSortedDescending ? 'SortDown' : 'SortUp'} | ||
| /> | ||
| )} | ||
|
|
||
| {column.isGrouped && <Icon className={styles.nearIcon} iconName={'GroupedDescending'} />} | ||
|
|
||
| {column.columnActionsMode === ColumnActionsMode.hasDropdown && | ||
| !column.isIconOnly && ( | ||
| <Icon | ||
| className={css('ms-DetailsHeader-filterChevron', styles.filterChevron)} | ||
| iconName={'ChevronDown'} | ||
| /> | ||
| )} | ||
| </span> | ||
| ) | ||
| }, | ||
| this._onRenderColumnHeaderTooltip | ||
| )} | ||
| </div>, | ||
| column.ariaLabel && !this.props.onRenderColumnHeaderTooltip ? ( | ||
| <label key={`${column.key}_label`} id={`${parentId}-${column.key}-tooltip`} className={styles.accessibleLabel}> | ||
| {column.ariaLabel} | ||
| </label> | ||
| ) : null | ||
| ]; | ||
| } | ||
|
|
||
| public componentDidMount(): void { | ||
| if (this._dragDropSubscription) { | ||
| this._dragDropSubscription.dispose(); | ||
| delete this._dragDropSubscription; | ||
| } | ||
|
|
||
| if (this.props.dragDropHelper && this.props.isDraggable!) { | ||
| this._dragDropSubscription = this.props.dragDropHelper.subscribe( | ||
| this._root.current as HTMLElement, | ||
| this._events, | ||
| this._getColumnDragDropOptions() | ||
| ); | ||
|
|
||
| // We need to use native on this to avoid MarqueeSelection from handling the event before us. | ||
| this._events.on(this._root.current, 'mousedown', this._onRootMouseDown); | ||
| } | ||
| if (this.props.isDropped) { | ||
| if (this._root!.current!) { | ||
| this._root!.current!.classList!.add(styles.borderAfterDropping); | ||
| } | ||
| setTimeout(() => { | ||
| if (this._root!.current!) { | ||
| this._root!.current!.classList!.remove(styles.borderAfterDropping); | ||
| } | ||
| }, 1500); | ||
| } | ||
| } | ||
|
|
||
| public componentWillUnmount(): void { | ||
| if (this._dragDropSubscription) { | ||
| this._dragDropSubscription.dispose(); | ||
| delete this._dragDropSubscription; | ||
| } | ||
| } | ||
|
|
||
| public componentDidUpdate(): void { | ||
| if (!this._dragDropSubscription && this.props.dragDropHelper && this.props.isDraggable!) { | ||
| this._dragDropSubscription = this.props.dragDropHelper.subscribe( | ||
| this._root.value as HTMLElement, | ||
| this._events, | ||
| this._getColumnDragDropOptions() | ||
| ); | ||
|
|
||
| // We need to use native on this to avoid MarqueeSelection from handling the event before us. | ||
| this._events.on(this._root.current, 'mousedown', this._onRootMouseDown); | ||
| } | ||
| if (this._dragDropSubscription && !this.props.isDraggable!) { | ||
| this._dragDropSubscription.dispose(); | ||
| this._events.off(this._root.current, 'mousedown'); | ||
| delete this._dragDropSubscription; | ||
| } | ||
| } | ||
|
|
||
| private _onRenderColumnHeaderTooltip = ( | ||
| tooltipHostProps: ITooltipHostProps, | ||
| defaultRender?: IRenderFunction<ITooltipHostProps> | ||
| ): JSX.Element => { | ||
| return <span className={tooltipHostProps.hostClassName}>{tooltipHostProps.children}</span>; | ||
| }; | ||
|
|
||
| private _onColumnClick(column: IColumn, ev: React.MouseEvent<HTMLElement>): void { | ||
| const { onColumnClick } = this.props; | ||
| if (column.onColumnClick) { | ||
| column.onColumnClick(ev, column); | ||
| } | ||
| if (onColumnClick) { | ||
| onColumnClick(ev, column); | ||
| } | ||
| } | ||
|
|
||
| private _getColumnDragDropOptions(): IDragDropOptions { | ||
| const { columnIndex } = this.props; | ||
| const options = { | ||
| selectionIndex: columnIndex, | ||
| context: { data: columnIndex, index: columnIndex }, | ||
| canDrag: () => this.props.isDraggable!, | ||
| canDrop: () => false, | ||
| onDragStart: this._onDragStart, | ||
| updateDropState: () => undefined, | ||
| onDrop: () => undefined, | ||
| onDragEnd: this._onDragEnd | ||
| }; | ||
| return options; | ||
| } | ||
|
|
||
| private _onDragStart(item?: any, itemIndex?: number, selectedItems?: any[], event?: MouseEvent): void { | ||
| if (itemIndex && this.props.setDraggedItemIndex) { | ||
| this.props.setDraggedItemIndex(itemIndex); | ||
| this._root.current.classList.add(styles.borderWhileDragging); | ||
| } | ||
| } | ||
|
|
||
| private _onDragEnd(item?: any, event?: MouseEvent): void { | ||
| if (this.props.setDraggedItemIndex) { | ||
| this.props.setDraggedItemIndex(-1); | ||
| this._root.current.classList.remove(styles.borderWhileDragging); | ||
| } | ||
| } | ||
|
|
||
| private _onColumnContextMenu(column: IColumn, ev: React.MouseEvent<HTMLElement>): void { | ||
| const { onColumnContextMenu } = this.props; | ||
| if (column.onColumnContextMenu) { | ||
| column.onColumnContextMenu(column, ev); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just curious, why we need both column.onColumnContextMenu and props.onColumnContextMenu. We'll end up executing both if they are provided, not sure if that is intentional too. #Resolved
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an existing function which was there in ColumnHeader, i just moved it as is. #Resolved
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| ev.preventDefault(); | ||
| } | ||
| if (onColumnContextMenu) { | ||
| onColumnContextMenu(column, ev); | ||
| ev.preventDefault(); | ||
| } | ||
| } | ||
|
|
||
| private _onRootMouseDown = (ev: MouseEvent): void => { | ||
| const { isDraggable } = this.props; | ||
| // Ignore anything except the primary button. | ||
| if (isDraggable && ev.button === MOUSEDOWN_PRIMARY_BUTTON) { | ||
| ev.stopPropagation(); | ||
| } | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be 'minor'