8000 feat: add UI badges for labeling beta features by lizard-boy · Pull Request #11 · grepdemos/coder · GitHub
[go: up one dir, main page]

Skip to content

feat: add UI badges for labeling beta features #11

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

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
wip: commit progress on FeatureBadge update
  • Loading branch information
Parkreiner committed Sep 13, 2024
commit 6adea6b60652a7691c749b2df0b3d970089ecefe
23 changes: 2 additions & 21 deletions site/src/components/FeatureBadge/FeatureBadge.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,9 @@ const meta: Meta<typeof FeatureBadge> = {
export default meta;
type Story = StoryObj<typeof FeatureBadge>;

export const Small: Story = {
export const SmallInteractive: Story = {
args: {
size: "sm",
},
};

export const Medium: Story = {
args: {
size: "md",
},
};

export const HighlightedSmall: Story = {
args: {
size: "sm",
highlighted: true,
},
};

export const HighlightedMedium: Story = {
args: {
size: "md",
highlighted: true,
variant: "interactive",
},
};
134 changes: 121 additions & 13 deletions site/src/components/FeatureBadge/FeatureBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import type { Interpolation, Theme } from "@emotion/react";
import Link from "@mui/material/Link";
import { visuallyHidden } from "@mui/utils";
import type { FC, HTMLAttributes, ReactNode } from "react";
import { HelpTooltipContent } from "components/HelpTooltip/HelpTooltip";
import { Popover, PopoverTrigger } from "components/Popover/Popover";
import {
useEffect,
useState,
type FC,
type HTMLAttributes,
type ReactNode,
} from "react";
import { docs } from "utils/docs";

/**
* All types of feature that we are currently supporting. Defined as record to
* ensure that we can't accidentally make typos when writing the badge text.
*/
const featureBadgeTypes = {
beta: "beta",
experimental: "experimental",
} as const satisfies Record<string, ReactNode>;

const styles = {
Expand All @@ -16,11 +27,12 @@ const styles = {
// more types of HTML elements without creating invalid markdown, but we
// still want the default display behavior to be div-like
display: "block",
maxWidth: "fit-content",

// Base style assumes that small badges will be the default
fontSize: "0.75rem",

maxWidth: "fit-content",
cursor: "default",
flexShrink: 0,
padding: "4px 8px",
lineHeight: 1,
Expand All @@ -31,42 +43,138 @@ const styles = {
borderRadius: "6px",
}),

highlighted: (theme) => ({
badgeHover: (theme) => ({
color: theme.palette.text.primary,
borderColor: theme.palette.text.primary,
}),

mediumText: {
badgeLargeText: {
fontSize: "1rem",
},

tooltipTitle: {
fontWeight: 600,
fontFamily: "inherit",
fontSize: 18,
margin: 0,
lineHeight: 1,
paddingBottom: "8px",
},

tooltipDescription: {
margin: 0,
lineHeight: 1.2,
},
} as const satisfies Record<string, Interpolation<Theme>>;

type FeatureBadgeProps = Readonly<
Omit<HTMLAttributes<HTMLSpanElement>, "children"> & {
type: keyof typeof featureBadgeTypes;
size?: "sm" | "md";
highlighted?: boolean;
size?: "sm" | "lg";

/**
* Defines how the FeatureBadge should render.
* - interactive (default) - The badge functions like a link and
* controls its own hover styling.
* - static - The badge is completely static and has no interaction
* behavior.
* - staticHover - The badge is completely static, but displays badge
hover styling (but nothing related to links). Useful if you want a
parent component to control the hover styling.
*/
variant?: "interactive" | "static" | "staticHover";
}
>;

export const FeatureBadge: FC<FeatureBadgeProps> = ({
type,
size = "sm",
highlighted = false,
variant = "interactive",
onPointerEnter,
onPointerLeave,
...del 8000 egatedProps
}) => {
return (
const [isBadgeHovering, setIsBadgeHovering] = useState(false);
useEffect(() => {
const onWindowBlur = () => {
setIsBadgeHovering(false);
};

window.addEventListener("blur", onWindowBlur);
return () => window.removeEventListener("blur", onWindowBlur);
}, []);

const featureType = featureBadgeTypes[type];
const showHoverStyles =
variant === "staticHover" || (variant === "interactive" && isBadgeHovering);

const coreContent = (
<span
css={[
styles.badge,
size === "md" && styles.mediumText,
highlighted && styles.highlighted,
size === "lg" && styles.badgeLargeText,
showHoverStyles && styles.badgeHover,
]}
{...delegatedProps}
>
<span style={visuallyHidden}> (This feature is</span>
{featureBadgeTypes[type]}
<span style={visuallyHidden}>)</span>
<span style={visuallyHidden}> (This is a</span>
{featureType}
<span style={visuallyHidden}> feature)</span>
</span>
);

if (variant !== "interactive") {
return coreContent;
}

return (
<Popover mode="hover">
<PopoverTrigger
onPointerEnter={(event) => {
setIsBadgeHovering(true);
onPointerEnter?.(event);
}}
onPointerLeave={(event) => {
setIsBadgeHovering(false);
onPointerLeave?.(event);
}}
>
{coreContent}
</PopoverTrigger>

<HelpTooltipContent
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
transformOrigin={{ vertical: "top", horizontal: "center" }}
>
<h5 css={styles.tooltipTitle}>
{capitalizeFirstLetter(featureType)} Feature
</h5>

<p css={styles.tooltipDescription}>
This is {getGrammaticalArticle(featureType)} {featureType} feature. It
has not yet been marked for general availability.
</p>

<Link
href={docs("/contributing/feature-stages")}
target="_blank"
rel="noreferrer"
css={{ fontWeight: 600 }}
>
Feature stage documentation
<span style={visuallyHidden}> (link opens in new tab)</span>
</Link>
</HelpTooltipContent>
</Popover>
);
};

function getGrammaticalArticle(nextWord: string): string {
const vowels = ["a", "e", "i", "o", "u"];
const firstLetter = nextWord.slice(0, 1).toLowerCase();
return vowels.includes(firstLetter) ? "an" : "a";
}

function capitalizeFirstLetter(text: string): string {
return text.slice(0, 1).toUpperCase() + text.slice(1);
}
0