3232import org .patternfly .component .Severity ;
3333import org .patternfly .component .button .Button ;
3434import org .patternfly .core .Aria ;
35+ import org .patternfly .core .Attributes ;
3536import org .patternfly .handler .CloseHandler ;
36- import org .patternfly .popover . NativeAnchor ;
37+ import org .patternfly .position . AnchorPositioning ;
3738import org .patternfly .popper .Placement ;
3839import org .patternfly .style .Modifiers .NoPadding ;
39-
4040import elemental2 .dom .Element ;
4141import elemental2 .dom .Event ;
4242import elemental2 .dom .HTMLDivElement ;
4343import elemental2 .dom .HTMLElement ;
4444import elemental2 .dom .MutationRecord ;
4545
46+ import static elemental2 .dom .DomGlobal .clearTimeout ;
4647import static elemental2 .dom .DomGlobal .document ;
48+ import static elemental2 .dom .DomGlobal .setTimeout ;
49+ import static org .gwtproject .event .shared .HandlerRegistrations .compose ;
4750import static org .jboss .elemento .Elements .div ;
4851import static org .jboss .elemento .Elements .failSafeRemoveFromParent ;
4952import static org .jboss .elemento .Elements .insertFirst ;
5053import static org .jboss .elemento .EventType .bind ;
5154import static org .jboss .elemento .EventType .click ;
55+ import static org .jboss .elemento .EventType .focusin ;
56+ import static org .jboss .elemento .EventType .focusout ;
57+ import static org .jboss .elemento .EventType .mouseenter ;
58+ import static org .jboss .elemento .EventType .mouseleave ;
59+ import static org .jboss .elemento .EventType .scroll ;
5260import static org .patternfly .component .button .Button .button ;
5361import static org .patternfly .component .popover .NativePopoverHeader .popoverHeader ;
5462import static org .patternfly .core .Aria .describedBy ;
5765import static org .patternfly .core .Aria .modal ;
5866import static org .patternfly .core .Attributes .role ;
5967import static org .patternfly .core .Roles .dialog ;
60- import static org .patternfly .core .Validation .verifyEnum ;
6168import static org .patternfly .handler .CloseHandler .fireEvent ;
6269import static org .patternfly .handler .CloseHandler .shouldClose ;
6370import static org .patternfly .icon .IconSets .fas .times ;
64- import static org .patternfly .popper .Placement .bottom ;
65- import static org .patternfly .popper .Placement .left ;
66- import static org .patternfly .popper .Placement .right ;
71+ import static org .patternfly .position .CssPositioning .popoverEnabled ;
72+ import static org .patternfly .popper .Placement .auto ;
6773import static org .patternfly .popper .Placement .top ;
6874import static org .patternfly .style .Classes .arrow ;
6975import static org .patternfly .style .Classes .close ;
@@ -109,30 +115,47 @@ public static NativePopover nativePopover(Supplier<HTMLElement> trigger) {
109115 private static final Logger logger = Logger .getLogger (NativePopover .class .getName ());
110116
111117 public static final int DISTANCE = 20 ;
118+ public static final int ENTRY_DELAY = 300 ;
119+ public static final int EXIT_DELAY = 300 ;
112120
113- private final NativeAnchor anchor ;
121+ private final AnchorPositioning anchorPositioning ;
114122 private final HTMLElement contentElement ;
115123 private final List <CloseHandler <NativePopover >> closeHandler ;
124+
116125 private boolean visible ;
117126 private boolean showClose ;
127+ private boolean hoverable ;
128+ private int entryDelay ;
129+ private int exitDelay ;
130+ private double showTimeout ;
131+ private double hideTimeout ;
132+ private Placement placement ;
118133 private Severity severity ;
119134 private Button closeButton ;
120135 private NativePopoverHeader header ;
121136 private HandlerRegistration triggerHandlers ;
137+ private HandlerRegistration anchorHandlers ;
122138 private HandlerRegistration outsideClickHandler ;
139+ private HandlerRegistration scrollHandler ;
123140
124141 NativePopover (Supplier <HTMLElement > trigger ) {
125- super (ComponentType .NativePopover , div ().css (component (popover ), top .modifier )
142+ super (ComponentType .NativePopover , div ().css (component (popover ), top .modifier () )
126143 .attr (role , dialog )
127144 .aria (modal , true )
128- .attr (" popover" , "manual" )
145+ .attr (Attributes . popover , "manual" )
129146 .element ());
130147
131148 String id = Id .unique (componentType ().id );
132- this .anchor = new NativeAnchor (id , DISTANCE , element (), trigger );
149+ this .anchorPositioning = new AnchorPositioning (id , element (), trigger , DISTANCE , popoverEnabled () );
133150 this .closeHandler = new ArrayList <>();
134151 this .visible = false ;
135152 this .showClose = true ;
153+ this .hoverable = false ;
154+ this .entryDelay = ENTRY_DELAY ;
155+ this .exitDelay = EXIT_DELAY ;
156+ this .showTimeout = 0 ;
157+ this .hideTimeout = 0 ;
158+ this .placement = auto ;
136159
137160 String bodyId = Id .unique (componentType ().id , "body" );
138161 element ().appendChild (div ().css (component (popover , arrow )).element ());
@@ -150,11 +173,32 @@ public void attach(MutationRecord mutationRecord) {
150173 failSafeRemoveFromParent (closeButton );
151174 }
152175
153- HTMLElement trigger = anchor .attach ();
176+ HTMLElement trigger = anchorPositioning .attach ();
154177 if (trigger != null ) {
155- // click trigger: toggle on click
156- triggerHandlers = bind (trigger , click , this ::togglePopover );
157- } else if (anchor .hasTriggerSupplier ()) {
178+ // top is the default for auto and recalculated on show()
179+ anchorPositioning .applyPlacement (placement == auto ? top : placement );
180+
181+ if (hoverable ) {
182+ triggerHandlers = compose (
183+ bind (trigger , mouseenter , e -> scheduleShow ()),
184+ bind (trigger , mouseleave , e -> scheduleHide ()),
185+ bind (trigger , focusin , e -> scheduleShow ()),
186+ bind (trigger , focusout , e -> scheduleHide ()));
187+ anchorHandlers = compose (
188+ bind (element (), mouseenter , e -> cancelTimers ()),
189+ bind (element (), mouseleave , e -> scheduleHide ()));
190+ if (!anchorPositioning .cssPositioning ()) {
191+ scrollHandler = bind (document , scroll , true , e -> recalculatePlacement ());
192+ }
193+
194+ } else {
195+ triggerHandlers = bind (trigger , click , this ::togglePopover );
196+ if (!anchorPositioning .cssPositioning ()) {
197+ scrollHandler = bind (document , scroll , true , e -> recalculatePlacement ());
198+ }
199+ }
200+
201+ } else if (anchorPositioning .hasTriggerSupplier ()) {
158202 logger .error ("Unable to find trigger element for popover %o" , element ());
159203 } else {
160204 logger .error ("No trigger element defined for popover %o" , element ());
@@ -163,17 +207,26 @@ public void attach(MutationRecord mutationRecord) {
163207
164208 @ Override
165209 public void detach (MutationRecord mutationRecord ) {
210+ cancelTimers ();
166211 if (visible ) {
167212 element ().hidePopover ();
168213 visible = false ;
169214 }
215+ if (scrollHandler != null ) {
216+ scrollHandler .removeHandler ();
217+ }
218+ if (anchorHandlers != null ) {
219+ anchorHandlers .removeHandler ();
220+ }
170221 if (outsideClickHandler != null ) {
171222 outsideClickHandler .removeHandler ();
172223 }
173224 if (triggerHandlers != null ) {
174225 triggerHandlers .removeHandler ();
175226 }
176- anchor .detach ();
227+ if (anchorPositioning != null ) {
228+ anchorPositioning .detach ();
229+ }
177230 }
178231
179232 // ------------------------------------------------------ add
@@ -251,7 +304,26 @@ public NativePopover closable(CloseHandler<NativePopover> closeHandler) {
251304 }
252305
253306 public NativePopover distance (int distance ) {
254- anchor .distance (distance );
307+ anchorPositioning .distance (distance );
308+ return this ;
309+ }
310+
311+ public NativePopover entryDelay (int delay ) {
312+ this .entryDelay = delay ;
313+ return this ;
314+ }
315+
316+ public NativePopover exitDelay (int delay ) {
317+ this .exitDelay = delay ;
318+ return this ;
319+ }
320+
321+ public NativePopover hoverable () {
322+ return hoverable (true );
323+ }
324+
325+ public NativePopover hoverable (boolean hoverable ) {
326+ this .hoverable = hoverable ;
255327 return this ;
256328 }
257329
@@ -276,9 +348,7 @@ public NativePopover noClose() {
276348 }
277349
278350 public NativePopover placement (Placement placement ) {
279- if (verifyEnum (element (), "placement" , placement , top , right , bottom , left )) {
280- anchor .applyPlacement (placement );
281- }
351+ this .placement = placement ;
282352 return this ;
283353 }
284354
@@ -297,22 +367,22 @@ public NativePopover status(Severity severity, String screenReaderText) {
297367 }
298368
299369 public NativePopover trigger (String trigger ) {
300- anchor .trigger (trigger );
370+ anchorPositioning .trigger (trigger );
301371 return this ;
302372 }
303373
304374 public NativePopover trigger (By trigger ) {
305- anchor .trigger (trigger );
375+ anchorPositioning .trigger (trigger );
306376 return this ;
307377 }
308378
309379 public NativePopover trigger (HTMLElement trigger ) {
310- anchor .trigger (trigger );
380+ anchorPositioning .trigger (trigger );
311381 return this ;
312382 }
313383
314384 public NativePopover trigger (Supplier <HTMLElement > trigger ) {
315- anchor .trigger (trigger );
385+ anchorPositioning .trigger (trigger );
316386 return this ;
317387 }
318388
@@ -324,7 +394,7 @@ public NativePopover that() {
324394 // ------------------------------------------------------ aria
325395
326396 /**
327- * Accessible label for the popover, required when header is not present.
397+ * Accessible label for the popover, required when a header is not present.
328398 */
329399 public NativePopover ariaLabel (String label ) {
330400 return aria (Aria .label , label );
@@ -353,10 +423,22 @@ public NativePopover onClose(CloseHandler<NativePopover> closeHandler) {
353423 // ------------------------------------------------------ api
354424
355425 public void show () {
356- if (!visible && anchor .trigger () != null ) {
357- element ().showPopover ();
426+ if (!visible && anchorPositioning .trigger () != null ) {
427+ if (anchorPositioning .cssPositioning ()) {
428+ // CSS handles position-try-fallbacks; just apply preferred placement and show
429+ anchorPositioning .applyPlacement (placement == auto ? top : placement );
430+ element ().showPopover ();
431+ } else {
432+ // JS calculates best placement via viewport measurements
433+ style ("visibility" , "hidden" );
434+ element ().showPopover ();
435+ anchorPositioning .applyBestPlacement (placement );
436+ element ().style .removeProperty ("visibility" );
437+ }
358438 visible = true ;
359- outsideClickHandler = bind (document , click .name , this ::onOutsideClick );
439+ if (!hoverable ) {
440+ outsideClickHandler = bind (document , click .name , this ::onOutsideClick );
441+ }
360442 }
361443 }
362444
@@ -377,6 +459,27 @@ public void close(Event event, boolean fireEvent) {
377459
378460 // ------------------------------------------------------ internal
379461
462+ private void scheduleShow () {
463+ cancelTimers ();
464+ showTimeout = setTimeout (e -> show (), entryDelay );
465+ }
466+
467+ private void scheduleHide () {
468+ cancelTimers ();
469+ hideTimeout = setTimeout (e -> close (new Event ("" ), true ), exitDelay );
470+ }
471+
472+ private void recalculatePlacement () {
473+ if (visible ) {
474+ anchorPositioning .applyBestPlacement (placement );
475+ }
476+ }
477+
478+ private void cancelTimers () {
479+ clearTimeout (showTimeout );
480+ clearTimeout (hideTimeout );
481+ }
482+
380483 private void togglePopover (Event event ) {
381484 if (visible ) {
382485 close (event , true );
@@ -386,7 +489,7 @@ private void togglePopover(Event event) {
386489 }
387490
388491 private void onOutsideClick (Event event ) {
389- HTMLElement trigger = anchor .trigger ();
492+ HTMLElement trigger = anchorPositioning .trigger ();
390493 if (visible && trigger != null ) {
391494 Element target = (Element ) event .target ;
392495 if (!element ().contains (target ) && !trigger .contains (target )) {
0 commit comments