Skip to content

Commit abcc6a1

Browse files
committed
💫 UPDATE: convert expandable, heading, expandable-heading components to TypeScript
1 parent 1541dbb commit abcc6a1

3 files changed

Lines changed: 93 additions & 69 deletions

File tree

client/layout/sidebar/expandable-heading.jsx renamed to client/layout/sidebar/expandable-heading.tsx

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
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+
compactCount?: boolean;
13+
onClick?: ( event?: MouseEvent< HTMLAnchorElement > ) => void;
14+
customIcon?: ReactNode;
15+
icon?: string | null;
16+
materialIcon?: string | null;
17+
materialIconStyle?: string | null;
18+
expanded?: boolean;
19+
menuId?: string;
20+
hideExpandableIcon?: boolean;
21+
inlineText?: ReactNode;
22+
expandableIconClick?: () => void;
23+
prependContent?: ReactNode;
24+
appendContent?: ReactNode;
25+
navigationLabel?: string;
26+
url?: string;
27+
}
928

1029
const ExpandableSidebarHeading = ( {
1130
title,
@@ -23,8 +42,10 @@ const ExpandableSidebarHeading = ( {
2342
prependContent,
2443
appendContent,
2544
...props
26-
} ) => {
45+
}: ExpandableSidebarHeadingProps ) => {
2746
const translate = useTranslate();
47+
const renderedTitle = typeof title === 'string' ? decodeEntities( title ) : title;
48+
2849
return (
2950
<SidebarHeading
3051
aria-controls={ menuId }
@@ -37,12 +58,12 @@ const ExpandableSidebarHeading = ( {
3758
<MaterialIcon
3859
className="sidebar__menu-icon"
3960
icon={ materialIcon }
40-
style={ materialIconStyle }
61+
style={ materialIconStyle ?? undefined }
4162
/>
4263
) }
4364
{ undefined !== customIcon && customIcon }
4465
<span className="sidebar__expandable-title">
45-
{ decodeEntities( title ) }
66+
{ renderedTitle }
4667
{ undefined !== count && <Count count={ count } compact={ compactCount } /> }
4768
{ inlineText && <span className="sidebar__inline-text">{ inlineText }</span> }
4869
</span>
@@ -52,11 +73,11 @@ const ExpandableSidebarHeading = ( {
5273
<Button
5374
variant="link"
5475
className="sidebar__expandable-button"
55-
onClick={ ( ev ) => {
76+
onClick={ ( ev: MouseEvent< HTMLButtonElement > ) => {
5677
ev.stopPropagation();
5778
expandableIconClick();
5879
} }
59-
onKeyDown={ ( ev ) => {
80+
onKeyDown={ ( ev: KeyboardEvent< HTMLButtonElement > ) => {
6081
// Prevent bubbling or the SidebarHeading's onClick will also trigger.
6182
if ( ev.key === 'Enter' ) {
6283
ev.stopPropagation();
@@ -72,19 +93,4 @@ const ExpandableSidebarHeading = ( {
7293
);
7394
};
7495

75-
ExpandableSidebarHeading.propTypes = {
76-
title: PropTypes.oneOfType( [ TranslatableString, PropTypes.element ] ).isRequired,
77-
count: PropTypes.number,
78-
compactCount: PropTypes.bool,
79-
onClick: PropTypes.func,
80-
customIcon: PropTypes.node,
81-
icon: PropTypes.string,
82-
materialIcon: PropTypes.string,
83-
materialIconStyle: PropTypes.string,
84-
hideExpandableIcon: PropTypes.bool,
85-
expandableIconClick: PropTypes.func,
86-
prependContent: PropTypes.node,
87-
appendContent: PropTypes.node,
88-
};
89-
9096
export default ExpandableSidebarHeading;
Lines changed: 52 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,71 @@
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+
compactCount?: boolean;
30+
onClick?: ( event?: MouseEvent< HTMLAnchorElement > ) => void;
31+
icon?: string | null;
32+
materialIcon?: string | null;
33+
materialIconStyle?: string | null;
34+
customIcon?: ReactNode;
35+
children?: ReactNode;
36+
disableFlyout?: boolean;
37+
expanded?: boolean | null;
38+
expandableIconClick?: () => void;
39+
prependContent?: ReactNode;
40+
appendContent?: ReactNode;
41+
}
42+
43+
function containsSelectedSidebarItem( children: ReactNode ): boolean {
1344
let selectedItemFound = false;
1445

1546
Children.forEach( children, ( child ) => {
1647
if ( selectedItemFound ) {
17-
return true;
48+
return;
1849
}
1950

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

25-
if ( descendants ) {
26-
selectedItemFound = containsSelectedSidebarItem( descendants );
27-
}
53+
if ( props?.selected ) {
54+
selectedItemFound = true;
55+
} else if ( props?.children ) {
56+
selectedItemFound = containsSelectedSidebarItem( props.children );
2857
}
2958
} );
3059

3160
return selectedItemFound;
3261
}
3362

34-
const offScreen = ( submenu ) => {
63+
const offScreen = ( submenu: HTMLElement ) => {
3564
const rect = submenu.getBoundingClientRect();
3665
return rect.y + rect.height > window.innerHeight;
3766
};
3867

39-
export const ExpandableSidebarMenu = ( menuProps ) => {
68+
export const ExpandableSidebarMenu = ( menuProps: ExpandableSidebarMenuProps ) => {
4069
const {
4170
className,
4271
title,
@@ -49,23 +78,21 @@ export const ExpandableSidebarMenu = ( menuProps ) => {
4978
customIcon,
5079
children,
5180
disableFlyout,
81+
expanded: expandedProp,
5282
prependContent,
5383
appendContent,
5484
...props
5585
} = menuProps;
56-
let { expanded } = props;
57-
const menu = createRef(); // Needed for HoverIntent.
58-
const submenu = useRef();
86+
// A `null` `expanded` prop means "auto-detect" from whether a child is selected.
87+
const expanded = null === expandedProp ? containsSelectedSidebarItem( children ) : expandedProp;
88+
const menu = createRef< HTMLUListElement >(); // Needed for HoverIntent.
89+
const submenu = useRef< HTMLLIElement >( null );
5990
const [ submenuHovered, setSubmenuHovered ] = useState( false );
6091

6192
if ( submenu.current ) {
6293
// Sets flyout to expand towards bottom.
6394
submenu.current.style.bottom = 'auto';
64-
submenu.current.style.top = 0;
65-
}
66-
67-
if ( null === expanded ) {
68-
expanded = containsSelectedSidebarItem( children );
95+
submenu.current.style.top = '0';
6996
}
7097

7198
const classes = clsx( className, {
@@ -94,9 +121,9 @@ export const ExpandableSidebarMenu = ( menuProps ) => {
94121
const menuId = useMemo( () => 'menu' + crypto.randomUUID(), [] );
95122

96123
useLayoutEffect( () => {
97-
if ( submenuHovered && offScreen( submenu.current ) ) {
124+
if ( submenuHovered && submenu.current && offScreen( submenu.current ) ) {
98125
// Sets flyout to expand towards top.
99-
submenu.current.style.bottom = 0;
126+
submenu.current.style.bottom = '0';
100127
submenu.current.style.top = 'auto';
101128
}
102129
}, [ submenuHovered ] );
@@ -116,7 +143,7 @@ export const ExpandableSidebarMenu = ( menuProps ) => {
116143
compactCount={ compactCount }
117144
onClick={ ( event ) => {
118145
setSubmenuHovered( false );
119-
onClick( event );
146+
onClick?.( event );
120147
} }
121148
customIcon={ customIcon }
122149
icon={ icon }
@@ -142,22 +169,4 @@ export const ExpandableSidebarMenu = ( menuProps ) => {
142169
);
143170
};
144171

145-
ExpandableSidebarMenu.propTypes = {
146-
className: PropTypes.string,
147-
title: PropTypes.oneOfType( [ TranslatableString, PropTypes.element ] ).isRequired,
148-
count: PropTypes.number,
149-
compactCount: PropTypes.bool,
150-
onClick: PropTypes.func,
151-
customIcon: PropTypes.node,
152-
icon: PropTypes.string,
153-
materialIcon: PropTypes.string,
154-
materialIconStyle: PropTypes.string,
155-
expanded: PropTypes.bool,
156-
disableFlyout: PropTypes.bool,
157-
expandableIconClick: PropTypes.func,
158-
prependContent: PropTypes.node,
159-
appendContent: PropTypes.node,
160-
children: PropTypes.node,
161-
};
162-
163172
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();

0 commit comments

Comments
 (0)