Skip to content

Commit 8d48426

Browse files
committed
feat(outline-jump-nav): add mobile select dropdown.
1 parent 87b4929 commit 8d48426

File tree

3 files changed

+118
-14
lines changed

3 files changed

+118
-14
lines changed

packages/outline-jump-nav/src/outline-jump-nav.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919

2020
.outline-jump-nav--container {
2121
width: 100%;
22+
padding: 0.5rem 0;
23+
@media (min-width: 768px) {
24+
padding: 0;
25+
}
2226
}
2327

2428
.outline-jump-nav--list {
@@ -57,3 +61,18 @@
5761
background-color: darkblue;
5862
bottom: 0;
5963
}
64+
65+
.outline-jump-nav--select {
66+
padding: 0.5rem 0.25rem;
67+
margin: 0 auto;
68+
background-color: lightskyblue;
69+
font-family: 'Courier New', Courier, monospace;
70+
font-size: 1.25rem;
71+
font-weight: 700;
72+
border-radius: 8px;
73+
}
74+
75+
.outline-jump-nav--label {
76+
margin: 0 auto;
77+
width: fit-content;
78+
}

packages/outline-jump-nav/src/outline-jump-nav.ts

Lines changed: 96 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import { CSSResultGroup, TemplateResult, html } from 'lit';
22
import { OutlineElement } from '@phase2/outline-core';
33
import { customElement, property, state, query } from 'lit/decorators.js';
44
import componentStyles from './outline-jump-nav.css.lit';
5+
import { MobileController } from '@phase2/outline-core';
56

67
export type OutlineJumpNavJumps = { [key: string]: string };
78
export 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')
1417
export 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
}

packages/outline-storybook/stories/components/outline-jump-nav.stories.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ const Template = ({ slug, nav, hero }: any): TemplateResult => html`
7373
`
7474

7575

76-
export const JumpNavNoJumps: any = Template.bind({});
77-
JumpNavNoJumps.args = {slug: 'outline-jump-nav--'}
78-
JumpNavNoJumps.parameters = {
76+
export const JumpNav: any = Template.bind({});
77+
JumpNav.args = {slug: 'outline-jump-nav--'}
78+
JumpNav.parameters = {
7979
layout: 'fullscreen'
8080
};
8181

0 commit comments

Comments
 (0)