Skip to content

Commit

Permalink
New layout implementation and sidenav design (#6344)
Browse files Browse the repository at this point in the history
- Updates the sidenav static generation logic to match `flutter/website`
for improved build performance and consistency. Also removes the need
for specifying `match-page-url-exactly` on directory index pages.
- This changes the 11ty related `filters.ts` and `eleventyComputed.js`
files. These changes have already been reviewed and been in use by
`flutter/website` for a while.
- Redesigns the sidenav for improved accessibility and reducing
dependence on Bootstrap.
- Reimplements the site layout in preparation for Bootstrap removal and
improving scrolling.
- Removes a bunch of now unneeded logic and styles, further supporting
the later removal of Bootstrap.

Fixes #5791
Fixes #5789
Contributes to #4164
Contributes to #3849
Prepares for #2625
  • Loading branch information
parlough authored Jan 21, 2025
1 parent a3a7da9 commit 736baa3
Show file tree
Hide file tree
Showing 23 changed files with 415 additions and 502 deletions.
63 changes: 37 additions & 26 deletions src/_11ty/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function registerFilters(eleventyConfig: UserConfig): void {
eleventyConfig.addFilter('toSimpleDate', toSimpleDate);
eleventyConfig.addFilter('regexReplace', regexReplace);
eleventyConfig.addFilter('toISOString', toISOString);
eleventyConfig.addFilter('activeNavEntryIndexArray', activeNavEntryIndexArray);
eleventyConfig.addFilter('activeNavForPage', activeNavForPage);
eleventyConfig.addFilter('arrayToSentenceString', arrayToSentenceString);
eleventyConfig.addFilter('underscoreBreaker', underscoreBreaker);
eleventyConfig.addFilter('throwError', function (error: any) {
Expand Down Expand Up @@ -60,38 +60,49 @@ function toISOString(input: string | Date): string {
}
}

function activeNavEntryIndexArray(navEntryTree: any, pageUrlPath: string = ''): number[] | null {
const activeEntryIndexes = _getActiveNavEntries(navEntryTree, pageUrlPath);
return activeEntryIndexes.length === 0 ? null : activeEntryIndexes;
}
function activeNavForPage(pageUrlPath: string, activeNav: any) {
// Split the path for this page, dropping everything before the path.
// Example: dart.dev/tools/pub/package-layout ->
// [tools, pub, package-layout]
const parts = pageUrlPath.toLowerCase().split('/').slice(1);
let currentPathPairs = activeNav;
let lastAllowedBackupActive = [];

parts.forEach(part => {
// If the current entry has active data,
// allow its active data to be a backup if a later path isn't found.
const currentEntryActiveData = currentPathPairs['active'];
if (currentEntryActiveData) {
lastAllowedBackupActive = currentEntryActiveData;
}

function _getActiveNavEntries(navEntryTree: any, pageUrlPath = ''): number[] {
// TODO(parlough): Simplify once standardizing with the Flutter site.
for (let i = 0; i < navEntryTree.length; i++) {
const entry = navEntryTree[i];

if (entry.children) {
const descendantIndexes = _getActiveNavEntries(
entry.children,
pageUrlPath,
);
if (descendantIndexes.length > 0) {
return [i + 1, ...descendantIndexes];
}
const paths = currentPathPairs['paths'];

// If the current entry has children paths, explore those next.
if (paths) {
currentPathPairs = paths;
}

if (entry.permalink) {
const isMatch = entry['match-page-url-exactly']
? pageUrlPath === entry.permalink
: pageUrlPath.includes(entry.permalink);
// Get the data for the next part.
const nextPair = currentPathPairs[part];

if (isMatch) {
return [i + 1];
}
// If the next part of the path doesn't have data,
// use the active data for the current backup.
if (nextPair === undefined || nextPair === null) {
return lastAllowedBackupActive;
}

currentPathPairs = nextPair;
});

// If the last path part has active data, use that,
// otherwise fall back to the backup active data.
let activeEntries = currentPathPairs['active'];
if (activeEntries === undefined || activeEntries === null) {
activeEntries = lastAllowedBackupActive;
}

return [];
return activeEntries;
}

function arrayToSentenceString(list: string[], joiner: string = 'and'): string {
Expand Down
99 changes: 99 additions & 0 deletions src/_data/eleventyComputed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// noinspection JSUnusedGlobalSymbols
export default {
activeNav: (data) => {
const sidenav = data['sidenav'];

const results = {};
_visitPermalinks(results, sidenav, []);
return results;
},
};

/**
* @param results {object}
* @param navTree {object[]}
* @param path {number[]}
*/
function _visitPermalinks(results, navTree, path) {
navTree.forEach((entry, i) => {
const permalink = entry['permalink'];
const newPath = [...path, i + 1];
const children = entry['children'];
const hasChildren = Array.isArray(children);

if (typeof permalink === 'string' || permalink instanceof String) {
_addLink(results, permalink, newPath, hasChildren);
}

if (hasChildren) {
_visitPermalinks(results, children, newPath);
}
});
}

/**
* @param results {object}
* @param permalink {string}
* @param path {number[]}
* @param hasChildren {boolean}
*/
function _addLink(results, permalink, path, hasChildren) {
// Skip external links.
if (permalink.startsWith('http')) {
return;
}

// Throw an error if a permalink doesn't start with a `/`.
if (!permalink.startsWith('/')) {
throw new Error(`${permalink} does not begin with a '/'`);
}

// Split the specified permalink into parts.
const parts = permalink.split('/');

// Add active nav data for the specified permalink.
_addPart(results, path, parts, 1, hasChildren);
}

/**
* @param result {object}
* @param path {number[]}
* @param parts {string[]}
* @param index {number}
* @param hasChildren {boolean}
*/
function _addPart(result, path, parts, index, hasChildren = false) {
const isLast = index === parts.length - 1;
let current = result[parts[index]];

if (!current) {
// If the current part isn't in the result map yet, add a new map.
current = {};
result[parts[index]] = current;
}

// If this is the last part of the path,
// store the active nav data.
if (isLast) {
const active = current['active'];
// Override active nav data if
// it doesn't already exist for this part,
// or the current active data was from an entry with children.
if (!active) {
current['active'] = path;
if (hasChildren) {
current['has-children'] = true;
}
} else if (!hasChildren && current['has-children'] === true) {
current['active'] = path;
current['has-children'] = false;
}
} else {
if (!current['paths']) {
current['paths'] = {};
}

// Continue to the next part.
_addPart(current['paths'], path, parts, index + 1, hasChildren);
}
}
11 changes: 0 additions & 11 deletions src/_data/side-nav.yml → src/_data/sidenav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
expanded: false
children:
- title: Introduction
match-page-url-exactly: true
permalink: /language
- title: Syntax basics
children:
Expand Down Expand Up @@ -73,7 +72,6 @@
- title: Class modifiers
children:
- title: Overview & usage
match-page-url-exactly: true
permalink: /language/class-modifiers
- title: Class modifiers for API maintainers
permalink: /language/class-modifiers-for-apis
Expand All @@ -91,7 +89,6 @@
expanded: false
children:
- title: Sound null safety
match-page-url-exactly: true
permalink: /null-safety
- title: Migrating to null safety
permalink: /null-safety/migration-guide
Expand All @@ -106,7 +103,6 @@
expanded: false
children:
- title: Overview
match-page-url-exactly: true
permalink: /libraries
- title: dart:core
permalink: /libraries/dart-core
Expand Down Expand Up @@ -139,7 +135,6 @@
permalink: /effective-dart
children:
- title: Overview
match-page-url-exactly: true
permalink: /effective-dart
- title: Style
permalink: /effective-dart/style
Expand Down Expand Up @@ -205,7 +200,6 @@
- title: Command-line & server apps
children:
- title: Overview
match-page-url-exactly: true
permalink: /server
- title: Get started
permalink: /tutorials/server/get-started
Expand All @@ -222,7 +216,6 @@
- title: Web apps
children:
- title: Overview
match-page-url-exactly: true
permalink: /web
- title: Get started
permalink: /web/get-started
Expand All @@ -248,7 +241,6 @@
children:
- title: Overview
permalink: /interop/js-interop
match-page-url-exactly: true
- title: Usage
permalink: /interop/js-interop/usage
- title: JS types
Expand All @@ -265,7 +257,6 @@
expanded: false
children:
- title: Overview
match-page-url-exactly: true
permalink: /tools
- title: Editors & debuggers
children:
Expand All @@ -278,7 +269,6 @@
- title: DartPad
children:
- title: Overview
match-page-url-exactly: true
permalink: /tools/dartpad
- title: Troubleshooting DartPad
permalink: /tools/dartpad/troubleshoot
Expand Down Expand Up @@ -372,7 +362,6 @@
- title: Videos
permalink: /resources/videos
- title: Tutorials
match-page-url-exactly: true
permalink: /tutorials

- title: Related sites
Expand Down
2 changes: 1 addition & 1 deletion src/_data/site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ yt:
watch: 'https://www.youtube.com/watch'
playlist: 'https://www.youtube.com/playlist?list='

show_banner: true
show_banner: false

# Increment this global og:image URL version number (used as a query parameter)
# when you update any og:image file. (Also increment the corresponding number
Expand Down
2 changes: 1 addition & 1 deletion src/_includes/navigation-main.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% assign page_url_path = page.url | regexReplace: '/index$|/index\.html$|\.html$|/$' | prepend: '/*' | append: '/' -%}

<nav id="mainnav" class="site-header">
<div id="menu-toggle"><i class="material-symbols">menu</i></div>
<div id="menu-toggle"><span class="material-symbols" title="Toggle side navigation menu." aria-label="Toggle side navigation menu." type="button">menu</span></div>
<a href="/" class="brand" title="{{site.title}}">
<img src="/assets/img/logo/logo-white-text.svg" alt="{{site.title}}">
</a>
Expand Down
41 changes: 20 additions & 21 deletions src/_includes/navigation-side.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,25 @@
id="search-side" autocomplete="off" placeholder="Search" aria-label="Search">
</form>

<div class="site-sidebar">
<ul class="navbar-nav">
<li class="nav-item">
<a href="/overview" class="nav-link">Overview</a>
</li>
<li class="nav-item">
<a href="/community" class="nav-link">Community</a>
</li>
<li class="nav-item">
<a href="https://dartpad.dev" class="nav-link">Try Dart</a>
</li>
<li class="nav-item">
<a href="/get-dart" class="nav-link">Get Dart</a>
</li>
<li class="nav-item">
<a href="/docs" class="nav-link">Docs</a>
</li>
<li aria-hidden="true"><div class="sidebar-primary-divider"></div></li>
</ul>
<ul class="navbar-nav">
<li aria-hidden="true"><div class="sidenav-divider"></div></li>
<li class="nav-item">
<a href="/overview" class="nav-link">Overview</a>
</li>
<li class="nav-item">
<a href="/community" class="nav-link">Community</a>
</li>
<li class="nav-item">
<a href="https://dartpad.dev" class="nav-link">Try Dart</a>
</li>
<li class="nav-item">
<a href="/get-dart" class="nav-link">Get Dart</a>
</li>
<li class="nav-item">
<a href="/docs" class="nav-link">Docs</a>
</li>
<li aria-hidden="true"><div class="sidenav-divider"></div></li>
</ul>

{% render 'sidenav-level-1.html', url:page.url, nav:side-nav %}
</div>
{% render 'sidenav-level-1.html', url:page.url, nav:sidenav, activeNav:activeNav %}
</div>
31 changes: 18 additions & 13 deletions src/_includes/sidenav-level-1.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{% assign page_url_path = url | regexReplace: '/index$|/index\.html$|\.html$|/$' -%}
{% assign active_entries = nav | activeNavEntryIndexArray: page_url_path -%}
{% assign pageUrlPath = url | regexReplace: '/index$|/index\.html$|\.html$|/$' -%}
{% assign activeEntries = pageUrlPath | activeNavForPage: activeNav -%}

<ul class="nav flex-column">
<ul class="nav">
{%- for entry in nav -%}
{% if entry == 'divider' -%}
<li aria-hidden="true"><div class="sidebar-primary-divider"></div></li>
{% elsif entry contains 'header' -%}
<li aria-hidden="true"><div class="sidenav-divider"></div></li>
{% elsif entry.header -%}
<li class="nav-header">{{entry.header}}</li>
{% else -%}
{% assign id = 'sidenav-' | append: forloop.index -%}
{% if forloop.index == active_entries[0] -%}
{% assign id = base_id | append: '-sidenav-' | append: forloop.index -%}
{% if forloop.index == activeEntries[0] -%}
{% assign isActive = true -%}
{% assign class = 'active' -%}
{% else -%}
Expand All @@ -26,13 +26,15 @@
{% assign show = '' -%}
{% endif -%}
<li class="nav-item">
<a class="nav-link {{class}} collapsible" data-toggle="collapse" href="#{{id}}" role="button" aria-expanded="{{expanded}}" aria-controls="{{id}}">{{entry.title}}</a>

<ul class="nav flex-column flex-nowrap collapse {{show}}" id="{{id}}">
<button class="nav-link {{class}} collapsible" data-toggle="collapse" data-target="#{{id}}" role="button" aria-expanded="{{expanded}}" aria-controls="{{id}}">
<span>{{entry.title}}</span>
<span class="material-symbols expander" aria-hidden="true">expand_more</span>
</button>
<ul class="nav collapse {{show}}" id="{{id}}">
{% if isActive -%}
{%- render 'sidenav-level-2.html', parent_id:id, children:entry.children, active_entries:active_entries -%}
{%- render 'sidenav-level-2.html', parentId:id, children:entry.children, activeEntries:activeEntries -%}
{% else -%}
{%- render 'sidenav-level-2.html', parent_id:id, children:entry.children -%}
{%- render 'sidenav-level-2.html', parentId:id, children:entry.children -%}
{% endif -%}
</ul>
</li>
Expand All @@ -45,7 +47,10 @@
<li class="nav-item">
<a class="nav-link {{class}}" href="{{entry.permalink}}"
{%- if isExternal %} target="_blank" rel="noopener" {%- endif -%}>
{{entry.title}}
<div>
<span>{{entry.title}}</span>
{%- if isExternal %}<span class="material-symbols" aria-hidden="true">open_in_new</span>{%- endif -%}
</div>
</a>
</li>
{% endif -%}
Expand Down
Loading

0 comments on commit 736baa3

Please sign in to comment.