Skip to content

Commit ae59ae4

Browse files
authored
Reader: Add unread count on lists (#112469)
- Add unread count to Lists dropdown available on sidebar. - Migrate related files to TypeScript
1 parent ff7d3a0 commit ae59ae4

22 files changed

Lines changed: 559 additions & 360 deletions

File tree

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,38 @@
11
import { Count, Gridicon, MaterialIcon } from '@automattic/components';
22
import { Button } from '@wordpress/components';
33
import { Icon, chevronDown } from '@wordpress/icons';
4-
import { useTranslate } from 'i18n-calypso';
5-
import PropTypes from 'prop-types';
6-
import TranslatableString from 'calypso/components/translatable/proptype';
4+
import { useTranslate, type TranslateResult } from 'i18n-calypso';
75
import SidebarHeading from 'calypso/layout/sidebar/heading';
86
import { decodeEntities } from 'calypso/lib/formatting';
7+
import type { KeyboardEvent, MouseEvent, ReactNode } from 'react';
8+
9+
interface ExpandableSidebarHeadingProps {
10+
title: TranslateResult;
11+
count?: number;
12+
countLabel?: string;
13+
compactCount?: boolean;
14+
onClick?: ( event?: MouseEvent< HTMLAnchorElement > ) => void;
15+
customIcon?: ReactNode;
16+
icon?: string | null;
17+
materialIcon?: string | null;
18+
materialIconStyle?: string | null;
19+
expanded?: boolean;
20+
menuId?: string;
21+
hideExpandableIcon?: boolean;
22+
inlineText?: ReactNode;
23+
expandableIconClick?: () => void;
24+
prependContent?: ReactNode;
25+
appendContent?: ReactNode;
26+
navigationLabel?: string;
27+
url?: string;
28+
moreMenuActions?: JSX.Element;
29+
}
930

1031
const ExpandableSidebarHeading = ( {
1132
title,
1233
count,
34+
countLabel,
35+
compactCount,
1336
icon,
1437
customIcon,
1538
materialIcon,
@@ -23,8 +46,10 @@ const ExpandableSidebarHeading = ( {
2346
appendContent,
2447
moreMenuActions,
2548
...props
26-
} ) => {
49+
}: ExpandableSidebarHeadingProps ) => {
2750
const translate = useTranslate();
51+
const renderedTitle = typeof title === 'string' ? decodeEntities( title ) : title;
52+
2853
return (
2954
<SidebarHeading
3055
aria-controls={ menuId }
@@ -37,15 +62,17 @@ const ExpandableSidebarHeading = ( {
3762
<MaterialIcon
3863
className="sidebar__menu-icon"
3964
icon={ materialIcon }
40-
style={ materialIconStyle }
65+
style={ materialIconStyle ?? undefined }
4166
/>
4267
) }
4368
{ undefined !== customIcon && customIcon }
4469
<span className="sidebar__expandable-title">
45-
{ decodeEntities( title ) }
70+
{ renderedTitle }
4671
<span className="sidebar__actions-and-count">
4772
{ moreMenuActions }
48-
{ count > 0 && <Count count={ count } /> }
73+
{ count && count > 0 ? (
74+
<Count count={ count } compact={ compactCount } aria-label={ countLabel } />
75+
) : null }
4976
</span>
5077
{ inlineText && <span className="sidebar__inline-text">{ inlineText }</span> }
5178
</span>
@@ -55,11 +82,11 @@ const ExpandableSidebarHeading = ( {
5582
<Button
5683
variant="link"
5784
className="sidebar__expandable-button"
58-
onClick={ ( ev ) => {
85+
onClick={ ( ev: MouseEvent< HTMLButtonElement > ) => {
5986
ev.stopPropagation();
6087
expandableIconClick();
6188
} }
62-
onKeyDown={ ( ev ) => {
89+
onKeyDown={ ( ev: KeyboardEvent< HTMLButtonElement > ) => {
6390
// Prevent bubbling or the SidebarHeading's onClick will also trigger.
6491
if ( ev.key === 'Enter' ) {
6592
ev.stopPropagation();
@@ -75,19 +102,4 @@ const ExpandableSidebarHeading = ( {
75102
);
76103
};
77104

78-
ExpandableSidebarHeading.propTypes = {
79-
title: PropTypes.oneOfType( [ TranslatableString, PropTypes.element ] ).isRequired,
80-
count: PropTypes.number,
81-
onClick: PropTypes.func,
82-
customIcon: PropTypes.node,
83-
icon: PropTypes.string,
84-
materialIcon: PropTypes.string,
85-
materialIconStyle: PropTypes.string,
86-
hideExpandableIcon: PropTypes.bool,
87-
expandableIconClick: PropTypes.func,
88-
prependContent: PropTypes.node,
89-
appendContent: PropTypes.node,
90-
moreMenuActions: PropTypes.node,
91-
};
92-
93105
export default ExpandableSidebarHeading;
Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,102 @@
11
import clsx from 'clsx';
2-
import PropTypes from 'prop-types';
3-
import { Children, createRef, useMemo, useState, useRef, useLayoutEffect } from 'react';
4-
import TranslatableString from 'calypso/components/translatable/proptype';
2+
import {
3+
Children,
4+
createRef,
5+
isValidElement,
6+
useMemo,
7+
useState,
8+
useRef,
9+
useLayoutEffect,
10+
} from 'react';
511
import SidebarMenu from 'calypso/layout/sidebar/menu';
612
import HoverIntent from 'calypso/lib/hover-intent';
713
import { hasTouch } from 'calypso/lib/touch-detect';
814
import ExpandableSidebarHeading from './expandable-heading';
15+
import type { TranslateResult } from 'i18n-calypso';
16+
import type { MouseEvent, ReactNode } from 'react';
917

1018
const isTouch = hasTouch();
1119

12-
function containsSelectedSidebarItem( children ) {
20+
interface SidebarChildProps {
21+
selected?: boolean;
22+
children?: ReactNode;
23+
}
24+
25+
interface ExpandableSidebarMenuProps {
26+
className?: string;
27+
title: TranslateResult;
28+
count?: number;
29+
countLabel?: string;
30+
compactCount?: boolean;
31+
onClick?: ( event?: MouseEvent< HTMLAnchorElement > ) => void;
32+
icon?: string | null;
33+
materialIcon?: string | null;
34+
materialIconStyle?: string | null;
35+
customIcon?: ReactNode;
36+
children?: ReactNode;
37+
disableFlyout?: boolean;
38+
expanded?: boolean | null;
39+
expandableIconClick?: () => void;
40+
prependContent?: ReactNode;
41+
appendContent?: ReactNode;
42+
moreMenuActions?: JSX.Element;
43+
}
44+
45+
function containsSelectedSidebarItem( children: ReactNode ): boolean {
1346
let selectedItemFound = false;
1447

1548
Children.forEach( children, ( child ) => {
1649
if ( selectedItemFound ) {
17-
return true;
50+
return;
1851
}
1952

20-
if ( child?.props?.selected ) {
21-
selectedItemFound = true;
22-
} else {
23-
const descendants = child?.props?.children;
53+
const props = isValidElement< SidebarChildProps >( child ) ? child.props : undefined;
2454

25-
if ( descendants ) {
26-
selectedItemFound = containsSelectedSidebarItem( descendants );
27-
}
55+
if ( props?.selected ) {
56+
selectedItemFound = true;
57+
} else if ( props?.children ) {
58+
selectedItemFound = containsSelectedSidebarItem( props.children );
2859
}
2960
} );
3061

3162
return selectedItemFound;
3263
}
3364

34-
const offScreen = ( submenu ) => {
65+
const offScreen = ( submenu: HTMLElement ) => {
3566
const rect = submenu.getBoundingClientRect();
3667
return rect.y + rect.height > window.innerHeight;
3768
};
3869

39-
export const ExpandableSidebarMenu = ( menuProps ) => {
70+
export const ExpandableSidebarMenu = ( menuProps: ExpandableSidebarMenuProps ) => {
4071
const {
4172
className,
4273
title,
4374
count,
75+
countLabel,
76+
compactCount,
4477
onClick,
4578
icon,
4679
materialIcon,
4780
materialIconStyle,
4881
customIcon,
4982
children,
5083
disableFlyout,
84+
expanded: expandedProp,
5185
prependContent,
5286
appendContent,
5387
moreMenuActions,
5488
...props
5589
} = menuProps;
56-
let { expanded } = props;
57-
const menu = createRef(); // Needed for HoverIntent.
58-
const submenu = useRef();
90+
// A `null` `expanded` prop means "auto-detect" from whether a child is selected.
91+
const expanded = null === expandedProp ? containsSelectedSidebarItem( children ) : expandedProp;
92+
const menu = createRef< HTMLUListElement >(); // Needed for HoverIntent.
93+
const submenu = useRef< HTMLLIElement >( null );
5994
const [ submenuHovered, setSubmenuHovered ] = useState( false );
6095

6196
if ( submenu.current ) {
6297
// Sets flyout to expand towards bottom.
6398
submenu.current.style.bottom = 'auto';
64-
submenu.current.style.top = 0;
65-
}
66-
67-
if ( null === expanded ) {
68-
expanded = containsSelectedSidebarItem( children );
99+
submenu.current.style.top = '0';
69100
}
70101

71102
const classes = clsx( className, {
@@ -94,9 +125,9 @@ export const ExpandableSidebarMenu = ( menuProps ) => {
94125
const menuId = useMemo( () => 'menu' + crypto.randomUUID(), [] );
95126

96127
useLayoutEffect( () => {
97-
if ( submenuHovered && offScreen( submenu.current ) ) {
128+
if ( submenuHovered && submenu.current && offScreen( submenu.current ) ) {
98129
// Sets flyout to expand towards top.
99-
submenu.current.style.bottom = 0;
130+
submenu.current.style.bottom = '0';
100131
submenu.current.style.top = 'auto';
101132
}
102133
}, [ submenuHovered ] );
@@ -113,6 +144,8 @@ export const ExpandableSidebarMenu = ( menuProps ) => {
113144
<ExpandableSidebarHeading
114145
title={ title }
115146
count={ count }
147+
countLabel={ countLabel }
148+
compactCount={ compactCount }
116149
onClick={
117150
typeof onClick === 'function'
118151
? ( event ) => {
@@ -146,22 +179,4 @@ export const ExpandableSidebarMenu = ( menuProps ) => {
146179
);
147180
};
148181

149-
ExpandableSidebarMenu.propTypes = {
150-
className: PropTypes.string,
151-
title: PropTypes.oneOfType( [ TranslatableString, PropTypes.element ] ).isRequired,
152-
count: PropTypes.number,
153-
onClick: PropTypes.func,
154-
customIcon: PropTypes.node,
155-
icon: PropTypes.string,
156-
materialIcon: PropTypes.string,
157-
materialIconStyle: PropTypes.string,
158-
expanded: PropTypes.bool,
159-
disableFlyout: PropTypes.bool,
160-
expandableIconClick: PropTypes.func,
161-
prependContent: PropTypes.node,
162-
appendContent: PropTypes.node,
163-
moreMenuActions: PropTypes.node,
164-
children: PropTypes.node,
165-
};
166-
167182
export default ExpandableSidebarMenu;
Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1-
const SidebarHeading = ( { children, onClick, ...props } ) => {
1+
import type { KeyboardEvent, KeyboardEventHandler, MouseEvent } from 'react';
2+
3+
interface SidebarHeadingProps {
4+
children?: React.ReactNode;
5+
navigationLabel?: string;
6+
url?: string;
7+
onClick?: ( event?: MouseEvent< HTMLAnchorElement > ) => void;
8+
}
9+
10+
const SidebarHeading = ( { children, onClick, ...props }: SidebarHeadingProps ) => {
211
const tabIndex = onClick ? 0 : -1;
312

4-
let onKeyDown = null;
13+
let onKeyDown: KeyboardEventHandler< HTMLAnchorElement > | undefined;
514

615
if ( onClick ) {
7-
onKeyDown = ( event ) => {
16+
onKeyDown = ( event: KeyboardEvent< HTMLAnchorElement > ) => {
817
// Trigger click for enter, similarly to default brower behavior for <a> or <button>
918
if ( 13 === event.keyCode ) {
1019
event.preventDefault();

client/reader/list-manage/types.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ export type ReaderList = {
1919
is_owner?: boolean;
2020
is_public?: boolean;
2121
is_immutable?: boolean;
22+
feeds: {
23+
feed_id: number;
24+
unseen_count: number;
25+
}[];
2226
};
2327

2428
export type Item = {

client/reader/list/components/list-header/test/index.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const defaultList: ComponentProps< typeof ReaderListHeader >[ 'list' ] = {
1717
owner: 'test_user',
1818
is_owner: true,
1919
is_public: true,
20+
feeds: [],
2021
};
2122

2223
function renderReaderListHeader(

client/reader/list/views/test/sites.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const defaultList: ComponentProps< typeof ListSites >[ 'list' ] = {
3131
slug: 'my-list',
3232
owner: 'test_user',
3333
is_owner: true,
34+
feeds: [],
3435
};
3536

3637
let queryClient: QueryClient;

client/reader/sidebar/reader-sidebar-lists/index.jsx

Lines changed: 0 additions & 46 deletions
This file was deleted.

0 commit comments

Comments
 (0)