Skip to content

Commit 0223191

Browse files
committed
Add CSS variable and modifier class to toggle CSS positioning fallbacks ans anchor based container queries
1 parent fc1b863 commit 0223191

16 files changed

Lines changed: 657 additions & 303 deletions

File tree

components/src/main/java/org/patternfly/component/popover/NativePopover.java

Lines changed: 131 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,31 @@
3232
import org.patternfly.component.Severity;
3333
import org.patternfly.component.button.Button;
3434
import org.patternfly.core.Aria;
35+
import org.patternfly.core.Attributes;
3536
import org.patternfly.handler.CloseHandler;
36-
import org.patternfly.popover.NativeAnchor;
37+
import org.patternfly.position.AnchorPositioning;
3738
import org.patternfly.popper.Placement;
3839
import org.patternfly.style.Modifiers.NoPadding;
39-
4040
import elemental2.dom.Element;
4141
import elemental2.dom.Event;
4242
import elemental2.dom.HTMLDivElement;
4343
import elemental2.dom.HTMLElement;
4444
import elemental2.dom.MutationRecord;
4545

46+
import static elemental2.dom.DomGlobal.clearTimeout;
4647
import static elemental2.dom.DomGlobal.document;
48+
import static elemental2.dom.DomGlobal.setTimeout;
49+
import static org.gwtproject.event.shared.HandlerRegistrations.compose;
4750
import static org.jboss.elemento.Elements.div;
4851
import static org.jboss.elemento.Elements.failSafeRemoveFromParent;
4952
import static org.jboss.elemento.Elements.insertFirst;
5053
import static org.jboss.elemento.EventType.bind;
5154
import 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;
5260
import static org.patternfly.component.button.Button.button;
5361
import static org.patternfly.component.popover.NativePopoverHeader.popoverHeader;
5462
import static org.patternfly.core.Aria.describedBy;
@@ -57,13 +65,11 @@
5765
import static org.patternfly.core.Aria.modal;
5866
import static org.patternfly.core.Attributes.role;
5967
import static org.patternfly.core.Roles.dialog;
60-
import static org.patternfly.core.Validation.verifyEnum;
6168
import static org.patternfly.handler.CloseHandler.fireEvent;
6269
import static org.patternfly.handler.CloseHandler.shouldClose;
6370
import 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;
6773
import static org.patternfly.popper.Placement.top;
6874
import static org.patternfly.style.Classes.arrow;
6975
import 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

Comments
 (0)