Skip to content

Commit

Permalink
Shared button component (#10347)
Browse files Browse the repository at this point in the history
The `ui/components/shared/Button` component has long confused me. It exports a lot of component variants, many of which don't seem to be used directly (rather the base `Button` component is used with various combinations of props) and many of which don't actually work (combinations of attributes that don't produce usable styles). In addition to that, most of our newer DevTools code can't use these buttons because the `replay-next` package can't import from the legacy `ui` package.

This PR adds a new `Button` component to `replay-next` that supports our two style variants (solid vs outlined) and colors (primary vs secondary). The API is more constrained (e.g. it doesn't try to support a dozen colors, when we only use two and it doesn't export a dozen components when there is, conceptually, only one button).

## Comparison screenshots

Here is a side by side comparison of the old and new buttons.

| Dark theme | Light theme |
| :--- | :--- |
| ![Screenshot 2024-02-22 at 10 06 52 AM](https://github.com/replayio/devtools/assets/29597/857900c1-e41f-440a-aeb8-37af1230b67c) | ![Screenshot 2024-02-22 at 10 07 02 AM](https://github.com/replayio/devtools/assets/29597/b25c4da6-1bc8-4456-9497-a24dd33df4c3) |

The new button component fixes some issues with the old one, some of which can be seen in the comparison above:
* Legacy variants that should work, but don't (like secondary + outline)
* Legacy hover states that don't work (secondary)
* Disabled colors for dark theme
* Add disabled state for outline variant

## Example replacements

Below I've updated a couple of pages to use the new button component instead.

| Before | After |
| :--- | :--- |
| ![Screenshot 2024-02-22 at 10 07 25 AM](https://github.com/replayio/devtools/assets/29597/844bed44-eb23-4f6f-a179-309d6b50d822) | ![Screenshot 2024-02-22 at 10 07 44 AM](https://github.com/replayio/devtools/assets/29597/94b14a2d-4483-4cfb-84bb-f920df028997) |
| ![Screenshot 2024-02-22 at 10 07 32 AM](https://github.com/replayio/devtools/assets/29597/5c473b45-fb88-4bb3-93c2-fc9bb07dee06) | ![Screenshot 2024-02-22 at 10 07 50 AM](https://github.com/replayio/devtools/assets/29597/a27d1e12-77ac-465f-b57d-3f74099b0328) |
| ![Screenshot 2024-02-22 at 10 07 38 AM](https://github.com/replayio/devtools/assets/29597/28fd8a56-3116-4b77-aad7-113e2dc444f2) | ![Screenshot 2024-02-22 at 10 08 01 AM](https://github.com/replayio/devtools/assets/29597/7bb03cbc-83dc-48b4-9215-69fe2fcda819) |
| ![Screenshot 2024-02-22 at 10 58 38 AM](https://github.com/replayio/devtools/assets/29597/67132ac2-9774-49bb-b4ec-de0c5e23db5e) | ![Screenshot 2024-02-22 at 10 58 42 AM](https://github.com/replayio/devtools/assets/29597/2c454dc5-1e79-4c19-a5a9-7c7b8099b8df) |
| ![Screenshot 2024-02-22 at 10 59 24 AM](https://github.com/replayio/devtools/assets/29597/504c3fda-3adf-4da4-9d0d-84d9b60bce92) | ![Screenshot 2024-02-22 at 10 59 28 AM](https://github.com/replayio/devtools/assets/29597/3ba59f62-97d1-4b27-87e0-f6cf10219349) |
| ![Screenshot 2024-02-22 at 10 59 39 AM](https://github.com/replayio/devtools/assets/29597/d1ad8a77-616c-4691-8514-965b8bd240e1) | ![Screenshot 2024-02-22 at 10 59 44 AM](https://github.com/replayio/devtools/assets/29597/bb962d4d-72b9-46bc-ae3d-04929b71baad) |

Note that by updating this page we fix the following problems:
* Bright disabled button style on a dark theme page
* Bright background on outline button (bad contrast– blue on white)
* "Done" button is overlapped/hidden entirely when selecting elements.
* Generally simplifies the code required to use our standard button UI/UX
* Removes the only instance of a "red" button color in favor of a standard color; (this might be controversial and we could change this if needed)

## Follow up work to this PR

- Update `replay-next` code to use these buttons instead of one-off re-implementing the styles everywhere
- Gradually remove references to `ui/components/shared/Button` and replace with new buttons
- Delete all `ui/components/shared/Button` components once references have been removed
  • Loading branch information
bvaughn authored Feb 23, 2024
1 parent 5bbfdf4 commit 1e1620f
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 78 deletions.
58 changes: 58 additions & 0 deletions packages/replay-next/components/Button.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
.Button {
font-family: var(--font-family-default);
font-size: 0.875rem;
line-height: 1rem;
font-weight: 500;
padding: 0.5rem 0.75rem;
border: 1px solid transparent;
border-radius: 0.375rem;
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 1ch;
}
.Button:disabled {
--accent-color: var(--background-color-disabled-button);
--accent-color-hover: var(--background-color-disabled-button);
--contrast-color: var(--color-disabled-button);

color: var(--color-dimmer);
}

.Button[data-color="primary"]:not(:disabled) {
--accent-color: var(--primary-accent);
--accent-color-hover: var(--primary-accent-hover);
--contrast-color: var(--color-primary-button);
}
.Button[data-color="secondary"]:not(:disabled) {
--accent-color: var(--secondary-accent);
--accent-color-hover: var(--secondary-accent-hover);
--contrast-color: var(--color-secondary-button);
}

.Button[data-variant="outline"] {
border-color: var(--accent-color);
}
.Button[data-variant="outline"]:hover {
border-color: var(--accent-color-hover);
}
.Button[data-variant="solid"] {
background-color: var(--accent-color);
color: var(--contrast-color);
}
.Button[data-variant="solid"]:hover {
background-color: var(--accent-color-hover);
}

.Button[data-size="large"] {
font-size: 1rem;
line-height: 1.5rem;
padding: 0.75rem 1.5rem;
border-radius: 0.375rem;
}
.Button[data-size="small"] {
font-size: 0.75rem;
line-height: 1rem;
padding: 0.375rem 0.625rem;
border-radius: 0.25rem;
}
25 changes: 25 additions & 0 deletions packages/replay-next/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ButtonHTMLAttributes } from "react";

import styles from "./Button.module.css";

export function Button({
className = "",
color = "primary",
size = "normal",
variant = "solid",
...rest
}: ButtonHTMLAttributes<HTMLButtonElement> & {
color?: "primary" | "secondary";
size?: "normal" | "large" | "small";
variant?: "outline" | "solid";
}) {
return (
<button
className={`${className} ${styles.Button}`}
data-color={color}
data-size={size}
data-variant={variant}
{...rest}
/>
);
}
6 changes: 3 additions & 3 deletions packages/replay-next/pages/variables.css
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@
--color-link: var(--primary-accent);
--color-primary-button: #ffffff;
--color-secondary-button: #ffffff;
--color-disabled-button: #ffffff;
--color-disabled-button: var(--color-default);
--color-high-contrast-button: #ffffff;
--color-warning: #fce2a1;
--color-search-results: #d7d7db;
Expand Down Expand Up @@ -745,7 +745,7 @@
--background-color-contrast-5: #e1e6ed;

--background-color-default: #e9e9e9;
--background-color-disabled-button: #666666;
--background-color-disabled-button: #e6e7eb;
--background-color-error: #fcefee;
--background-color-highlight-change: mark;
--background-color-high-contrast-button: #f2f2f2;
Expand Down Expand Up @@ -777,7 +777,7 @@
--color-default: #223344;
--color-dim: rgba(135, 135, 137, 0.9);
--color-dimmer: #aaaaaa;
--color-disabled-button: var(--theme-base-100);
--color-disabled-button: var(--color-default);
--color-error: #ff0000;
--color-highlight-change: marktext;
--color-high-contrast-button: #000000;
Expand Down
21 changes: 0 additions & 21 deletions src/ui/components/Library/Library.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,6 @@
border-bottom: 1px solid var(--chrome);
}

:root:global(.theme-dark) .editButton {
background-color: var(--theme-base-80);
color: #fff;
border: none;
}

:root:global(.theme-dark) .editButton:hover {
background-color: var(--primary-accent-hover);
color: #fff;
border: none;
}

:root:global(.theme-dark) .recordingsBackground {
background-color: transparent;
color: var(--body-color);
Expand Down Expand Up @@ -102,15 +90,6 @@
border-bottom: 1px solid var(--theme-base-90);
}

:root:global(.theme-light) .editButton {
background-color: #fff;
}

:root:global(.theme-light) .editButton:hover {
background-color: var(--primary-accent-hover);
color: #fff;
}

:root:global(.theme-light) .recordingsBackground {
background-color: transparent;
color: var(--body-color);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { RecordingId } from "@replayio/protocol";
import classNames from "classnames";
import React, { useState } from "react";
import { useState } from "react";

import { assert } from "protocol/utils";
import { Button } from "replay-next/components/Button";
import { Recording } from "shared/graphql/types";
import { useGetTeamIdFromRoute } from "ui/components/Library/Team/utils";
import { isTestSuiteReplay } from "ui/components/TestSuite/utils/isTestSuiteReplay";
import hooks from "ui/hooks";
import { WorkspaceId } from "ui/state/app";
import { useIsPublicEnabled } from "ui/utils/org";

import { DisabledButton, getButtonClasses } from "../../../../../shared/Button";
import { useConfirm } from "../../../../../shared/Confirm";
import MaterialIcon from "../../../../../shared/MaterialIcon";
import PortalDropdown from "../../../../../shared/PortalDropdown";
Expand Down Expand Up @@ -98,23 +97,22 @@ export default function BatchActionDropdown({

if (!selectedIds.length) {
return (
<DisabledButton className="space-x-1 leading-4">
<Button disabled>
<MaterialIcon outlined className="font-bold" iconSize="sm">
expand_more
</MaterialIcon>
<span>{`${selectedIds.length} item${selectedIds.length > 1 ? "s" : ""} selected`}</span>
</DisabledButton>
</Button>
);
}

const buttonClasses = classNames("bg-white", getButtonClasses("blue", "secondary", "md"));
const button = (
<span className={"flex flex-row items-center space-x-1 leading-4 text-primaryAccent"}>
<Button variant="outline">
<MaterialIcon outlined className="font-bold" iconSize="sm">
expand_more
</MaterialIcon>
<span>{`${selectedIds.length} item${selectedIds.length > 1 ? "s" : ""} selected`}</span>
</span>
</Button>
);

let allRecordingsOwnedByCurrentUser = true;
Expand Down Expand Up @@ -147,7 +145,6 @@ export default function BatchActionDropdown({
return (
<PortalDropdown
buttonContent={button}
buttonStyle={buttonClasses}
setExpanded={setExpanded}
expanded={expanded}
distance={0}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { useRouter } from "next/router";
import React from "react";

import { Button } from "replay-next/components/Button";
import { Recording } from "shared/graphql/types";
import { Workspace } from "shared/graphql/types";
import { setModal } from "ui/actions/app";
import { MY_LIBRARY_TEAM } from "ui/components/Library/Team/TeamContextRoot";
import { useGetTeamIdFromRoute } from "ui/components/Library/Team/utils";
import hooks from "ui/hooks";
import { useAppDispatch } from "ui/setup/hooks";

import { PrimaryButton, SecondaryButton } from "../../../../../shared/Button";
import BatchActionDropdown from "./BatchActionDropdown";
import TeamTrialEnd from "./TeamTrialEnd";
import styles from "../../../../Library.module.css";
Expand Down Expand Up @@ -40,9 +38,7 @@ function ViewerHeaderActions({
selectedIds={selectedIds}
recordings={recordings}
/>
<PrimaryButton color="blue" onClick={handleDoneEditing}>
Done
</PrimaryButton>
<Button onClick={handleDoneEditing}>Done</Button>
</>
);
}
Expand All @@ -56,26 +52,14 @@ function ViewerHeaderActions({
return (
<>
{!isLibrary ? (
<SecondaryButton
className={styles.editButton}
color="blue"
onClick={() => launchWorkspaceSettings()}
>
Add team member
</SecondaryButton>
<Button onClick={() => launchWorkspaceSettings()}>Add team member</Button>
) : (
<></>
)}

{recordings.length != 0 ? (
<>
<SecondaryButton
className={styles.editButton}
color="blue"
onClick={() => setIsEditing(true)}
>
Edit
</SecondaryButton>
<Button onClick={() => setIsEditing(true)}>Edit</Button>
</>
) : (
<></>
Expand Down Expand Up @@ -113,7 +97,7 @@ export default function ViewerHeader({
);

return (
<div className={`m-2 flex flex-row items-center justify-between ${styles.libraryHeaderButton}`}>
<div className={`m-2 flex flex-row items-center justify-between`}>
{HeaderLeft}
<div className="flex flex-row items-center space-x-3">
{currentWorkspaceId ? <TeamTrialEnd currentWorkspaceId={currentWorkspaceId} /> : null}
Expand Down
8 changes: 2 additions & 6 deletions src/ui/components/shared/APIKeys.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useEffect, useMemo, useRef, useState } from "react";

import { Button } from "replay-next/components/Button";
import { ApiKey, ApiKeyResponse, ApiKeyScope } from "shared/graphql/types";

import { Button, DisabledButton, PrimaryButton } from "./Button";
import { useConfirm } from "./Confirm";
import TextInput from "./Forms/TextInput";
import MaterialIcon from "./MaterialIcon";
Expand Down Expand Up @@ -176,11 +176,7 @@ export default function APIKeys({
value={label}
/>

{canSubmit ? (
<PrimaryButton color="blue">Add</PrimaryButton>
) : (
<DisabledButton>Add</DisabledButton>
)}
<Button disabled={!canSubmit}>Add</Button>
</fieldset>
{scopes && scopes.length > 1 ? (
<fieldset className="w-full">
Expand Down
9 changes: 0 additions & 9 deletions src/ui/components/shared/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,3 @@ export const PrimaryButton = (props: ButtonProps & { color: Colors }) => (
export const SecondaryButton = (props: ButtonProps & { color: Colors }) => (
<Button {...props} size="md" style="secondary" />
);
export const DisabledButton = ({ className, ...rest }: ButtonProps) => (
<Button
{...rest}
size="md"
style="disabled"
className={classNames(className, "cursor-default")}
color="gray"
/>
);
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { ChangeEvent, useMemo, useState } from "react";

import { Button } from "replay-next/components/Button";
import { WorkspaceUser } from "shared/graphql/types";
import * as actions from "ui/actions/app";
import { useRedirectToTeam } from "ui/components/Library/Team/utils";
Expand All @@ -12,7 +13,6 @@ import { validateEmail } from "ui/utils/helpers";
import { trackEvent } from "ui/utils/telemetry";

import Base64Image from "../Base64Image";
import { Button, DisabledButton, PrimaryButton } from "../Button";
import { useConfirm } from "../Confirm";
import { TextInput } from "../Forms";
import InvitationLink from "../NewWorkspaceModal/InvitationLink";
Expand Down Expand Up @@ -117,13 +117,7 @@ function WorkspaceForm() {
onChange={onChange}
className="border-inputBorder"
/>
{isLoading ? (
<DisabledButton>Loading...</DisabledButton>
) : canSubmit ? (
<PrimaryButton color="blue">Invite</PrimaryButton>
) : (
<DisabledButton>Invite</DisabledButton>
)}
<Button disabled={isLoading || !canSubmit}>{isLoading ? "Loading..." : "Invite"}</Button>
</div>
{errorMessage ? <div className="py-3 text-xs text-red-500">{errorMessage}</div> : null}
</form>
Expand Down Expand Up @@ -225,7 +219,7 @@ const settings: Settings<
<div className="font-semibold">Delete this team</div>
<div className="">{`This cannot be reversed.`}</div>
</div>
<Button color="red" onClick={handleDeleteTeam} size="md" style="primary">
<Button color="secondary" onClick={handleDeleteTeam}>
Delete this team
</Button>
</div>
Expand Down

0 comments on commit 1e1620f

Please sign in to comment.