@@ -2,9 +2,12 @@ import { CSSResultGroup, TemplateResult, html } from 'lit';
22import { OutlineElement } from '@phase2/outline-core' ;
33import { customElement , property , state , query } from 'lit/decorators.js' ;
44import componentStyles from './outline-jump-nav.css.lit' ;
5+ import { MobileController } from '@phase2/outline-core' ;
56
67export type OutlineJumpNavJumps = { [ key : string ] : string } ;
78export type OutlineJumpNavVisibility = { [ key : string ] : number } ;
9+ export const outlineJumpNavStatuses = [ 'loading' , true , false ] as const ;
10+ export type OutlineJumpNavStatus = typeof outlineJumpNavStatuses [ number ] ;
811
912/**
1013 * The OutlineJumpNav component
@@ -13,6 +16,7 @@ export type OutlineJumpNavVisibility = { [key: string]: number };
1316@customElement ( 'outline-jump-nav' )
1417export class OutlineJumpNav extends OutlineElement {
1518 resizeObserver : ResizeObserver ;
19+ mobileController = new MobileController ( this ) ;
1620 static styles : CSSResultGroup = [ componentStyles ] ;
1721
1822 /**
@@ -52,11 +56,23 @@ export class OutlineJumpNav extends OutlineElement {
5256 jumps : OutlineJumpNavJumps = { } ;
5357
5458 /**
55- * Ref to the ul element
59+ * Ref to the desktop ul element
5660 */
5761 @query ( '.outline-jump-nav--list' )
5862 ul : HTMLElement ;
5963
64+ /**
65+ * Ref to the mobile select element
66+ */
67+ @query ( '.outline-jump-nav--select' )
68+ select : HTMLElement ;
69+
70+ /**
71+ * Indicates if the component is first loading or if a toggle between desktop/mobile is required.
72+ */
73+ @property ( { type : String || Boolean } )
74+ status : OutlineJumpNavStatus = 'loading' ;
75+
6076 /**
6177 * Current height of the jump-nav. Used to determine true viewable space.
6278 */
@@ -85,23 +101,44 @@ export class OutlineJumpNav extends OutlineElement {
85101 render ( ) : TemplateResult {
86102 return html `< section class ="outline-jump-nav ">
87103 < outline-container class ="outline-jump-nav--container ">
88- < nav class =" outline-jump-nav--nav " aria-label =" jump navigation " >
89- < ul class =" outline-jump-nav--list " > </ ul >
90- </ nav >
104+ ${ this . mobileController . isMobile
105+ ? this . mobileTemplate ( )
106+ : this . desktopTemplate ( ) }
91107 </ outline-container >
92108 </ section > ` ;
93109 }
110+
111+ desktopTemplate ( ) {
112+ return html `
113+ < nav class ="outline-jump-nav--nav " aria-label ="jump navigation ">
114+ < ul class ="outline-jump-nav--list "> </ ul >
115+ </ nav >
116+ ` ;
117+ }
118+
119+ mobileTemplate ( ) {
120+ return html `
121+ < label class ="outline-jump-nav--label " for ="jump-nav "> Scroll To</ label >
122+ < select
123+ @change =${ this . scrollHandler }
124+ class ="outline-jump-nav--select"
125+ id="jump-nav"
126+ > </ select >
127+ ` ;
128+ }
129+
94130 firstUpdated ( ) {
95131 this . initializeJumpsAndVisibility ( ) ;
96132 this . setOffsets ( ) ;
97- this . generateLinks ( ) ;
133+ this . toggleLinks ( ) ;
98134 this . setReduceMotion ( ) ;
99135 this . determineViewStatus ( ) ;
100136 this . resizeObserver . observe ( this ) ;
101137 }
102138
103139 updated ( ) {
104140 this . setOffsets ( ) ;
141+ this . toggleLinks ( ) ;
105142 }
106143
107144 connectedCallback ( ) : void {
@@ -122,6 +159,9 @@ export class OutlineJumpNav extends OutlineElement {
122159 super . disconnectedCallback ( ) ;
123160 }
124161
162+ /**
163+ * If user has prefers reduced motion set, prevents scrolling behavior and jumps the page to the link.
164+ */
125165 setReduceMotion ( ) {
126166 if ( window . matchMedia ( '(prefers-reduced-motion: reduce)' ) . matches ) {
127167 this . preventScroll = true ;
@@ -178,13 +218,52 @@ export class OutlineJumpNav extends OutlineElement {
178218 }
179219
180220 /**
181- * On click "scroll handler" to initiate scrolling when jump link is clicked.
221+ * Generates mobile select options from this.jumps object.
222+ */
223+ generateMobileSelectOptions ( ) {
224+ Object . entries ( this . jumps ) . forEach ( jump => {
225+ const option = document . createElement ( 'option' ) ;
226+ option . setAttribute ( 'value' , `${ jump [ 0 ] } ` ) ;
227+ option . innerText = `${ jump [ 1 ] } ` . toUpperCase ( ) ;
228+ this . select . appendChild ( option ) ;
229+ } ) ;
230+ }
231+
232+ /**
233+ * Generates correct markup depending on screen width. Forces setActive to make sure all styles are toggled.
234+ */
235+ toggleLinks ( ) {
236+ if ( this . mobileController . isMobile === this . status ) {
237+ return ;
238+ } else {
239+ this . mobileController . isMobile
240+ ? this . generateMobileSelectOptions ( )
241+ : this . generateLinks ( ) ;
242+ this . status = this . mobileController . isMobile ;
243+ }
244+ if ( this . isActive ) {
245+ this . setActive ( this . isActive , true ) ;
246+ }
247+ }
248+
249+ /**
250+ * On click/change "scroll handler" to initiate scrolling.
182251 */
183252 scrollHandler ( e : Event ) {
184253 e . preventDefault ( ) ;
185254 const host = document . querySelector ( 'outline-jump-nav' ) as OutlineJumpNav ;
186- const target = e . target as HTMLAnchorElement ;
187- const targetHref = target . getAttribute ( 'href' ) ;
255+ let target ;
256+ let targetHref ;
257+
258+ if ( host . mobileController . isMobile ) {
259+ target = e . target as HTMLOptionElement ;
260+ targetHref = `#${ target . value } ` ;
261+ }
262+ if ( ! host . mobileController . isMobile ) {
263+ target = e . target as HTMLAnchorElement ;
264+ targetHref = target . getAttribute ( 'href' ) ;
265+ }
266+
188267 const scrollTarget = document . querySelector ( `${ targetHref } ` ) as HTMLElement ;
189268
190269 if ( scrollTarget ) {
@@ -341,7 +420,7 @@ export class OutlineJumpNav extends OutlineElement {
341420 }
342421
343422 /**
344- * Sorts through multiple elements that are the same percentage in view, and passes the if of the element highest on the page to setActive.
423+ * Sorts through multiple elements that are the same percentage in view, and passes the id of the element highest on the page to setActive.
345424 */
346425 getTopPositions ( ids : [ string , number ] [ ] ) {
347426 const topPositions : { [ key : string ] : number } = { } ;
@@ -365,16 +444,22 @@ export class OutlineJumpNav extends OutlineElement {
365444
366445 /**
367446 * Takes an ID and if not already the active ID, sets it as this.isActive, then handles the passing of the active-jump class to the correct link.
447+ * The force argument is used when the component switches between mobile and desktop to make sure the active class is applied.
368448 */
369- setActive ( id : string ) {
370- if ( this . isActive !== id ) {
449+ setActive ( id : string , force ?: boolean ) {
450+ if ( this . isActive !== id || force === true ) {
371451 this . isActive = id ;
372452 this . shadowRoot
373453 ?. querySelector ( `.active-jump` )
374454 ?. classList . remove ( 'active-jump' ) ;
375455 this . shadowRoot
376456 ?. querySelector ( `#${ id } -jump` )
377457 ?. classList . add ( 'active-jump' ) ;
458+
459+ if ( this . mobileController . isMobile ) {
460+ const selector = this . select as HTMLSelectElement ;
461+ selector . value = id ;
462+ }
378463 }
379464 }
380465}
0 commit comments