Skip to content

Commit 4a6380b

Browse files
paodbmlopezFC
authored andcommitted
feat: add mobile mode
Close #6
1 parent 03beaff commit 4a6380b

File tree

6 files changed

+200
-46
lines changed

6 files changed

+200
-46
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@ By default, there are few css variables that help you update the separator's sty
5858
| --vcf-breadcrumb-separator-margin | Margin of the separator icon | 0 |
5959
| --vcf-breadcrumb-separator-padding | Padding of the separator icon | 0 var(--lumo-space-xs) |
6060

61+
## Updates since version 2.2.0
62+
63+
Added support for [Mobile Mode](https://github.com/vaadin-component-factory/vcf-breadcrumb/issues/6). It can be triggered in two ways:
64+
- Based on a fixed breakpoint (same as other Vaadin components): `(max-width: 450px), (max-height: 450px)` or
65+
- Programmatically, using the flag `forceMobileMode`, which allows to enable mobile layout manually
66+
67+
When in Mobile Mode, Breadcrumbs are styled for mobile navigation showing only back path.
68+
- Shows the last breadcrumb unless it's the current one
69+
- Shows the breadcrumb directly before the current one
70+
71+
By default, mobile mode shows a back icon that can be customized using the CSS variable: `--vcf-breadcrumb-mobile-back-symbol`
72+
6173
## Running demo
6274

6375
1. Fork the `vcf-breadcrumb` repository and clone it locally.

demo/index.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,24 @@ <h3>Styling Demo Sample</h3>
100100
</vcf-breadcrumbs>
101101
</template>
102102
</demo-snippet>
103+
<h3>Enabling mobile mode by flag Demo Sample</h3>
104+
<demo-snippet>
105+
<template>
106+
<style>
107+
#mobile-mode-flag-example .breadcrumb-anchor {
108+
color: var(--lumo-primary-text-color);
109+
text-decoration: none;
110+
}
111+
</style>
112+
<vcf-breadcrumbs id="mobile-mode-flag-example" force-mobile-mode>
113+
<vcf-breadcrumb href="#">Home</vcf-breadcrumb>
114+
<vcf-breadcrumb href="#">Directory</vcf-breadcrumb>
115+
<vcf-breadcrumb href="#" collapse>Components</vcf-breadcrumb>
116+
<vcf-breadcrumb href="#">VCF Components</vcf-breadcrumb>
117+
<vcf-breadcrumb>Breadcrumb</vcf-breadcrumb>
118+
</vcf-breadcrumbs>
119+
</template>
120+
</demo-snippet>
103121
</main>
104122
</body>
105123
</html>

package.json

Lines changed: 7 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.1.1",
3+
"version": "2.2.0",
44
"description": "Web Component providing an easy way to display breadcrumb.",
55
"author": "Vaadin Ltd",
66
"type": "module",
@@ -97,6 +97,12 @@
9797
{
9898
"ignorePackages": true
9999
}
100+
],
101+
"sort-imports": [
102+
"error",
103+
{
104+
"ignoreDeclarationSort": true
105+
}
100106
]
101107
}
102108
},

src/component/vcf-breadcrumb.ts

Lines changed: 1 addition & 1 deletion
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.1.1';
64+
return '2.2.0';
6565
}
6666

6767
render() {

src/component/vcf-breadcrumbs.ts

Lines changed: 147 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
* #L%
1818
*/
1919
import { html, LitElement, css } from "lit";
20-
import { customElement} from 'lit/decorators.js';
20+
import { customElement, property, state } from 'lit/decorators.js';
2121
import { ThemableMixin } from "@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js";
2222
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
23+
import { MediaQueryController } from '@vaadin/component-base/src/media-query-controller.js';
2324
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
2425
import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
2526
import '@vaadin/popover';
@@ -36,6 +37,11 @@ import '@vaadin/vertical-layout';
3637
* - Uses a `vaadin-popover` to display hidden breadcrumbs when the ellipsis is clicked.
3738
* - Themeable via Vaadin's ThemableMixin.
3839
*
40+
* Since version 2.2.0, mobile mode is added, which can be triggered in two ways:
41+
* - Based on a fixed breakpoint (same as other Vaadin components):
42+
* `(max-width: 450px), (max-height: 450px)`
43+
* - Programmatically, using the flag `forceMobileMode`, which allows to enable mobile layout manually.
44+
*
3945
* Example usage:
4046
* ```html
4147
* <vcf-breadcrumbs>
@@ -57,12 +63,35 @@ import '@vaadin/vertical-layout';
5763
@customElement("vcf-breadcrumbs")
5864
export class VcfBreadcrumbs extends ResizeMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElement)))) {
5965

66+
/**
67+
* Flag to indicate if the component is in mobile mode.
68+
* Set based on the value of _mobileMediaQuery.
69+
*/
70+
@state()
71+
private _mobile = false;
72+
73+
/**
74+
* Media query definition to determine if the component is in mobile mode.
75+
* This is used to apply responsive styles and behavior.
76+
* The value is set to match the same breakpoint as other Vaadin components:
77+
* `(max-width: 450px), (max-height: 450px)`.
78+
*/
79+
@state()
80+
private _mobileMediaQuery = '(max-width: 450px), (max-height: 450px)';
81+
82+
/**
83+
* Flag to force mobile mode, which allows the component to display in a mobile-friendly layout regardless of the screen size.
84+
* @attr {boolean} force-mobile-mode
85+
*/
86+
@property({ type: Boolean })
87+
forceMobileMode = false;
88+
6089
static get is() {
6190
return 'vcf-breadcrumbs';
6291
}
6392

6493
static get version() {
65-
return '2.1.1';
94+
return '2.2.0';
6695
}
6796

6897
static get styles() {
@@ -84,30 +113,35 @@ export class VcfBreadcrumbs extends ResizeMixin(ElementMixin(ThemableMixin(Polyl
84113
}
85114

86115
/**
87-
* Updates the visibility of breadcrumbs based on available space.
88-
*
116+
* Updates the visibility of breadcrumbs based on available space and mobile mode.
117+
*
118+
* Behavior summary:
89119
* - If all breadcrumbs have enough space, they are fully visible with no shrinking.
90120
* - If space is limited and some breadcrumbs have the "collapse" attribute:
91121
* - Consecutive collapsed items are grouped into ranges.
92122
* - These ranges are hidden when necessary and replaced with an ellipsis element.
93123
* - The ellipsis element serves as an interactive control, revealing hidden breadcrumbs in a popover.
94124
* - If more space becomes available, hidden items are restored, and unnecessary ellipses are removed.
95125
* - The first breadcrumb remains fully visible and does not shrink.
96-
*/
126+
* - On mobile mode (either responsive or forced):
127+
* - Breadcrumbs are styled for mobile navigation showing only back path.
128+
* - Shows the last breadcrumb unless it's the current one.
129+
* - Shows the breadcrumb directly before the current one.
130+
* - When returning to desktop mode:
131+
* - Mobile-specific styles and classes are removed.
132+
* - Breadcrumbs are adjusted for width and collapsing if needed.
133+
*
134+
* Mobile mode can be triggered in two ways:
135+
* - Based on a fixed breakpoint (same as other Vaadin components):
136+
* `(max-width: 450px), (max-height: 450px)`
137+
* - Programmatically, using the flag `forceMobileMode`, which allows to enable mobile layout manually.
138+
*/
97139
_updateBreadcrumbs() {
98140
// Remove existing ellipsis elements before recalculating
99141
this.querySelectorAll('[part="ellipsis"]').forEach((el) => el.remove());
100142

101143
// Get all breadcrumbs elements
102-
const breadcrumbs = Array.from(this.children) as HTMLElement[];
103-
104-
// If no breadcrumb has attribute "collapse", show all of them without shrinking
105-
if(breadcrumbs.every(breadcrumb => !breadcrumb.hasAttribute("collapse"))) {
106-
breadcrumbs.forEach((breadcrumb) => {
107-
breadcrumb.style.flexShrink = '0';
108-
});
109-
return;
110-
}
144+
const breadcrumbs = Array.from(this.querySelectorAll('vcf-breadcrumb')) as HTMLElement[];
111145

112146
// Reset all breadcrumbs to default visibility and allow middle items to shrink
113147
breadcrumbs.forEach((breadcrumb) => {
@@ -119,41 +153,81 @@ export class VcfBreadcrumbs extends ResizeMixin(ElementMixin(ThemableMixin(Polyl
119153
}
120154
});
121155

122-
// Ensure first item do not shrink
123-
const firstBreadcrumb = breadcrumbs[0];
124-
firstBreadcrumb.style.flexShrink = '0';
125-
firstBreadcrumb.style.minWidth = 'auto';
156+
// If mobile mode is active (responsive or forced), apply mobile-specific logic
157+
if(this._mobile || this.forceMobileMode) {
158+
breadcrumbs.forEach((breadcrumb) => {
159+
breadcrumb.classList.add("mobile-back");
160+
});
161+
162+
// Handle the last breadcrumb: if it's not current, show it with a mobile back icon
163+
const lastItem = breadcrumbs[breadcrumbs.length - 1];
164+
if (!lastItem.hasAttribute('aria-current')) {
165+
lastItem.classList.add('is-last-not-current');
166+
lastItem.querySelector(".breadcrumb-anchor")?.classList.add('add-mobile-back-icon');
167+
}
126168

127-
// Get available space in the container
128-
const containerWidth = this.getClientRects()[0].width;
169+
// Iterate through all breadcrumb items except the last, to find the one just before the current item
170+
for (let i = 0; i < breadcrumbs.length - 1; i++) {
171+
const currentItem = breadcrumbs[i];
172+
const nextItem = breadcrumbs[i + 1];
173+
// If the next breadcrumb is the current one, mark this as the item before current
174+
if (nextItem.hasAttribute('aria-current')) {
175+
currentItem.classList.add('is-before-current');
176+
currentItem.querySelector(".breadcrumb-anchor")?.classList.add('add-mobile-back-icon');
177+
}
178+
}
129179

130-
// Calculate total width of all breadcrumbs
131-
let totalWidth = breadcrumbs.reduce((sum, item) => sum + item.getClientRects()[0].width, 0);
180+
} else {
181+
// If not in mobile mode, remove mobile-specific classes
182+
breadcrumbs.forEach((breadcrumb) => {
183+
breadcrumb.classList.remove("mobile-back", 'is-last-not-current', 'is-before-current');
184+
breadcrumb.querySelector(".breadcrumb-anchor")?.classList.remove('add-mobile-back-icon');
185+
});
132186

133-
// Find collapse ranges
134-
const collapseRanges = this._findCollapseRanges(breadcrumbs);
187+
// If no breadcrumb has attribute "collapse", show all of them without shrinking
188+
if(breadcrumbs.every(breadcrumb => !breadcrumb.hasAttribute("collapse"))) {
189+
breadcrumbs.forEach((breadcrumb) => {
190+
breadcrumb.style.flexShrink = '0';
191+
});
192+
return;
193+
}
135194

136-
// If space is very limited, handle collapsing logic
137-
if (totalWidth > (containerWidth + 1)) {
138-
collapseRanges.forEach(({ start }) => {
139-
const collapseItem = breadcrumbs[start];
195+
// Ensure first item do not shrink
196+
const firstBreadcrumb = breadcrumbs[0];
197+
firstBreadcrumb.style.flexShrink = '0';
198+
firstBreadcrumb.style.minWidth = 'auto';
140199

141-
// save the collapsed items
142-
let hiddenItems = [];
143-
144-
// Hide collapsed items within this range
145-
for (let i = start; i <= collapseRanges.find(r => r.start === start)?.end!; i++) {
146-
breadcrumbs[i].style.display = 'none';
147-
hiddenItems.push(breadcrumbs[i]);
148-
}
149-
150-
// Insert an ellipsis element if it doesn't already exist
151-
if (collapseItem.previousElementSibling?.getAttribute("part") != "ellipsis") {
152-
let ellipsis = this._createEllipsisBreadcrumb(hiddenItems);
153-
collapseItem.insertAdjacentElement("beforebegin", ellipsis);
154-
}
155-
});
156-
}
200+
// Get available space in the container
201+
const containerWidth = this.getClientRects()[0].width;
202+
203+
// Calculate total width of all breadcrumbs
204+
let totalWidth = breadcrumbs.reduce((sum, item) => sum + item.getClientRects()[0].width, 0);
205+
206+
// Find collapse ranges
207+
const collapseRanges = this._findCollapseRanges(breadcrumbs);
208+
209+
// If space is very limited, handle collapsing logic
210+
if (totalWidth > (containerWidth + 1)) {
211+
collapseRanges.forEach(({ start }) => {
212+
const collapseItem = breadcrumbs[start];
213+
214+
// save the collapsed items
215+
let hiddenItems = [];
216+
217+
// Hide collapsed items within this range
218+
for (let i = start; i <= collapseRanges.find(r => r.start === start)?.end!; i++) {
219+
breadcrumbs[i].style.display = 'none';
220+
hiddenItems.push(breadcrumbs[i]);
221+
}
222+
223+
// Insert an ellipsis element if it doesn't already exist
224+
if (collapseItem.previousElementSibling?.getAttribute("part") != "ellipsis") {
225+
let ellipsis = this._createEllipsisBreadcrumb(hiddenItems);
226+
collapseItem.insertAdjacentElement("beforebegin", ellipsis);
227+
}
228+
});
229+
}
230+
}
157231
}
158232

159233
/**
@@ -260,6 +334,35 @@ export class VcfBreadcrumbs extends ResizeMixin(ElementMixin(ThemableMixin(Polyl
260334
// Add aria tags to the component
261335
this.setAttribute('aria-label', 'breadcrumb');
262336
this.setAttribute('role', 'navigation');
337+
338+
// Attach a media query controller to detect mobile mode responsively
339+
// Updates the `_mobile` state based on a fixed breakpoint
340+
this.addController(
341+
new MediaQueryController(this._mobileMediaQuery, (matches) => {
342+
this._mobile = matches;
343+
}),
344+
);
345+
346+
// Inject a scoped <style> element to define the mobile back icon behavior
347+
const style = document.createElement('style');
348+
style.textContent = `
349+
/*
350+
* This rule targets an <a> element with the 'breadcrumb-anchor' and
351+
* 'add-mobile-back-icon' classes that is a direct child of <vcf-breadcrumb>.
352+
*
353+
* Although technically global, it's scoped through the component selector
354+
* and only applies to breadcrumb anchors styled for mobile mode.
355+
*/
356+
vcf-breadcrumb > a.breadcrumb-anchor.add-mobile-back-icon::before {
357+
display: inline;
358+
font-family: var(--vcf-breadcrumb-separator-font-family);
359+
content: var(--vcf-breadcrumb-mobile-back-symbol);
360+
font-size: var(--vcf-breadcrumb-separator-size);
361+
margin: var(--vcf-breadcrumb-separator-margin);
362+
color: inherit;
363+
}
364+
`;
365+
this.appendChild(style);
263366
}
264367

265368
}

src/theme/lumo/vcf-breadcrumb-styles.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ registerStyles(
1818
--vcf-breadcrumb-separator-size: var(--lumo-font-size-s);
1919
--vcf-breadcrumb-separator-margin: 0;
2020
--vcf-breadcrumb-separator-padding: 0 var(--lumo-space-xs);
21+
--vcf-breadcrumb-mobile-back-symbol: var(--lumo-icons-angle-left);
2122
}
2223
2324
:host {
@@ -52,5 +53,19 @@ registerStyles(
5253
::slotted(a:focus) {
5354
outline: none;
5455
}
56+
57+
/* mobile back mode */
58+
:host(.mobile-back) {
59+
display: none;
60+
}
61+
62+
:host(.mobile-back) [part='separator'] {
63+
display: none;
64+
}
65+
66+
:host(.mobile-back.is-last-not-current),
67+
:host(.mobile-back.is-before-current) {
68+
display: inline-block;
69+
}
5570
`
5671
);

0 commit comments

Comments
 (0)