Skip to content
Closed
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions .changeset/admin-toolbar-control-height.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@emdash-cms/admin": patch
---

Fixes search boxes and filters on the admin's list pages coming in two heights. The content list and Users toolbars now match the other list pages and the page header buttons.
3 changes: 1 addition & 2 deletions packages/admin/src/components/BylineFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,11 @@ export function BylineFilter({ value, onChange, locale }: BylineFilterProps) {
<Popover.Trigger asChild>
<Button
variant="secondary"
size="sm"
aria-label={t`Filter by byline`}
className="emdash-byline-filter-trigger gap-1 px-3.5 font-normal"
>
<span className="max-w-[140px] truncate">{label}</span>
<CaretDown className="size-3 shrink-0" aria-hidden="true" />
<CaretDown className="size-4 shrink-0" aria-hidden="true" />
</Button>
</Popover.Trigger>

Expand Down
58 changes: 42 additions & 16 deletions packages/admin/src/components/ContentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1056,7 +1056,6 @@ function FilterBar({
const clearButton = (
<Button
variant="ghost"
size="sm"
shape="square"
onClick={handleClear}
disabled={!hasActiveFilter}
Expand All @@ -1070,26 +1069,28 @@ function FilterBar({
return (
<>
<Select
size="sm"
className="emdash-status-filter-trigger min-w-32 ps-3.5"
aria-label={t`Filter by status`}
value={statusFilter}
onValueChange={(v) => onStatusFilterChange((v as ContentStatusFilter) ?? "all")}
renderValue={(v) =>
renderStatusLabel(typeof v === "string" && Object.hasOwn(statusItems, v) ? v : "all")
}
renderValue={(v) => (
<StableSelectValue
value={typeof v === "string" && Object.hasOwn(statusItems, v) ? v : "all"}
options={Object.keys(statusItems)}
renderLabel={(option) => renderStatusLabel(option as ContentStatusFilter)}
/>
)}
items={statusItems}
>
{Object.entries(statusItems).map(([value]) => (
<Select.Option key={value} value={value} className="emdash-compact-select-option text-xs">
<Select.Option key={value} value={value} className="emdash-compact-select-option">
{renderStatusLabel(value as ContentStatusFilter)}
</Select.Option>
))}
</Select>

{showAuthorFilter && (
<Select
size="sm"
aria-label={t`Filter by author`}
value={authorFilter}
onValueChange={(v) => onAuthorFilterChange?.(v ?? "")}
Expand All @@ -1114,21 +1115,23 @@ function FilterBar({
{showDateFilter && (
<>
<Select
size="sm"
className="emdash-date-field-filter-trigger min-w-28 ps-3.5"
aria-label={t`Date field to filter on`}
value={dateFilter.field}
onValueChange={(v) =>
onDateFilterChange?.({ ...dateFilter, field: (v as ContentDateField) ?? "createdAt" })
}
renderValue={(v) => (
<StableSelectValue
value={typeof v === "string" ? v : dateFilter.field}
options={Object.keys(dateFieldItems)}
renderLabel={(option) => dateFieldItems[option]}
/>
)}
items={dateFieldItems}
>
{Object.entries(dateFieldItems).map(([value, label]) => (
<Select.Option
key={value}
value={value}
className="emdash-compact-select-option text-xs"
>
<Select.Option key={value} value={value} className="emdash-compact-select-option">
{label}
</Select.Option>
))}
Expand All @@ -1145,6 +1148,30 @@ function FilterBar({
);
}

/** Lays every option's label in one cell, so the trigger keeps the width of its longest option. */
function StableSelectValue({
value,
options,
renderLabel,
}: {
value: string;
options: string[];
renderLabel: (option: string) => React.ReactNode;
}) {
return (
<span className="grid text-start">
{options.map((option) => (
<span
key={option}
className={cn("col-start-1 row-start-1", option !== value && "invisible")}
>
{renderLabel(option)}
</span>
))}
</span>
);
}

function DateRangeFilter({
value,
onChange,
Expand Down Expand Up @@ -1202,13 +1229,12 @@ function DateRangeFilter({
render={
<Button
variant="secondary"
size="sm"
icon={
<span
className="emdash-date-range-icon flex size-3 shrink-0 items-center justify-center"
className="emdash-date-range-icon flex size-4 shrink-0 items-center justify-center"
aria-hidden="true"
>
<Calendar className="size-3" />
<Calendar className="size-4" />
</span>
}
aria-label={t`Filter by date range: ${rangeLabel}`}
Expand Down
1 change: 0 additions & 1 deletion packages/admin/src/components/MediaLibrary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -898,7 +898,6 @@ export function MediaLibrary({
>
{(canSearch || activeProvider === "local") && (
<TableToolbarSearch
size="base"
placeholder={activeProvider === "local" ? t`Search by filename...` : t`Search...`}
aria-label={t`Search media`}
value={searchQuery}
Expand Down
1 change: 0 additions & 1 deletion packages/admin/src/components/MediaPickerModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1259,7 +1259,6 @@ export function MediaPickerModal({
>
{canSearch && (
<TableToolbarSearch
size="base"
placeholder={
activeSource === "local" ? t`Search by filename...` : t`Search...`
}
Expand Down
1 change: 0 additions & 1 deletion packages/admin/src/components/Redirects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,6 @@ export function Redirects() {
tab === "redirects" ? (
<>
<TableToolbarSearch
size="base"
placeholder={searchPlaceholder}
aria-label={searchPlaceholder}
value={search}
Expand Down
2 changes: 0 additions & 2 deletions packages/admin/src/components/Sections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,6 @@ export function Sections() {
tools={
<>
<TableToolbarSearch
size="base"
className="sm:w-72"
placeholder={searchPlaceholder}
aria-label={searchPlaceholder}
value={searchQuery}
Expand Down
4 changes: 1 addition & 3 deletions packages/admin/src/components/TableToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ interface TableToolbarSearchProps {
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
placeholder: string;
"aria-label": string;
size?: React.ComponentProps<typeof InputGroup>["size"];
maxLength?: number;
className?: string;
inputRef?: React.Ref<HTMLInputElement>;
Expand All @@ -40,13 +39,12 @@ export function TableToolbarSearch({
onChange,
placeholder,
"aria-label": ariaLabel,
size = "sm",
maxLength,
className,
inputRef,
}: TableToolbarSearchProps) {
return (
<InputGroup size={size} className={cn("w-full min-w-0 sm:w-64 sm:flex-none", className)}>
<InputGroup className={cn("w-full min-w-0 sm:w-64 sm:flex-none", className)}>
<InputGroup.Addon>
<MagnifyingGlass className="h-4 w-4" aria-hidden="true" />
</InputGroup.Addon>
Expand Down
1 change: 0 additions & 1 deletion packages/admin/src/components/TaxonomyManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1344,7 +1344,6 @@ export function TaxonomyManager({ taxonomyName, onDeleted }: TaxonomyManagerProp
tools={
taxonomyName === "tag" ? (
<TableToolbarSearch
size="base"
inputRef={tagSearchRef}
placeholder={t`Search tags…`}
aria-label={t`Search tags`}
Expand Down
1 change: 0 additions & 1 deletion packages/admin/src/components/comments/CommentInbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,6 @@ export function CommentInbox({
tools={
<>
<TableToolbarSearch
size="base"
placeholder={searchPlaceholder}
aria-label={t`Search comments`}
value={searchQuery}
Expand Down
2 changes: 1 addition & 1 deletion packages/admin/src/components/users/UserList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export function UserList({
aria-label={t`Search users`}
/>
<Select
size="sm"
className="w-full sm:w-auto"
value={roleFilter?.toString() ?? "all"}
onValueChange={(value) =>
onRoleFilterChange(
Expand Down
4 changes: 1 addition & 3 deletions packages/admin/src/routes/bylines.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,14 +311,12 @@ export function BylinesPage() {
tools={
<>
<TableToolbarSearch
size="base"
placeholder={t`Search bylines`}
aria-label={t`Search bylines`}
value={search}
onChange={(event) => setSearch(event.target.value)}
/>
<Select
size="base"
aria-label={t`Filter byline type`}
value={guestFilter}
onValueChange={(value) =>
Expand All @@ -329,7 +327,7 @@ export function BylinesPage() {
guest: t`Guest only`,
linked: t`Non-guest`,
}}
className="w-full sm:w-44"
className="w-full sm:w-auto"
/>
</>
}
Expand Down
32 changes: 30 additions & 2 deletions packages/admin/tests/components/ContentList.layout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { page } from "vitest/browser";

import "../../dist/styles.css";
import { ContentList } from "../../src/components/ContentList.js";
import {
ContentList,
type ContentDateFilter,
type ContentStatusFilter,
} from "../../src/components/ContentList.js";
import type { ContentItem } from "../../src/lib/api";
import { render } from "../utils/render.js";

Expand Down Expand Up @@ -145,7 +149,10 @@ describe("Content list pagination layout", () => {
});

describe("Content list filter row", () => {
function listInShell(statusFilter: "all" | "draft") {
function listInShell(
statusFilter: ContentStatusFilter,
dateField: ContentDateFilter["field"] = "createdAt",
) {
return (
<div style={{ display: "flex" }}>
<aside style={{ width: 260, flexShrink: 0 }} />
Expand All @@ -156,6 +163,7 @@ describe("Content list filter row", () => {
items={posts.slice(0, 3)}
statusFilter={statusFilter}
onStatusFilterChange={vi.fn()}
dateFilter={{ field: dateField, from: "", to: "" }}
onDateFilterChange={vi.fn()}
/>
</main>
Expand All @@ -179,6 +187,26 @@ describe("Content list filter row", () => {
expect(moved).toEqual([]);
});

it("keeps the status and date field filters one width whichever value is chosen", async () => {
const screen = await render(listInShell("all"));
const width = (name: string) =>
screen.getByRole("combobox", { name }).element().getBoundingClientRect().width;

const statusWidths = new Set<number>();
for (const status of ["all", "published", "draft", "scheduled", "archived"] as const) {
await screen.rerender(listInShell(status));
statusWidths.add(width("Filter by status"));
}
const dateFieldWidths = new Set<number>();
for (const field of ["createdAt", "updatedAt", "publishedAt"] as const) {
await screen.rerender(listInShell("all", field));
dateFieldWidths.add(width("Date field to filter on"));
}

expect([...statusWidths]).toHaveLength(1);
expect([...dateFieldWidths]).toHaveLength(1);
});

it("hides Clear filters from assistive technology until a filter is active", async () => {
const screen = await render(listInShell("all"));
await expect.element(screen.getByRole("combobox", { name: "Filter by status" })).toBeVisible();
Expand Down
Loading