Skip to content

Commit 8690c00

Browse files
committed
feat: show popover with hidden items on ellipsis element
1 parent 3498f47 commit 8690c00

File tree

4 files changed

+108
-35
lines changed

4 files changed

+108
-35
lines changed

demo/index.html

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ <h3>Basic Demo Sample</h3>
3232
<vcf-breadcrumbs id="basic-example">
3333
<vcf-breadcrumb href="#">Home</vcf-breadcrumb>
3434
<vcf-breadcrumb href="#">Directory</vcf-breadcrumb>
35-
<vcf-breadcrumb href="#">Components</vcf-breadcrumb>
35+
<vcf-breadcrumb href="#" collapse>Components</vcf-breadcrumb>
3636
<vcf-breadcrumb href="#">VCF Components</vcf-breadcrumb>
3737
<vcf-breadcrumb>Breadcrumb</vcf-breadcrumb>
3838
</vcf-breadcrumbs>
@@ -42,22 +42,23 @@ <h3>Collapse Demo Sample</h3>
4242
<demo-snippet>
4343
<template>
4444
<style>
45-
#collapse-example vcf-breadcrumb .breadcrumb-anchor {
45+
.collapse-example-class .breadcrumb-anchor, .collapse-example-class.hidden-breadcrumb-anchor {
4646
color: var(--lumo-primary-text-color);
4747
text-decoration: none;
4848
}
49-
#collapse-example vcf-breadcrumb[aria-current="page"] .breadcrumb-anchor {
49+
50+
.collapse-example-class[aria-current="page"] .breadcrumb-anchor {
5051
color: var(--lumo-body-text-color);
51-
}
52+
}
5253
</style>
5354
<vcf-breadcrumbs id="collapse-example">
54-
<vcf-breadcrumb href="#">Home</vcf-breadcrumb>
55-
<vcf-breadcrumb href="#" collapse>Directory</vcf-breadcrumb>
56-
<vcf-breadcrumb href="#" collapse>Flow</vcf-breadcrumb>
57-
<vcf-breadcrumb href="#" >Vaadin Latest</vcf-breadcrumb>
58-
<vcf-breadcrumb href="#" collapse>Components</vcf-breadcrumb>
59-
<vcf-breadcrumb href="#" collapse>VCF Components</vcf-breadcrumb>
60-
<vcf-breadcrumb>Breadcrumb</vcf-breadcrumb>
55+
<vcf-breadcrumb class="collapse-example-class" href="#">Home</vcf-breadcrumb>
56+
<vcf-breadcrumb class="collapse-example-class" href="#" collapse>Directory</vcf-breadcrumb>
57+
<vcf-breadcrumb class="collapse-example-class" href="#" collapse>Flow</vcf-breadcrumb>
58+
<vcf-breadcrumb class="collapse-example-class" href="#" collapse>Vaadin Latest</vcf-breadcrumb>
59+
<vcf-breadcrumb class="collapse-example-class" href="#" >Components</vcf-breadcrumb>
60+
<vcf-breadcrumb class="collapse-example-class" href="#" collapse>VCF Components</vcf-breadcrumb>
61+
<vcf-breadcrumb class="collapse-example-class">Breadcrumb</vcf-breadcrumb>
6162
</vcf-breadcrumbs>
6263
</template>
6364
</demo-snippet>
@@ -71,10 +72,12 @@ <h3>Styling Demo Sample</h3>
7172
--vcf-breadcrumb-separator-size: var(--lumo-font-size-l);
7273
color: var(--lumo-body-text-color);
7374
}
74-
#styling-example vcf-breadcrumb .breadcrumb-anchor {
75+
76+
.styling-example-class .breadcrumb-anchor, .styling-example-class.hidden-breadcrumb-anchor {
7577
color: var(--lumo-body-text-color);
7678
text-decoration: none;
7779
}
80+
7881
#styling-example vcf-breadcrumb[aria-current="page"] .breadcrumb-anchor {
7982
color: var(--lumo-contrast-60pct);
8083
}
@@ -90,10 +93,10 @@ <h3>Styling Demo Sample</h3>
9093
</style>
9194
<vcf-breadcrumbs id="styling-example">
9295
<vcf-breadcrumb href="#" class="first">Home</vcf-breadcrumb>
93-
<vcf-breadcrumb href="#">Directory</vcf-breadcrumb>
94-
<vcf-breadcrumb href="#" collapse>Components</vcf-breadcrumb>
95-
<vcf-breadcrumb href="#">VCF Components</vcf-breadcrumb>
96-
<vcf-breadcrumb>Breadcrumb</vcf-breadcrumb>
96+
<vcf-breadcrumb href="#" collapse class="styling-example-class">Directory</vcf-breadcrumb>
97+
<vcf-breadcrumb href="#" collapse class="styling-example-class">Components</vcf-breadcrumb>
98+
<vcf-breadcrumb href="#" class="styling-example-class">VCF Components</vcf-breadcrumb>
99+
<vcf-breadcrumb class="styling-example-class">Breadcrumb</vcf-breadcrumb>
97100
</vcf-breadcrumbs>
98101
</template>
99102
</demo-snippet>

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@vaadin-component-factory/vcf-breadcrumb",
3-
"version": "2.0.1",
3+
"version": "2.1.0",
44
"description": "Web Component providing an easy way to display breadcrumb.",
55
"author": "Vaadin Ltd",
66
"type": "module",
@@ -43,8 +43,10 @@
4343
},
4444
"dependencies": {
4545
"@vaadin/component-base": "^24.5.8",
46+
"@vaadin/popover": "^24.5.8",
4647
"@vaadin/vaadin-lumo-styles": "^24.5.8",
4748
"@vaadin/vaadin-themable-mixin": "^24.5.8",
49+
"@vaadin/vertical-layout": "^24.5.8",
4850
"lit": "^3.0.0"
4951
},
5052
"devDependencies": {

src/component/vcf-breadcrumb.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class VcfBreadcrumb extends ElementMixin(ThemableMixin(PolylitMixin(LitElement))
6161
}
6262

6363
static get version() {
64-
return '2.0.1';
64+
return '2.1.0';
6565
}
6666

6767
render() {
@@ -84,7 +84,7 @@ class VcfBreadcrumb extends ElementMixin(ThemableMixin(PolylitMixin(LitElement))
8484
display: flex;
8585
align-items: center;
8686
min-width: 40px;
87-
}
87+
}
8888
`];
8989
}
9090

@@ -101,6 +101,19 @@ class VcfBreadcrumb extends ElementMixin(ThemableMixin(PolylitMixin(LitElement))
101101
const labelText = labelSlot.assignedNodes({ flatten: true })[0];
102102
anchor.appendChild(labelText);
103103

104+
// Add popover for ellipsis mode
105+
if (this._isEllipsisElement()) {
106+
// Add tabindex="0" to make it keyboard-accessible
107+
anchor.setAttribute("tabindex", "0");
108+
anchor.style.cursor = "pointer";
109+
anchor.setAttribute("id", this.id);
110+
const popover = this.querySelector('vaadin-popover[for="' + this.id + '"]');
111+
if (popover) {
112+
anchor.appendChild(popover);
113+
}
114+
this.removeAttribute("id");
115+
}
116+
104117
this.appendChild(anchor);
105118
}
106119

src/component/vcf-breadcrumbs.ts

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { ThemableMixin } from "@vaadin/vaadin-themable-mixin/vaadin-themable-mix
2222
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
2323
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
2424
import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
25+
import '@vaadin/popover';
26+
import '@vaadin/vertical-layout';
2527

2628
/**
2729
* A Web Component based on LitElement for displaying breadcrumbs.
@@ -31,6 +33,7 @@ import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
3133
* - Uses `ResizeMixin` to dynamically update visibility based on available space.
3234
* - The first breadcrumb always remains visible and does not shrink.
3335
* - Implements accessibility attributes to improved usability.
36+
* - Uses a `vaadin-popover` to display hidden breadcrumbs when the ellipsis is clicked.
3437
* - Themeable via Vaadin's ThemableMixin.
3538
*
3639
* Example usage:
@@ -62,18 +65,18 @@ export class VcfBreadcrumbs extends ResizeMixin(ElementMixin(ThemableMixin(Polyl
6265
}
6366

6467
static get version() {
65-
return '2.0.1';
68+
return '2.1.0';
6669
}
6770

6871
static get styles() {
6972
return css`
7073
:host {
7174
display: block;
72-
}
75+
}
7376
`;
7477
}
7578

76-
/**
79+
/**
7780
* Implement callback from `ResizeMixin` to update the vcf-breadcrumb elements visibility.
7881
*
7982
* @protected
@@ -87,12 +90,13 @@ export class VcfBreadcrumbs extends ResizeMixin(ElementMixin(ThemableMixin(Polyl
8790
* Updates the visibility of breadcrumbs based on available space.
8891
*
8992
* - If all breadcrumbs have enough space, they are fully visible with no shrinking.
90-
* - If some breadcrumbs have the "collapse" attribute and space is limited:
93+
* - If space is limited and some breadcrumbs have the "collapse" attribute:
9194
* - Consecutive collapsed items are grouped into ranges.
92-
* - Each range is hidden when necessary and replaced with an ellipsis element.
93-
* - If space allows, previously hidden items are restored and ellipses are removed.
95+
* - These ranges are hidden when necessary and replaced with an ellipsis element.
96+
* - The ellipsis element serves as an interactive control, revealing hidden breadcrumbs in a popover.
97+
* - If more space becomes available, hidden items are restored, and unnecessary ellipses are removed.
9498
* - The first breadcrumb remains fully visible and does not shrink.
95-
*/
99+
*/
96100
_updateBreadcrumbs() {
97101
// Get all breadcrumbs elements
98102
const breadcrumbs = Array.from(this.children) as HTMLElement[];
@@ -136,22 +140,26 @@ export class VcfBreadcrumbs extends ResizeMixin(ElementMixin(ThemableMixin(Polyl
136140
if (totalWidth > containerWidth) {
137141
collapseRanges.forEach(({ start }) => {
138142
const collapseItem = breadcrumbs[start];
143+
144+
// save the collapsed items
145+
let hiddenItems = [];
139146

140147
// Hide collapsed items within this range
141148
for (let i = start; i <= collapseRanges.find(r => r.start === start)?.end!; i++) {
142149
breadcrumbs[i].style.display = 'none';
150+
hiddenItems.push(breadcrumbs[i]);
143151
}
144152

145153
// Insert an ellipsis element if it doesn't already exist
146154
if (collapseItem.previousElementSibling?.getAttribute("part") != "ellipsis") {
147-
let ellipsis = this._createEllipsisBreadcrumb();
155+
let ellipsis = this._createEllipsisBreadcrumb(hiddenItems);
148156
collapseItem.insertAdjacentElement("beforebegin", ellipsis);
149-
}
157+
}
150158
});
151159
}
152160
}
153161

154-
/**
162+
/**
155163
* Finds ranges of consecutive elements that have the "collapse" attribute.
156164
*/
157165
_findCollapseRanges(breadcrumbs: HTMLElement[]) {
@@ -179,21 +187,68 @@ export class VcfBreadcrumbs extends ResizeMixin(ElementMixin(ThemableMixin(Polyl
179187
/**
180188
* Creates an ellipsis breadcrumb element to represent hidden items.
181189
*
182-
* - The element is a `<vcf-breadcrumb>` with an "ellipsis" part.
183-
* - It displays as "…" to indicate collapsed breadcrumbs.
184-
* - It does not shrink and maintains minimal width to avoid layout shifts.
185-
* - This element is inserted dynamically when space constraints require hiding breadcrumbs.
190+
* - The element is a `<vcf-breadcrumb>` with a unique ID and "ellipsis" part.
191+
* - It displays "…" to indicate collapsed breadcrumbs.
192+
* - It does not shrink and maintains minimal width to prevent layout shifts.
193+
* - A `vaadin-popover` is attached to display the hidden breadcrumbs as a vertical list.
194+
* - Clicking an item inside the popover closes the popover.
195+
* - The ellipsis is dynamically inserted and removed as needed based on available space.
186196
*
187-
* @returns {HTMLElement} An ellipsis breadcrumb element.
197+
* @param {HTMLElement[]} hiddenItems - The list of breadcrumbs that will be hidden and represented by the ellipsis
198+
* @returns {HTMLElement} An ellipsis breadcrumb element with an associated popover
188199
*/
189-
_createEllipsisBreadcrumb() {
200+
_createEllipsisBreadcrumb(hiddenItems: HTMLElement[]) {
190201
let ellipsis = document.createElement("vcf-breadcrumb");
202+
const id = "ellipsis-" + crypto.randomUUID();
203+
ellipsis.setAttribute("id", id);
191204
ellipsis.setAttribute("part", "ellipsis");
205+
ellipsis.setAttribute("aria-label", "Hidden breadcrumbs");
192206
ellipsis.innerText = "…";
193207
// Make sure the ellipsis is visible and positioned correctly
194208
ellipsis.style.display = 'inline-block';
195209
ellipsis.style.flexShrink = '0';
196210
ellipsis.style.minWidth = '0';
211+
212+
// Create a popover to show the hidden breadcumbs and add it to the ellipsis element
213+
let popover = document.createElement("vaadin-popover");
214+
popover.setAttribute("for", id);
215+
popover.setAttribute("overlay-role", "menu");
216+
popover.setAttribute('accessible-name-ref', "hidden breadcrumbs");
217+
popover.setAttribute("theme", "hidden-breadcrumbs");
218+
popover.setAttribute("position", "bottom-start");
219+
popover.setAttribute("modal", "true");
220+
221+
popover.renderer = (root) => {
222+
// Ensure content is only added once
223+
if (!root.firstChild) {
224+
const verticalLayout = document.createElement('vaadin-vertical-layout');
225+
verticalLayout.classList.add('hidden-breadcrumbs-layout');
226+
227+
// create new anchor elements for the hidden items and add them to the vertical layout
228+
hiddenItems.forEach((element) => {
229+
const item = document.createElement('a');
230+
item.textContent = element.textContent;
231+
item.setAttribute("href", element.getAttribute('href') ?? '');
232+
item.setAttribute("role", "menuitem");
233+
item.classList.add();
234+
// Copy element class list
235+
const elementClasses = Array.from(element.classList);
236+
item.classList.add(...elementClasses);
237+
item.classList.add("hidden-breadcrumb-anchor");
238+
239+
// Add click event to close popover when clicking an item
240+
item.addEventListener("click", () => {
241+
popover.opened = false;
242+
});
243+
244+
verticalLayout.appendChild(item);
245+
});
246+
root.appendChild(verticalLayout);
247+
}
248+
};
249+
250+
// append popover to ellipsis to move it later to the anchor within the container
251+
ellipsis.appendChild(popover);
197252
return ellipsis;
198253
}
199254

0 commit comments

Comments
 (0)