11import 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' ;
511import SidebarMenu from 'calypso/layout/sidebar/menu' ;
612import HoverIntent from 'calypso/lib/hover-intent' ;
713import { hasTouch } from 'calypso/lib/touch-detect' ;
814import ExpandableSidebarHeading from './expandable-heading' ;
15+ import type { TranslateResult } from 'i18n-calypso' ;
16+ import type { MouseEvent , ReactNode } from 'react' ;
917
1018const 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-
167182export default ExpandableSidebarMenu ;
0 commit comments