From 029a71848e237dac3b03e26d641d532bf9696c0d Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Sun, 25 Oct 2026 16:51:27 -0400 Subject: [PATCH] refactor(renderer): model chevron icon choice as a direction Replace the un-narrowable FC chevron icon field with a closed direction union and resolve icons through one exhaustive registry. --- .../notifications/AccountHeader.tsx | 7 ++++--- .../notifications/RepositoryHeader.tsx | 7 ++++--- src/renderer/types.ts | 10 ++++++--- src/renderer/utils/ui/display.test.ts | 8 +++---- src/renderer/utils/ui/display.ts | 12 +++++------ src/renderer/utils/ui/icons.test.ts | 10 +++++++++ src/renderer/utils/ui/icons.ts | 21 ++++++++++++++++++- 7 files changed, 53 insertions(+), 22 deletions(-) diff --git a/src/renderer/components/notifications/AccountHeader.tsx b/src/renderer/components/notifications/AccountHeader.tsx index 82a960d7d..7c97b43ba 100644 --- a/src/renderer/components/notifications/AccountHeader.tsx +++ b/src/renderer/components/notifications/AccountHeader.tsx @@ -13,6 +13,7 @@ import { type Account, type GitifyError, Size } from '../../types'; import { getAdapter } from '../../utils/forges/registry'; import { openAccountProfile, openHostIssues, openHostPulls } from '../../utils/system/links'; import { getChevronDetails } from '../../utils/ui/display'; +import { getChevronIcon } from '../../utils/ui/icons'; import { AvatarWithFallback } from '../avatars/AvatarWithFallback'; export interface AccountHeaderProps { @@ -30,7 +31,7 @@ export function AccountHeader({ isCollapsed, onToggle, }: Readonly) { - const Chevron = getChevronDetails(notificationCount > 0, !isCollapsed, 'account'); + const chevron = getChevronDetails(notificationCount > 0, !isCollapsed, 'account'); return ( diff --git a/src/renderer/components/notifications/RepositoryHeader.tsx b/src/renderer/components/notifications/RepositoryHeader.tsx index cfd150251..960b0f606 100644 --- a/src/renderer/components/notifications/RepositoryHeader.tsx +++ b/src/renderer/components/notifications/RepositoryHeader.tsx @@ -16,6 +16,7 @@ import { isMarkAsDoneFeatureSupported } from '../../utils/api/features'; import { shouldRemoveNotificationsFromState } from '../../utils/notifications/remove'; import { openRepository } from '../../utils/system/links'; import { getChevronDetails } from '../../utils/ui/display'; +import { getChevronIcon } from '../../utils/ui/icons'; import { AvatarWithFallback } from '../avatars/AvatarWithFallback'; export interface RepositoryHeaderProps { @@ -52,7 +53,7 @@ export function RepositoryHeader({ (notification) => !notification.unread, ); - const Chevron = getChevronDetails(true, !isCollapsed, 'repository'); + const chevron = getChevronDetails(true, !isCollapsed, 'repository'); return ( diff --git a/src/renderer/types.ts b/src/renderer/types.ts index 98234e682..e1559ab9b 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -283,15 +283,19 @@ export enum Size { XLARGE = 20, } +/** + * The direction a chevron points. + */ +export type ChevronDirection = 'down' | 'left' | 'right'; + /** * Details for Chevron header accordion. */ export interface Chevron { /** - * The chevron icon. + * The direction the chevron points. */ - // TODO - improve the type here to be more specific about which icons are allowed, if possible - icon: FC; + direction: ChevronDirection; /** * The chevron label. diff --git a/src/renderer/utils/ui/display.test.ts b/src/renderer/utils/ui/display.test.ts index d49dd4195..69a20d20a 100644 --- a/src/renderer/utils/ui/display.test.ts +++ b/src/renderer/utils/ui/display.test.ts @@ -1,22 +1,20 @@ -import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from '@primer/octicons-react'; - import { getChevronDetails, parseInlineCode } from './display'; describe('renderer/utils/ui/display.ts', () => { describe('getChevronDetails', () => { it('returns correct chevron details', () => { expect(getChevronDetails(true, true, 'account')).toEqual({ - icon: ChevronDownIcon, + direction: 'down', label: 'Hide account notifications', }); expect(getChevronDetails(true, false, 'account')).toEqual({ - icon: ChevronRightIcon, + direction: 'right', label: 'Show account notifications', }); expect(getChevronDetails(false, false, 'account')).toEqual({ - icon: ChevronLeftIcon, + direction: 'left', label: 'No notifications for account', }); }); diff --git a/src/renderer/utils/ui/display.ts b/src/renderer/utils/ui/display.ts index 020aeaa70..af843c4be 100644 --- a/src/renderer/utils/ui/display.ts +++ b/src/renderer/utils/ui/display.ts @@ -1,5 +1,3 @@ -import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from '@primer/octicons-react'; - import type { Chevron } from '../../types'; export interface ParsedCodePart { @@ -9,12 +7,12 @@ export interface ParsedCodePart { } /** - * Returns the appropriate chevron icon and accessible label for a collapsible section. + * Returns the appropriate chevron direction and accessible label for a collapsible section. * * @param hasNotifications - Whether the section has any notifications to display. * @param isVisible - Whether the section is currently expanded/visible. * @param type - The type of section (`'account'` or `'repository'`). - * @returns A `Chevron` object containing the icon component and its accessible label. + * @returns A `Chevron` object containing the direction and its accessible label. */ export function getChevronDetails( hasNotifications: boolean, @@ -23,20 +21,20 @@ export function getChevronDetails( ): Chevron { if (!hasNotifications) { return { - icon: ChevronLeftIcon, + direction: 'left', label: `No notifications for ${type}`, }; } if (isVisible) { return { - icon: ChevronDownIcon, + direction: 'down', label: `Hide ${type} notifications`, }; } return { - icon: ChevronRightIcon, + direction: 'right', label: `Show ${type} notifications`, }; } diff --git a/src/renderer/utils/ui/icons.test.ts b/src/renderer/utils/ui/icons.test.ts index ee9ab4c45..7a7b6b53a 100644 --- a/src/renderer/utils/ui/icons.test.ts +++ b/src/renderer/utils/ui/icons.test.ts @@ -1,5 +1,8 @@ import { CheckIcon, + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, CommentIcon, FeedPersonIcon, FileDiffIcon, @@ -10,6 +13,7 @@ import { import { IconColor } from '../../types'; import { + getChevronIcon, getDefaultUserIcon, getPlatformIcon, getPullRequestReviewIcon, @@ -125,4 +129,10 @@ describe('renderer/utils/icons.ts', () => { expect(getDefaultUserIcon('Organization')).toBe(OrganizationIcon); expect(getDefaultUserIcon('User')).toBe(FeedPersonIcon); }); + + it('getChevronIcon', () => { + expect(getChevronIcon('down')).toBe(ChevronDownIcon); + expect(getChevronIcon('left')).toBe(ChevronLeftIcon); + expect(getChevronIcon('right')).toBe(ChevronRightIcon); + }); }); diff --git a/src/renderer/utils/ui/icons.ts b/src/renderer/utils/ui/icons.ts index be4bbba60..4d14ca29e 100644 --- a/src/renderer/utils/ui/icons.ts +++ b/src/renderer/utils/ui/icons.ts @@ -2,9 +2,13 @@ import type { FC } from 'react'; import { CheckIcon, + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, CommentIcon, FeedPersonIcon, FileDiffIcon, + type Icon, MarkGithubIcon, type OcticonProps, OrganizationIcon, @@ -12,7 +16,12 @@ import { ShieldCheckIcon, } from '@primer/octicons-react'; -import { IconColor, type PullRequestApprovalIcon, type UserType } from '../../types'; +import { + type ChevronDirection, + IconColor, + type PullRequestApprovalIcon, + type UserType, +} from '../../types'; import type { PlatformType } from '../auth/types'; import { BitbucketIcon } from '../../icons/BitbucketIcon'; @@ -90,3 +99,13 @@ export function getDefaultUserIcon(userType: UserType) { return FeedPersonIcon; } } + +const CHEVRON_ICONS: Record = { + down: ChevronDownIcon, + left: ChevronLeftIcon, + right: ChevronRightIcon, +}; + +export function getChevronIcon(direction: ChevronDirection): Icon { + return CHEVRON_ICONS[direction]; +}