Skip to content
Open
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
237 changes: 237 additions & 0 deletions components/HistoryView/HistoryFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import FilterIcon from "@components/Icons/FilterIcon";
import {
Badge,
Box,
Button,
Flex,
Popover,
PopoverContent,
PopoverTrigger,
Text,
} from "@livepeer/design-system";

interface HistoryFilterProps {
selectedEventTypes: string[];
isOpen: boolean;
onOpenChange: (open: boolean) => void;
onToggleEventType: (eventType: string) => void;
onClearFilters: () => void;
allEventTypes: string[];
eventTypeLabels: Record<string, string>;
}

const HistoryFilter = ({
selectedEventTypes,
isOpen,
onOpenChange,
onToggleEventType,
onClearFilters,
allEventTypes,
eventTypeLabels,
}: HistoryFilterProps) => {
return (
<Popover open={isOpen} onOpenChange={onOpenChange}>
<PopoverTrigger asChild>
<Button
role="button"
css={{
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "$neutral4",
color: "$hiContrast",
minHeight: "44px",
width: "120px",
padding: "$2 $3",
"&:hover": {
backgroundColor: "$neutral5",
},
}}
>
<FilterIcon size={16} css={{ marginRight: "$1" }} />
<Text css={{ marginRight: "$2" }}>Filter</Text>
{selectedEventTypes.length > 0 && (
<Badge
css={{
backgroundColor: "$primary9",
color: "white",
borderRadius: "50%",
width: "20px",
height: "20px",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "$1",
fontWeight: 600,
padding: 0,
}}
>
{selectedEventTypes.length}
</Badge>
)}
</Button>
</PopoverTrigger>
<PopoverContent
data-history-filter-popover
css={{
width: "280px",
backgroundColor: "$neutral4",
borderRadius: "$3",
padding: 0,
boxShadow:
"0px 5px 14px rgba(0, 0, 0, 0.22), 0px 0px 2px rgba(0, 0, 0, 0.2)",
border: "1px solid $neutral6",
zIndex: 9,
display: "flex",
flexDirection: "column",
maxHeight: "400px",
marginRight: "$3",
overflow: "hidden",
}}
onPointerEnterCapture={undefined}
onPointerLeaveCapture={undefined}
placeholder={undefined}
>
{/* Header - Sticky */}
<Flex
css={{
padding: "$3",
borderBottom: "1px solid $neutral6",
borderTopLeftRadius: "$3",
borderTopRightRadius: "$3",
alignItems: "center",
justifyContent: "space-between",
flexShrink: 0,
backgroundColor: "$neutral4",
position: "sticky",
top: 0,
zIndex: 1,
}}
>
<Button
onClick={onClearFilters}
css={{
color: "$neutral11",
fontSize: "$2",
padding: "$1",
backgroundColor: "transparent",
"&:hover": {
color: "$hiContrast",
backgroundColor: "transparent",
},
}}
>
Clear
</Button>
<Text css={{ fontWeight: 600, fontSize: "$3" }}>Filters</Text>
<Button
onClick={() => onOpenChange(false)}
css={{
color: "$primary11",
fontSize: "$2",
padding: "$1",
backgroundColor: "transparent",
"&:hover": {
backgroundColor: "transparent",
},
}}
>
Done
</Button>
</Flex>

{/* Event type section - Scrollable */}
<Flex
data-history-filter-scrollable
css={{
flexDirection: "column",
overflowY: "auto",
flex: 1,
borderBottomLeftRadius: "$3",
borderBottomRightRadius: "$3",
}}
>
<Box css={{ padding: "$3" }}>
<Text
css={{
fontWeight: 500,
fontSize: "$2",
marginBottom: "$2",
color: "$hiContrast",
}}
>
Event Type
</Text>
<Flex css={{ flexDirection: "column", gap: "$2" }}>
{allEventTypes.map((eventType) => {
const isChecked = selectedEventTypes.includes(eventType);
return (
<Flex
key={eventType}
css={{
alignItems: "center",
cursor: "pointer",
padding: "$1",
borderRadius: "$1",
"&:hover": {
backgroundColor: "$neutral5",
},
}}
onClick={() => onToggleEventType(eventType)}
>
Comment on lines +169 to +181
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The filter options are implemented as clickable Flex rows with a custom checkbox UI, but they don’t expose checkbox semantics (no role="checkbox", aria-checked, keyboard toggle with Space/Enter, or focus styling). This makes the filter list hard/impossible to use with keyboard and assistive tech. Use a semantic checkbox/input (or add proper ARIA roles + keyboard handlers) for each option.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

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

Filter checkbox options lack semantic HTML and ARIA attributes, making them inaccessible to keyboard and screen reader users

Fix on Vercel

<Box
css={{
width: "18px",
height: "18px",
border: "2px solid",
borderColor: isChecked ? "$primary11" : "$neutral8",
backgroundColor: isChecked
? "$primary11"
: "transparent",
borderRadius: "$1",
marginRight: "$2",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.2s",
}}
>
{isChecked && (
<Box
as="svg"
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
>
<path
d="M2 6L4.5 8.5L10 3"
stroke="white"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</Box>
)}
</Box>
<Text
css={{
fontSize: "$2",
color: "$hiContrast",
userSelect: "none",
}}
>
{eventTypeLabels[eventType]}
</Text>
</Flex>
);
})}
</Flex>
</Box>
</Flex>
</PopoverContent>
</Popover>
);
};

