Skip to content

feat: create UI badges for labeling beta features #14661

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 48 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
f843f23
chore: finish draft work for FeatureBadge component
Parkreiner Sep 12, 2024
43874a4
fix: add visually-hidden helper text for screen readers
Parkreiner Sep 12, 2024
fb70781
chore: add stories for highlighted state
Parkreiner Sep 12, 2024
c2470f9
fix: update base styles
Parkreiner Sep 12, 2024
693946f
chore: remove debug display option
Parkreiner Sep 12, 2024
c2d7cda
Merge branch 'main' into mes/beta-badges
Parkreiner Sep 13, 2024
129613b
chore: update Popover to propagate events
Parkreiner Sep 13, 2024
6adea6b
wip: commit progress on FeatureBadge update
Parkreiner Sep 13, 2024
d4455a8
wip: commit more progress
Parkreiner Sep 13, 2024
8ae71d3
chore: update tag definitions to satify Biome
Parkreiner Sep 13, 2024
0ad68af
chore: update all colors for preview role
Parkreiner Sep 13, 2024
781a609
fix: make sure badge shows as hovered while inside tooltip
Parkreiner Sep 13, 2024
a981864
wip: commit progress on adding story for controlled variant
Parkreiner Sep 13, 2024
f47d059
fix: sort imports
Parkreiner Sep 16, 2024
ad61763
refactor: change component API to be more obvious/ergonomic
Parkreiner Sep 16, 2024
6e16aaa
fix: add biome-ignore comments to more base files
Parkreiner Sep 16, 2024
6a19b61
fix: update import order again
Parkreiner Sep 16, 2024
a233867
chore: revert biome-ignore comment
Parkreiner Sep 16, 2024
8cbe639
Merge branch 'main' into mes/beta-badges
Parkreiner Sep 17, 2024
7e1ec68
chore: update body text for tooltip
Parkreiner Sep 17, 2024
1b58e4d
chore: update dark preivew role to use sky palette
Parkreiner Sep 17, 2024
ebc5397
chore: update color palettes for light/darkBlue themes
Parkreiner Sep 17, 2024
a9b6897
chore: add beta badge to organizations subheader
Parkreiner Sep 17, 2024
31e1fa7
chore: add beta badge to organizations settings page
Parkreiner Sep 17, 2024
e16b140
chore: beef up font weight for form header
Parkreiner Sep 17, 2024
3aeb3c0
fix: update text contrast for org menu list
Parkreiner Sep 17, 2024
71323ac
chore: add beta badge to deployment dropdown
Parkreiner Sep 17, 2024
da31c84
fix: run biome on imports
Parkreiner Sep 17, 2024
c7308c3
chore: remove API for controlling FeatureBadge hover styling externally
Parkreiner Sep 17, 2024
230aa1d
chore: add xs size for badge
Parkreiner Sep 17, 2024
fb4b734
fix: update font weight for xs feature badges
Parkreiner Sep 18, 2024
5a7bbd3
chore: add beta badges to all org headers
Parkreiner Sep 18, 2024
9521f25
Merge branch 'main' into mes/beta-badges
Parkreiner Sep 18, 2024
6638942
fix: turn badges and tooltips into separate concerns
Parkreiner Sep 18, 2024
9a18e51
fix: update hover styling
Parkreiner Sep 18, 2024
b093a99
docs: update wording on comment
Parkreiner Sep 18, 2024
9647fc6
fix: apply formatting
Parkreiner Sep 18, 2024
0197466
chore: rename FeatureBadge to FeatureStageBadge
Parkreiner Sep 18, 2024
fb86964
refactor: redefine FeatureStageBadge
Parkreiner Sep 20, 2024
0f64533
chore: update stories
Parkreiner Sep 20, 2024
6a2cfcf
fix: add blur behavior to popover
Parkreiner Sep 20, 2024
eb2b1c2
chore: revert theme colors
Parkreiner Sep 20, 2024
0b46eca
chore: create featureStage branding namespace
Parkreiner Sep 20, 2024
4302b3d
fix: make sure cleanup function is set up properly
Parkreiner Sep 20, 2024
be8121e
docs: update wording on comment for clarity
Parkreiner Sep 20, 2024
1e5d62b
Merge branch 'main' into mes/beta-badges
Parkreiner Sep 20, 2024
a824404
Merge branch 'main' into mes/beta-badges
Parkreiner Sep 20, 2024
39daf80
refactor: move styles down
Parkreiner Sep 20, 2024
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
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"stretchr",
"STTY",
"stuntest",
"subpage",
"tailbroker",
"tailcfg",
"tailexchange",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Meta, StoryObj } from "@storybook/react";
import { FeatureStageBadge } from "./FeatureStageBadge";

