11// Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
22// SPDX-License-Identifier: Apache-2.0
3-
4- import { Fragment , useEffect , useRef , useState } from "react" ;
3+ import { useEffect , useRef , useState } from "react" ;
54
65import { AgColumn } from "ag-grid-community" ;
76
@@ -19,41 +18,50 @@ export interface ColumnHeaderProps {
1918}
2019
2120interface MenuItem {
22- name : string ;
2321 checked ?: boolean ;
24- onPress ?: ( ) => void ;
2522 children ?: ( MenuItem | string ) [ ] ;
2623 disabled ?: boolean ;
24+ name : string ;
25+ onPress ?: ( ) => void ;
2726}
2827
2928const GridMenu = ( {
29+ left : incomingLeft ,
3030 menuItems,
31+ parentDimensions,
32+ subMenu,
3133 theme,
3234 top,
33- left : incomingLeft ,
34- subMenu,
35- parentDimensions,
3635} : {
36+ left ?: number ;
3737 menuItems : ( MenuItem | string ) [ ] ;
38+ parentDimensions ?: { left : number ; width : number } ;
39+ subMenu ?: boolean ;
3840 theme : string ;
3941 top : number ;
40- left ?: number ;
41- subMenu ?: boolean ;
42- parentDimensions ?: { left : number ; width : number } ;
4342} ) => {
4443 const menuRef = useRef < HTMLDivElement > ( undefined ) ;
44+ const [ activeIndex , setActiveIndex ] = useState ( - 1 ) ;
4545 const [ subMenuItems , setSubMenuItems ] = useState < ( MenuItem | string ) [ ] > ( [ ] ) ;
4646 const className = subMenu
4747 ? `ag-menu ag-ltr ag-popup-child ${ theme } `
4848 : `ag-menu ag-column-menu ag-ltr ag-popup-child ag-popup-positioned-under ${ theme } ` ;
49- const [ activeIndex , setActiveIndex ] = useState ( - 1 ) ;
5049
50+ // The following useEffect positions our column header menu. There are three general
51+ // ways of laying things out.
52+ // - option 1. If there is enough room for the parent menu and child menu to the right, the
53+ //. menus are displayed left to right.
54+ // - option 2. If the parent menu has enough room, we don't shift it. If the child menu doesn't
55+ //. fit to the right, we move it to the left side.
56+ // - option 3. The parent menu doesn't fit on the screen, so we shift it to the left and
57+ // put the child menu on the left side.
5158 const [ left , setLeft ] = useState ( parentDimensions ?. left ?? incomingLeft ) ;
5259 const [ displayed , setDisplayed ] = useState ( false ) ;
5360 useEffect ( ( ) => {
5461 const clientWidth = menuRef . current . closest ( "body" ) . clientWidth ;
5562 const width = menuRef . current . getBoundingClientRect ( ) . width ;
5663
64+ setDisplayed ( true ) ;
5765 if ( parentDimensions ) {
5866 // First, lets put the child menu to the right
5967 let adjustedLeft = parentDimensions . left + parentDimensions . width ;
@@ -62,17 +70,15 @@ const GridMenu = ({
6270 adjustedLeft = parentDimensions . left - width ;
6371 }
6472 setLeft ( adjustedLeft ) ;
65- setDisplayed ( true ) ;
6673 return ;
6774 }
6875 if ( left + width > clientWidth ) {
6976 setLeft ( left - ( left + width - clientWidth + 15 ) ) ;
7077 }
71- setDisplayed ( true ) ;
72- } , [ ] ) ;
78+ } , [ ] ) ; // eslint-disable-line react-hooks/exhaustive-deps
7379
7480 return (
75- < Fragment >
81+ < >
7682 { subMenuItems . length > 0 && (
7783 < GridMenu
7884 menuItems = { subMenuItems }
@@ -106,19 +112,7 @@ const GridMenu = ({
106112 > </ div >
107113 { menuItems . map ( ( menuItem , index ) => {
108114 if ( typeof menuItem === "string" ) {
109- return (
110- < div
111- className = "ag-menu-separator"
112- aria-hidden = "true"
113- key = { index }
114- >
115- { " " }
116- < div className = "ag-menu-separator-part" > </ div > { " " }
117- < div className = "ag-menu-separator-part" > </ div > { " " }
118- < div className = "ag-menu-separator-part" > </ div > { " " }
119- < div className = "ag-menu-separator-part" > </ div > { " " }
120- </ div >
121- ) ;
115+ return < Separator key = { index } /> ;
122116 }
123117 return (
124118 < div
@@ -146,6 +140,7 @@ const GridMenu = ({
146140 setActiveIndex ( - 1 ) ;
147141 const targetInPopup = Array . from (
148142 document . querySelectorAll ( ".ag-popup" ) ,
143+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
149144 ) . some ( ( t ) => t . contains ( e . target as HTMLElement ) ) ;
150145 if ( ! targetInPopup ) {
151146 setSubMenuItems ( [ ] ) ;
@@ -154,20 +149,17 @@ const GridMenu = ({
154149 >
155150 < span
156151 className = "ag-menu-option-part ag-menu-option-icon"
157- data-ref = "eIcon"
158152 role = "presentation"
159153 >
160154 { menuItem . checked && (
161155 < span
162156 className = "ag-icon ag-icon-tick"
163157 role = "presentation"
164- unselectable = "on"
165- > </ span >
158+ />
166159 ) }
167160 </ span >
168161 < span
169162 className = "ag-menu-option-part ag-menu-option-text"
170- data-ref = "eName"
171163 onClick = { ( ) => {
172164 if ( menuItem . disabled ) {
173165 return ;
@@ -182,20 +174,13 @@ const GridMenu = ({
182174 >
183175 { menuItem . name }
184176 </ span >
185- < span
186- className = "ag-menu-option-part ag-menu-option-shortcut"
187- data-ref = "eShortcut"
188- > </ span >
177+ < span className = "ag-menu-option-part ag-menu-option-shortcut" > </ span >
189178 { menuItem . children && menuItem . children . length > 0 && (
190- < span
191- className = "ag-menu-option-part ag-menu-option-popup-pointer"
192- data-ref = "ePopupPointer"
193- >
179+ < span className = "ag-menu-option-part ag-menu-option-popup-pointer" >
194180 < span
195181 className = "ag-icon ag-icon-small-right"
196182 role = "presentation"
197- unselectable = "on"
198- > </ span >
183+ />
199184 </ span >
200185 ) }
201186 </ div >
@@ -204,11 +189,11 @@ const GridMenu = ({
204189 < div
205190 className = "ag-tab-guard ag-tab-guard-bottom"
206191 role = "presentation"
207- > </ div >
192+ / >
208193 </ div >
209194 </ div >
210195 </ div >
211- </ Fragment >
196+ </ >
212197 ) ;
213198} ;
214199
@@ -226,13 +211,13 @@ const ColumnHeaderMenu = ({
226211} : ColumnHeaderProps ) => {
227212 const menuItems = [
228213 {
229- name : t [ " Sort" ] ,
214+ name : t . Sort ,
230215 children : [
231216 {
232217 name :
233218 hasSort && ! column . sort
234219 ? t [ "Ascending (add to sorting)" ]
235- : t [ " Ascending" ] ,
220+ : t . Ascending ,
236221 checked : column . sort === "asc" ,
237222 onPress : ( ) => {
238223 sortColumn ( "asc" ) ;
@@ -243,7 +228,7 @@ const ColumnHeaderMenu = ({
243228 name :
244229 hasSort && ! column . sort
245230 ? t [ "Descending (add to sorting)" ]
246- : t [ " Descending" ] ,
231+ : t . Descending ,
247232 checked : column . sort === "desc" ,
248233 onPress : ( ) => {
249234 sortColumn ( "desc" ) ;
@@ -274,4 +259,14 @@ const ColumnHeaderMenu = ({
274259 return < GridMenu menuItems = { menuItems } top = { top } left = { left } theme = { theme } /> ;
275260} ;
276261
262+ const Separator = ( ) => (
263+ < div className = "ag-menu-separator" aria-hidden = "true" >
264+ { " " }
265+ < div className = "ag-menu-separator-part" > </ div > { " " }
266+ < div className = "ag-menu-separator-part" > </ div > { " " }
267+ < div className = "ag-menu-separator-part" > </ div > { " " }
268+ < div className = "ag-menu-separator-part" > </ div > { " " }
269+ </ div >
270+ ) ;
271+
277272export default ColumnHeaderMenu ;
0 commit comments