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
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
@@ -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>
) : (
<>
110 changes: 59 additions & 51 deletions components/dashboard/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -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";
@@ -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,
@@ -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
Member

@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}
/>
);
}
},
);

Original file line number Diff line number Diff line change
@@ -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";
@@ -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>
17 changes: 8 additions & 9 deletions components/dashboard/src/projects/Projects.tsx
Original file line number Diff line number Diff line change
@@ -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();
@@ -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>
@@ -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
Original file line number Diff line number Diff line change
@@ -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";
@@ -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");
@@ -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">
@@ -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>
) : (
<>
11 changes: 4 additions & 7 deletions components/dashboard/src/workspaces/EmptyWorkspacesContent.tsx
Original file line number Diff line number Diff line change
@@ -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";
@@ -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>
9 changes: 3 additions & 6 deletions components/dashboard/src/workspaces/WorkspacesSearchBar.tsx
Original file line number Diff line number Diff line change
@@ -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";
@@ -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>
);
};