const meta: Meta<typeof FeatureStageBadge> = {
title: "components/FeatureStageBadge",
component: FeatureStageBadge,
args: {
contentType: "beta",
},
};

export default meta;
type Story = StoryObj<typeof FeatureStageBadge>;

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

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

export const LargeBeta: Story = {
args: {
size: "lg",
},
};

export const MediumExperimental: Story = {
args: {
size: "md",
contentType: "experimental",
},
};
133 changes: 133 additions & 0 deletions site/src/components/FeatureStageBadge/FeatureStageBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import type { Interpolation, Theme } from "@emotion/react";
import Link from "@mui/material/Link";
import { visuallyHidden } from "@mui/utils";
import { HelpTooltipContent } from "components/HelpTooltip/HelpTooltip";
import { Popover, PopoverTrigger } from "components/Popover/Popover";
import type { FC, HTMLAttributes, 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 featureStageBadgeTypes = {
beta: "beta",
experimental: "experimental",
} as const satisfies Record<string, ReactNode>;

type FeatureStageBadgeProps = Readonly<
Omit<HTMLAttributes<HTMLSpanElement>, "children"> & {
contentType: keyof typeof featureStageBadgeTypes;
size?: "sm" | "md" | "lg";
}
>;

export const FeatureStageBadge: FC<FeatureStageBadgeProps> = ({
contentType,
size = "md",
...delegatedProps
}) => {
return (
<Popover mode="hover">
<PopoverTrigger>
{({ isOpen }) => (
<span
css={[
styles.badge,
size === "sm" && styles.badgeSmallText,
size === "lg" && styles.badgeLargeText,
isOpen && styles.badgeHover,
]}
{...delegatedProps}
>
<span style={visuallyHidden}> (This is a</span>
{featureStageBadgeTypes[contentType]}
<span style={visuallyHidden}> feature)</span>
</span>
)}
</PopoverTrigger>

<HelpTooltipContent
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
transformOrigin={{ vertical: "top", horizontal: "center" }}
>
<p css={styles.tooltipDescription}>
This feature has not yet reached general availability (GA).
</p>

<Link
href={docs("/contributing/feature-stages")}
target="_blank"
rel="noreferrer"
css={styles.tooltipLink}
>
Learn about feature stages
<span style={visuallyHidden}> (link opens in new tab)</span>
</Link>
</HelpTooltipContent>
</Popover>
);
};

const styles = {
Copy link
Member

Choose a reason for hiding this comment

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

I think we generally define styles below the component

badge: (theme) => ({
// Base type is based on a span so that the element can be placed inside
// 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 medium badges will be the default
fontSize: "0.75rem",

cursor: "default",
flexShrink: 0,
padding: "4px 8px",
lineHeight: 1,
whiteSpace: "nowrap",
border: `1px solid ${theme.branding.featureStage.border}`,
color: theme.branding.featureStage.text,
backgroundColor: theme.branding.featureStage.background,
borderRadius: "6px",
transition:
"color 0.2s ease-in-out, border-color 0.2s ease-in-out, background-color 0.2s ease-in-out",
}),

badgeHover: (theme) => ({
color: theme.branding.featureStage.hover.text,
borderColor: theme.branding.featureStage.hover.border,
backgroundColor: theme.branding.featureStage.hover.background,
}),

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

badgeSmallText: {
// Have to beef up font weight so that the letters still maintain the
// same relative thickness as all our other main UI text
fontWeight: 500,
fontSize: "0.625rem",
},

tooltipTitle: (theme) => ({
color: theme.palette.text.primary,
fontWeight: 600,
fontFamily: "inherit",
fontSize: 18,
margin: 0,
lineHeight: 1,
paddingBottom: "8px",
}),

tooltipDescription: {
margin: 0,
lineHeight: 1.4,
paddingBottom: "8px",
},

tooltipLink: {
fontWeight: 600,
lineHeight: 1.2,
},
} as const satisfies Record<string, Interpolation<Theme>>;
2 changes: 1 addition & 1 deletion site/src/components/Form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ const styles = {
formSectionInfoTitle: (theme) => ({
fontSize: 20,
color: theme.palette.text.primary,
fontWeight: 400,
fontWeight: 500,
margin: 0,
marginBottom: 8,
display: "flex",
Expand Down
78 changes: 59 additions & 19 deletions site/src/components/Popover/Popover.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import MuiPopover, {
type PopoverProps as MuiPopoverProps,
// biome-ignore lint/nursery/noRestrictedImports: Used as base component
// biome-ignore lint/nursery/noRestrictedImports: This is the base component that our custom popover is based on
Copy link
Member Author

Choose a reason for hiding this comment

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

I might have Biome configured wrong because my Biome plugin is flagging all of these comments as not doing anything

Accidentally removed this at first, but that's definitely a me problem, right?

Copy link
Member

Choose a reason for hiding this comment

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

I think it doesn't like that our biome.json config is in the site/ directory 🙄 I hate that extensions never bother to handle subdirectories well.

} from "@mui/material/Popover";
import {
type FC,
type HTMLAttributes,
type PointerEvent,
type PointerEventHandler,
type ReactElement,
type ReactNode,
type RefObject,
cloneElement,
createContext,
useContext,
useEffect,
useId,
useRef,
useState,
Expand All @@ -20,10 +23,13 @@ type TriggerMode = "hover" | "click";

type TriggerRef = RefObject<HTMLElement>;

type TriggerElement = ReactElement<{
ref: TriggerRef;
onClick?: () => void;
}>;
// Have to append ReactNode type to satisfy React's cloneElement function. It
// has absolutely no bearing on what happens at runtime
type TriggerElement = ReactNode &
ReactElement<{
ref: TriggerRef;
onClick?: () => void;
}>;

type PopoverContextValue = {
id: string;
Expand Down Expand Up @@ -61,6 +67,15 @@ export const Popover: FC<PopoverProps> = (props) => {
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
const triggerRef: TriggerRef = useRef(null);

// Helps makes sure that popovers close properly when the user switches to
// a different tab. This won't help with controlled instances of the
// component, but this is basically the most we can do from here
useEffect(() => {
const closeOnTabSwitch = () => setUncontrolledOpen(false);
window.addEventListener("blur", closeOnTabSwitch);
return () => window.removeEventListener("blur", closeOnTabSwitch);
}, []);

const value: PopoverContextValue = {
triggerRef,
id: `${hookId}-popover`,
Expand All @@ -86,30 +101,47 @@ export const usePopover = () => {
return context;
};

export const PopoverTrigger = (
props: HTMLAttributes<HTMLElement> & {
children: TriggerElement;
},
) => {
type PopoverTriggerRenderProps = Readonly<{
isOpen: boolean;
}>;

type PopoverTriggerProps = Readonly<
Omit<HTMLAttributes<HTMLElement>, "children"> & {
children:
| TriggerElement
| ((props: PopoverTriggerRenderProps) => TriggerElement);
}
>;

export const PopoverTrigger: FC<PopoverTriggerProps> = (props) => {
const popover = usePopover();
const { children, ...elementProps } = props;
const { children, onClick, onPointerEnter, onPointerLeave, ...elementProps } =
props;

const clickProps = {
onClick: () => {
onClick: (event: PointerEvent<HTMLElement>) => {
popover.setOpen(true);
onClick?.(event);
},
};

const hoverProps = {
onPointerEnter: () => {
onPointerEnter: (event: PointerEvent<HTMLElement>) => {
popover.setOpen(true);
onPointerEnter?.(event);
},
onPointerLeave: () => {
onPointerLeave: (event: PointerEvent<HTMLElement>) => {
popover.setOpen(false);
onPointerLeave?.(event);
},
};

return cloneElement(props.children, {
const evaluatedChildren =
typeof children === "function"
? children({ isOpen: popover.open })
: children;

return cloneElement(evaluatedChildren, {
...elementProps,
...(popover.mode === "click" ? clickProps : hoverProps),
"aria-haspopup": true,
Expand All @@ -130,6 +162,8 @@ export type PopoverContentProps = Omit<

export const PopoverContent: FC<PopoverContentProps> = ({
horizontal = "left",
onPointerEnter,
onPointerLeave,
...popoverProps
}) => {
const popover = usePopover();
Expand All @@ -152,7 +186,7 @@ export const PopoverContent: FC<PopoverContentProps> = ({
},
}}
{...horizontalProps(horizontal)}
{...modeProps(popover)}
{...modeProps(popover, onPointerEnter, onPointerLeave)}
{...popoverProps}
id={popover.id}
open={popover.open}
Expand All @@ -162,14 +196,20 @@ export const PopoverContent: FC<PopoverContentProps> = ({
);
};

const modeProps = (popover: PopoverContextValue) => {
const modeProps = (
popover: PopoverContextValue,
externalOnPointerEnter: PointerEventHandler<HTMLDivElement> | undefined,
externalOnPointerLeave: PointerEventHandler<HTMLDivElement> | undefined,
) => {
if (popover.mode === "hover") {
return {
onPointerEnter: () => {
onPointerEnter: (event: PointerEvent<HTMLDivElement>) => {
popover.setOpen(true);
externalOnPointerEnter?.(event);
},
onPointerLeave: () => {
onPointerLeave: (event: PointerEvent<HTMLDivElement>) => {
popover.setOpen(false);
externalOnPointerLeave?.(event);
},
};
}
Expand Down
Loading
Loading