kopia lustrzana https://github.com/wagtail/wagtail
Refactor Wagtail userbar as a web component (#9816)
* Add a border around the userbar menu in Windows high-contrast mode so it can be identified * Make sure browser font resizing applies to the userbar * Switch userbar to initialise a Web Component to avoid styling clashes * Refactor userbar stylesheets to use the same CSS loading as the rest of the adminpull/9822/head
rodzic
5eb2064574
commit
5cf621660c
|
@ -51,6 +51,8 @@ Changelog
|
|||
* Fix: Fix horizontal positioning of rich text inline toolbar (Thibaud Colas)
|
||||
* Fix: Ensure that `DecimalBlock` correctly handles `None`, when `required=False`, values (Natarajan Balaji)
|
||||
* Fix: Close the userbar when clicking its toggle (Albina Starykova)
|
||||
* Fix: Add a border around the userbar menu in Windows high-contrast mode so it can be identified (Albina Starykova)
|
||||
* Fix: Make sure browser font resizing applies to the userbar (Albina Starykova)
|
||||
* Docs: Add custom permissions section to permissions documentation page (Dan Hayden)
|
||||
* Docs: Add documentation for how to get started with contributing translations for the Wagtail admin (Ogunbanjo Oluwadamilare)
|
||||
* Docs: Officially recommend `fnm` over `nvm` in development documentation (LB (Ben) Johnston)
|
||||
|
@ -89,6 +91,8 @@ Changelog
|
|||
* Maintenance: Update `tsconfig` to better support modern TypeScript development and clean up some code quality issues via Eslint (Loveth Omokaro)
|
||||
* Maintenance: Set up Stimulus application initialisation according to RFC 78 (LB (Ben) Johnston)
|
||||
* Maintenance: Refactor submit-on-change search filters for image and document listings to use Stimulus (LB (Ben) Johnston)
|
||||
* Maintenance: Switch userbar to initialise a Web Component to avoid styling clashes (Albina Starykova)
|
||||
* Maintenance: Refactor userbar stylesheets to use the same CSS loading as the rest of the admin (Albina Starykova)
|
||||
|
||||
|
||||
4.1.2 (xx.xx.xxxx) - IN DEVELOPMENT
|
||||
|
|
|
@ -5,154 +5,166 @@
|
|||
// This component implements a roving tab index for keyboard navigation
|
||||
// Learn more about roving tabIndex: https://w3c.github.io/aria-practices/#kbd_roving_tabindex
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const userbar = document.querySelector('[data-wagtail-userbar]');
|
||||
const trigger = userbar.querySelector('[data-wagtail-userbar-trigger]');
|
||||
const list = userbar.querySelector('[role=menu]');
|
||||
const listItems = list.querySelectorAll('li');
|
||||
const isActiveClass = 'is-active';
|
||||
class Userbar extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const template = document.getElementById('wagtail-userbar-template');
|
||||
const shadowRoot = this.attachShadow({
|
||||
mode: 'open',
|
||||
});
|
||||
shadowRoot.appendChild(template.content.cloneNode(true));
|
||||
// Removes the template from html after it's being used
|
||||
template.remove();
|
||||
|
||||
// querySelector for all items that can be focused
|
||||
// tabIndex has been removed for roving tabindex compatibility
|
||||
// source: https://stackoverflow.com/questions/1599660/which-html-elements-can-receive-focus
|
||||
const focusableItemSelector = `a[href],
|
||||
const userbar = shadowRoot.querySelector('[data-wagtail-userbar]');
|
||||
const trigger = userbar.querySelector('[data-wagtail-userbar-trigger]');
|
||||
const list = userbar.querySelector('[role=menu]');
|
||||
const listItems = list.querySelectorAll('li');
|
||||
const isActiveClass = 'is-active';
|
||||
|
||||
// Avoid Web Component FOUC while stylesheets are loading.
|
||||
userbar.style.display = 'none';
|
||||
|
||||
// querySelector for all items that can be focused
|
||||
// tabIndex has been removed for roving tabindex compatibility
|
||||
// source: https://stackoverflow.com/questions/1599660/which-html-elements-can-receive-focus
|
||||
const focusableItemSelector = `a[href],
|
||||
button:not([disabled]),
|
||||
input:not([disabled])`;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
trigger.addEventListener('click', toggleUserbar, false);
|
||||
|
||||
// make sure userbar is hidden when navigating back
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
window.addEventListener('pageshow', hideUserbar, false);
|
||||
|
||||
// Handle keyboard events on the trigger
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
userbar.addEventListener('keydown', handleTriggerKeyDown);
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
list.addEventListener('focusout', handleFocusChange);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
resetItemsTabIndex(); // On initialisation, all menu items should be disabled for roving tab index
|
||||
|
||||
function showUserbar(shouldFocus) {
|
||||
userbar.classList.add(isActiveClass);
|
||||
trigger.setAttribute('aria-expanded', 'true');
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
list.addEventListener('click', sandboxClick, false);
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
window.addEventListener('click', clickOutside, false);
|
||||
trigger.addEventListener('click', toggleUserbar, false);
|
||||
|
||||
// Start handling keyboard input now that the userbar is open.
|
||||
// make sure userbar is hidden when navigating back
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
userbar.addEventListener('keydown', handleUserbarItemsKeyDown, false);
|
||||
window.addEventListener('pageshow', hideUserbar, false);
|
||||
|
||||
// The userbar has role=menu which means that the first link should be focused on popup
|
||||
// For weird reasons shifting focus only works after some amount of delay
|
||||
// Which is why we are forced to use setTimeout
|
||||
if (shouldFocus) {
|
||||
// Find the first focusable element (if any) and focus it
|
||||
if (list.querySelector(focusableItemSelector)) {
|
||||
setTimeout(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
setFocusToFirstItem();
|
||||
}, 300); // Less than 300ms doesn't seem to work
|
||||
// Handle keyboard events on the trigger
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
userbar.addEventListener('keydown', handleTriggerKeyDown);
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
list.addEventListener('focusout', handleFocusChange);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
resetItemsTabIndex(); // On initialisation, all menu items should be disabled for roving tab index
|
||||
|
||||
function showUserbar(shouldFocus) {
|
||||
userbar.classList.add(isActiveClass);
|
||||
trigger.setAttribute('aria-expanded', 'true');
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
list.addEventListener('click', sandboxClick, false);
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
window.addEventListener('click', clickOutside, false);
|
||||
|
||||
// Start handling keyboard input now that the userbar is open.
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
userbar.addEventListener('keydown', handleUserbarItemsKeyDown, false);
|
||||
|
||||
// The userbar has role=menu which means that the first link should be focused on popup
|
||||
// For weird reasons shifting focus only works after some amount of delay
|
||||
// Which is why we are forced to use setTimeout
|
||||
if (shouldFocus) {
|
||||
// Find the first focusable element (if any) and focus it
|
||||
if (list.querySelector(focusableItemSelector)) {
|
||||
setTimeout(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
setFocusToFirstItem();
|
||||
}, 300); // Less than 300ms doesn't seem to work
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hideUserbar() {
|
||||
userbar.classList.remove(isActiveClass);
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
list.addEventListener('click', sandboxClick, false);
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
window.removeEventListener('click', clickOutside, false);
|
||||
function hideUserbar() {
|
||||
userbar.classList.remove(isActiveClass);
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
list.addEventListener('click', sandboxClick, false);
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
window.removeEventListener('click', clickOutside, false);
|
||||
|
||||
// Cease handling keyboard input now that the userbar is closed.
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
userbar.removeEventListener('keydown', handleUserbarItemsKeyDown, false);
|
||||
}
|
||||
|
||||
function toggleUserbar(e2) {
|
||||
e2.stopPropagation();
|
||||
if (userbar.classList.contains(isActiveClass)) {
|
||||
hideUserbar();
|
||||
} else {
|
||||
showUserbar(true);
|
||||
// Cease handling keyboard input now that the userbar is closed.
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
userbar.removeEventListener('keydown', handleUserbarItemsKeyDown, false);
|
||||
}
|
||||
}
|
||||
|
||||
function isFocusOnItems() {
|
||||
return (
|
||||
document.activeElement &&
|
||||
!!document.activeElement.closest('.wagtail-userbar-items')
|
||||
);
|
||||
}
|
||||
function toggleUserbar(e2) {
|
||||
e2.stopPropagation();
|
||||
if (userbar.classList.contains(isActiveClass)) {
|
||||
hideUserbar();
|
||||
} else {
|
||||
showUserbar(true);
|
||||
}
|
||||
}
|
||||
|
||||
/** Reset all focusable menu items to `tabIndex = -1` */
|
||||
function resetItemsTabIndex() {
|
||||
listItems.forEach((listItem) => {
|
||||
function isFocusOnItems() {
|
||||
return (
|
||||
shadowRoot.activeElement &&
|
||||
!!shadowRoot.activeElement.closest('.w-userbar-nav')
|
||||
);
|
||||
}
|
||||
|
||||
/** Reset all focusable menu items to `tabIndex = -1` */
|
||||
function resetItemsTabIndex() {
|
||||
listItems.forEach((listItem) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
listItem.firstElementChild.tabIndex = -1;
|
||||
});
|
||||
}
|
||||
|
||||
/** Focus element using a roving tab index */
|
||||
function focusElement(el) {
|
||||
resetItemsTabIndex();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
listItem.firstElementChild.tabIndex = -1;
|
||||
});
|
||||
}
|
||||
|
||||
/** Focus element using a roving tab index */
|
||||
function focusElement(el) {
|
||||
resetItemsTabIndex();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
el.tabIndex = 0;
|
||||
setTimeout(() => {
|
||||
el.focus();
|
||||
}, 100); // Workaround, changing focus only works after a timeout
|
||||
}
|
||||
|
||||
function setFocusToTrigger() {
|
||||
setTimeout(() => trigger.focus(), 300);
|
||||
resetItemsTabIndex();
|
||||
}
|
||||
|
||||
function setFocusToFirstItem() {
|
||||
if (listItems.length > 0) {
|
||||
focusElement(listItems[0].firstElementChild);
|
||||
el.tabIndex = 0;
|
||||
setTimeout(() => {
|
||||
el.focus();
|
||||
}, 100); // Workaround, changing focus only works after a timeout
|
||||
}
|
||||
}
|
||||
|
||||
function setFocusToLastItem() {
|
||||
if (listItems.length > 0) {
|
||||
focusElement(listItems[listItems.length - 1].firstElementChild);
|
||||
function setFocusToTrigger() {
|
||||
setTimeout(() => trigger.focus(), 300);
|
||||
resetItemsTabIndex();
|
||||
}
|
||||
}
|
||||
|
||||
function setFocusToNextItem() {
|
||||
listItems.forEach((element, idx) => {
|
||||
// Check which item is currently focused
|
||||
if (element.firstElementChild === document.activeElement) {
|
||||
if (idx + 1 < listItems.length) {
|
||||
focusElement(listItems[idx + 1].firstElementChild);
|
||||
} else {
|
||||
// Loop around
|
||||
setFocusToFirstItem();
|
||||
}
|
||||
function setFocusToFirstItem() {
|
||||
if (listItems.length > 0) {
|
||||
focusElement(listItems[0].firstElementChild);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setFocusToPreviousItem() {
|
||||
listItems.forEach((element, idx) => {
|
||||
// Check which item is currently focused
|
||||
if (element.firstElementChild === document.activeElement) {
|
||||
if (idx > 0) {
|
||||
focusElement(listItems[idx - 1].firstElementChild);
|
||||
} else {
|
||||
setFocusToLastItem();
|
||||
}
|
||||
function setFocusToLastItem() {
|
||||
if (listItems.length > 0) {
|
||||
focusElement(listItems[listItems.length - 1].firstElementChild);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
function setFocusToNextItem() {
|
||||
listItems.forEach((element, idx) => {
|
||||
// Check which item is currently focused
|
||||
if (element.firstElementChild === shadowRoot.activeElement) {
|
||||
if (idx + 1 < listItems.length) {
|
||||
focusElement(listItems[idx + 1].firstElementChild);
|
||||
} else {
|
||||
// Loop around
|
||||
setFocusToFirstItem();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setFocusToPreviousItem() {
|
||||
listItems.forEach((element, idx) => {
|
||||
// Check which item is currently focused
|
||||
if (element.firstElementChild === shadowRoot.activeElement) {
|
||||
if (idx > 0) {
|
||||
focusElement(listItems[idx - 1].firstElementChild);
|
||||
} else {
|
||||
setFocusToLastItem();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
This handler is responsible for keyboard input when items inside the userbar are focused.
|
||||
It should only listen when the userbar is open.
|
||||
|
||||
|
@ -160,94 +172,96 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
- Shifting focus using the arrow / home / end keys.
|
||||
- Closing the menu when 'Escape' is pressed.
|
||||
*/
|
||||
function handleUserbarItemsKeyDown(event) {
|
||||
// Only handle keyboard input if the userbar is open
|
||||
if (trigger.getAttribute('aria-expanded') === 'true') {
|
||||
if (event.key === 'Escape') {
|
||||
hideUserbar();
|
||||
setFocusToTrigger();
|
||||
return false;
|
||||
}
|
||||
function handleUserbarItemsKeyDown(event) {
|
||||
// Only handle keyboard input if the userbar is open
|
||||
if (trigger.getAttribute('aria-expanded') === 'true') {
|
||||
if (event.key === 'Escape') {
|
||||
hideUserbar();
|
||||
setFocusToTrigger();
|
||||
return false;
|
||||
}
|
||||
|
||||
// List items are in focus, move focus if needed
|
||||
if (isFocusOnItems()) {
|
||||
// List items are in focus, move focus if needed
|
||||
if (isFocusOnItems()) {
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
setFocusToNextItem();
|
||||
return false;
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
setFocusToPreviousItem();
|
||||
return false;
|
||||
case 'Home':
|
||||
event.preventDefault();
|
||||
setFocusToFirstItem();
|
||||
return false;
|
||||
case 'End':
|
||||
event.preventDefault();
|
||||
setFocusToLastItem();
|
||||
return false;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleFocusChange(event) {
|
||||
// Is the focus is still in the menu? If so, don't to anything
|
||||
if (
|
||||
event.relatedTarget == null ||
|
||||
(event.relatedTarget && event.relatedTarget.closest('.w-userbar-nav'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// List items not in focus - the menu should close
|
||||
resetItemsTabIndex();
|
||||
hideUserbar();
|
||||
}
|
||||
|
||||
/**
|
||||
This handler is responsible for opening the userbar with the arrow keys
|
||||
if it's focused and not open yet. It should always be listening.
|
||||
*/
|
||||
function handleTriggerKeyDown(event) {
|
||||
// Check if the userbar is focused (but not open yet) and should be opened by keyboard input
|
||||
if (
|
||||
trigger === document.activeElement &&
|
||||
trigger.getAttribute('aria-expanded') === 'false'
|
||||
) {
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
setFocusToNextItem();
|
||||
return false;
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
setFocusToPreviousItem();
|
||||
return false;
|
||||
case 'Home':
|
||||
showUserbar(false);
|
||||
|
||||
// Workaround for focus bug
|
||||
// Needs extra delay to account for the userbar open animation. Otherwise won't focus properly.
|
||||
setTimeout(() => setFocusToLastItem(), 300);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
setFocusToFirstItem();
|
||||
return false;
|
||||
case 'End':
|
||||
event.preventDefault();
|
||||
setFocusToLastItem();
|
||||
return false;
|
||||
showUserbar(false);
|
||||
|
||||
// Workaround for focus bug
|
||||
// Needs extra delay to account for the userbar open animation. Otherwise won't focus properly.
|
||||
setTimeout(() => setFocusToFirstItem(), 300);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleFocusChange(event) {
|
||||
// Is the focus is still in the menu? If so, don't to anything
|
||||
if (
|
||||
event.relatedTarget == null ||
|
||||
(event.relatedTarget &&
|
||||
event.relatedTarget.closest('.wagtail-userbar-nav'))
|
||||
) {
|
||||
return;
|
||||
function sandboxClick(e2) {
|
||||
e2.stopPropagation();
|
||||
}
|
||||
// List items not in focus - the menu should close
|
||||
resetItemsTabIndex();
|
||||
hideUserbar();
|
||||
}
|
||||
|
||||
/**
|
||||
This handler is responsible for opening the userbar with the arrow keys
|
||||
if it's focused and not open yet. It should always be listening.
|
||||
*/
|
||||
function handleTriggerKeyDown(event) {
|
||||
// Check if the userbar is focused (but not open yet) and should be opened by keyboard input
|
||||
if (
|
||||
trigger === document.activeElement &&
|
||||
trigger.getAttribute('aria-expanded') === 'false'
|
||||
) {
|
||||
switch (event.key) {
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
showUserbar(false);
|
||||
|
||||
// Workaround for focus bug
|
||||
// Needs extra delay to account for the userbar open animation. Otherwise won't focus properly.
|
||||
setTimeout(() => setFocusToLastItem(), 300);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
showUserbar(false);
|
||||
|
||||
// Workaround for focus bug
|
||||
// Needs extra delay to account for the userbar open animation. Otherwise won't focus properly.
|
||||
setTimeout(() => setFocusToFirstItem(), 300);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
function clickOutside() {
|
||||
hideUserbar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sandboxClick(e2) {
|
||||
e2.stopPropagation();
|
||||
}
|
||||
|
||||
function clickOutside() {
|
||||
hideUserbar();
|
||||
}
|
||||
});
|
||||
customElements.define('wagtail-userbar', Userbar);
|
||||
|
|
|
@ -146,7 +146,8 @@ module.exports = {
|
|||
*/
|
||||
plugin(({ addBase }) => {
|
||||
addBase({
|
||||
':root': {
|
||||
/** Support for web components */
|
||||
':root, :host': {
|
||||
'--w-font-sans': fontFamily.sans.join(', '),
|
||||
'--w-font-mono': fontFamily.mono.join(', '),
|
||||
...generateColorVariables(colors),
|
||||
|
|
|
@ -70,6 +70,8 @@ This feature was developed by Sage Abdullah.
|
|||
* Fix horizontal positioning of rich text inline toolbar (Thibaud Colas)
|
||||
* Ensure that `DecimalBlock` correctly handles `None`, when `required=False`, values (Natarajan Balaji)
|
||||
* Close the userbar when clicking its toggle (Albina Starykova)
|
||||
* Add a border around the userbar menu in Windows high-contrast mode so it can be identified (Albina Starykova)
|
||||
* Make sure browser font resizing applies to the userbar (Albina Starykova)
|
||||
|
||||
### Documentation
|
||||
|
||||
|
@ -113,6 +115,8 @@ This feature was developed by Sage Abdullah.
|
|||
* Move `identity` JavaScript util into shared utils folder (LB (Ben Johnston))
|
||||
* Remove unnecessary declaration of function to determine URL query params, instead use `URLSearchParams` (Loveth Omokaro)
|
||||
* Update `tsconfig` to better support modern TypeScript development and clean up some code quality issues via Eslint (Loveth Omokaro)
|
||||
* Switch userbar to initialise a Web Component to avoid styling clashes (Albina Starykova)
|
||||
* Refactor userbar stylesheets to use the same CSS loading as the rest of the admin (Albina Starykova)
|
||||
|
||||
## Upgrade considerations
|
||||
|
||||
|
@ -152,3 +156,15 @@ Python code that uses the `InlinePanel` panel type is not affected by this chang
|
|||
### `WAGTAILADMIN_GLOBAL_PAGE_EDIT_LOCK` setting is now `WAGTAILADMIN_GLOBAL_EDIT_LOCK`
|
||||
|
||||
The `WAGTAILADMIN_GLOBAL_PAGE_EDIT_LOCK` setting has been renamed to [`WAGTAILADMIN_GLOBAL_EDIT_LOCK`](wagtailadmin_global_edit_lock).
|
||||
|
||||
### Wagtail userbar as a web component
|
||||
|
||||
The [`wagtailuserbar`](wagtailuserbar_tag) template tag now initialises the userbar as a [Web Component](https://developer.mozilla.org/en-US/docs/Web/Web_Components), with a `wagtail-userbar` custom element using shadow DOM to apply styles without any collisions with the host page.
|
||||
|
||||
For any site customising the position of the userbar, target the styles to `wagtail-userbar::part(userbar)` instead of `.wagtail-userbar`. For example:
|
||||
|
||||
```css
|
||||
wagtail-userbar::part(userbar) {
|
||||
bottom: 30px;
|
||||
}
|
||||
```
|
||||
|
|
|
@ -223,7 +223,7 @@ This tag provides a contextual flyout menu for logged-in users. The menu gives e
|
|||
|
||||
This tag may be used on standard Django views, without page object. The user bar will contain one item pointing to the admin.
|
||||
|
||||
We recommend putting the tag near the top of the `<body>` element to allow keyboard users to reach it. You should consider putting the tag after any `[skip links](https://webaim.org/techniques/skipnav/)` but before the navigation and main content of your page.
|
||||
We recommend putting the tag near the top of the `<body>` element to allow keyboard users to reach it. You should consider putting the tag after any [skip links](https://webaim.org/techniques/skipnav/)` but before the navigation and main content of your page.
|
||||
|
||||
```html+django
|
||||
{% load wagtailuserbar %}
|
||||
|
@ -254,11 +254,10 @@ By default, the User Bar appears in the bottom right of the browser window, inse
|
|||
The userbar can be positioned where it works best with your design. Alternatively, you can position it with a CSS rule in your own CSS files, for example:
|
||||
|
||||
```css
|
||||
.wagtail-userbar {
|
||||
top: 200px !important;
|
||||
left: 10px !important;
|
||||
wagtail-userbar::part(userbar) {
|
||||
bottom: 30px;
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
## Varying output between preview and live
|
||||
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
@use 'sass:math';
|
||||
@use 'sass:string';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
||||
@import '../../../../../client/scss/settings';
|
||||
@import '../../../../../client/scss/tools';
|
||||
|
||||
|
@ -12,15 +15,10 @@
|
|||
$size-home-button: 3.5em;
|
||||
$position: 2em;
|
||||
$width-arrow: 0.6em;
|
||||
$box-shadow-props: 0 0 1px 0 rgba(107, 214, 230, 1);
|
||||
$box-shadow-props: 0 0 1px 0 rgba(107, 214, 230, 1),
|
||||
0 1px 10px 0 rgba(107, 214, 230, 0.7);
|
||||
$max-items: 12;
|
||||
$userbar-radius: 6px;
|
||||
$color-black: #000;
|
||||
$color-white: #fff;
|
||||
$color-grey-1: #262626;
|
||||
|
||||
// Classnames will start with this parameter, eg .wagtail-
|
||||
$namespace: 'wagtail';
|
||||
|
||||
// Possible positions for the userbar to exist in. These are set through the
|
||||
// {% wagtailuserbar 'bottom-left' %} template tag.
|
||||
|
@ -50,23 +48,16 @@ $positions: (
|
|||
// =============================================================================
|
||||
// Wagtail userbar proper
|
||||
// =============================================================================
|
||||
.#{$namespace}-userbar-reset {
|
||||
all: initial;
|
||||
// Copy our font sans variable so it can be used without Tailwind.
|
||||
--w-font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui,
|
||||
Roboto, 'Helvetica Neue', Arial, sans-serif, Apple Color Emoji,
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
|
||||
.#{$namespace}-userbar {
|
||||
.w-userbar {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
// stylelint-disable-next-line declaration-no-important
|
||||
font-size: initial !important;
|
||||
font-size: initial;
|
||||
line-height: initial;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
// Stop hiding the userbar once stylesheets are loaded.
|
||||
// stylelint-disable-next-line declaration-no-important
|
||||
display: block !important;
|
||||
border: 0;
|
||||
width: auto;
|
||||
height: auto;
|
||||
|
@ -77,34 +68,32 @@ $positions: (
|
|||
}
|
||||
|
||||
@media print {
|
||||
.#{$namespace}-userbar {
|
||||
.w-userbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// stylelint-disable declaration-no-important
|
||||
.#{$namespace}-userbar-trigger {
|
||||
all: initial;
|
||||
.w-userbar-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: $size-home-button;
|
||||
height: $size-home-button;
|
||||
margin: 0 !important;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background-color: $color-white;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 50%;
|
||||
color: $color-black;
|
||||
padding: 0 !important;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
box-shadow: $box-shadow-props, 0 1px 10px 0 rgba(107, 214, 230, 0.7);
|
||||
box-shadow: $box-shadow-props;
|
||||
transition: all 0.2s ease-in-out;
|
||||
font-size: 16px;
|
||||
text-decoration: none !important;
|
||||
font-size: 1rem;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
|
||||
.#{$namespace}-userbar-help-text {
|
||||
.w-userbar-help-text {
|
||||
// Visually hide the help text
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
|
@ -115,9 +104,9 @@ $positions: (
|
|||
width: 1px;
|
||||
}
|
||||
|
||||
.#{$namespace}-icon:before {
|
||||
.w-icon:before {
|
||||
transition: color 0.2s ease;
|
||||
font-size: 32px;
|
||||
font-size: 2rem;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -127,8 +116,7 @@ $positions: (
|
|||
}
|
||||
}
|
||||
|
||||
.#{$namespace}-userbar-items {
|
||||
all: revert;
|
||||
.w-userbar-items {
|
||||
display: block;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
|
@ -136,17 +124,17 @@ $positions: (
|
|||
min-width: 210px;
|
||||
visibility: hidden;
|
||||
font-family: $font-sans;
|
||||
font-size: 14px;
|
||||
font-size: 0.875rem;
|
||||
padding-inline-start: 0;
|
||||
text-decoration: none;
|
||||
|
||||
.#{$namespace}-userbar.is-active & {
|
||||
.w-userbar.is-active & {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
// Arrow
|
||||
.#{$namespace}-userbar-items:after {
|
||||
.w-userbar-items:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 0;
|
||||
|
@ -157,29 +145,28 @@ $positions: (
|
|||
transition-timing-function: cubic-bezier(0.55, 0, 0.1, 1);
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
transition: none !important;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.#{$namespace}-userbar.is-active & {
|
||||
.w-userbar.is-active & {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition-delay: 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.#{$namespace}-userbar-nav {
|
||||
background: transparent !important;
|
||||
.w-userbar-nav {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
margin: 0 !important;
|
||||
display: block !important;
|
||||
margin: 0;
|
||||
display: block;
|
||||
|
||||
.#{$namespace}-action {
|
||||
.w-action {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.#{$namespace}-userbar__item {
|
||||
all: revert;
|
||||
.w-userbar__item {
|
||||
margin: 0;
|
||||
background-color: $color-grey-1;
|
||||
opacity: 0;
|
||||
|
@ -187,14 +174,14 @@ $positions: (
|
|||
transition-duration: 0.125s;
|
||||
transition-timing-function: cubic-bezier(0.55, 0, 0.1, 1);
|
||||
font-family: $font-sans;
|
||||
font-size: 16px !important;
|
||||
text-decoration: none !important;
|
||||
font-size: 1rem;
|
||||
text-decoration: none;
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
transition: none !important;
|
||||
transition: none;
|
||||
|
||||
// Force disable transitions for all items
|
||||
transition-delay: 0s !important;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
|
@ -224,14 +211,14 @@ $positions: (
|
|||
}
|
||||
|
||||
a,
|
||||
.#{$namespace}-action {
|
||||
.w-action {
|
||||
color: $color-white;
|
||||
display: block;
|
||||
text-decoration: none !important;
|
||||
transform: none !important;
|
||||
transition: none !important;
|
||||
margin: 0 !important;
|
||||
font-size: 14px !important;
|
||||
text-decoration: none;
|
||||
transform: none;
|
||||
transition: none;
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
|
@ -250,7 +237,7 @@ $positions: (
|
|||
}
|
||||
}
|
||||
|
||||
.#{$namespace}-icon {
|
||||
.w-icon {
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
|
@ -263,7 +250,7 @@ $positions: (
|
|||
|
||||
a,
|
||||
button {
|
||||
font-size: 14px !important;
|
||||
font-size: 0.875rem;
|
||||
text-align: start;
|
||||
padding: 0.8em;
|
||||
}
|
||||
|
@ -279,9 +266,17 @@ $positions: (
|
|||
//Media for Windows High Contrast
|
||||
|
||||
@media (forced-colors: $media-forced-colours) {
|
||||
.#{$namespace}-userbar-icon {
|
||||
.w-userbar-icon {
|
||||
fill: $system-color-link-text;
|
||||
}
|
||||
|
||||
.w-userbar__item {
|
||||
border: 1px solid $system-color-button-text;
|
||||
}
|
||||
|
||||
.w-userbar-items::after {
|
||||
border: $width-arrow solid Canvas;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
@ -293,17 +288,17 @@ $positions: (
|
|||
$horizontal: map.get($attrs, horizontal);
|
||||
$arrow: map.get($attrs, arrow);
|
||||
|
||||
.#{$namespace}-userbar--#{$pos} {
|
||||
.w-userbar--#{$pos} {
|
||||
#{$vertical}: $position;
|
||||
#{$horizontal}: $position;
|
||||
|
||||
.#{$namespace}-userbar-items {
|
||||
.w-userbar-items {
|
||||
#{$vertical}: 100%;
|
||||
#{$horizontal}: 0;
|
||||
padding-#{$vertical}: $width-arrow * 2;
|
||||
}
|
||||
|
||||
.#{$namespace}-userbar-nav .#{$namespace}-userbar__item {
|
||||
.w-userbar-nav .w-userbar__item {
|
||||
@if $vertical == 'bottom' {
|
||||
transform: translateY(1em);
|
||||
} @else {
|
||||
|
@ -311,21 +306,21 @@ $positions: (
|
|||
}
|
||||
}
|
||||
|
||||
.#{$namespace}-userbar-items:after {
|
||||
.w-userbar-items:after {
|
||||
#{$vertical}: 2px;
|
||||
#{$horizontal}: math.div($size-home-button, 2) -
|
||||
math.div($width-arrow, 2);
|
||||
border-#{$arrow}-color: $color-grey-1;
|
||||
|
||||
@if $vertical == 'bottom' {
|
||||
transform: translateY(-$width-arrow);
|
||||
transform: translateY($width-arrow);
|
||||
}
|
||||
@if $vertical == 'top' {
|
||||
transform: translateY($width-arrow);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active .#{$namespace}-userbar__item {
|
||||
&.is-active .w-userbar__item {
|
||||
@for $i from 1 through $max-items {
|
||||
@if $vertical == 'bottom' {
|
||||
&:nth-last-child(#{$i}) {
|
||||
|
@ -348,7 +343,7 @@ $positions: (
|
|||
// =============================================================================
|
||||
|
||||
// Active state for the list items comes last.
|
||||
.#{$namespace}-userbar.is-active .#{$namespace}-userbar__item {
|
||||
.w-userbar.is-active .w-userbar__item {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{% load wagtailadmin_tags i18n %}
|
||||
<!-- Wagtail user bar embed code -->
|
||||
<div class="wagtail-userbar-reset">
|
||||
<div class="wagtail-userbar wagtail-userbar--{{ position|default:'bottom-right' }}" data-wagtail-userbar>
|
||||
<template id="wagtail-userbar-template">
|
||||
<div class="w-userbar w-userbar--{{ position|default:'bottom-right' }}" data-wagtail-userbar part="userbar">
|
||||
<link rel="stylesheet" href="{% versioned_static 'wagtailadmin/css/userbar.css' %}">
|
||||
<div class="wagtail-userbar-nav">
|
||||
<button aria-controls="wagtail-userbar-items" aria-haspopup="true" class="wagtail-userbar-trigger" id="wagtail-userbar-trigger" data-wagtail-userbar-trigger>
|
||||
<div class="w-userbar-nav">
|
||||
<button aria-controls="wagtail-userbar-items" aria-haspopup="true" class="w-userbar-trigger" id="wagtail-userbar-trigger" data-wagtail-userbar-trigger>
|
||||
{% block branding_logo %}
|
||||
<div style="display: none">
|
||||
<svg>
|
||||
|
@ -19,19 +19,20 @@
|
|||
</svg>
|
||||
</div>
|
||||
{% comment %} Intentionally not using the icon template tag to show as SVG only {% endcomment %}
|
||||
<svg class="wagtail-userbar-icon" aria-hidden="true">
|
||||
<svg class="w-userbar-icon" aria-hidden="true">
|
||||
<use href="#icon-wagtail-icon"></use>
|
||||
</svg>
|
||||
{% endblock %}
|
||||
<span class="wagtail-userbar-help-text">{% trans 'View Wagtail quick actions' %}</span>
|
||||
<span class="w-userbar-help-text">{% trans 'View Wagtail quick actions' %}</span>
|
||||
</button>
|
||||
<ul aria-labelledby="wagtail-userbar-trigger" class="wagtail-userbar-items" id="wagtail-userbar-items" role="menu">
|
||||
<ul aria-labelledby="wagtail-userbar-trigger" class="w-userbar-items" id="wagtail-userbar-items" role="menu">
|
||||
{% for item in items %}
|
||||
{{ item|safe }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<script src="{% versioned_static 'wagtailadmin/js/userbar.js' %}"></script>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<wagtail-userbar></wagtail-userbar>
|
||||
<script src="{% versioned_static 'wagtailadmin/js/userbar.js' %}"></script>
|
||||
<!-- end Wagtail user bar embed code -->
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
{% load i18n wagtailadmin_tags %}
|
||||
|
||||
{% block item_content %}
|
||||
<a href="{% url 'wagtailadmin_home' %}" target="_parent" class="wagtail-userbar-link" role="menuitem">
|
||||
{% icon name="wagtail-icon" class_name="wagtail-action-icon" %}
|
||||
<a href="{% url 'wagtailadmin_home' %}" target="_parent" role="menuitem">
|
||||
{% icon name="wagtail-icon" class_name="w-action-icon" %}
|
||||
{% trans 'Go to Wagtail admin' %}
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<li class="wagtail-userbar__item {% block item_classes %}{% endblock %}" role="presentation">
|
||||
<li class="w-userbar__item {% block item_classes %}{% endblock %}" role="presentation">
|
||||
{% block item_content %}{% endblock %}
|
||||
</li>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
{% block item_content %}
|
||||
<a href="{% url 'wagtailadmin_pages:add_subpage' self.page.id %}" target="_parent" role="menuitem">
|
||||
{% icon name="plus" class_name="wagtail-action-icon" %}
|
||||
{% icon name="plus" class_name="w-action-icon" %}
|
||||
{% trans 'Add a child page' %}
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<form action="{% url 'wagtailadmin_pages:approve_moderation' self.revision.id %}" target="_parent" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" value="{% trans 'Approve' %}" class="button" role="menuitem">
|
||||
{% icon name="tick" class_name="wagtail-action-icon" %}
|
||||
{% icon name="tick" class_name="w-action-icon" %}
|
||||
{% trans 'Approve' %}
|
||||
</button>
|
||||
</form>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
{% block item_content %}
|
||||
<a href="{% url 'wagtailadmin_pages:edit' self.page.id %}" target="_parent" role="menuitem">
|
||||
{% icon name="edit" class_name="wagtail-action-icon" %}
|
||||
{% icon name="edit" class_name="w-action-icon" %}
|
||||
{% trans 'Edit this page' %}
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
{% block item_content %}
|
||||
<a href="{% url 'wagtailadmin_explore' self.parent_page.id %}" target="_parent" role="menuitem">
|
||||
{% icon name="folder-open-inverse" class_name="wagtail-action-icon" %}
|
||||
{% icon name="folder-open-inverse" class_name="w-action-icon" %}
|
||||
{% trans 'Show in Explorer' %}
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<form action="{% url 'wagtailadmin_pages:reject_moderation' self.revision.id %}" target="_parent" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" value="{% trans 'Reject' %}" class="button" role="menuitem">
|
||||
{% icon name="cross" class_name="wagtail-action-icon" %}
|
||||
{% icon name="cross" class_name="w-action-icon" %}
|
||||
{% trans 'Reject' %}
|
||||
</button>
|
||||
</form>
|
||||
|
|
|
@ -197,7 +197,7 @@ class TestUserbarAddLink(TestCase, WagtailTestUtils):
|
|||
)
|
||||
needle = f"""
|
||||
<a href="{expected_url}" target="_parent" role="menuitem">
|
||||
<svg class="icon icon-plus wagtail-action-icon" aria-hidden="true">
|
||||
<svg class="icon icon-plus w-action-icon" aria-hidden="true">
|
||||
<use href="#icon-plus"></use>
|
||||
</svg>
|
||||
Add a child page
|
||||
|
@ -238,7 +238,7 @@ class TestUserbarModeration(TestCase, WagtailTestUtils):
|
|||
expected_approve_html = """
|
||||
<form action="/admin/pages/moderation/{}/approve/" target="_parent" method="post">
|
||||
<input type="hidden" name="csrfmiddlewaretoken">
|
||||
<div class="wagtail-action">
|
||||
<div class="w-action">
|
||||
<input type="submit" value="Approve" class="button" />
|
||||
</div>
|
||||
</form>
|
||||
|
@ -250,7 +250,7 @@ class TestUserbarModeration(TestCase, WagtailTestUtils):
|
|||
expected_reject_html = """
|
||||
<form action="/admin/pages/moderation/{}/reject/" target="_parent" method="post">
|
||||
<input type="hidden" name="csrfmiddlewaretoken">
|
||||
<div class="wagtail-action">
|
||||
<div class="w-action">
|
||||
<input type="submit" value="Reject" class="button" />
|
||||
</div>
|
||||
</form>
|
||||
|
|
Ładowanie…
Reference in New Issue