Skip to content

Commit ebbd25c

Browse files
authored
[JENKINS-75851] implement a dropdown indicator (#10803)
2 parents 6d05cae + 98b5291 commit ebbd25c

File tree

7 files changed

+203
-116
lines changed

7 files changed

+203
-116
lines changed

core/src/main/resources/lib/layout/breadcrumb.jelly

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,45 @@ THE SOFTWARE.
4747
</st:documentation>
4848

4949
<j:if test="${mode=='breadcrumbs'}">
50+
<j:set var="baseUrl" value="${request2.originalRequestURI}" />
5051
<j:set var="hasLink" value="${attrs.href != null}" />
51-
<li id="${attrs.id}" class="jenkins-breadcrumbs__list-item" data-type="breadcrumb-item" aria-current="${hasLink ? null : 'page'}" data-has-menu="${attrs.hasMenu}">
52+
<j:set var="isCurrent" value="${baseUrl == attrs.href}" />
53+
<j:set var="inPageNav" value="${attrs.id.contains('inpage-nav')}" />
54+
<j:set var="shouldShowTitle" value="${attrs.title.length() > 26}" />
55+
56+
<li aria-current="${(isCurrent or !hasLink)? 'page' : null}"
57+
id="${attrs.id}" class="jenkins-breadcrumbs__list-item"
58+
data-type="breadcrumb-item" data-has-menu="${attrs.hasMenu}"
59+
>
5260
<j:choose>
53-
<j:when test="${!hasLink}">
54-
<span class="${attrs.hasMenu ? 'hoverable-model-link' : ''}">${attrs.title}</span>
61+
<j:when test="${(!hasLink and !attrs.hasMenu) or (isCurrent and !inPageNav)}">
62+
<j:choose>
63+
<j:when test="${shouldShowTitle}">
64+
<span tooltip="${attrs.title}">${attrs.title}</span>
65+
</j:when>
66+
<j:otherwise>
67+
<span>${attrs.title}</span>
68+
</j:otherwise>
69+
</j:choose>
5570
</j:when>
5671
<j:otherwise>
57-
<a href="${attrs.href}" class="${attrs.hasMenu ? 'hoverable-model-link' : ''} ${attrs.hasChildrenMenu ? 'hoverable-children-model-link' : ''}">
58-
${attrs.title}
59-
</a>
72+
<j:choose>
73+
<j:when test="${shouldShowTitle}">
74+
<a tooltip="${attrs.title}" href="${attrs.href}">
75+
${attrs.title}
76+
</a>
77+
</j:when>
78+
<j:otherwise>
79+
<a href="${attrs.href}">
80+
${attrs.title}
81+
</a>
82+
</j:otherwise>
83+
</j:choose>
84+
<j:if test="${attrs.hasMenu or attrs.hasChildrenMenu}">
85+
<div aria-label="dropdown menu for ${attrs.title}" data-iscurrent="${isCurrent}" data-href="${attrs.href}" data-base="${baseUrl}" tabindex="0" class="dropdown-indicator" data-model="${attrs.hasMenu}" data-children="${attrs.hasChildrenMenu}" >
86+
<l:icon class="icon-sm jenkins-!-text-color-secondary" src="symbol-chevron-down" />
87+
</div>
88+
</j:if>
6089
</j:otherwise>
6190
</j:choose>
6291
</li>

src/main/js/components/dropdowns/inpage-jumplist.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { toId } from "@/util/dom";
55
* sections on the page (if using <f:breadcrumb-config-outline />)
66
*/
77
function init() {
8-
const inpageNavigationBreadcrumb = document.querySelector("#inpage-nav span");
8+
const inpageNavigationBreadcrumb = document.querySelector("#inpage-nav div");
99

1010
if (inpageNavigationBreadcrumb) {
1111
inpageNavigationBreadcrumb.items = Array.from(

src/main/js/components/dropdowns/jumplists.js

Lines changed: 124 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
1-
import Path from "@/util/path";
2-
import behaviorShim from "@/util/behavior-shim";
31
import Utils from "@/components/dropdowns/utils";
2+
import behaviorShim from "@/util/behavior-shim";
3+
import { createElementFromHtml } from "@/util/dom";
4+
import Path from "@/util/path";
45

56
function init() {
67
generateJumplistAccessors();
78
generateDropdowns();
89
}
10+
function generateDropdownChevron(element) {
11+
const isFirefox = navigator.userAgent.indexOf("Firefox") !== -1;
12+
// Firefox adds unwanted lines when copying buttons in text, so use a span instead
13+
const dropdownChevron = document.createElement(isFirefox ? "span" : "button");
14+
dropdownChevron.className = "jenkins-menu-dropdown-chevron";
15+
dropdownChevron.dataset.href = element.href;
16+
dropdownChevron.addEventListener("click", (event) => {
17+
event.preventDefault();
18+
});
19+
element.appendChild(dropdownChevron);
20+
}
921

1022
/*
1123
* Appends a ⌄ button at the end of links which support jump lists
1224
*/
1325
function generateJumplistAccessors() {
1426
behaviorShim.specify("A.model-link", "-jumplist-", 999, (link) => {
15-
const isFirefox = navigator.userAgent.indexOf("Firefox") !== -1;
16-
// Firefox adds unwanted lines when copying buttons in text, so use a span instead
17-
const dropdownChevron = document.createElement(
18-
isFirefox ? "span" : "button",
19-
);
20-
dropdownChevron.className = "jenkins-menu-dropdown-chevron";
21-
dropdownChevron.dataset.href = link.href;
22-
dropdownChevron.addEventListener("click", (event) => {
23-
event.preventDefault();
24-
});
25-
link.appendChild(dropdownChevron);
27+
generateDropdownChevron(link);
2628
});
2729
}
2830

@@ -37,74 +39,12 @@ function generateDropdowns() {
3739
(element) =>
3840
Utils.generateDropdown(
3941
element,
40-
(instance) => {
41-
if (element.items) {
42-
instance.setContent(Utils.generateDropdownItems(element.items));
43-
return;
44-
}
45-
46-
const href = element.href;
47-
48-
const hasModelLink = element.classList.contains(
49-
"hoverable-model-link",
50-
);
51-
const hasChildrenLink = element.classList.contains(
52-
"hoverable-children-model-link",
53-
);
54-
55-
const sections = {
56-
model: null,
57-
children: null,
58-
};
59-
60-
const fetchSection = function (urlSuffix) {
61-
return fetch(Path.combinePath(href, urlSuffix))
62-
.then((response) => response.json())
63-
.then((json) => {
64-
const items = mapChildrenItemsToDropdownItems(json.items);
65-
const section = document.createElement("div");
66-
section.appendChild(Utils.generateDropdownItems(items));
67-
return section;
68-
});
69-
};
70-
71-
const promises = [];
72-
73-
if (hasModelLink) {
74-
promises.push(
75-
fetchSection("contextMenu").then((section) => {
76-
sections.model = section;
77-
}),
78-
);
79-
}
80-
81-
if (hasChildrenLink) {
82-
promises.push(
83-
fetchSection("childrenContextMenu").then((section) => {
84-
sections.children = section;
85-
}),
86-
);
87-
}
88-
89-
Promise.all(promises)
90-
.then(() => {
91-
const container = document.createElement("div");
92-
container.className = "jenkins-dropdown__split-container";
93-
if (sections.model) {
94-
container.appendChild(sections.model);
95-
}
96-
if (sections.children) {
97-
container.appendChild(sections.children);
98-
}
99-
instance.setContent(container);
100-
})
101-
.catch((error) => {
102-
console.log(`Dropdown fetch failed: ${error}`);
103-
})
104-
.finally(() => {
105-
instance.loaded = true;
106-
});
107-
},
42+
createDropdownContent(
43+
element,
44+
element.classList.contains("hoverable-model-link"),
45+
element.classList.contains("hoverable-children-model-link"),
46+
element.href,
47+
),
10848
element.items != null,
10949
{
11050
trigger: "mouseenter",
@@ -115,6 +55,29 @@ function generateDropdowns() {
11555
),
11656
);
11757

58+
behaviorShim.specify(
59+
".dropdown-indicator",
60+
"-clickable-dropdown-",
61+
1000,
62+
(element) =>
63+
Utils.generateDropdown(
64+
element,
65+
createDropdownContent(
66+
element,
67+
element.getAttribute("data-model"),
68+
element.getAttribute("data-children"),
69+
element.getAttribute("data-href"),
70+
),
71+
element.items != null,
72+
{
73+
trigger: "click focus",
74+
offset: [-16, 10],
75+
animation: "tooltip",
76+
touch: false,
77+
},
78+
),
79+
);
80+
11881
behaviorShim.specify(
11982
"li.children, .jenkins-jumplist-link, #menuSelector, .jenkins-menu-dropdown-chevron",
12083
"-dropdown-",
@@ -147,6 +110,86 @@ function generateDropdowns() {
147110
);
148111
}
149112

113+
function createDropdownContent(element, hasModelLink, hasChildrenLink, href) {
114+
return (instance) => {
115+
if (element.items) {
116+
instance.setContent(Utils.generateDropdownItems(element.items));
117+
return;
118+
}
119+
const sections = {
120+
model: null,
121+
children: null,
122+
};
123+
124+
const fetchSection = function (urlSuffix) {
125+
return fetch(Path.combinePath(href, urlSuffix))
126+
.then((response) => response.json())
127+
.then((json) => {
128+
const items = Utils.generateDropdownItems(
129+
mapChildrenItemsToDropdownItems(json.items),
130+
);
131+
return items;
132+
});
133+
};
134+
135+
const promises = [];
136+
137+
if (hasModelLink === "true") {
138+
promises.push(
139+
fetchSection("contextMenu").then((section) => {
140+
const dContainer = section;
141+
dContainer.prepend(
142+
createElementFromHtml(
143+
`<p class="jenkins-dropdown__heading">Actions</p>`,
144+
),
145+
);
146+
sections.model = dContainer;
147+
}),
148+
);
149+
}
150+
151+
if (hasChildrenLink === "true") {
152+
promises.push(
153+
fetchSection("childrenContextMenu").then((section) => {
154+
const dContainer = section;
155+
// add a header for the section
156+
dContainer.prepend(
157+
createElementFromHtml(
158+
`<p class="jenkins-dropdown__heading">Navigation</p>`,
159+
),
160+
);
161+
sections.children = dContainer;
162+
}),
163+
);
164+
}
165+
166+
Promise.all(promises)
167+
.then(() => {
168+
const container = document.createElement("div");
169+
container.className = "jenkins-dropdown__split-container";
170+
if (sections.model && !sections.children) {
171+
container.appendChild(sections.model);
172+
} else if (!sections.model && sections.children) {
173+
container.appendChild(sections.children);
174+
} else if (sections.model && sections.children) {
175+
// use the first dropdown and add the second dropdowns choices this way the a11y stays intact
176+
const dropbox = sections.model;
177+
Array.from(sections.children.children).forEach((item) => {
178+
dropbox.appendChild(item);
179+
});
180+
container.appendChild(dropbox);
181+
}
182+
instance.setContent(container);
183+
})
184+
.catch((error) => {
185+
console.log(`Dropdown fetch failed: ${error}`);
186+
})
187+
.finally(() => {
188+
instance.loaded = true;
189+
});
190+
};
191+
}
192+
150193
/*
151194
* Generates the contents for the dropdown
152195
*/

src/main/js/components/dropdowns/templates.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ function menuItem(options) {
8484
const tag = itemOptions.type === "link" ? "a" : "button";
8585

8686
const item = createElementFromHtml(`
87-
<${tag} class="jenkins-dropdown__item ${itemOptions.clazz ? xmlEscape(itemOptions.clazz) : ""}" ${itemOptions.url ? `href="${xmlEscape(itemOptions.url)}"` : ""} ${itemOptions.id ? `id="${xmlEscape(itemOptions.id)}"` : ""}>
87+
<${tag} class="jenkins-dropdown__item ${itemOptions.clazz ? xmlEscape(itemOptions.clazz) : ""}"
88+
${itemOptions.url ? `href="${xmlEscape(itemOptions.url)}"` : ""} ${itemOptions.id ? `id="${xmlEscape(itemOptions.id)}"` : ""}
89+
${itemOptions.tooltip ? `data-html-tooltip="${xmlEscape(itemOptions.tooltip)}"` : ""}>
8890
${
8991
itemOptions.icon
9092
? `<div class="jenkins-dropdown__item__icon">${

src/main/js/components/header/breadcrumbs-overflow.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,28 @@ export default function computeBreadcrumbs() {
3636
(instance) => {
3737
const mappedItems = items.map((e) => {
3838
let href = e.querySelector("a");
39+
let tooltip;
3940
if (href) {
4041
href = href.href;
4142
}
43+
if (e.textContent.length > 26) {
44+
tooltip = e.textContent;
45+
}
4246

4347
return {
4448
type: "link",
49+
clazz: "jenkins-breadcrumbs__overflow-item",
4550
label: e.textContent,
4651
url: href,
52+
tooltip,
4753
};
4854
});
4955

5056
instance.setContent(Utils.generateDropdownItems(mappedItems));
5157
},
5258
true,
5359
{
54-
trigger: "mouseenter focus",
60+
trigger: "click focus",
5561
offset: [0, 10],
5662
animation: "tooltip",
5763
},

0 commit comments

Comments
 (0)