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+ 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-
163172export default ExpandableSidebarMenu ;
0 commit comments