Repository navigation
perf: lazy-load My Sites toolbar #2016
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
59822cc
18017e6
7fd1225
5b2891a
6bc30dc
bc14d22
381373a
de4521a
c01459c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| (function (window, document) { | ||
|
|
||
| 'use strict'; | ||
|
|
||
| const config = window.wu_my_sites_toolbar || {}; | ||
| const labels = config.labels || {}; | ||
|
|
||
| function createMenuItem(id, label, url) { | ||
|
|
||
| const item = document.createElement('li'); | ||
| const link = document.createElement('a'); | ||
|
|
||
| item.id = 'wp-admin-bar-' + id; | ||
| item.setAttribute('role', 'group'); | ||
| link.className = 'ab-item'; | ||
| link.href = url; | ||
| link.setAttribute('role', 'menuitem'); | ||
| link.textContent = label; | ||
| item.appendChild(link); | ||
|
|
||
| return item; | ||
| } | ||
|
|
||
| function createSiteItem(site) { | ||
|
|
||
| const item = createMenuItem('blog-' + site.id, site.name, site.url); | ||
| const menu = document.createElement('ul'); | ||
| const actions = [ | ||
| [ 'dashboard', site.dashboard_url ], | ||
| [ 'new_post', site.new_post_url ], | ||
| [ 'comments', site.comments_url ], | ||
| [ 'visit', site.visit_url ], | ||
| ]; | ||
|
|
||
| item.classList.add('menupop'); | ||
| item.querySelector('.ab-item').setAttribute('aria-expanded', 'false'); | ||
| menu.className = 'ab-submenu'; | ||
| menu.setAttribute('role', 'menu'); | ||
|
|
||
| actions.forEach(function (action) { | ||
|
|
||
| if (action[ 1 ]) { | ||
| menu.appendChild(createMenuItem('blog-' + site.id + '-' + action[ 0 ], labels[ action[ 0 ] ], action[ 1 ])); | ||
| } | ||
| }); | ||
|
|
||
| if (menu.children.length) { | ||
| const wrapper = document.createElement('div'); | ||
|
|
||
| wrapper.className = 'ab-sub-wrapper'; | ||
|
Comment on lines
+35
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Handle Enter and ArrowDown on dynamically inserted site submenus. Each fetched site entry is a focusable link. Its action links (dashboard, posts, comments, visit) sit in a hidden This gap exists whatever the Add a handler to each site item that responds to Enter and ArrowDown on the site trigger: Suggested fix if (menu.children.length) {
+ const siteTrigger = item.querySelector('.ab-item');
const wrapper = document.createElement('div');
wrapper.className = 'ab-sub-wrapper';
wrapper.appendChild(menu);
item.appendChild(wrapper);
+ item.addEventListener('keydown', function (event) {
+
+ if (event.target !== siteTrigger || ('Enter' !== event.key && 'ArrowDown' !== event.key)) {
+ return;
+ }
+
+ event.preventDefault();
+ item.classList.toggle('hover', 'ArrowDown' === event.key || ! item.classList.contains('hover'));
+ });
}The handler is attached only when the submenu exists. On an item with a submenu, Enter toggles the submenu instead of following the site link. The site's links remain in the submenu. 🤖 Prompt for AI Agents |
||
| wrapper.appendChild(menu); | ||
| item.appendChild(wrapper); | ||
| } | ||
|
|
||
| return item; | ||
| } | ||
|
|
||
| function init() { | ||
|
|
||
| const root = document.getElementById('wp-admin-bar-my-sites'); | ||
| const list = document.getElementById('wp-admin-bar-my-sites-list'); | ||
| const allSites = document.getElementById('wp-admin-bar-wu-my-sites-toolbar-all'); | ||
|
|
||
| if (! root || ! list || ! allSites || ! config.ajax_url || ! config.nonce) { | ||
| return; | ||
| } | ||
|
|
||
| const trigger = root.querySelector(':scope > .ab-item'); | ||
| const status = document.createElement('li'); | ||
| const state = { | ||
| loading: false, | ||
| loaded: false, | ||
| nextPage: 1, | ||
| }; | ||
|
|
||
| status.id = 'wp-admin-bar-wu-my-sites-toolbar-status'; | ||
| status.setAttribute('role', 'status'); | ||
| status.setAttribute('aria-live', 'polite'); | ||
| list.insertBefore(status, allSites); | ||
|
|
||
| function setStatus(message, retry) { | ||
|
|
||
| status.textContent = ''; | ||
|
|
||
| if (! message) { | ||
| return; | ||
| } | ||
|
|
||
| if (! retry) { | ||
| status.textContent = message; | ||
| return; | ||
| } | ||
|
|
||
| const link = document.createElement('a'); | ||
|
|
||
| link.className = 'ab-item'; | ||
| link.href = '#'; | ||
| link.setAttribute('role', 'menuitem'); | ||
| link.textContent = message + ' ' + (labels.retry || 'Retry'); | ||
| link.addEventListener('click', function (event) { | ||
|
|
||
| event.preventDefault(); | ||
| load(state.nextPage); | ||
| }); | ||
| status.appendChild(link); | ||
| } | ||
|
|
||
| function addLoadMore() { | ||
|
|
||
| const existing = document.getElementById('wp-admin-bar-wu-my-sites-toolbar-more'); | ||
|
|
||
| if (existing || ! state.nextPage) { | ||
| return; | ||
| } | ||
|
|
||
| const more = createMenuItem('wu-my-sites-toolbar-more', labels.load_more || 'Load more sites', '#'); | ||
|
|
||
| more.querySelector('.ab-item').addEventListener('click', function (event) { | ||
|
|
||
| event.preventDefault(); | ||
| more.remove(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Keep keyboard focus when loading another page. If a keyboard user activates “Load more sites,” 🤖 Prompt for AI Agents |
||
| load(state.nextPage); | ||
| }); | ||
| list.insertBefore(more, allSites); | ||
| } | ||
|
|
||
| function load(page) { | ||
|
|
||
| if (state.loading || ! page) { | ||
| return; | ||
| } | ||
|
|
||
| state.loading = true; | ||
| setStatus(labels.loading || 'Loading sites…'); | ||
|
|
||
| const request = new window.URLSearchParams({ | ||
| action: config.action, | ||
| nonce: config.nonce, | ||
| page, | ||
| }); | ||
|
|
||
| window.fetch(config.ajax_url, { | ||
| method: 'POST', | ||
| credentials: 'same-origin', | ||
| headers: { | ||
| 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', | ||
| }, | ||
| body: request.toString(), | ||
| }).then(function (response) { | ||
|
|
||
| if (! response.ok) { | ||
| throw new Error('request_failed'); | ||
| } | ||
|
|
||
| return response.json(); | ||
|
|
||
| }).then(function (response) { | ||
|
|
||
| if (! response || ! response.success || ! response.data) { | ||
| throw new Error('invalid_response'); | ||
| } | ||
|
|
||
| const data = response.data; | ||
|
|
||
| (data.sites || []).forEach(function (site) { | ||
| list.insertBefore(createSiteItem(site), allSites); | ||
| }); | ||
|
|
||
| state.loaded = true; | ||
| state.nextPage = data.next_page || 0; | ||
| setStatus((data.sites || []).length ? '' : (labels.empty || 'No additional sites are available.')); | ||
| addLoadMore(); | ||
|
|
||
| }).catch(function () { | ||
|
|
||
| setStatus(labels.failed || 'Sites could not be loaded.', true); | ||
|
|
||
| }).finally(function () { | ||
|
|
||
| state.loading = false; | ||
|
|
||
| }); | ||
| } | ||
|
|
||
| function loadInitialPage() { | ||
|
|
||
| if (! state.loaded) { | ||
| load(1); | ||
| } | ||
| } | ||
|
|
||
| root.addEventListener('mouseenter', loadInitialPage); | ||
| root.addEventListener('focusin', loadInitialPage); | ||
| root.addEventListener('pointerdown', function (event) { | ||
|
|
||
| if ('touch' === event.pointerType) { | ||
| loadInitialPage(); | ||
| } | ||
| }); | ||
|
|
||
| if (trigger) { | ||
| trigger.addEventListener('keydown', function (event) { | ||
|
|
||
| if ('Enter' === event.key || ' ' === event.key || 'ArrowDown' === event.key) { | ||
| loadInitialPage(); | ||
| } | ||
|
Comment on lines
+204
to
+206
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Open the menu for Space and ArrowDown. If the trigger has focus, Space or ArrowDown calls Based on learnings, custom Space and Arrow-key handlers should prevent browser defaults. 🤖 Prompt for AI AgentsSource: Learnings |
||
| }); | ||
| } | ||
|
|
||
| root.addEventListener('keydown', function (event) { | ||
|
|
||
| if ('Escape' === event.key && trigger && root.contains(root.ownerDocument.activeElement)) { | ||
| event.preventDefault(); | ||
| trigger.focus(); | ||
| } | ||
|
Comment on lines
+212
to
+215
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Close the menu when Escape returns focus. If focus is on a dynamically added link, Escape focuses 🤖 Prompt for AI Agents |
||
| }); | ||
| } | ||
|
|
||
| if ('loading' === document.readyState) { | ||
| document.addEventListener('DOMContentLoaded', init); | ||
| } else { | ||
| init(); | ||
| } | ||
|
|
||
| }(window, document)); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Do not leave expanded submenus marked as collapsed.
Each lazy site link receives
aria-expanded="false", but the controller never changes that value when its submenu opens. Assistive technology therefore receives a collapsed state while the dashboard actions are visible. Update the attribute with the submenu state, or omit it rather than reporting a false state.🤖 Prompt for AI Agents