Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: add middle click support for workspace rows #9834

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

Merged
merged 12 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion site/src/components/CopyableValue/CopyableValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const CopyableValue: FC<CopyableValueProps> = ({
...props
}) => {
const { isCopied, copy } = useClipboard(value);
const clickableProps = useClickable(copy);
const clickableProps = useClickable<HTMLSpanElement>(copy);
const styles = useStyles();

return (
Expand Down
3 changes: 2 additions & 1 deletion site/src/components/FileUpload/FileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ export const FileUpload: FC<FileUploadProps> = ({
const styles = useStyles();
const inputRef = useRef<HTMLInputElement>(null);
const tarDrop = useFileDrop(onUpload, fileTypeRequired);
const clickable = useClickable(() => {

const clickable = useClickable<HTMLDivElement>(() => {
if (inputRef.current) {
inputRef.current.click();
}
Expand Down
23 changes: 13 additions & 10 deletions site/src/components/Timeline/TimelineEntry.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import { makeStyles } from "@mui/styles";
import TableRow, { TableRowProps } from "@mui/material/TableRow";
import { PropsWithChildren } from "react";
import { type PropsWithChildren, forwardRef } from "react";
import { combineClasses } from "utils/combineClasses";

interface TimelineEntryProps {
clickable?: boolean;
}
type TimelineEntryProps = PropsWithChildren<
TableRowProps & {
clickable?: boolean;
}
>;

export const TimelineEntry = ({
children,
clickable = true,
...props
}: PropsWithChildren<TimelineEntryProps & TableRowProps>): JSX.Element => {
export const TimelineEntry = forwardRef(function TimelineEntry(
{ children, clickable = true, ...props }: TimelineEntryProps,
ref?: React.ForwardedRef<HTMLTableRowElement>,
) {
const styles = useStyles();

return (
<TableRow
ref={ref}
className={combineClasses({
[styles.timelineEntry]: true,
[styles.clickable]: clickable,
Expand All @@ -24,7 +27,7 @@ export const TimelineEntry = ({
{children}
</TableRow>
);
};
});

const useStyles = makeStyles((theme) => ({
clickable: {
Expand Down
51 changes: 43 additions & 8 deletions site/src/hooks/useClickable.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,55 @@
import { KeyboardEvent } from "react";
import {
type KeyboardEventHandler,
type MouseEventHandler,
type RefObject,
useRef,
} from "react";

export interface UseClickableResult {
// Literally any object (ideally an HTMLElement) that has a .click method
type ClickableElement = {
click: () => void;
};

export interface UseClickableResult<
T extends ClickableElement = ClickableElement,
> {
ref: RefObject<T>;
tabIndex: 0;
role: "button";
onClick: () => void;
onKeyDown: (event: KeyboardEvent) => void;
onClick: MouseEventHandler<T>;
onKeyDown: KeyboardEventHandler<T>;
}

export const useClickable = (onClick: () => void): UseClickableResult => {
/**
* Exposes props to add basic click/interactive behavior to HTML elements that
* don't traditionally have support for them.
*/
export const useClickable = <
// T doesn't have a default type on purpose; the hook should error out if it
// doesn't have an explicit type, or a type it can infer from onClick
T extends ClickableElement,
>(
// Even though onClick isn't used in any of the internal calculations, it's
// still a required argument, just to make sure that useClickable can't
// accidentally be called in a component without also defining click behavior
onClick: MouseEventHandler<T>,
): UseClickableResult<T> => {
const ref = useRef<T>(null);

return {
ref,
tabIndex: 0,
role: "button",
onClick,
onKeyDown: (event: KeyboardEvent) => {
if (event.key === "Enter") {
onClick();

// Most interactive elements automatically make Space/Enter trigger onClick
// callbacks, but you explicitly have to add it for non-interactive elements
onKeyDown: (event) => {
if (event.key === "Enter" || event.key === " ") {
// Can't call onClick from here because onKeydown's keyboard event isn't
// compatible with mouse events. Have to use a ref to simulate a click
ref.current?.click();
event.stopPropagation();
}
},
};
Expand Down
48 changes: 38 additions & 10 deletions site/src/hooks/useClickableTableRow.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,49 @@
import { type MouseEventHandler } from "react";
import { type TableRowProps } from "@mui/material/TableRow";
import { makeStyles } from "@mui/styles";
import { useClickable, UseClickableResult } from "./useClickable";
import { useClickable, type UseClickableResult } from "./useClickable";

interface UseClickableTableRowResult extends UseClickableResult {
className: string;
hover: true;
}
type UseClickableTableRowResult = UseClickableResult<HTMLTableRowElement> &
TableRowProps & {
className: string;
hover: true;
onAuxClick: MouseEventHandler<HTMLTableRowElement>;
};

// Awkward type definition (the hover preview in VS Code isn't great, either),
// but this basically takes all click props from TableRowProps, but makes
// onClick required, and adds an optional onMiddleClick
type UseClickableTableRowConfig = {
Copy link
Member Author

@Parkreiner Parkreiner Sep 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The more I think about this, the more I think I should probably update this with this "less-clever"/"actually maintainable" type:

type UseClickableRowConfig = {
  onClick: MouseEventHandler<HTMLTableRowElement>;
  onAuxClick?: MouseEventHandler<HTMLTableRowElement>;
  onDoubleClick?: MouseEventHandler<HTMLTableRowElement>;
  onMiddleClick?: MouseEventHandler<HTMLTableRowElement>;
};

You lose the explicit connection to the underlying TableRowProps type, so any changes to it might not immediately get flagged at the compiler level, but it's way more readable and gives you better information when you hover over the type in VS Code

[Key in keyof TableRowProps as Key extends `on${string}Click`
? Key
: never]: UseClickableTableRowResult[Key];
} & {
onClick: MouseEventHandler<HTMLTableRowElement>;
onMiddleClick?: MouseEventHandler<HTMLTableRowElement>;
};

export const useClickableTableRow = (
onClick: () => void,
): UseClickableTableRowResult => {
export const useClickableTableRow = ({
onClick,
onAuxClick: externalOnAuxClick,
onDoubleClick,
onMiddleClick,
}: UseClickableTableRowConfig): UseClickableTableRowResult => {
const styles = useStyles();
const clickable = useClickable(onClick);
const clickableProps = useClickable(onClick);

return {
...clickable,
...clickableProps,
className: styles.row,
hover: true,
onDoubleClick,
onAuxClick: (event) => {
const isMiddleMouseButton = event.button === 1;
if (isMiddleMouseButton) {
onMiddleClick?.(event);
}

externalOnAuxClick?.(event);
},
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ export const VersionRow: React.FC<VersionRowProps> = ({
}) => {
const styles = useStyles();
const navigate = useNavigate();
const clickableProps = useClickableTableRow(() => {
navigate(version.name);

const clickableProps = useClickableTableRow({
onClick: () => navigate(version.name),
});

return (
Expand Down
5 changes: 2 additions & 3 deletions site/src/pages/TemplatesPage/TemplatesPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,9 @@ const TemplateRow: FC<{ template: Template }> = ({ template }) => {
const hasIcon = template.icon && template.icon !== "";
const navigate = useNavigate();
const styles = useStyles();

const { className: clickableClassName, ...clickableRow } =
useClickableTableRow(() => {
navigate(templatePageLink);
});
useClickableTableRow({ onClick: () => navigate(templatePageLink) });

return (
<TableRow
Expand Down
2 changes: 1 addition & 1 deletion site/src/pages/WorkspacePage/BuildRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const BuildRow: React.FC<BuildRowProps> = ({ build }) => {
const styles = useStyles();
const initiatedBy = getDisplayWorkspaceBuildInitiatedBy(build);
const navigate = useNavigate();
const clickableProps = useClickable(() =>
const clickableProps = useClickable<HTMLTableRowElement>(() =>
navigate(`builds/${build.build_number}`),
);

Expand Down
22 changes: 19 additions & 3 deletions site/src/pages/WorkspacesPage/WorkspacesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,15 +250,31 @@ const WorkspacesRow: FC<{
checked: boolean;
}> = ({ workspace, children, checked }) => {
const navigate = useNavigate();

const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}`;
const clickable = useClickableTableRow(() => {
navigate(workspacePageLink);
const openLinkInNewTab = () => window.open(workspacePageLink, "_blank");

const clickableProps = useClickableTableRow({
onMiddleClick: openLinkInNewTab,
onClick: (event) => {
// Order of booleans actually matters here for Windows-Mac compatibility;
// meta key is Cmd on Macs, but on Windows, it's either the Windows key,
// or the key does nothing at all (depends on the browser)
const shouldOpenInNewTab =
event.shiftKey || event.metaKey || event.ctrlKey;

if (shouldOpenInNewTab) {
openLinkInNewTab();
} else {
navigate(workspacePageLink);
}
},
});

return (
<TableRow
{...clickableProps}
data-testid={`workspace-${workspace.id}`}
{...clickable}
sx={{
backgroundColor: (theme) =>
checked ? theme.palette.action.hover : undefined,
Expand Down