Add year headings to compare version select (#12138)

* Add year headings to compare version select

* Address review feedback on heading options

Use stable keys for heading options and set explicit displayName.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Tom Moor
2026-04-22 19:04:35 -04:00
committed by GitHub
parent 5b2283386d
commit eefa8d4222
3 changed files with 67 additions and 2 deletions
+25 -1
View File
@@ -21,6 +21,7 @@ import {
InputSelectContent,
InputSelectItem,
InputSelectSeparator,
InputSelectHeading,
InputSelectTrigger,
type TriggerButtonProps,
} from "./primitives/InputSelect";
@@ -35,6 +36,13 @@ type Separator = {
type: "separator";
};
type Heading = {
/* Denotes a non-selectable heading rendered above a group of options. */
type: "heading";
/* Text shown as the heading label. */
label: string;
};
export type Item = {
/* Denotes a selectable option in the menu. */
type: "item";
@@ -48,7 +56,7 @@ export type Item = {
icon?: React.ReactElement;
};
export type Option = Item | Separator;
export type Option = Item | Separator | Heading;
type Props = Omit<React.HTMLAttributes<HTMLButtonElement>, "onChange"> & {
/* Options to display in the select menu. */
@@ -118,6 +126,14 @@ export const InputSelect = React.forwardRef<HTMLButtonElement, Props>(
return <InputSelectSeparator key={`separator-${idx}`} />;
}
if (option.type === "heading") {
return (
<InputSelectHeading key={`heading-${option.label}`}>
{option.label}
</InputSelectHeading>
);
}
return (
<InputSelectItem key={option.value} value={option.value}>
<Option option={option} optionsHaveIcon={optionsHaveIcon} />
@@ -244,6 +260,14 @@ const MobileSelect = React.forwardRef<HTMLButtonElement, MobileSelectProps>(
return <InputSelectSeparator key={`separator-${idx}`} />;
}
if (option.type === "heading") {
return (
<InputSelectHeading key={`heading-${option.label}`}>
{option.label}
</InputSelectHeading>
);
}
const isSelected = option === selectedOption;
return (
+27
View File
@@ -1,6 +1,7 @@
import * as InputSelectPrimitive from "@radix-ui/react-select";
import * as React from "react";
import styled from "styled-components";
import Text from "@shared/components/Text";
import { depths, s } from "@shared/styles";
import type { Props as ButtonProps } from "~/components/Button";
import { fadeAndSlideDown, fadeAndSlideUp } from "~/styles/animations";
@@ -110,6 +111,31 @@ const Separator = styled.hr`
margin: 6px 0;
`;
/** Non-selectable heading rendered to group options in the menu. */
const InputSelectHeading = React.forwardRef<
HTMLSpanElement,
{ children?: React.ReactNode }
>(({ children }, ref) => (
<InputSelectPrimitive.Group>
<InputSelectPrimitive.Label asChild>
<Heading ref={ref}>{children}</Heading>
</InputSelectPrimitive.Label>
</InputSelectPrimitive.Group>
));
InputSelectHeading.displayName = "InputSelectHeading";
const Heading = styled(Text).attrs({
type: "tertiary",
size: "xsmall",
weight: "bold",
})`
display: block;
padding-block: 8px 4px;
padding-inline: 8px;
text-transform: uppercase;
letter-spacing: 0.04em;
`;
/** Styled components. */
const StyledContent = styled(InputSelectPrimitive.Content)`
z-index: ${depths.menu};
@@ -143,4 +169,5 @@ export {
InputSelectContent,
InputSelectItem,
InputSelectSeparator,
InputSelectHeading,
};
@@ -63,12 +63,26 @@ export function HighlightChangesControl({
? RevisionHelper.latestId(document.id)
: undefined;
const currentYear = new Date().getFullYear();
let lastHeadingYear: number | undefined;
for (const rev of revisionItems) {
if (rev.id === resolvedSelectedId) {
continue;
}
const dateLabel = formatDate(new Date(rev.createdAt), "MMM do, h:mm a", {
const revDate = new Date(rev.createdAt);
const revYear = revDate.getFullYear();
if (revYear !== currentYear && revYear !== lastHeadingYear) {
options.push({
type: "heading",
label: String(revYear),
});
lastHeadingYear = revYear;
}
const dateLabel = formatDate(revDate, "MMM do, h:mm a", {
locale,
});
const collaboratorText = revisionCollaboratorText(rev, t);