Skip to content

Commit

Permalink
feat: Add alt+click shortcut to copy cell and column headers (#1694)
Browse files Browse the repository at this point in the history
Adds a alt/opt+click shortcut to copy the value of a cell or column
header.

Resolves #1585.
  • Loading branch information
georgecwan authored Dec 22, 2023
1 parent 73e0b65 commit 4a8a81a
Show file tree
Hide file tree
Showing 16 changed files with 321 additions and 71 deletions.
15 changes: 15 additions & 0 deletions packages/code-studio/src/assets/svg/cursor-copy.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions packages/code-studio/src/main/AppMainContainer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ $nav-space: 4px; // give a gap around some buttons for focus area that are in na
}
}

.grid-cursor-copy {
cursor:
url('../assets/svg/cursor-copy.svg') 8 8,
copy;
}

.grid-cursor-linker {
cursor:
url('../assets/svg/cursor-linker.svg') 8 8,
Expand Down
3 changes: 3 additions & 0 deletions packages/components/src/context-actions/ContextActionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export interface ContextAction {
icon?: IconDefinition | React.ReactElement;
iconColor?: string;
shortcut?: Shortcut;

/* Display text for the shortcut if the shortcut is not wired up through the Shortcut class */
shortcutText?: string;
isGlobal?: boolean;
group?: number;
order?: number;
Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/context-actions/ContextMenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ const ContextMenuItem = React.forwardRef<HTMLDivElement, ContextMenuItemProps>(
'data-testid': dataTestId,
} = props;

const displayShortcut = menuItem.shortcut?.getDisplayText();
const displayShortcut =
menuItem.shortcutText ?? menuItem.shortcut?.getDisplayText();
let icon: IconDefinition | React.ReactElement | null = null;
if (menuItem.icon) {
const menuItemIcon = menuItem.icon;
Expand Down
4 changes: 4 additions & 0 deletions packages/dashboard-core-plugins/src/panels/IrisGridPanel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ $panel-message-overlay-top: 30px;
.grid-cursor-linker {
cursor: crosshair;
}

.grid-cursor-copy {
cursor: copy;
}
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,7 @@ export class IrisGridPanel extends PureComponent<
)}
columnAllowedCursor="linker"
columnNotAllowedCursor="linker-not-allowed"
copyCursor="copy"
customColumns={customColumns}
customColumnFormatMap={customColumnFormatMap}
columnSelectionValidator={this.isColumnSelectionValid}
Expand Down
3 changes: 2 additions & 1 deletion packages/grid/src/Grid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ function mouseDoubleClick(

function keyDown(key: string, component: Grid, extraArgs?: KeyboardEventInit) {
const args = { key, ...extraArgs };
component.handleKeyDown(
component.notifyKeyboardHandlers(
'onDown',
new KeyboardEvent('keydown', args) as unknown as React.KeyboardEvent
);
}
Expand Down
30 changes: 25 additions & 5 deletions packages/grid/src/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ import {
GridTokenMouseHandler,
} from './mouse-handlers';
import './Grid.scss';
import KeyHandler, { GridKeyboardEvent } from './KeyHandler';
import KeyHandler, {
GridKeyHandlerFunctionName,
GridKeyboardEvent,
} from './KeyHandler';
import {
EditKeyHandler,
PasteKeyHandler,
Expand Down Expand Up @@ -342,7 +345,9 @@ class Grid extends PureComponent<GridProps, GridState> {
this.handleEditCellChange = this.handleEditCellChange.bind(this);
this.handleEditCellCommit = this.handleEditCellCommit.bind(this);
this.handleDoubleClick = this.handleDoubleClick.bind(this);
this.notifyKeyboardHandlers = this.notifyKeyboardHandlers.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleKeyUp = this.handleKeyUp.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseDrag = this.handleMouseDrag.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
Expand Down Expand Up @@ -1715,14 +1720,20 @@ class Grid extends PureComponent<GridProps, GridState> {
}

/**
* Handle a key down event from the keyboard. Pass the event to the registered keyboard handlers until one handles it.
* @param event Keyboard event
* Notify all of the keyboard handlers for this grid of a keyboard event.
* @param functionName The name of the function in the keyboard handler to call
* @param event The keyboard event to notify
*/
handleKeyDown(event: GridKeyboardEvent): void {
notifyKeyboardHandlers(
functionName: GridKeyHandlerFunctionName,
event: GridKeyboardEvent
): void {
const keyHandlers = this.getKeyHandlers();
for (let i = 0; i < keyHandlers.length; i += 1) {
const keyHandler = keyHandlers[i];
const result = keyHandler.onDown(event, this);
const result =
keyHandler[functionName] != null &&
keyHandler[functionName](event, this);
if (result !== false) {
const options = result as EventHandlerResultOptions;
if (options?.stopPropagation ?? true) event.stopPropagation();
Expand All @@ -1732,6 +1743,14 @@ class Grid extends PureComponent<GridProps, GridState> {
}
}

handleKeyDown(event: GridKeyboardEvent): void {
this.notifyKeyboardHandlers('onDown', event);
}

handleKeyUp(event: GridKeyboardEvent): void {
this.notifyKeyboardHandlers('onUp', event);
}

/**
* Notify all of the mouse handlers for this grid of a mouse event.
* @param functionName The name of the function in the mouse handler to call
Expand Down Expand Up @@ -2229,6 +2248,7 @@ class Grid extends PureComponent<GridProps, GridState> {
onContextMenu={this.handleContextMenu}
onDoubleClick={this.handleDoubleClick}
onKeyDown={this.handleKeyDown}
onKeyUp={this.handleKeyUp}
onMouseDown={this.handleMouseDown}
onMouseMove={this.handleMouseMove}
onMouseLeave={this.handleMouseLeave}
Expand Down
12 changes: 12 additions & 0 deletions packages/grid/src/KeyHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import type Grid from './Grid';
*/
export type GridKeyboardEvent = KeyboardEvent | React.KeyboardEvent;

export type GridKeyHandlerFunctionName = 'onDown' | 'onUp';

export class KeyHandler {
order: number;

Expand All @@ -33,6 +35,16 @@ export class KeyHandler {
onDown(event: GridKeyboardEvent, grid: Grid): EventHandlerResult {
return false;
}

/**
* Handle a keyup event on the grid.
* @param event The keyboard event
* @param grid The grid component the key press is on
* @returns Response indicating if the key was consumed
*/
onUp(event: GridKeyboardEvent, grid: Grid): EventHandlerResult {
return false;
}
}

export default KeyHandler;
5 changes: 4 additions & 1 deletion packages/iris-grid/src/IrisGrid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ function makeComponent(

function keyDown(key, component, extraArgs?) {
const args = { key, ...extraArgs };
component.grid.handleKeyDown(new KeyboardEvent('keydown', args));
component.grid.notifyKeyboardHandlers(
'onDown',
new KeyboardEvent('keydown', args)
);
}

it('renders without crashing', () => {
Expand Down
40 changes: 34 additions & 6 deletions packages/iris-grid/src/IrisGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ import {
IrisGridColumnSelectMouseHandler,
IrisGridColumnTooltipMouseHandler,
IrisGridContextMenuHandler,
IrisGridCopyCellMouseHandler,
IrisGridDataSelectMouseHandler,
IrisGridFilterMouseHandler,
IrisGridRowTreeMouseHandler,
Expand Down Expand Up @@ -311,6 +312,9 @@ export interface IrisGridProps {

// eslint-disable-next-line react/no-unused-prop-types
columnNotAllowedCursor: string;

// eslint-disable-next-line react/no-unused-prop-types
copyCursor: string;
name: string;
onlyFetchVisibleColumns: boolean;

Expand Down Expand Up @@ -485,6 +489,7 @@ export class IrisGrid extends Component<IrisGridProps, IrisGridState> {
columnSelectionValidator: null,
columnAllowedCursor: null,
columnNotAllowedCursor: null,
copyCursor: null,
name: 'table',
onlyFetchVisibleColumns: true,
showSearchBar: false,
Expand Down Expand Up @@ -617,6 +622,8 @@ export class IrisGrid extends Component<IrisGridProps, IrisGridState> {

this.gotoRowRef = React.createRef();

this.isCopying = false;

this.toggleFilterBarAction = {
action: () => this.toggleFilterBar(),
shortcut: SHORTCUTS.TABLE.TOGGLE_QUICK_FILTER,
Expand Down Expand Up @@ -694,15 +701,12 @@ export class IrisGrid extends Component<IrisGridProps, IrisGridState> {
columnHeaderGroups,
} = props;

const { dh } = model;
const keyHandlers: KeyHandler[] = [
new ReverseKeyHandler(this),
new ClearFilterKeyHandler(this),
];
if (canCopy) {
keyHandlers.push(new CopyKeyHandler(this));
}
const { dh } = model;
const mouseHandlers = [
const mouseHandlers: GridMouseHandler[] = [
new IrisGridCellOverflowMouseHandler(this),
new IrisGridRowTreeMouseHandler(this),
new IrisGridTokenMouseHandler(this),
Expand All @@ -714,7 +718,10 @@ export class IrisGrid extends Component<IrisGridProps, IrisGridState> {
new IrisGridDataSelectMouseHandler(this),
new PendingMouseHandler(this),
];

if (canCopy) {
keyHandlers.push(new CopyKeyHandler(this));
mouseHandlers.push(new IrisGridCopyCellMouseHandler(this));
}
const movedColumns =
movedColumnsProp.length > 0
? movedColumnsProp
Expand Down Expand Up @@ -1010,6 +1017,8 @@ export class IrisGrid extends Component<IrisGridProps, IrisGridState> {

gotoRowRef: React.RefObject<GotoRowElement>;

isCopying: boolean;

toggleFilterBarAction: Action;

toggleSearchBarAction: Action;
Expand Down Expand Up @@ -2031,6 +2040,25 @@ export class IrisGrid extends Component<IrisGridProps, IrisGridState> {
}
}

copyColumnHeader(columnIndex: GridRangeIndex, columnDepth = 0): void {
if (columnIndex === null) {
return;
}
const { canCopy } = this.props;
const { movedColumns } = this.state;

if (canCopy) {
const copyOperation = {
columnIndex,
columnDepth,
movedColumns,
};
this.setState({ copyOperation });
} else {
log.error('Attempted copyColumnHeader for user without copy permission.');
}
}

/**
* Copy the provided ranges to the clipboard
* @paramranges The ranges to copy
Expand Down
Loading

0 comments on commit 4a8a81a

Please sign in to comment.