Skip to content

Fix link button focus targets #18007

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

Closed
wants to merge 7 commits into from
Closed
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
6 changes: 3 additions & 3 deletions components/dashboard/src/components/AuthorizeGit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ export const AuthorizeGit: FC<{ className?: string }> = ({ className }) => {
{!!org.data?.isOwner ? (
<div className="px-6">
<Subheading>You need to configure at least one Git integration.</Subheading>
<Link to="/settings/git">
<Button className="mt-6 w-full">Add a Git integration</Button>
</Link>
<Button href="/settings/git" className="mt-6 w-full">
Add a Git integration
</Button>
</div>
) : (
<>
Expand Down
110 changes: 59 additions & 51 deletions components/dashboard/src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import classNames from "classnames";
import { FC, ForwardedRef, ReactNode, forwardRef } from "react";
import { ReactComponent as SpinnerWhite } from "../icons/SpinnerWhite.svg";
import { Link } from "react-router-dom";

export type ButtonProps = {
type?: "primary" | "secondary" | "danger" | "danger.secondary" | "transparent";
Expand All @@ -21,17 +22,19 @@ export type ButtonProps = {
icon?: ReactNode;
children?: ReactNode;
onClick?: ButtonOnClickHandler;
href?: string;
};

// Allow w/ or w/o handling event argument
type ButtonOnClickHandler = React.DOMAttributes<HTMLButtonElement>["onClick"] | (() => void);

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(
(
{
type = "primary",
className,
htmlType = "button",
href,
disabled = false,
loading = false,
autoFocus = false,
Expand All @@ -41,57 +44,62 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
children,
onClick,
},
ref: ForwardedRef<HTMLButtonElement>,
ref: ForwardedRef<HTMLButtonElement | HTMLAnchorElement>,
) => {
return (
<button
type={htmlType}
className={classNames(
"cursor-pointer my-auto",
"text-sm font-medium whitespace-nowrap",
"rounded-xl focus:outline-none focus:ring transition ease-in-out",
spacing === "compact" ? ["px-1 py-1"] : null,
spacing === "default" ? ["px-4 py-2"] : null,
type === "primary"
? [
"bg-gray-900 hover:bg-gray-800 dark:bg-kumquat-base dark:hover:bg-kumquat-ripe",
"text-gray-50 dark:text-gray-900",
]
: null,
type === "secondary"
? [
"bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600",
"text-gray-500 dark:text-gray-100 hover:text-gray-600",
]
: null,
type === "danger" ? ["bg-red-600 hover:bg-red-700", "text-gray-100 dark:text-red-100"] : null,
type === "danger.secondary"
? [
"bg-red-50 dark:bg-red-300 hover:bg-red-100 dark:hover:bg-red-200",
"text-red-600 hover:text-red-700",
]
: null,
type === "transparent"
? [
"bg-transparent hover:bg-gray-600 hover:bg-opacity-10 dark:hover:bg-gray-200 dark:hover:bg-opacity-10",
]
: null,
{
"w-full": size === "block",
"cursor-default opacity-50 pointer-events-none": disabled || loading,
},
className,
)}
ref={ref}
disabled={disabled}
autoFocus={autoFocus}
onClick={onClick}
>
<ButtonContent loading={loading} icon={icon}>
{children}
</ButtonContent>
</button>
);
const commonProps = {
className: classNames(
"cursor-pointer my-auto",
"text-sm font-medium whitespace-nowrap",
"rounded-xl focus:outline-none focus:ring transition ease-in-out",
spacing === "compact" ? ["px-1 py-1"] : null,
spacing === "default" ? ["px-4 py-2"] : null,
type === "primary"
? [
"bg-gray-900 hover:bg-gray-800 dark:bg-kumquat-base dark:hover:bg-kumquat-ripe",
"text-gray-50 dark:text-gray-900",
]
: null,
type === "secondary"
? [
"bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600",
"text-gray-500 dark:text-gray-100 hover:text-gray-600",
]
: null,
type === "danger" ? ["bg-red-600 hover:bg-red-700", "text-gray-100 dark:text-red-100"] : null,
type === "danger.secondary"
? [
"bg-red-50 dark:bg-red-300 hover:bg-red-100 dark:hover:bg-red-200",
"text-red-600 hover:text-red-700",
]
: null,
type === "transparent"
? [
"bg-transparent hover:bg-gray-600 hover:bg-opacity-10 dark:hover:bg-gray-200 dark:hover:bg-opacity-10",
]
: null,
{
"w-full": size === "block",
"cursor-default opacity-50 pointer-events-none": disabled || loading,
},
className,
),
children: <ButtonContent loading={loading} icon={icon}>{children}</ButtonContent>
};

if (href) {
return <Link ref={ref as ForwardedRef<HTMLAnchorElement>} to={href} {...commonProps} />;
} else {
return (
<button
type={htmlType}
ref={ref as ForwardedRef<HTMLButtonElement>}
Copy link
Contributor

@svenefftinge svenefftinge Jun 26, 2023

Choose a reason for hiding this comment

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

do you want to handle ref being an anchor element differently?

Copy link
Member Author

@filiptronicek filiptronicek Sep 14, 2023

Choose a reason for hiding this comment

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

Nope, this was an oversight and I've now put it on the link as well... thanks for catching it

disabled={disabled}
autoFocus={autoFocus}
onClick={onClick}
{...commonProps}
/>
);
}
},
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
import dayjs from "dayjs";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useLocation } from "react-router";
import { Link } from "react-router-dom";
import Modal, { ModalBody, ModalFooter, ModalHeader } from "../components/Modal";
import { useCurrentOrg } from "../data/organizations/orgs-query";
import { ReactComponent as Spinner } from "../icons/Spinner.svg";
Expand Down Expand Up @@ -284,15 +283,16 @@ export default function UsageBasedBillingConfig({ hideSubheading = false }: Prop
</div>
</div>
<div>
<Link
to={`/usage?org=${
<Button
href={`/usage?org=${
attrId?.kind === "team" ? attrId.teamId : "0"
}&start=${billingCycleFrom.format("YYYY-MM-DD")}&end=${billingCycleTo.format(
"YYYY-MM-DD",
)}`}
className="secondary"
>
<button className="secondary">View Usage →</button>
</Link>
View Usage →
</Button>
</div>
</div>
</div>
Expand Down
17 changes: 8 additions & 9 deletions components/dashboard/src/projects/Projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import projectsEmpty from "../images/projects-empty.svg";
import { ThemeContext } from "../theme-context";
import { ProjectListItem } from "./ProjectListItem";
import { projectsPathNew } from "./projects.routes";
import { Button } from "../components/Button";

export default function ProjectsPage() {
const history = useHistory();
Expand Down Expand Up @@ -82,13 +83,11 @@ export default function ProjectsPage() {
</a>
</p>
<div className="flex space-x-2 justify-center mt-7">
<Link to={projectsPathNew}>
<button>New Project</button>
</Link>
<Button href={projectsPathNew}>New Project</Button>
{team && (
<Link to="./members">
<button className="secondary">Invite Members</button>
</Link>
<Button href="./members" className="secondary">
Invite Members
</Button>
)}
</div>
</div>
Expand All @@ -113,9 +112,9 @@ export default function ProjectsPage() {
<div className="flex-1" />
<div className="py-2 pl-3"></div>
{team && (
<Link to="./members" className="flex">
<button className="ml-2 secondary">Invite Members</button>
</Link>
<Button href="./members" className="ml-2 secondary">
Invite Members
</Button>
)}
<button className="ml-2" onClick={() => onNewProject()}>
New Project
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import { PersonalAccessToken } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_pb";
import { useCallback, useEffect, useState } from "react";
import { Redirect, useLocation } from "react-router";
import { Link } from "react-router-dom";
import { personalAccessTokensService } from "../service/public-api";
import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu";
import { settingsPathPersonalAccessTokenCreate, settingsPathPersonalAccessTokenEdit } from "./settings.routes";
Expand All @@ -23,6 +22,7 @@ import ShowTokenModal from "./ShowTokenModal";
import Pagination from "../Pagination/Pagination";
import { Heading2, Subheading } from "../components/typography/headings";
import { useFeatureFlag } from "../data/featureflag-query";
import { Button } from "../components/Button";

export default function PersonalAccessTokens() {
const enablePersonalAccessTokens = useFeatureFlag("personalAccessTokensEnabled");
Expand Down Expand Up @@ -178,11 +178,7 @@ function ListAccessTokensView() {
</a>
</Subheading>
</div>
{tokens.length > 0 && (
<Link to={settingsPathPersonalAccessTokenCreate}>
<button>New Access Token</button>
</Link>
)}
{tokens.length > 0 && <Button href={settingsPathPersonalAccessTokenCreate}>New Access Token</Button>}
</div>
{errorMsg.length > 0 && (
<Alert type="error" className="mb-2">
Expand Down Expand Up @@ -233,9 +229,7 @@ function ListAccessTokensView() {
<Subheading className="text-center pb-6 w-96">
Generate an access token for applications that need access to the Gitpod API.{" "}
</Subheading>
<Link to={settingsPathPersonalAccessTokenCreate}>
<button>New Access Token</button>
</Link>
<Button href={settingsPathPersonalAccessTokenCreate}>New Access Token</Button>
</div>
) : (
<>
Expand Down
11 changes: 4 additions & 7 deletions components/dashboard/src/workspaces/EmptyWorkspacesContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* See License.AGPL.txt in the project root for license information.
*/

import { Link } from "react-router-dom";
import { Heading2 } from "../components/typography/headings";
import { StartWorkspaceModalKeyBinding } from "../App";
import { Button } from "../components/Button";
Expand All @@ -30,12 +29,10 @@ export const EmptyWorkspacesContent = () => {
</a>
</div>
<span>
<Link to={"/new"}>
<Button>
New Workspace{" "}
<span className="opacity-60 hidden md:inline">{StartWorkspaceModalKeyBinding}</span>
</Button>
</Link>
<Button href="/new">
New Workspace{" "}
<span className="opacity-60 hidden md:inline">{StartWorkspaceModalKeyBinding}</span>
</Button>
</span>
</div>
</div>
Expand Down
9 changes: 3 additions & 6 deletions components/dashboard/src/workspaces/WorkspacesSearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*/

import { FunctionComponent } from "react";
import { Link } from "react-router-dom";
import { StartWorkspaceModalKeyBinding } from "../App";
import DropDown from "../components/DropDown";
import search from "../icons/search.svg";
Expand Down Expand Up @@ -67,11 +66,9 @@ export const WorkspacesSearchBar: FunctionComponent<WorkspacesSearchBarProps> =
]}
/>
</div>
<Link to={"/new"}>
<Button className="ml-2">
New Workspace <span className="opacity-60 hidden md:inline">{StartWorkspaceModalKeyBinding}</span>
</Button>
</Link>
<Button className="ml-2" href="/new">
New Workspace <span className="opacity-60 hidden md:inline">{StartWorkspaceModalKeyBinding}</span>
</Button>
</div>
);
};