Summary: Changelog: Standardized DataList component This diff standardizes the DataList component, by reusing the DataList. This is done to be able to take full advantage of all its features like virtualisation, keyboard support, datasource support, etc. Also cleaned up DataTable properties a bit, by prefixing all flags with `enableXXX` and setting clear defaults Reviewed By: passy Differential Revision: D28119721 fbshipit-source-id: b7b241ea18d788bfa035389cc8c6ae7ea95ecadb
209 lines
5.3 KiB
TypeScript
209 lines
5.3 KiB
TypeScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
*/
|
|
|
|
import React, {
|
|
useCallback,
|
|
memo,
|
|
createRef,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
} from 'react';
|
|
import {DataFormatter} from './DataFormatter';
|
|
import {Layout} from './Layout';
|
|
import {Typography} from 'antd';
|
|
import {
|
|
DataTable,
|
|
DataTableColumn,
|
|
DataTableProps,
|
|
ItemRenderer,
|
|
} from './data-table/DataTable';
|
|
import {RightOutlined} from '@ant-design/icons';
|
|
import {theme} from './theme';
|
|
import styled from '@emotion/styled';
|
|
import {DataTableManager} from './data-table/DataTableManager';
|
|
import {Atom, createState} from '../state/atom';
|
|
import {useAssertStableRef} from '../utils/useAssertStableRef';
|
|
|
|
const {Text} = Typography;
|
|
|
|
interface Item {
|
|
id: string;
|
|
title: string;
|
|
description?: string;
|
|
}
|
|
|
|
interface DataListBaseProps<T extends Item> {
|
|
/**
|
|
* Defines the styling of the component. By default shows a list, but alternatively the items can be displayed in a drop down
|
|
*/
|
|
type?: 'default' /* | 'compact' | 'dropdown' */;
|
|
/**
|
|
* By default the data list will take all available space and scroll if items aren't otherwise visible.
|
|
* By setting `scrollable={false}` the list will only take its natural size
|
|
*/
|
|
scrollable?: boolean;
|
|
/**
|
|
* The current selection
|
|
*/
|
|
selection: Atom<string | undefined>;
|
|
/**
|
|
* Handler that is fired if selection is changed
|
|
*/
|
|
onSelect?(id: string | undefined, value: T | undefined): void;
|
|
className?: string;
|
|
style?: React.CSSProperties;
|
|
/**
|
|
* Items to display. Per item at least a title and unique id should be provided
|
|
*/
|
|
items: readonly Item[];
|
|
/**
|
|
* Custom render function. By default the component will render the `title` in bold and description (if any) below it
|
|
*/
|
|
onRenderItem?: ItemRenderer<T>;
|
|
/**
|
|
* Show a right arrow by default
|
|
*/
|
|
enableArrow?: boolean;
|
|
}
|
|
|
|
export type DataListProps<T extends Item> = DataListBaseProps<T> &
|
|
// Some table props are set by DataList instead, so override them
|
|
Omit<DataTableProps<T>, 'records' | 'dataSource' | 'columns' | 'onSelect'>;
|
|
|
|
export const DataList: React.FC<DataListProps<any>> = function DataList<
|
|
T extends Item
|
|
>({
|
|
selection: baseSelection,
|
|
onSelect,
|
|
className,
|
|
style,
|
|
items,
|
|
onRenderItem,
|
|
enableArrow,
|
|
...tableProps
|
|
}: DataListProps<T>) {
|
|
// if a tableManagerRef is provided, we piggy back on that same ref
|
|
// eslint-disable-next-line
|
|
const tableManagerRef = tableProps.tableManagerRef ?? createRef<undefined | DataTableManager<T>>();
|
|
|
|
useAssertStableRef(baseSelection, 'selection');
|
|
// create local selection atom if none provided
|
|
// eslint-disable-next-line
|
|
const selection = baseSelection ?? useState(() => createState<string|undefined>())[0];
|
|
|
|
const handleSelect = useCallback(
|
|
(item: T | undefined) => {
|
|
selection.set(item?.id);
|
|
},
|
|
[selection],
|
|
);
|
|
|
|
const dataListColumns: DataTableColumn<T>[] = useMemo(
|
|
() => [
|
|
{
|
|
key: 'id' as const,
|
|
wrap: true,
|
|
onRender(item: T, selected: boolean, index: number) {
|
|
return onRenderItem ? (
|
|
onRenderItem(item, selected, index)
|
|
) : (
|
|
<DataListItem
|
|
title={item.title}
|
|
description={item.description}
|
|
enableArrow={enableArrow}
|
|
/>
|
|
);
|
|
},
|
|
},
|
|
],
|
|
[onRenderItem, enableArrow],
|
|
);
|
|
|
|
useEffect(
|
|
function updateSelection() {
|
|
return selection.subscribe((valueFromAtom) => {
|
|
const m = tableManagerRef.current;
|
|
if (!m) {
|
|
return;
|
|
}
|
|
if (!valueFromAtom && m.getSelectedItem()) {
|
|
m.clearSelection();
|
|
} else if (valueFromAtom && m.getSelectedItem()?.id !== valueFromAtom) {
|
|
// find valueFromAtom in the selection
|
|
m.selectItemById(valueFromAtom);
|
|
}
|
|
});
|
|
},
|
|
[selection, tableManagerRef],
|
|
);
|
|
|
|
return (
|
|
<Layout.Container style={style} className={className} grow>
|
|
<DataTable<any>
|
|
{...tableProps}
|
|
tableManagerRef={tableManagerRef}
|
|
records={items}
|
|
recordsKey="id"
|
|
columns={dataListColumns}
|
|
onSelect={handleSelect}
|
|
/>
|
|
</Layout.Container>
|
|
);
|
|
};
|
|
|
|
DataList.defaultProps = {
|
|
type: 'default',
|
|
scrollable: true,
|
|
enableSearchbar: false,
|
|
enableColumnHeaders: false,
|
|
enableArrow: true,
|
|
};
|
|
|
|
const DataListItem = memo(
|
|
({
|
|
title,
|
|
description,
|
|
enableArrow,
|
|
}: {
|
|
// TODO: add icon support
|
|
title: string;
|
|
description?: string;
|
|
enableArrow?: boolean;
|
|
}) => {
|
|
return (
|
|
<Layout.Horizontal center grow shrink padv>
|
|
<Layout.Container grow shrink>
|
|
<Text strong ellipsis>
|
|
{DataFormatter.format(title)}
|
|
</Text>
|
|
{description != null && (
|
|
<Text type="secondary" ellipsis>
|
|
{DataFormatter.format(description)}
|
|
</Text>
|
|
)}
|
|
</Layout.Container>
|
|
{enableArrow && (
|
|
<ArrowWrapper>
|
|
<RightOutlined />
|
|
</ArrowWrapper>
|
|
)}
|
|
</Layout.Horizontal>
|
|
);
|
|
},
|
|
);
|
|
|
|
const ArrowWrapper = styled.div({
|
|
flex: 0,
|
|
paddingLeft: theme.space.small,
|
|
'.anticon': {
|
|
lineHeight: '14px',
|
|
},
|
|
});
|