Skip to content

Commit

Permalink
Data Table Fixes
Browse files Browse the repository at this point in the history
shift+click on a row will select multiple rows

Navigating with keyboard on checkbox column, space can now be used to select the checkbox and focus does not go crazy

Triggering edit with the keyboard now works properly in all cases regardless of the field type

Focus is now trapped in the edit popover and focus is returned when the modal closes

The right-click context menu is now properly keyboard navigable

resolves #1103
  • Loading branch information
paustint committed Dec 7, 2024
1 parent 045d620 commit 0b29ebd
Show file tree
Hide file tree
Showing 20 changed files with 593 additions and 365 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"root": true,
"ignorePatterns": ["**/*"],
"plugins": ["@nx/eslint-plugin"],
"plugins": ["@nx/eslint-plugin", "eslint-plugin-react-compiler"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "prettier"],
"rules": {
"@typescript-eslint/explicit-member-accessibility": "off",
Expand Down Expand Up @@ -43,6 +43,7 @@
{
"files": ["*.tsx"],
"rules": {
"react-compiler/react-compiler": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": "warn"
}
Expand Down
18 changes: 18 additions & 0 deletions apps/jetstream/src/main.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
@import '@salesforce-ux/design-system/scss/_design-tokens';

@layer rdg.rdg-checkbox-input {
.rdg-checkbox-input {
inline-size: 14px; /* Override the width */
block-size: 14px; /* Override the height */
// background-color: #0176d3; /* Example: add a custom background */
// accent-color: white;
}
}

// TODO: play around with this as we can potentially figure out how to style the checkbox

// .rdg .rdg-checkbox-input {
// // inline-size: 25px !important;
// // block-size: 25px !important;
// background-color: white !important; /* Ensure it overrides */
// accent-color: #0176d3 !important; /* Ensure it overrides */
// }

html {
background-color: rgb(17, 24, 39);
}
Expand Down
16 changes: 3 additions & 13 deletions libs/features/deploy/src/utils/deploy-metadata.utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,7 @@ import {
SalesforceDeployHistoryType,
SalesforceOrgUi,
} from '@jetstream/types';
import {
ColumnWithFilter,
Grid,
Icon,
SelectFormatter,
SelectHeaderGroupRenderer,
SelectHeaderRenderer,
setColumnFromType,
Spinner,
} from '@jetstream/ui';
import { ColumnWithFilter, Grid, Icon, SelectFormatter, SelectHeaderGroupRenderer, setColumnFromType, Spinner } from '@jetstream/ui';
import { composeQuery, getField, Query } from '@jetstreamapp/soql-parser-js';
import { formatISO } from 'date-fns/formatISO';
import { parseISO } from 'date-fns/parseISO';
Expand Down Expand Up @@ -232,16 +223,15 @@ export function getColumnDefinitions(): ColumnWithFilter<DeployMetadataTableRow>
</Grid>
);
}
return <SelectFormatter {...args} />;
return SelectColumn.renderCell?.(args) || <SelectFormatter {...args} />;
},
renderHeaderCell: SelectHeaderRenderer,
renderGroupCell: (args) => {
const { childRows } = args;
// Don't allow selection if child rows are loading
if (childRows.length === 0 || (childRows.length === 1 && (childRows[0].loading || !childRows[0].metadata))) {
return null;
}
return <SelectHeaderGroupRenderer {...args} />;
return SelectColumn.renderGroupCell?.(args) || <SelectHeaderGroupRenderer {...args} />;
},
colSpan: (args) => {
if (args.type === 'ROW') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { css } from '@emotion/react';
import { orderValues } from '@jetstream/shared/utils';
import { AutoFullHeightContainer, ColumnWithFilter, ContextMenuActionData, ContextMenuItem, DataTree } from '@jetstream/ui';
import { ContextMenuItem } from '@jetstream/types';
import { AutoFullHeightContainer, ColumnWithFilter, ContextMenuActionData, DataTree } from '@jetstream/ui';
import copyToClipboard from 'copy-to-clipboard';
import groupBy from 'lodash/groupBy';
import { FunctionComponent, useCallback, useEffect, useState } from 'react';
Expand Down
15 changes: 15 additions & 0 deletions libs/types/src/lib/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,21 @@ export interface DropDownItem<T = any> {
metadata?: T;
}

export interface ContextMenuItem<T = any> {
subheader?: string;
label: string | ReactNode;
value: T;
icon?: {
type: string;
icon: string;
description?: string;
}; // FIXME: unable to import cross module boundaries
trailingDivider?: boolean;
disabled?: boolean;
title?: string;
metadata?: T;
}

export interface QueryFieldHeader {
label: string;
accessor: string;
Expand Down
2 changes: 1 addition & 1 deletion libs/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export * from './lib/form/combobox/ComboboxWithItems';
export * from './lib/form/combobox/ComboboxWithItemsTypeAhead';
export * from './lib/form/combobox/ComboboxWithItemsVirtual';
export * from './lib/form/combobox/useFieldListItemsWithDrillIn';
export * from './lib/form/context-menu/ContextMenu';
export * from './lib/form/controlled-inputs/ControlledInput';
export * from './lib/form/controlled-inputs/ControlledTextarea';
export * from './lib/form/date-time/DateTime';
Expand Down Expand Up @@ -113,7 +114,6 @@ export * from './lib/nav/Navbar';
export * from './lib/nav/NavbarAppLauncher';
export * from './lib/nav/NavbarItem';
export * from './lib/nav/NavbarMenuItems';
export * from './lib/popover/ContextMenu';
export * from './lib/popover/Popover';
export * from './lib/popover/PopoverErrorButton';
export * from './lib/progress-indicator/ProgressIndicator';
Expand Down
79 changes: 51 additions & 28 deletions libs/ui/src/lib/data-table/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { SalesforceOrgUi } from '@jetstream/types';
import { ContextMenuItem, SalesforceOrgUi } from '@jetstream/types';
import { forwardRef } from 'react';
import DataGrid, { DataGridProps, SortColumn } from 'react-data-grid';
import 'react-data-grid/lib/styles.css';
import { ContextMenuContext, ContextMenuItem } from '../popover/ContextMenu';
import { ContextMenu } from '../form/context-menu/ContextMenu';
import { DataTableFilterContext, DataTableGenericContext } from './data-table-context';
import './data-table-styles.scss';
import { ColumnWithFilter, ContextMenuActionData, RowWithKey } from './data-table-types';
Expand Down Expand Up @@ -76,10 +76,13 @@ export const DataTable = forwardRef<any, DataTableProps<any>>(
reorderedColumns,
filterSetValues,
filteredRows,
contextMenuProps,
setSortColumns,
updateFilter,
handleReorderColumns,
handleCellKeydown,
handleCellContextMenu,
handleCloseContextMenu,
} = useDataTable({
data,
columns: _columns,
Expand All @@ -100,33 +103,53 @@ export const DataTable = forwardRef<any, DataTableProps<any>>(
});

return (
<ContextMenuContext.Provider value={new Map()}>
<DataTableGenericContext.Provider value={{ ...context, rows: filteredRows, columns }}>
<DataTableFilterContext.Provider
value={{
filterSetValues,
filters,
portalRefForFilters: context?.portalRefForFilters,
updateFilter,
}}
>
<DataGrid
data-id={gridId}
className="rdg-light fill-grid"
columns={reorderedColumns}
rows={filteredRows}
renderers={renderers}
sortColumns={sortColumns}
onSortColumnsChange={setSortColumns}
rowKeyGetter={getRowKey}
defaultColumnOptions={{ resizable: true, sortable: true, ...rest.defaultColumnOptions }}
onCellKeyDown={handleCellKeydown}
onColumnsReorder={handleReorderColumns}
{...rest}
<DataTableGenericContext.Provider value={{ ...context, rows: filteredRows, columns }}>
<DataTableFilterContext.Provider
value={{
filterSetValues,
filters,
portalRefForFilters: context?.portalRefForFilters,
updateFilter,
}}
>
<DataGrid
data-id={gridId}
className="rdg-light fill-grid"
columns={reorderedColumns}
rows={filteredRows}
// @ts-expect-error Types are incorrect, but they are generic and difficult to get correct
renderers={renderers}
sortColumns={sortColumns}
onSortColumnsChange={setSortColumns}
// @ts-expect-error Types are incorrect, but they are generic and difficult to get correct
rowKeyGetter={getRowKey}
defaultColumnOptions={{ resizable: true, sortable: true, ...rest.defaultColumnOptions } as any}
// @ts-expect-error Types are incorrect, but they are generic and difficult to get correct
onCellKeyDown={handleCellKeydown}
onColumnsReorder={handleReorderColumns}
// @ts-expect-error Types are incorrect, but they are generic and difficult to get correct
onCellContextMenu={handleCellContextMenu}
{...rest}
/>
{contextMenuProps && contextMenuItems && contextMenuAction && (
<ContextMenu
parentElement={contextMenuProps.element}
items={contextMenuItems}
onSelected={(item) => {
contextMenuAction(item, {
row: filteredRows[contextMenuProps.rowIdx] as T,
rowIdx: contextMenuProps.rowIdx,
rows: filteredRows as T[],
column: columns[contextMenuProps.rowIdx],
columns,
});
handleCloseContextMenu();
}}
onClose={handleCloseContextMenu}
/>
</DataTableFilterContext.Provider>
</DataTableGenericContext.Provider>
</ContextMenuContext.Provider>
)}
</DataTableFilterContext.Provider>
</DataTableGenericContext.Provider>
);
}
);
12 changes: 9 additions & 3 deletions libs/ui/src/lib/data-table/DataTableEditors.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FocusTrap } from '@headlessui/react';
import { logger } from '@jetstream/shared/client-logger';
import { SFDC_BLANK_PICKLIST_VALUE } from '@jetstream/shared/constants';
import { describeSObject, query } from '@jetstream/shared/data';
Expand Down Expand Up @@ -59,7 +60,7 @@ function DataTableEditorPopover({
ref={popoverRef}
isOpen
referenceElement={referenceElement as any}
className={`slds-popover slds-popover slds-popover_edit`}
className="slds-popover slds-popover slds-popover_edit"
role="dialog"
offset={[0, -28.5]}
usePortal
Expand All @@ -69,7 +70,11 @@ function DataTableEditorPopover({
}
}}
>
{referenceElement && <div className="slds-p-around_x-small">{children}</div>}
{referenceElement && (
<FocusTrap>
<div className="slds-p-around_x-small">{children}</div>
</FocusTrap>
)}
</PopoverContainer>
</OutsideClickHandler>
);
Expand Down Expand Up @@ -247,6 +252,7 @@ export function DataTableEditorDate<TRow extends { _idx: number }, TSummaryRow>(
className="d-block"
initialSelectedDate={currDate}
openOnInit
inputProps={{ autoFocus: true }}
onChange={(value) => {
/** setTimeout is used to avoid a React error about flushSync being called during a render */
setTimeout(() => {
Expand Down Expand Up @@ -328,7 +334,7 @@ export const dataTableEditorRecordLookup = ({ sobject }: { sobject: string }) =>
);

if (!org || !sobject) {
return <DataTableEditorText column={column} onClose={onClose} onRowChange={onRowChange} row={row} />;
return <DataTableEditorText rowIdx={row._idx} column={column} onClose={onClose} onRowChange={onRowChange} row={row} />;
}

return (
Expand Down
30 changes: 6 additions & 24 deletions libs/ui/src/lib/data-table/DataTableRenderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,29 +56,9 @@ export function configIdLinkRenderer(serverUrl: string, org: SalesforceOrgUi, sk

// HEADER RENDERERS

/**
* SELECT ALL CHECKBOX HEADER
*/
export function SelectHeaderRenderer<T>(props: RenderHeaderCellProps<T>) {
const { column } = props;
const [isRowSelected, onRowSelectionChange] = useRowSelection();

return (
<Checkbox
id={`checkbox-${column.name}_header`} // TODO: need way to get row id
label="Select all"
hideLabel
checked={isRowSelected}
onChange={(checked) => onRowSelectionChange({ type: 'HEADER', checked })}
// WAITING ON: https://github.com/adazzle/react-data-grid/issues/3058
// indeterminate={props.row.getIsSomeSelected()}
/>
);
}

export function SelectHeaderGroupRenderer<T>(props: RenderGroupCellProps<T>) {
const { column, groupKey, row, childRows } = props;
const [isRowSelected, onRowSelectionChange] = useRowSelection();
const { isRowSelectionDisabled, isRowSelected, onRowSelectionChange } = useRowSelection();

return (
<DataTableSelectedContext.Consumer>
Expand All @@ -88,8 +68,9 @@ export function SelectHeaderGroupRenderer<T>(props: RenderGroupCellProps<T>) {
label="Select all"
hideLabel
checked={isRowSelected}
disabled={isRowSelectionDisabled}
indeterminate={selectedRowIds.size > 0 && childRows.some((childRow) => selectedRowIds.has((getRowKey || getRowId)(childRow)))}
onChange={(checked) => onRowSelectionChange({ type: 'ROW', row: row, checked, isShiftClick: false })}
onChange={(checked) => onRowSelectionChange({ row: row, checked, isShiftClick: false })}
/>
)}
</DataTableSelectedContext.Consumer>
Expand Down Expand Up @@ -511,15 +492,16 @@ export function GenericRenderer(RenderCellProps: RenderCellProps<RowWithKey>) {

export function SelectFormatter<T>(props: RenderCellProps<T>) {
const { column, row } = props;
const [isRowSelected, onRowSelectionChange] = useRowSelection();
const { isRowSelectionDisabled, isRowSelected, onRowSelectionChange } = useRowSelection();

return (
<Checkbox
id={`checkbox-${column.name}-${getRowId(row)}`} // TODO: need way to get row id
label="Select row"
hideLabel
checked={isRowSelected}
onChange={(checked) => onRowSelectionChange({ type: 'ROW', row, checked, isShiftClick: false })}
disabled={isRowSelectionDisabled}
onChange={(checked) => onRowSelectionChange({ row, checked, isShiftClick: false })}
/>
);
}
Expand Down
3 changes: 1 addition & 2 deletions libs/ui/src/lib/data-table/DataTableSubqueryRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { queryMore } from '@jetstream/shared/data';
import { copyRecordsToClipboard, formatNumber } from '@jetstream/shared/ui-utils';
import { flattenRecord } from '@jetstream/shared/utils';
import { Maybe, QueryResult, SalesforceOrgUi } from '@jetstream/types';
import { ContextMenuItem, Maybe, QueryResult, SalesforceOrgUi } from '@jetstream/types';
import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { RenderCellProps } from 'react-data-grid';
import RecordDownloadModal from '../file-download-modal/RecordDownloadModal';
import Grid from '../grid/Grid';
import AutoFullHeightContainer from '../layout/AutoFullHeightContainer';
import Modal from '../modal/Modal';
import { ContextMenuItem } from '../popover/ContextMenu';
import Icon from '../widgets/Icon';
import Spinner from '../widgets/Spinner';
import { DataTable } from './DataTable';
Expand Down
Loading

0 comments on commit 0b29ebd

Please sign in to comment.