FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Add anchored container queries for tooltips based on the popover API · patternfly-java/patternfly-java@9195ac2 · GitHub

Commit 9195ac2

Browse files
committed
Add anchored container queries for tooltips based on the popover API
1 parent b44cde0 commit 9195ac2

9 files changed

Lines changed: 872 additions & 206 deletions

File tree

‎components/src/main/java/org/patternfly/component/ComponentType.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ public enum ComponentType {
161161

162162
Tooltip("tt", "PF6/Component/Tooltip"),
163163

164+
Tooltip2("tt2", "PF6/Component/Tooltip2"),
165+
164166
TreeView("tv", "PF6/Component/TreeView"),
165167

166168
Truncate("tr", "PF6/Component/Truncate"),
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
/*
2+
* Copyright 2023 Red Hat
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.patternfly.component.tooltip;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
import java.util.function.Supplier;
21+
22+
import org.gwtproject.event.shared.HandlerRegistration;
23+
import org.jboss.elemento.Attachable;
24+
import org.jboss.elemento.By;
25+
import org.jboss.elemento.ElementContainerDelegate;
26+
import org.jboss.elemento.ElementTextDelegate;
27+
import org.jboss.elemento.Elements;
28+
import org.jboss.elemento.Id;
29+
import org.jboss.elemento.logger.Logger;
30+
import org.patternfly.component.BaseComponent;
31+
import org.patternfly.component.Closeable;
32+
import org.patternfly.component.ComponentType;
33+
import org.patternfly.core.Roles;
34+
import org.patternfly.handler.CloseHandler;
35+
import org.patternfly.popper.Placement;
36+
import elemental2.dom.Element;
37+
import elemental2.dom.Event;
38+
import elemental2.dom.HTMLDivElement;
39+
import elemental2.dom.HTMLElement;
40+
import elemental2.dom.MutationRecord;
41+
42+
import static elemental2.dom.DomGlobal.clearTimeout;
43+
import static elemental2.dom.DomGlobal.document;
44+
import static elemental2.dom.DomGlobal.setTimeout;
45+
import static org.gwtproject.event.shared.HandlerRegistrations.compose;
46+
import static org.jboss.elemento.Elements.div;
47+
import static org.jboss.elemento.EventType.bind;
48+
import static org.jboss.elemento.EventType.focusin;
49+
import static org.jboss.elemento.EventType.focusout;
50+
import static org.jboss.elemento.EventType.mouseenter;
51+
import static org.jboss.elemento.EventType.mouseleave;
52+
import static org.patternfly.component.tooltip.TriggerAria.describedBy;
53+
import static org.patternfly.component.tooltip.TriggerAria.none;
54+
import static org.patternfly.core.Aria.live;
55+
import static org.patternfly.core.Attributes.role;
56+
import static org.patternfly.handler.CloseHandler.fireEvent;
57+
import static org.patternfly.handler.CloseHandler.shouldClose;
58+
import static org.patternfly.popper.Placement.top;
59+
import static org.patternfly.style.Classes.arrow;
60+
import static org.patternfly.style.Classes.component;
61+
import static org.patternfly.style.Classes.content;
62+
import static org.patternfly.style.Classes.modifier;
63+
import static org.patternfly.style.Classes.textAlignLeft;
64+
import static org.patternfly.style.Classes.tooltip;
65+
66+
/**
67+
* A tooltip is in-app messaging used to identify elements on a page with short, clarifying text.
68+
* <p>
69+
* This implementation uses the Popover API and CSS anchor positioning instead of Popper.js. The tooltip uses the browser's
70+
* top-layer rendering for correct stacking, eliminating z-index issues.
71+
*/
72+
public class Tooltip2 extends BaseComponent<HTMLDivElement, Tooltip2> implements
73+
Attachable,
74+
Closeable<HTMLDivElement, Tooltip2>,
75+
ElementContainerDelegate<HTMLDivElement, Tooltip2>,
76+
ElementTextDelegate<HTMLDivElement, Tooltip2> {
77+
78+
// ------------------------------------------------------ factory
79+
80+
public static Tooltip2 tooltip2() {
81+
return new Tooltip2(null, null);
82+
}
83+
84+
public static Tooltip2 tooltip2(String text) {
85+
return new Tooltip2(null, text);
86+
}
87+
88+
public static Tooltip2 tooltip2(By trigger) {
89+
return new Tooltip2(() -> Elements.querySelector(document.body, trigger), null);
90+
}
91+
92+
public static Tooltip2 tooltip2(By trigger, String text) {
93+
return new Tooltip2(() -> Elements.querySelector(document.body, trigger), text);
94+
}
95+
96+
public static Tooltip2 tooltip2(HTMLElement trigger) {
97+
return new Tooltip2(() -> trigger, null);
98+
}
99+
100+
public static Tooltip2 tooltip2(HTMLElement trigger, String text) {
101+
return new Tooltip2(() -> trigger, text);
102+
}
103+
104+
public static Tooltip2 tooltip2(Supplier<HTMLElement> trigger) {
105+
return new Tooltip2(trigger, null);
106+
}
107+
108+
public static Tooltip2 tooltip2(Supplier<HTMLElement> trigger, String text) {
109+
return new Tooltip2(trigger, text);
110+
}
111+
112+
// ------------------------------------------------------ instance
113+
114+
private static final Logger logger = Logger.getLogger(Tooltip2.class.getName());
115+
116+
public static final int ENTRY_DELAY = 300;
117+
public static final int EXIT_DELAY = 300;
118+
public static final int DISTANCE = 15;
119+
120+
private final String id;
121+
private final HTMLElement contentElement;
122+
private final List<CloseHandler<Tooltip2>> closeHandler;
123+
private Supplier<HTMLElement> triggerSupplier;
124+
private HTMLElement trigger;
125+
private boolean visible;
126+
private int distance;
127+
private int entryDelay;
128+
private int exitDelay;
129+
private double showTimeout;
130+
private double hideTimeout;
131+
private Placement placement;
132+
private TriggerAria aria;
133+
private HandlerRegistration triggerHandlers;
134+
private HandlerRegistration anchorHandlers;
135+
136+
Tooltip2(Supplier<HTMLElement> trigger, String text) {
137+
super(ComponentType.Tooltip2, div().css(component(tooltip))
138+
.attr(role, Roles.tooltip)
139+
.aria(live, "polite")
140+
.attr("popover", "manual")
141+
.element());
142+
143+
this.id = Id.unique(componentType().id);
144+
this.triggerSupplier = trigger;
145+
this.closeHandler = new ArrayList<>();
146+
this.visible = false;
147+
this.distance = DISTANCE;
148+
this.entryDelay = ENTRY_DELAY;
149+
this.exitDelay = EXIT_DELAY;
150+
this.placement = top;
151+
this.aria = describedBy;
152+
this.showTimeout = 0;
153+
this.hideTimeout = 0;
154+
155+
id(id);
156+
element().appendChild(div().css(component(tooltip, arrow)).element());
157+
element().appendChild(contentElement = div().css(component(tooltip, content)).element());
158+
if (text != null) {
159+
contentElement.textContent = text;
160+
}
161+
162+
Attachable.register(this, this);
163+
}
164+
165+
@Override
166+
public Element containerDelegate() {
167+
return contentElement;
168+
}
169+
170+
@Override
171+
public Element textDelegate() {
172+
return contentElement;
173+
}
174+
175+
@Override
176+
public void attach(MutationRecord mutationRecord) {
177+
if (triggerSupplier != null) {
178+
trigger = triggerSupplier.get();
179+
if (trigger != null) {
180+
// CSS anchor positioning
181+
String anchorName = "--" + id;
182+
trigger.style.setProperty("anchor-name", anchorName);
183+
style("position-anchor", anchorName);
184+
style("margin", distance + "px");
185+
applyPlacement();
186+
187+
// event listeners on trigger
188+
triggerHandlers = compose(
189+
bind(trigger, mouseenter, this::scheduleShow),
190+
bind(trigger, mouseleave, this::scheduleHide),
191+
bind(trigger, focusin, this::scheduleShow),
192+
bind(trigger, focusout, this::scheduleHide));
193+
194+
// event listeners on tooltip itself:
195+
// hovering into the tooltip cancels the hide timer,
196+
// hovering out of the tooltip schedules hide
197+
anchorHandlers = compose(
198+
bind(element(), mouseenter, this::cancelTimers),
199+
bind(element(), mouseleave, this::scheduleHide));
200+
} else {
201+
logger.error("Unable to find trigger element for tooltip %o", element());
202+
}
203+
} else {
204+
logger.error("No trigger element defined for tooltip %o", element());
205+
}
206+
}
207+
208+
@Override
209+
public void detach(MutationRecord mutationRecord) {
210+
cancelTimers(new Event(""));
211+
if (visible) {
212+
element().hidePopover();
213+
visible = false;
214+
}
215+
if (anchorHandlers != null) {
216+
anchorHandlers.removeHandler();
217+
}
218+
if (triggerHandlers != null) {
219+
triggerHandlers.removeHandler();
220+
}
221+
if (trigger != null) {
222+
trigger.style.removeProperty("anchor-name");
223+
trigger = null;
224+
}
225+
}
226+
227+
// ------------------------------------------------------ builder
228+
229+
public Tooltip2 entryDelay(int delay) {
230+
this.entryDelay = delay;
231+
return this;
232+
}
233+
234+
public Tooltip2 exitDelay(int delay) {
235+
this.exitDelay = delay;
236+
return this;
237+
}
238+
239+
public Tooltip2 distance(int distance) {
240+
this.distance = distance;
241+
return this;
242+
}
243+
244+
public Tooltip2 leftAligned() {
245+
contentElement.classList.add(modifier(textAlignLeft));
246+
return this;
247+
}
248+
249+
public Tooltip2 placement(Placement placement) {
250+
this.placement = placement;
251+
return this;
252+
}
253+
254+
public Tooltip2 trigger(String trigger) {
255+
this.triggerSupplier = () -> Elements.querySelector(document.body, By.selector(trigger));
256+
return this;
257+
}
258+
259+
public Tooltip2 trigger(By trigger) {
260+
this.triggerSupplier = () -> Elements.querySelector(document.body, trigger);
261+
return this;
262+
}
263+
264+
public Tooltip2 trigger(HTMLElement trigger) {
265+
this.triggerSupplier = () -> trigger;
266+
return this;
267+
}
268+
269+
public Tooltip2 trigger(Supplier<HTMLElement> trigger) {
270+
this.triggerSupplier = trigger;
271+
return this;
272+
}
273+
274+
@Override
275+
public Tooltip2 that() {
276+
return this;
277+
}
278+
279+
// ------------------------------------------------------ aria
280+
281+
public Tooltip2 aria(TriggerAria aria) {
282+
this.aria = aria;
283+
return this;
284+
}
285+
286+
// ------------------------------------------------------ events
287+
288+
@Override
289+
public Tooltip2 onClose(CloseHandler<Tooltip2> closeHandler) {
290+
if (closeHandler != null) {
291+
this.closeHandler.add(closeHandler);
292+
}
293+
return this;
294+
}
295+
296+
// ------------------------------------------------------ api
297+
298+
public void show() {
299+
show(new Event(""));
300+
}
301+
302+
public void show(Event event) {
303+
if (!visible) {
304+
element().showPopover();
305+
visible = true;
306+
if (aria != none && trigger != null) {
307+
trigger.setAttribute(aria.attribute, id);
308+
}
309+
}
310+
}
311+
312+
@Override
313+
public void close(Event event, boolean fireEvent) {
314+
if (shouldClose(this, closeHandler, event, fireEvent)) {
315+
if (visible) {
316+
element().hidePopover();
317+
visible = false;
318+
if (aria != none && trigger != null) {
319+
trigger.removeAttribute(aria.attribute);
320+
}
321+
fireEvent(this, closeHandler, event, fireEvent);
322+
}
323+
}
324+
}
325+
326+
@Override
327+
public String text() {
328+
return Elements.textNode(contentElement);
329+
}
330+
331+
// ------------------------------------------------------ internal
332+
333+
private void scheduleShow(Event event) {
334+
cancelTimers(event);
335+
showTimeout = setTimeout(e -> show(), entryDelay);
336+
}
337+
338+
private void scheduleHide(Event event) {
339+
cancelTimers(event);
340+
hideTimeout = setTimeout(e -> close(new Event(""), true), exitDelay);
341+
}
342+
343+
private void cancelTimers(Event event) {
344+
clearTimeout(showTimeout);
345+
clearTimeout(hideTimeout);
346+
}
347+
348+
private void applyPlacement() {
349+
// remove old placement modifier
350+
for (String mod : Placement.modifiers) {
351+
classList().remove(mod);
352+
}
353+
354+
// apply placement modifier CSS class
355+
// controls both arrow direction (PF CSS) and position-area (core.css)
356+
if (placement.modifier != null && !placement.modifier.isEmpty()) {
357+
classList().add(placement.modifier);
358+
}
359+
}
360+
}

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL