1717 * #L%
1818 */
1919import { html , LitElement , css } from "lit" ;
20- import { customElement } from 'lit/decorators.js' ;
20+ import { customElement , property , state } from 'lit/decorators.js' ;
2121import { ThemableMixin } from "@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js" ;
2222import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js' ;
23+ import { MediaQueryController } from '@vaadin/component-base/src/media-query-controller.js' ;
2324import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js' ;
2425import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js' ;
2526import '@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" )
5864export 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}
0 commit comments