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+ moreMenuActions ?: JSX . Element ;
42+ }
43+
44+ function containsSelectedSidebarItem ( children : ReactNode ) : boolean {
1345 let selectedItemFound = false ;
1446
1547 Children . forEach ( children , ( child ) => {
1648 if ( selectedItemFound ) {
17- return true ;
49+ return ;
1850 }
1951
20- if ( child ?. props ?. selected ) {
21- selectedItemFound = true ;
22- } else {
23- const descendants = child ?. props ?. children ;
52+ const props = isValidElement < SidebarChildProps > ( child ) ? child . props : undefined ;
2453
25- if ( descendants ) {
26- selectedItemFound = containsSelectedSidebarItem ( descendants ) ;
27- }
54+ if ( props ?. selected ) {
55+ selectedItemFound = true ;
56+ } else if ( props ?. children ) {
57+ selectedItemFound = containsSelectedSidebarItem ( props . children ) ;
2858 }
2959 } ) ;
3060
3161 return selectedItemFound ;
3262}
3363
34- const offScreen = ( submenu ) => {
64+ const offScreen = ( submenu : HTMLElement ) => {
3565 const rect = submenu . getBoundingClientRect ( ) ;
3666 return rect . y + rect . height > window . innerHeight ;
3767} ;
3868
39- export const ExpandableSidebarMenu = ( menuProps ) => {
69+ export const ExpandableSidebarMenu = ( menuProps : ExpandableSidebarMenuProps ) => {
4070 const {
4171 className,
4272 title,
@@ -49,24 +79,22 @@ export const ExpandableSidebarMenu = ( menuProps ) => {
4979 customIcon,
5080 children,
5181 disableFlyout,
82+ expanded : expandedProp ,
5283 prependContent,
5384 appendContent,
5485 moreMenuActions,
5586 ...props
5687 } = menuProps ;
57- let { expanded } = props ;
58- const menu = createRef ( ) ; // Needed for HoverIntent.
59- const submenu = useRef ( ) ;
88+ // A `null` `expanded` prop means "auto-detect" from whether a child is selected.
89+ const expanded = null === expandedProp ? containsSelectedSidebarItem ( children ) : expandedProp ;
90+ const menu = createRef < HTMLUListElement > ( ) ; // Needed for HoverIntent.
91+ const submenu = useRef < HTMLLIElement > ( null ) ;
6092 const [ submenuHovered , setSubmenuHovered ] = useState ( false ) ;
6193
6294 if ( submenu . current ) {
6395 // Sets flyout to expand towards bottom.
6496 submenu . current . style . bottom = 'auto' ;
65- submenu . current . style . top = 0 ;
66- }
67-
68- if ( null === expanded ) {
69- expanded = containsSelectedSidebarItem ( children ) ;
97+ submenu . current . style . top = '0' ;
7098 }
7199
72100 const classes = clsx ( className , {
@@ -95,9 +123,9 @@ export const ExpandableSidebarMenu = ( menuProps ) => {
95123 const menuId = useMemo ( ( ) => 'menu' + crypto . randomUUID ( ) , [ ] ) ;
96124
97125 useLayoutEffect ( ( ) => {
98- if ( submenuHovered && offScreen ( submenu . current ) ) {
126+ if ( submenuHovered && submenu . current && offScreen ( submenu . current ) ) {
99127 // Sets flyout to expand towards top.
100- submenu . current . style . bottom = 0 ;
128+ submenu . current . style . bottom = '0' ;
101129 submenu . current . style . top = 'auto' ;
102130 }
103131 } , [ submenuHovered ] ) ;
@@ -148,23 +176,4 @@ export const ExpandableSidebarMenu = ( menuProps ) => {
148176 ) ;
149177} ;
150178
151- ExpandableSidebarMenu . propTypes = {
152- className : PropTypes . string ,
153- title : PropTypes . oneOfType ( [ TranslatableString , PropTypes . element ] ) . isRequired ,
154- count : PropTypes . number ,
155- compactCount : PropTypes . bool ,
156- onClick : PropTypes . func ,
157- customIcon : PropTypes . node ,
158- icon : PropTypes . string ,
159- materialIcon : PropTypes . string ,
160- materialIconStyle : PropTypes . string ,
161- expanded : PropTypes . bool ,
162- disableFlyout : PropTypes . bool ,
163- expandableIconClick : PropTypes . func ,
164- prependContent : PropTypes . node ,
165- appendContent : PropTypes . node ,
166- moreMenuActions : PropTypes . node ,
167- children : PropTypes . node ,
168- } ;
169-
170179export default ExpandableSidebarMenu ;
0 commit comments