export default HistoryFilter;
39 changes: 38 additions & 1 deletion components/HistoryView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import HistoryFilter from "@components/HistoryView/HistoryFilter";
import Spinner from "@components/Spinner";
import TransactionBadge from "@components/TransactionBadge";
import { Fm, parsePollIpfs } from "@lib/api/polls";
Expand All @@ -20,6 +21,7 @@ import {
useTransactionsQuery,
VoteEvent,
} from "apollo";
import { useHistoryFilter } from "hooks";
import { CHAIN_INFO, DEFAULT_CHAIN_ID } from "lib/chains";
import { useRouter } from "next/router";
import numbro from "numbro";
Expand Down Expand Up @@ -176,6 +178,18 @@ const Index = () => {
]
);

// Filter events using history hook
const {
filteredEvents,
selectedEventTypes,
toggleEventType,
clearFilters,
isFilterOpen,
setIsFilterOpen,
allEventTypes,
eventTypeLabels,
} = useHistoryFilter(mergedEvents);

if (error) {
console.error(error);
}
Expand Down Expand Up @@ -248,8 +262,31 @@ const Index = () => {
position: "relative",
}}
>
<Flex
css={{
justifyContent: "flex-end",
marginBottom: "$3",
alignItems: "center",
}}
>
<HistoryFilter
selectedEventTypes={selectedEventTypes}
isOpen={isFilterOpen}
onOpenChange={setIsFilterOpen}
onToggleEventType={toggleEventType}
onClearFilters={clearFilters}
allEventTypes={allEventTypes}
eventTypeLabels={eventTypeLabels}
/>
</Flex>
<Box css={{ paddingBottom: "$3" }}>
{mergedEvents.map((event, i: number) => renderSwitch(event, i))}
{filteredEvents.length > 0 ? (
filteredEvents.map((event, i: number) => renderSwitch(event, i))
) : (
<Box css={{ paddingTop: "$3", color: "$neutral11" }}>
No events match the selected filters
</Box>
)}
</Box>
{loading && data.transactions.length >= 10 && (
<Flex
Expand Down
28 changes: 28 additions & 0 deletions components/Icons/FilterIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Box } from "@livepeer/design-system";
import { CSS } from "@stitches/react";

interface FilterIconProps {
size?: number;
css?: CSS;
}

const FilterIcon = ({ size = 16, css }: FilterIconProps) => (
<Box
as="svg"
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
css={css}
aria-hidden="true"
>
<path
d="M2 4h12M4 8h8M6 12h4"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</Box>
);

export default FilterIcon;
Loading
Loading