0
0
mirror of https://github.com/wagtail/wagtail.git synced 2024-11-21 18:09:02 +01:00

Page editor underline tabs (#8266)

Co-authored-by: Thibaud Colas <thibaudcolas@gmail.com>
This commit is contained in:
Steve Stein 2022-04-12 09:11:08 -06:00 committed by GitHub
parent ec70921e52
commit 629ced01ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 937 additions and 753 deletions

View File

@ -112,7 +112,11 @@ module.exports = {
globals: { $: 'readonly' },
},
{
files: ['wagtail/**/**'],
files: [
'wagtail/**/**',
'client/src/entrypoints/documents/document-chooser-modal.js',
'client/src/entrypoints/images/image-chooser-modal.js',
],
globals: {
$: 'readonly',
addMessage: 'readonly',

View File

@ -49,6 +49,7 @@ Changelog
* Add the ability for choices to be separated by new lines instead of just commas within the form builder, commas will still be supported if used (Abdulmajeed Isa)
* Add internationalisation UI to modeladmin (Andrés Martano)
* Support chunking in `PageQuerySet.specific()` to reduce memory consumption (Andy Babic)
* Implement new tabs design across the admin interface (Steven Steinwand)
* Fix: When using `simple_translations` ensure that the user is redirected to the page edit view when submitting for a single locale (Mitchel Cabuloy)
* Fix: When previewing unsaved changes to `Form` pages, ensure that all added fields are correctly shown in the preview (Joshua Munn)
* Fix: When Documents (e.g. PDFs) have been configured to be served inline via `WAGTAILDOCS_CONTENT_TYPES` & `WAGTAILDOCS_INLINE_CONTENT_TYPES` ensure that the filename is correctly set in the `Content-Disposition` header so that saving the files will use the correct filename (John-Scott Atlakson)
@ -68,6 +69,7 @@ Changelog
* Fix: Page copy in Wagtail admin ignores `exclude_fields_in_copy` (John-Scott Atlakson)
* Fix: Translation key `IntegrityError` when publishing pages with translatable `Orderable`s that were copied without being published (Kalob Taulien, Dan Braghis)
* Fix: Ignore `GenericRelation` when copying pages (John-Scott Atlakson)
* Fix: Implement ARIA tabs markup and keyboards interactions for admin tabs (Steven Steinwand)
2.16.2 (11.04.2022)

View File

@ -69,28 +69,6 @@ header {
margin-bottom: 0;
}
&.tab-merged {
padding-inline-start: $desktop-nice-padding;
padding-inline-end: $desktop-nice-padding;
.right:last-child {
padding-inline-end: 0;
}
@include media-breakpoint-down(xs) {
.breadcrumb {
padding-inline-start: calc(#{$desktop-nice-padding} - 8px);
}
}
@include media-breakpoint-up(sm) {
.breadcrumb {
margin-inline-start: -$desktop-nice-padding;
margin-inline-end: -$desktop-nice-padding;
padding-inline-start: math.div($desktop-nice-padding, 2);
}
}
}
&.header-with-breadcrumb {
padding-top: 0;
@ -103,7 +81,6 @@ header {
}
}
&.tab-merged,
&.no-border {
border: 0;

View File

@ -124,10 +124,6 @@ $zindex-modal-background: 500;
h1 {
@apply w-text-white;
}
&.tab-merged {
padding-inline-start: 1.6em;
}
}
.header-title {
@ -135,22 +131,12 @@ $zindex-modal-background: 500;
padding-inline-start: 0 !important;
margin-inline-start: -36px;
}
.tab-merged .header-title {
margin-inline-start: 0;
}
}
@include media-breakpoint-up(sm) {
.modal-dialog {
padding: 0 0 2em $menu-width;
}
.modal-body {
header.tab-merged {
padding-inline-start: $desktop-nice-padding;
}
}
}
@include media-breakpoint-up(xl) {

View File

@ -1,111 +1,4 @@
.tab-nav {
@apply w-bg-grey-50;
@include row();
padding: 0;
li {
list-style-type: none;
width: 33%;
float: left;
padding: 0;
position: relative;
margin-inline-end: 2px;
&:first-of-type {
padding-inline-start: $desktop-nice-padding;
margin-inline-start: 0;
}
}
h2 {
margin: 0;
font-size: inherit;
}
a {
// border-top 0.3em is temporary until the new tab design is implemented
@apply w-bg-primary w-border-t-[0.3em] w-border-primary-200;
@include transition(border-color 0.2s ease);
font-weight: 600;
text-decoration: none;
display: block;
padding: 0.6em 0.7em 0.8em;
color: $color-white;
max-height: 1.44em;
overflow: hidden;
&:hover {
color: $color-white;
border-top-color: rgba(0, 0, 0, 0.35);
}
}
a.errors {
&:after {
border-radius: 50px;
box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.1);
position: absolute;
// Remove once we drop support for Safari 13.
// stylelint-disable-next-line property-disallowed-list
right: -0.5em;
inset-inline-end: -0.5em;
top: -0.5em;
z-index: 5;
min-width: 0.9em;
color: $color-white;
background: $color-red;
content: attr(data-count);
padding: 0 0.3em;
line-height: 1.4em;
text-align: center;
font-size: 0.8em;
}
}
li.active a {
box-shadow: none;
color: $color-grey-1;
background-color: $color-white;
border-top: 0.3em solid $color-grey-1;
}
// For cases where tab-nav should merge with header
.page-editor & {
&.merged {
@apply w-pt-2 sm:w-pt-4;
}
}
&.merged {
@apply w-mt-0;
}
li.right {
float: right;
margin-inline-end: 0;
margin-inline-start: 2px;
}
li.wide {
width: unset;
}
.right {
max-height: 1.44em;
overflow: visible;
}
}
.tab-content {
> section {
display: none;
padding-top: 1em;
&.active {
display: block;
}
}
.page-locked & {
cursor: not-allowed;
user-select: none;
@ -116,53 +9,72 @@
}
}
@include media-breakpoint-up(sm) {
.tab-nav {
// For cases where tab-nav should merge with header
&.merged {
@apply w-bg-grey-50;
}
.w-tabs {
&__wrapper {
@apply w-mb-10 w-overflow-x-auto w-scrollbar-thin;
}
li {
width: auto;
padding: 0;
}
&__list {
@include nice-padding();
@apply w-flex w-my-[3px] w-space-x-6 w-border-b w-border-grey-100 w-w-fit;
}
a {
padding-inline-start: $mobile-nice-padding;
padding-inline-end: $mobile-nice-padding;
}
&__tab {
@apply w-label-3
w-box-border
w-inline-flex
w-text-grey-400
hover:w-text-primary
w-whitespace-nowrap
w-py-4
w-font-medium
w-relative
after:w-block
after:w-w-0
after:w-h-[2px]
after:w-bg-primary
after:w-absolute
after:w-left-0
after:-w-bottom-px
after:w-transition-all
motion-reduce:after:w-transition-none
hover:after:w-w-full;
li.settings a {
padding-inline-start: 2em;
padding-inline-end: 2em;
&[aria-selected='true'] {
@apply after:w-w-full w-text-primary;
}
}
.modal-content .tab-nav li {
padding: 0;
min-width: 0;
&__errors {
@apply w-hidden
w-box-border
w-w-4
w-h-4
w-text-[0.75rem]
w-flex
w-justify-center
w-items-center
w-font-bold
w-bg-critical-200
w-text-white
w-border
w-border-white
w-rounded-full
w-absolute
w-top-[0.4375rem]
-w-right-[0.9375rem];
&:first-of-type {
padding-inline-start: $desktop-nice-padding;
&--active {
@apply w-flex;
}
}
// Optional animate attr for tabs to animate in
&[data-tabs-animate] &__panel {
@apply motion-reduce:w-transition-none w-transition w-duration-150 w-translate-y-1 w-opacity-0;
&.animate-in {
@apply w-translate-y-0 w-opacity-100;
}
}
}
@include media-breakpoint-down(xs) {
// To allow tabs on the edit page to be editable
.tab-nav li:first-of-type {
padding-inline-start: 1.6em;
}
.tab-nav li {
width: auto;
}
}
// Media for Windows High Contrast
@media (forced-colors: $media-forced-colours) {
.tab-nav li.active a {
border-bottom: 0.3em solid $system-color-link-text;
}
}

View File

@ -154,7 +154,6 @@ These are classes that provide overrides.
@import 'overrides/utilities.dropdowns';
@import 'overrides/utilities.focus';
@import 'overrides/utilities.visuallyhidden';
@import 'overrides/utilities.scrollbars';
// Legacy utilities
@import 'overrides/utilities.legacy';

View File

@ -45,3 +45,8 @@ img {
border-width: 0;
border-style: solid;
}
::before,
::after {
--tw-content: '';
}

View File

@ -1,25 +0,0 @@
.u-scrollbar-thin {
// Scrollbar styling for firefox
// https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color
scrollbar-color: theme('colors.grey.100') theme('colors.white.DEFAULT');
scrollbar-width: thin;
//Custom scrollbar styling for Safari & Chrome Windows / Mac / Android.
&::-webkit-scrollbar {
width: 5px;
height: 5px;
}
&::-webkit-scrollbar-button {
@apply w-hidden;
// Hide the scrollbar arrows on windows
}
&::-webkit-scrollbar-thumb {
@apply w-bg-grey-200 w-rounded-sm;
}
&::-webkit-scrollbar-track {
@apply w-bg-white;
}
}

View File

@ -202,7 +202,7 @@ export const Menu: React.FunctionComponent<MenuProps> = ({
};
const className =
'sidebar-main-menu u-scrollbar-thin' +
'sidebar-main-menu w-scrollbar-thin' +
(accountSettingsOpen ? ' sidebar-main-menu--open-footer' : '');
return (

View File

@ -4,7 +4,7 @@ exports[`Menu should render with the minimum required props 1`] = `
<Fragment>
<nav
aria-label="Main menu"
className="sidebar-main-menu u-scrollbar-thin"
className="sidebar-main-menu w-scrollbar-thin"
>
<ul
className="sidebar-main-menu__list"

View File

@ -289,7 +289,9 @@ window.comments = (() => {
.forEach(initAddCommentButton);
// Attach the commenting app to the tab navigation, if it exists
const tabNavElement = formElement.querySelector('[data-tab-nav]');
const tabNavElement = formElement.querySelector(
'[data-tabs] [role="tablist"]',
);
if (tabNavElement) {
commentApp.setCurrentTab(tabNavElement.dataset.currentTab);
tabNavElement.addEventListener('switch', (e) => {

View File

@ -11,6 +11,7 @@ function addMessage(status, text) {
clearTimeout(addMsgTimeout);
}, 100);
}
window.addMessage = addMessage;
function escapeHtml(text) {
@ -24,6 +25,7 @@ function escapeHtml(text) {
return text.replace(/[&<>"']/g, (char) => map[char]);
}
window.escapeHtml = escapeHtml;
function initTagField(id, autocompleteUrl, options) {
@ -45,6 +47,7 @@ function initTagField(id, autocompleteUrl, options) {
$('#' + id).tagit(finalOptions);
}
window.initTagField = initTagField;
/*
@ -218,6 +221,7 @@ function enableDirtyFormCheck(formSelector, options) {
}
});
}
window.enableDirtyFormCheck = enableDirtyFormCheck;
$(() => {
@ -240,7 +244,7 @@ $(() => {
});
/* Functions that need to run/rerun when active tabs are changed */
$(document).on('shown.bs.tab', () => {
document.addEventListener('tab-changed', () => {
// Resize autosize textareas
// eslint-disable-next-line func-names
$('textarea[data-autosize-on]').each(function () {
@ -249,42 +253,6 @@ $(() => {
});
});
/* tabs */
const showTab = (tabButtonElem) => {
$(tabButtonElem).tab('show');
// Update data-current-tab attribute on the [data-tab-nav] element
const tabNavElem = tabButtonElem.closest('[data-tab-nav]');
tabNavElem.dataset.currentTab = tabButtonElem.dataset.tab;
// Trigger switch event
tabNavElem.dispatchEvent(
new CustomEvent('switch', { detail: { tab: tabButtonElem.dataset.tab } }),
);
};
if (window.location.hash) {
/* look for a tab matching the URL hash and activate it if found */
const cleanedHash = window.location.hash.replace(/[^\w\-#]/g, '');
const tab = document.querySelector(
'a[href="' + cleanedHash + '"][data-tab]',
);
if (tab) showTab(tab);
}
// eslint-disable-next-line func-names
$(document).on('click', '[data-tab-nav] a', function (e) {
e.preventDefault();
showTab(this);
window.history.replaceState(null, null, $(this).attr('href'));
});
// eslint-disable-next-line func-names
$(document).on('click', '.tab-toggle', function (e) {
e.preventDefault();
$('[data-tab-nav] a[href="' + $(this).attr('href') + '"]').trigger('click');
});
// eslint-disable-next-line func-names
$('.dropdown').each(function () {
const $dropdown = $(this);

View File

@ -255,9 +255,11 @@ function initErrorDetection() {
// now identify them on each tab
// eslint-disable-next-line guard-for-in
for (const index in errorSections) {
$('[data-tab-nav] a[href="#' + index + '"]')
.addClass('errors')
.attr('data-count', errorSections[index]);
$('[data-tabs] a[href="#' + index + '"]')
.find('.w-tabs__errors')
.addClass('w-tabs__errors--active')
.find('.w-tabs__errors-count')
.text(errorSections[index]);
}
}

View File

@ -1,8 +1,9 @@
import $ from 'jquery';
import { initTabs } from '../../includes/tabs';
const ajaxifyTaskCreateTab = (modal, jsonData) => {
$(
'#new a.task-type-choice, #new a.choose-different-task-type',
'#tab-new a.task-type-choice, #tab-new a.choose-different-task-type',
modal.body,
).on('click', function onClickNew() {
modal.loadUrl(this.href);
@ -28,7 +29,7 @@ const ajaxifyTaskCreateTab = (modal, jsonData) => {
errorThrown +
' - ' +
response.status;
$('#new', modal.body).append(
$('#tab-new', modal.body).append(
'<div class="help-block help-critical">' +
'<strong>' +
jsonData.error_label +
@ -59,12 +60,6 @@ const TASK_CHOOSER_MODAL_ONLOAD_HANDLERS = {
fetchResults(this.href);
return false;
});
$('a.create-one-now').on('click', (e) => {
// Select upload form tab
$('a[href="#new"]').tab('show');
e.preventDefault();
});
}
const searchForm = $('form.task-search', modal.body);
@ -118,13 +113,16 @@ const TASK_CHOOSER_MODAL_ONLOAD_HANDLERS = {
const wait = setTimeout(search, 50);
$(this).data('timer', wait);
});
// Reinitialize tabs to hook up tab event listeners in the modal
initTabs();
},
task_chosen(modal, jsonData) {
modal.respond('taskChosen', jsonData.result);
modal.close();
},
reshow_create_tab(modal, jsonData) {
$('#new', modal.body).html(jsonData.htmlFragment);
$('#tab-new', modal.body).html(jsonData.htmlFragment);
ajaxifyTaskCreateTab(modal, jsonData);
},
};

View File

@ -2,6 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { Icon, Portal, initUpgradeNotification, initSkipLink } from '../..';
import { initModernDropdown, initTooltips } from '../../includes/initTooltips';
import { initTabs } from '../../includes/tabs';
if (process.env.NODE_ENV === 'development') {
// Run react-axe in development only, so it does not affect performance
@ -24,5 +25,6 @@ document.addEventListener('DOMContentLoaded', () => {
initUpgradeNotification();
initTooltips();
initModernDropdown();
initTabs();
initSkipLink();
});

View File

@ -1,3 +1,6 @@
import $ from 'jquery';
import { initTabs } from '../../includes/tabs';
function ajaxifyDocumentUploadForm(modal) {
$('form.document-upload', modal.body).on('submit', function () {
var formdata = new FormData(this);
@ -17,7 +20,7 @@ function ajaxifyDocumentUploadForm(modal) {
errorThrown +
' - ' +
response.status;
$('#upload', modal.body).append(
$('#tab-upload', modal.body).append(
'<div class="help-block help-critical">' +
'<strong>' +
jsonData.error_label +
@ -67,7 +70,7 @@ function ajaxifyDocumentUploadForm(modal) {
});
}
DOCUMENT_CHOOSER_MODAL_ONLOAD_HANDLERS = {
window.DOCUMENT_CHOOSER_MODAL_ONLOAD_HANDLERS = {
chooser: function (modal, jsonData) {
function ajaxifyLinks(context) {
$('a.document-choice', context).on('click', function () {
@ -87,8 +90,6 @@ DOCUMENT_CHOOSER_MODAL_ONLOAD_HANDLERS = {
$('#id_document-chooser-upload-collection').val(collectionId);
}
// Select upload form tab
$('a[href="#upload"]').tab('show');
e.preventDefault();
});
}
@ -134,13 +135,17 @@ DOCUMENT_CHOOSER_MODAL_ONLOAD_HANDLERS = {
});
$('#collection_chooser_collection_id').on('change', search);
// Reinitialize tabs to hook up tab event listeners in the modal
initTabs();
},
document_chosen: function (modal, jsonData) {
modal.respond('documentChosen', jsonData.result);
modal.close();
},
reshow_upload_form: function (modal, jsonData) {
$('#upload', modal.body).html(jsonData.htmlFragment);
$('#tab-upload', modal.body).replaceWith(jsonData.htmlFragment);
initTabs();
ajaxifyDocumentUploadForm(modal);
},
};

View File

@ -1,3 +1,6 @@
import $ from 'jquery';
import { initTabs } from '../../includes/tabs';
function ajaxifyImageUploadForm(modal) {
$('form.image-upload', modal.body).on('submit', function () {
var formdata = new FormData(this);
@ -29,7 +32,7 @@ function ajaxifyImageUploadForm(modal) {
errorThrown +
' - ' +
response.status;
$('#upload').append(
$('#tab-upload').append(
'<div class="help-block help-critical">' +
'<strong>' +
jsonData.error_label +
@ -80,7 +83,7 @@ function ajaxifyImageUploadForm(modal) {
});
}
IMAGE_CHOOSER_MODAL_ONLOAD_HANDLERS = {
window.IMAGE_CHOOSER_MODAL_ONLOAD_HANDLERS = {
chooser: function (modal, jsonData) {
var searchForm = $('form.image-search', modal.body);
var searchUrl = searchForm.attr('action');
@ -143,13 +146,17 @@ IMAGE_CHOOSER_MODAL_ONLOAD_HANDLERS = {
});
return false;
});
// Reinitialize tabs to hook up tab event listeners in the modal
initTabs();
},
image_chosen: function (modal, jsonData) {
modal.respond('imageChosen', jsonData.result);
modal.close();
},
reshow_upload_form: function (modal, jsonData) {
$('#upload', modal.body).replaceWith(jsonData.htmlFragment);
$('#tab-upload', modal.body).replaceWith(jsonData.htmlFragment);
initTabs();
ajaxifyImageUploadForm(modal);
},
select_format: function (modal) {

View File

@ -95,4 +95,5 @@ function createImageChooser(id) {
return chooser;
}
window.createImageChooser = createImageChooser;

View File

@ -1,5 +1,8 @@
export default function initCollapsibleBreadcrumbs() {
const breadcrumbsContainer = document.querySelector('[data-breadcrumb-next]');
if (!breadcrumbsContainer) {
return;
}
const breadcrumbsToggle = breadcrumbsContainer.querySelector(
'[data-toggle-breadcrumbs]',
);

327
client/src/includes/tabs.js Normal file
View File

@ -0,0 +1,327 @@
/**
* All tabs and tab content must be nested in an element with the data-tab attribute
* All tab buttons need the role="tab" attr and an href with the tab content ID
* Tab contents need to have the role="tabpanel" attribute and and ID attribute that matches the href of the tab link.
* Tab buttons should also be wrapped in an element with the role="tablist" attribute
*/
class Tabs {
constructor(node) {
this.tabContainer = node;
this.tabButtons = this.tabContainer.querySelectorAll('[role="tab"]');
this.tabList = this.tabContainer.querySelector('[role="tablist"]');
this.tabPanels = this.tabContainer.querySelectorAll('[role="tabpanel"]');
this.keydownEventListener = this.keydownEventListener.bind(this);
// Tab Options - Add these data attributes along side the data-tabs attribute
// Use this to enable fade-in animations on tab select
this.animate = this.tabContainer.hasAttribute('data-tabs-animate');
// Disable url hash from appearing on tab select (normally used in modals)
this.disableURL = this.tabContainer.hasAttribute('data-tabs-disable-url');
this.state = {
// Tab Settings
activeTabID: '',
transition: 150,
initialPageLoad: true,
// CSS Classes
css: {
animate: 'animate-in',
},
// Keyboard Keys
keys: {
end: 'End',
home: 'Home',
left: 'ArrowLeft',
up: 'ArrowUp',
right: 'ArrowRight',
down: 'ArrowDown',
},
direction: {
ArrowLeft: -1,
ArrowRight: 1,
},
};
this.onComponentLoaded();
}
onComponentLoaded() {
this.bindEvents();
// Set active tab from url or make first tab active
if (this.tabButtons) {
// Set each button's aria-controls attribute and select tab if aria-selected has already been set on the element
this.tabButtons.forEach((button) => {
button.setAttribute(
'aria-controls',
button.getAttribute('href').replace('#', ''),
);
});
// Check for active items set by the template
const tabActive = [...this.tabButtons].find(
(button) => button.getAttribute('aria-selected') === 'true',
);
if (window.location.hash && !this.disableURL) {
this.selectTabByURLHash();
} else if (tabActive) {
// If a tab isn't hidden for some reason hide it
this.tabPanels.forEach((tab) => {
// eslint-disable-next-line no-param-reassign
tab.hidden = true;
});
// Show aria-selected tab
this.selectTab(tabActive);
} else {
this.selectFirstTab();
}
}
}
/**
* @param {string}newTabId
*/
unSelectActiveTab(newTabId) {
// IF new tab ID is the current then don't transition out
if (newTabId === this.state.activeTabID || !this.state.activeTabID) {
return;
}
// Tab Content to deactivate
const tabContent = this.tabContainer.querySelector(
`#${this.state.activeTabID}`,
);
if (!tabContent) {
return;
}
if (this.animate) {
this.animateOut(tabContent);
} else {
tabContent.hidden = true;
}
const tab = this.tabContainer.querySelector(
`a[href='#${this.state.activeTabID}']`,
);
tab.setAttribute('aria-selected', 'false');
tab.setAttribute('tabindex', '-1');
}
selectTab(tab) {
if (!tab) {
return;
}
const tabContentId = tab.getAttribute('aria-controls');
// Unselect currently active tab
if (tabContentId) {
this.unSelectActiveTab(tabContentId);
}
this.state.activeTabID = tabContentId;
const linkedTab = this.tabContainer.querySelector(
`a[href="${tab.getAttribute('href')}"][role="tab"]`,
);
// If an external button was used to trigger the tab, make sure active tab is marked active
if (linkedTab) {
linkedTab.setAttribute('aria-selected', 'true');
linkedTab.removeAttribute('tabindex');
}
tab.setAttribute('aria-selected', 'true');
tab.removeAttribute('tabindex');
const tabContent = this.tabContainer.querySelector(`#${tabContentId}`);
if (!tabContent) {
return;
}
if (this.animate) {
this.animateIn(tabContent);
} else {
tabContent.hidden = false;
}
if (this.state.initialPageLoad) {
// On first load set the scroll to top to avoid scrolling to active section and header covering up tabs
setTimeout(() => {
window.scrollTo(0, 0);
}, this.state.transition * 2);
}
// Dispatch tab selected event for the rest of the admin to hook into if needed
// Trigger tab specific switch event
this.tabList.dispatchEvent(
new CustomEvent('switch', { detail: { tab: tab.dataset.tab } }),
);
// Dispatch tab-changed event on the document
document.dispatchEvent(new CustomEvent('tab-changed'));
// Set URL hash and browser history
if (!this.disableURL) {
this.setURLHash(tabContentId);
}
}
/**
* Fade Up and In animation
* @param tabContent{HTMLElement}
*/
animateIn(tabContent) {
setTimeout(() => {
// eslint-disable-next-line no-param-reassign
tabContent.hidden = false;
// Wait for hidden attribute to be applied then fade in
setTimeout(() => {
tabContent.classList.add(this.state.css.animate);
}, this.state.transition);
}, this.state.transition);
}
/**
* Fade Down and Out by removing css class
* @param tabContent{HTMLElement}
*/
animateOut(tabContent) {
// Wait element to transition out and then hide with hidden
tabContent.classList.remove(this.state.css.animate);
setTimeout(() => {
// eslint-disable-next-line no-param-reassign
tabContent.hidden = true;
}, this.state.transition);
}
bindEvents() {
if (!this.tabButtons) {
return;
}
this.tabButtons.forEach((tab, index) => {
tab.addEventListener('click', (e) => {
e.preventDefault();
this.selectTab(tab);
});
tab.addEventListener('focusin', () => {
this.selectTab(tab);
});
tab.addEventListener('keydown', this.keydownEventListener);
// Set index of tab used in keyboard controls
// eslint-disable-next-line no-param-reassign
tab.index = index;
});
// Select previous or next tab using history
window.addEventListener('popstate', (e) => {
if (e.state && e.state.tabContent) {
const tab = this.tabContainer.querySelector(
`a[href="#${e.state.tabContent}"][role="tab"]`,
);
if (tab) {
this.selectTab(tab);
}
}
});
}
/**
* Handle keydown on tabs
* @param {Event}event
*/
keydownEventListener(event) {
const keyPressed = event.key;
const { keys } = this.state;
switch (keyPressed) {
case keys.left:
case keys.right:
this.switchTabOnArrowPress(event);
break;
case keys.end:
event.preventDefault();
this.focusLastTab();
break;
case keys.home:
event.preventDefault();
this.focusFirstTab();
break;
default:
break;
}
}
selectTabByURLHash() {
if (window.location.hash) {
const cleanedHash = window.location.hash.replace(/[^\w\-#]/g, '');
const tab = this.tabContainer.querySelector(
`a[href="${cleanedHash}"][role="tab"]`,
);
if (tab) {
this.selectTab(tab);
} else {
// The hash doesn't match a tab on the page then select first tab
this.selectFirstTab();
}
}
}
/**
* Set url to have tab an tab hash at the end
*/
setURLHash(tabId) {
if (
!this.state.initialPageLoad &&
(!window.history.state || window.history.state.tabContent !== tabId)
) {
// Add a new history item to the stack
window.history.pushState({ tabContent: tabId }, null, `#${tabId}`);
}
this.state.initialPageLoad = false;
}
// Either focus the next, previous, first, or last tab depending on key pressed
switchTabOnArrowPress(event) {
const pressed = event.key;
const { direction } = this.state;
const { keys } = this.state;
const tabs = this.tabButtons;
if (direction[pressed]) {
const target = event.target;
if (target.index !== undefined) {
if (tabs[target.index + direction[pressed]]) {
tabs[target.index + direction[pressed]].focus();
} else if (pressed === keys.left) {
this.focusLastTab();
} else if (pressed === keys.right) {
this.focusFirstTab();
}
}
}
}
focusFirstTab() {
this.tabButtons[0].focus();
}
focusLastTab() {
this.tabButtons[this.tabButtons.length - 1].focus();
}
selectFirstTab() {
this.selectTab(this.tabButtons[0]);
this.state.activeTabID = this.tabButtons[0].getAttribute('aria-controls');
}
}
export default Tabs;
export const initTabs = (tabs = document.querySelectorAll('[data-tabs]')) => {
tabs.forEach((tabSet) => new Tabs(tabSet));
};

View File

@ -0,0 +1,33 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const plugin = require('tailwindcss/plugin');
module.exports = plugin(({ addComponents, theme }) => {
addComponents({
// Scrollbar styling for firefox
// https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color
'.scrollbar-thin': {
'scrollbarColor': `${theme('colors.grey.100')} ${theme(
'colors.white.DEFAULT',
)}`,
'scrollbarWidth': 'thin',
// Custom scrollbar styling for Safari & Chrome Windows / Mac / Android.
'&::-webkit-scrollbar': {
width: '5px',
height: '5px',
},
'&::-webkit-scrollbar-button': {
// Hide the scrollbar arrows on windows
display: 'none',
},
'&::-webkit-scrollbar-thumb': {
// Hide the scrollbar arrows on windows
backgroundColor: theme('colors.grey.200'),
borderRadius: theme('borderRadius.sm'),
},
'&::-webkit-scrollbar-track': {
background: theme('colors.transparent'),
},
},
});
});

View File

@ -1,6 +1,5 @@
const plugin = require('tailwindcss/plugin');
const vanillaRTL = require('tailwindcss-vanilla-rtl');
/**
* Design Tokens
*/
@ -24,6 +23,7 @@ const { spacing } = require('./src/tokens/spacing');
* Plugins
*/
const typeScale = require('./src/tokens/typeScale');
const scrollbarThin = require('./src/plugins/scrollbarThin');
/**
* Functions
@ -81,6 +81,7 @@ module.exports = {
plugins: [
typeScale,
vanillaRTL,
scrollbarThin,
/**
* forced-colors media query for Windows High-Contrast mode support
* See:

View File

@ -56,8 +56,16 @@ module.exports = function exports(env, argv) {
'workflow-status',
'bulk-actions',
],
'images': ['image-chooser', 'image-chooser-telepath'],
'documents': ['document-chooser', 'document-chooser-telepath'],
'images': [
'image-chooser',
'image-chooser-modal',
'image-chooser-telepath',
],
'documents': [
'document-chooser',
'document-chooser-modal',
'document-chooser-telepath',
],
'snippets': ['snippet-chooser', 'snippet-chooser-telepath'],
'contrib/table_block': ['table'],
'contrib/typed_table_block': ['typed_table_block'],

View File

@ -19,6 +19,7 @@ Here are other changes related to the redesign:
* Fully remove the legacy sidebar, with slim sidebar replacing it for all users (Thibaud Colas)
* Add support for adding custom attributes for link menu items in the slim sidebar (Thibaud Colas)
* Implement new slim page editor header with breadcrumb (Steven Steinwand, Karl Hobley)
* Implement new tabs design across the admin interface (Steven Steinwand)
### Removal of special-purpose field panel types
@ -78,6 +79,7 @@ class LandingPage(Page):
* Add the ability for choices to be separated by new lines instead of just commas within the form builder, commas will still be supported if used (Abdulmajeed Isa)
* Add internationalisation UI to modeladmin (Andrés Martano)
* Support chunking in `PageQuerySet.specific()` to reduce memory consumption (Andy Babic)
* Fix: Implement ARIA tabs markup and keyboards interactions for admin tabs (Steven Steinwand)
### Bug fixes
@ -126,7 +128,7 @@ wagtail updatemodulepaths # actually update the files
### Removed warning in Internet Explorer (IE11)
* IE11 support was officially dropped in Wagtail 2.15, as of this release there will no longer be a warning shown to users of this browser.
* Wagtail is fully compatible with Microsoft Edge, Microsofts replacement for Internet Explorer. You may consider using its `IE mode <https://docs.microsoft.com/en-us/deployedge/edge-ie-mode>`_ to keep access to IE11-only sites, while other sites and apps like Wagtail can leverage modern browser capabilities.
* Wagtail is fully compatible with Microsoft Edge, Microsofts replacement for Internet Explorer. You may consider using its [IE mode](https://docs.microsoft.com/en-us/deployedge/edge-ie-mode) to keep access to IE11-only sites, while other sites and apps like Wagtail can leverage modern browser capabilities.
### Replaced `content_json` `TextField` with `content` `JSONField` in `PageRevision`
@ -218,4 +220,3 @@ After setting the keyword argument, make sure to generate and run the migrations
### Removed support for Jinja2 2.x
Jinja2 2.x is no longer supported as of this release; if you are using Jinja2 templating on your project, please upgrade to Jinja2 3.0 or above.

View File

@ -1,138 +0,0 @@
/* ========================================================================
* Bootstrap: tab.js v3.0.0
* http://twbs.github.com/bootstrap/javascript.html#tabs
* ========================================================================
* Copyright 2012 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ======================================================================== */
+function ($) { "use strict";
// TAB CLASS DEFINITION
// ====================
var Tab = function (element) {
this.element = $(element)
}
Tab.prototype.show = function () {
var $this = this.element
var $ul = $this.closest('ul:not(.dropdown-menu)')
var selector = $this.attr('data-target')
if (!selector) {
selector = $this.attr('href')
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
}
if ($this.parent('li').hasClass('active')) return
var previous = $ul.find('.active:last a')[0]
var e = $.Event('show.bs.tab', {
relatedTarget: previous
})
$this.trigger(e)
if (e.isDefaultPrevented()) return
var $target = $(selector)
this.activate($this, $this.parent('li'), $ul)
this.activate($this, $target, $target.parent(), function () {
$this.trigger({
type: 'shown.bs.tab'
, relatedTarget: previous
})
})
}
Tab.prototype.activate = function (trigger, element, container, callback) {
var $active = container.find('> .active')
var transition = callback
&& $.support.transition
&& $active.hasClass('fade')
function next() {
element.parent().find('.active').removeClass('active');
$active
.removeClass('active')
.find('> .dropdown-menu > .active')
.removeClass('active')
trigger.addClass('active');
element.addClass('active');
if (transition) {
element[0].offsetWidth // reflow for transition
element.addClass('in')
} else {
element.removeClass('fade')
}
if (element.parent('.dropdown-menu')) {
element.closest('li.dropdown').addClass('active')
}
callback && callback()
}
transition ?
$active
.one($.support.transition.end, next)
.emulateTransitionEnd(150) :
next()
$active.removeClass('in')
}
// TAB PLUGIN DEFINITION
// =====================
var old = $.fn.tab
$.fn.tab = function ( option ) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.tab')
if (!data) $this.data('bs.tab', (data = new Tab(this)))
if (typeof option == 'string') data[option]()
})
}
$.fn.tab.Constructor = Tab
// TAB NO CONFLICT
// ===============
$.fn.tab.noConflict = function () {
$.fn.tab = old
return this
}
// TAB DATA-API
// ============
$(document).on('click.bs.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) {
e.preventDefault()
$(this).tab('show')
})
}(window.jQuery);

View File

@ -4,69 +4,87 @@
{% block titletag %}{% trans "Account" %}{% endblock %}
{% block content %}
{% trans "Account" as account_str %}
{% include "wagtailadmin/shared/header.html" with title=account_str merged=1 tabbed=1 %}
{% include "wagtailadmin/shared/header.html" with title=account_str merged=1 %}
<ul class="tab-nav merged" data-tab-nav>
{% for tab in panels_by_tab.keys %}
<li{% if forloop.first %} class="active"{% endif %}><a href="#{{ tab.name }}">{{ tab.title }}</a></li>
{% endfor %}
<div class="w-tabs" data-tabs data-tabs-animate>
<div class="w-tabs__wrapper">
<div role="tablist" class="w-tabs__list nice-padding">
{% for tab in panels_by_tab.keys %}
{% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id=tab.name title=tab.title %}
{% endfor %}
{% if menu_items %}
<li><a href="#actions">{% trans "More actions" %}</a></li>
{% endif %}
</ul>
{% if menu_items %}
{% trans 'More actions' as menu_items_title %}
{% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='actions' title=menu_items_title %}
{% endif %}
</div>
</div>
<form action="{% url 'wagtailadmin_account' %}" method="post" enctype="multipart/form-data" novalidate>
<div class="tab-content">
{% csrf_token %}
<form action="{% url 'wagtailadmin_account' %}" method="post" enctype="multipart/form-data" novalidate>
<div class="tab-content">
{% csrf_token %}
{% for tab, panels in panels_by_tab.items %}
<section id="{{ tab.name }}"{% if forloop.first %} class="active"{% endif %}>
<ul class="objects">
{% for panel in panels %}
<li class="object">
<div class="title-wrapper">
<label>{{ panel.title }}</label>
</div>
<div class="object-layout">
<div class="object-layout_big-part">
<div class="top-padding">
{{ panel.render }}
{% for tab, panels in panels_by_tab.items %}
<section
id="tab-{{ tab.name|cautious_slugify }}"
class="w-tabs__panel"
role="tabpanel"
hidden
aria-labelledby="tab-label-{{ tab.name|cautious_slugify }}"
>
<ul class="objects">
{% for panel in panels %}
<li class="object">
<div class="title-wrapper">
<label>{{ panel.title }}</label>
</div>
<div class="object-layout">
<div class="object-layout_big-part">
<div class="top-padding">
{{ panel.render }}
</div>
</div>
</div>
</div>
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
<div class="top-padding nice-padding">
<button type="submit" class="button">{% trans 'Save account details' %}</button>
</div>
</section>
{% endfor %}
<div class="top-padding nice-padding">
<button type="submit" class="button">{% trans 'Save account details' %}</button>
</div>
</section>
{% endfor %}
{% if menu_items %}
<section
id="tab-actions"
class="w-tabs__panel"
role="tabpanel"
hidden
aria-labelledby="tab-label-actions"
>
<ul class="listing">
{% for item in menu_items %}
<li class="row row-flush">
<div class="col6">
<a href="{{ item.url }}" class="button button-primary">{{ item.label }}</a>
</div>
<small class="col6">{{ item.help_text }}</small>
</li>
{% endfor %}
</ul>
</section>
{% endif %}
</div>
</form>
</div>
{% if menu_items %}
<section id="actions" class="nice-padding">
<ul class="listing">
{% for item in menu_items %}
<li class="row row-flush">
<div class="col6">
<a href="{{ item.url }}" class="button button-primary">{{ item.label }}</a>
</div>
<small class="col6">{{ item.help_text }}</small>
</li>
{% endfor %}
</ul>
</section>
{% endif %}
</div>
</form>
{% endblock %}
{% block extra_css %}
{{ block.super }}
{% include "wagtailadmin/pages/_editor_css.html" %}
<link rel="stylesheet" href="{% versioned_static 'wagtailadmin/css/layouts/account.css' %}" type="text/css" />
<link rel="stylesheet" href="{% versioned_static 'wagtailadmin/css/layouts/account.css' %}" type="text/css"/>
{{ media.css }}
{% endblock %}
{% block extra_js %}

View File

@ -1,31 +1,42 @@
{% load wagtailadmin_tags i18n %}
<div class="tab-nav merged">
<ul data-tab-nav role="tablist" data-current-tab="{{ self.children.0.heading|cautious_slugify }}">
{% for child in self.children %}
<li class="{{ child.classes|join:" " }} {% if forloop.first %}active{% endif %}" role="tab" aria-controls="tab-{{ child.heading|cautious_slugify }}">
<a href="#tab-{{ child.heading|cautious_slugify }}" class="{% if forloop.first %}active{% endif %}" data-tab="{{ child.heading|cautious_slugify }}">{{ child.heading }}</a>
</li>
{% endfor %}
</ul>
{% if self.form.show_comments_toggle %}
<div class="right wide">
<div class="comments-controls" hidden data-comment-notifications>
<div class="comment-notifications-toggle">
<label class="switch switch--teal-background">
{% trans "Comment notifications" %}
{{ self.form.comment_notifications }}
<span class="switch__toggle"></span>
</label>
<div class="w-tabs" data-tabs data-tabs-animate>
<div class="w-tabs__wrapper">
<div role="tablist" class="w-tabs__list w-px-5 sm:w-px-[4.5rem]">
{% for child in self.children %}
{% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id=child.heading title=child.heading classes=child.classes|join:" " %}
{% endfor %}
</div>
</div>
<template>
{# TODO To be re-implemented for comments side panel #}
{% if self.form.show_comments_toggle %}
<div class="right wide">
<div class="comments-controls" hidden data-comment-notifications>
<div class="comment-notifications-toggle">
<label class="switch switch--teal-background">
{% trans "Comment notifications" %}
{{ self.form.comment_notifications }}
<span class="switch__toggle"></span>
</label>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endif %}
</template>
<div class="tab-content">
{% for child in self.children %}
<section id="tab-{{ child.heading|cautious_slugify }}" class="{{ child.classes|join:" " }} {% if forloop.first %}active{% endif %}" role="tabpanel" aria-labelledby="tab-label-{{ child.heading|cautious_slugify }}" data-tab="{{ child.heading|cautious_slugify }}">
{{ child.render_as_object }}
</section>
{% endfor %}
<div class="tab-content">
{% for child in self.children %}
<section
id="tab-{{ child.heading|cautious_slugify }}"
class="w-tabs__panel {{ child.classes|join:" " }}"
role="tabpanel"
aria-labelledby="tab-label-{{ child.heading|cautious_slugify }}"
hidden
>
{{ child.render_as_object }}
</section>
{% endfor %}
</div>
</div>

View File

@ -8,7 +8,7 @@
{% with breadcrumb_link_classes='w-text-grey-600 w-text-14 w-no-underline w-outline-offset-inside hover:w-underline hover:w-text-primary' breadcrumb_item_classes='w-flex w-items-center w-overflow-hidden w-transition w-duration-300 w-whitespace-nowrap w-flex-shrink-0' icon_classes='w-w-4 w-h-4 w-mr-3' %}
{# Breadcrumbs are visible on mobile by default but hidden on desktop #}
<div class="w-flex w-flex-row w-items-center w-overflow-x-auto w-overflow-y-hidden u-scrollbar-thin" data-breadcrumb-next>
<div class="w-flex w-flex-row w-items-center w-overflow-x-auto w-overflow-y-hidden w-scrollbar-thin" data-breadcrumb-next>
<button
type="button"
data-toggle-breadcrumbs

View File

@ -8,16 +8,15 @@
- `search_url` - if present, display a search box. This is a URL route name (taking no parameters) to be used as the action for that search box
- `query_parameters` - a query string (without the '?') to be placed after the search URL
- `icon` - name of an icon to place against the title
- `tabbed` - if true, add the classname 'tab-merged'
- `merged` - if true, add the classname 'merged'
- `action_url` - if present, display an 'action' button. This is the URL to be used as the link URL for the button
- `action_text` - text for the 'action' button
- `action_icon` - icon for the 'action' button, default is 'icon-plus'
{% endcomment %}
<header class="{% if merged %}merged{% endif %} {% if tabbed %}tab-merged{% endif %} {% if search_form %}hasform{% endif %}">
<header class="{% if merged %}merged{% endif %} {% if search_form %}hasform{% endif %}">
{% block breadcrumb %}{% endblock %}
<div class="row{% if not tabbed %} nice-padding{% endif %}">
<div class="row nice-padding">
<div class="left">
<div class="col header-title">
<h1>{% if icon %}{% icon name=icon class_name="header-title-icon" %}{% endif %}

View File

@ -0,0 +1,19 @@
{% load wagtailadmin_tags i18n %}
{% comment %}
Variables accepted by this template:
- `tab_id` - {string} A unique tab id
- `title` - {string} Text that the tab button will display
- `active` - {boolean?} Force this to be active
- `classes` - {string?} Extra css classes to pass to this component
- `errors_count` - {number?} Show above the tab for errors count
{% endcomment %}
<a id="tab-label-{{ tab_id|cautious_slugify }}" href="#tab-{{ tab_id|cautious_slugify }}" class="w-tabs__tab {{ classes }}" role="tab" aria-selected="false" tabindex="-1">
<div class="w-tabs__errors {% if errors_count %}w-tabs__errors--active{% endif %}">
<span class="w-sr-only">{% trans 'Errors Count: ' %}</span>
<span class="w-tabs__errors-count">{{ errors_count }}</span>
</div>
{{ title }}
</a>

View File

@ -1,21 +1,51 @@
{% load i18n %}
{% trans "Choose a task" as choose_str %}
{% include "wagtailadmin/shared/header.html" with title=choose_str tabbed=1 merged=1 icon="thumbtack" %}
{% include "wagtailadmin/shared/header.html" with title=choose_str merged=1 icon="thumbtack" %}
{% if can_create %}
<ul class="tab-nav merged" data-tab-nav>
<li class="active"><a href="#new">{% trans "New" %}</a></li>
<li><a href="#existing">{% trans "Existing" %}</a></li>
</ul>
{% endif %}
<div class="w-tabs" data-tabs data-tabs-disable-url>
<div class="w-tabs__wrapper w-overflow-hidden">
{# Using nice-padding and full width class until the modal header is restyled #}
<div role="tablist" class="w-tabs__list w-w-full nice-padding">
{% trans "New" as new_text %}
{% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='new' title=new_text %}
{% trans "Existing" as existing_text %}
{% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='existing' title=existing_text %}
</div>
</div>
<div class="tab-content">
{% if can_create %}
<section id="new" class="active nice-padding">
{% include "wagtailadmin/workflows/task_chooser/includes/create_tab.html" %}
</section>
{% endif %}
<section id="existing" class="nice-padding{% if not can_create %} active{% endif %}">
<div class="tab-content nice-padding">
<section
id="tab-new"
class="w-tabs__panel"
role="tabpanel"
aria-labelledby="tab-label-new"
hidden
>
{% include "wagtailadmin/workflows/task_chooser/includes/create_tab.html" %}
</section>
<section
id="tab-existing"
class="w-tabs__panel"
role="tabpanel"
aria-labelledby="tab-label-existing"
hidden
>
<form class="task-search search-bar" action="{% url 'wagtailadmin_workflows:task_chooser_results' %}" method="GET" novalidate>
<ul class="fields">
{% for field in search_form %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field %}
{% endfor %}
</ul>
</form>
<div id="search-results" class="listing tasks">
{% include "wagtailadmin/workflows/task_chooser/includes/results.html" %}
</div>
</section>
</div>
</div>
{% else %}
<div class="nice-padding">
<form class="task-search search-bar" action="{% url 'wagtailadmin_workflows:task_chooser_results' %}" method="GET" novalidate>
<ul class="fields">
{% for field in search_form %}
@ -26,5 +56,5 @@
<div id="search-results" class="listing tasks">
{% include "wagtailadmin/workflows/task_chooser/includes/results.html" %}
</div>
</section>
</div>
</div>
{% endif %}

View File

@ -67,7 +67,7 @@
{% trans "You haven't created any tasks." %}
{% if can_create %}
{% blocktrans trimmed %}
Why not <a class="create-one-now" href="#">create one now</a>?
Why not <a class="create-one-now" href="#tab-new" role="tab">create one now</a>?
{% endblocktrans %}
{% endif %}
</p>

View File

@ -145,10 +145,11 @@ class TestPageCreation(TestCase, WagtailTestUtils):
self.assertEqual(response["Content-Type"], "text/html; charset=utf-8")
self.assertContains(
response,
'<a href="#tab-content" class="active" data-tab="content">Content</a>',
'<a id="tab-label-content" href="#tab-content" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
)
self.assertContains(
response, '<a href="#tab-promote" class="" data-tab="promote">Promote</a>'
response,
'<a id="tab-label-promote" href="#tab-promote" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
)
# test register_page_action_menu_item hook
self.assertContains(
@ -215,11 +216,9 @@ class TestPageCreation(TestCase, WagtailTestUtils):
self.assertEqual(response.status_code, 200)
self.assertContains(
response,
'<a href="#tab-content" class="active" data-tab="content">Content</a>',
)
self.assertNotContains(
response, '<a href="#tab-promote" class="" data-tab="promote">Promote</a>'
'<a id="tab-label-content" href="#tab-content" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
)
self.assertNotContains(response, "tab-promote")
def test_create_page_with_custom_tabs(self):
"""
@ -234,14 +233,15 @@ class TestPageCreation(TestCase, WagtailTestUtils):
self.assertEqual(response.status_code, 200)
self.assertContains(
response,
'<a href="#tab-content" class="active" data-tab="content">Content</a>',
)
self.assertContains(
response, '<a href="#tab-promote" class="" data-tab="promote">Promote</a>'
'<a id="tab-label-content" href="#tab-content" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
)
self.assertContains(
response,
'<a href="#tab-dinosaurs" class="" data-tab="dinosaurs">Dinosaurs</a>',
'<a id="tab-label-promote" href="#tab-promote" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
)
self.assertContains(
response,
'<a id="tab-label-dinosaurs" href="#tab-dinosaurs" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
)
def test_create_page_with_non_model_field(self):

View File

@ -419,23 +419,17 @@ class TestTabbedInterface(TestCase):
# result should contain tab buttons
self.assertIn(
'<a href="#tab-event-details" class="active" data-tab="event-details">Event details</a>',
'<a id="tab-label-event-details" href="#tab-event-details" class="w-tabs__tab shiny" role="tab" aria-selected="false" tabindex="-1">',
result,
)
self.assertIn(
'<a href="#tab-speakers" class="" data-tab="speakers">Speakers</a>', result
'<a id="tab-label-speakers" href="#tab-speakers" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
result,
)
# result should contain tab panels
self.assertIn('<div class="tab-content">', result)
self.assertIn(
'<section id="tab-event-details" class="shiny active" role="tabpanel" aria-labelledby="tab-label-event-details" data-tab="event-details">',
result,
)
self.assertIn(
'<section id="tab-speakers" class=" " role="tabpanel" aria-labelledby="tab-label-speakers" data-tab="speakers">',
result,
)
self.assertIn('aria-labelledby="tab-label-event-details"', result)
self.assertIn('aria-labelledby="tab-label-speakers"', result)
# result should contain rendered content from descendants
self.assertIn("Abergavenny sheepdog trials</textarea>", result)

View File

@ -21,7 +21,7 @@
{% block content %}
{% block header %}
{% include "modeladmin/includes/header_with_breadcrumb.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon tabbed=True %}
{% include "modeladmin/includes/header_with_breadcrumb.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon %}
{% endblock %}
<div>

View File

@ -35,7 +35,7 @@
{% block content %}
{% block header %}
{% include "wagtailadmin/shared/header_with_locale_selector.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon tabbed=1 merged=1 %}
{% include "wagtailadmin/shared/header_with_locale_selector.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon merged=1 %}
{% endblock %}
<form action="{% block form_action %}{{ view.create_url }}{% endblock %}{% if locale %}?locale={{ locale.language_code }}{% endif %}"{% if is_multipart %} enctype="multipart/form-data"{% endif %} method="POST" novalidate>

View File

@ -2,7 +2,7 @@
{% load i18n wagtailadmin_tags %}
{% block header %}
{% include "modeladmin/includes/header_with_history.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon tabbed=1 merged=1 latest_log_entry=latest_log_entry history_url=history_url %}
{% include "modeladmin/includes/header_with_history.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon merged=1 latest_log_entry=latest_log_entry history_url=history_url %}
{% endblock %}
{% block form_action %}{{ view.edit_url }}{% endblock %}

View File

@ -17,7 +17,7 @@
{% block content %}
{% block header %}
{% include "modeladmin/includes/header_with_breadcrumb.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon tabbed=True %}
{% include "modeladmin/includes/header_with_breadcrumb.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon %}
{% endblock %}
<div>

View File

@ -3,7 +3,7 @@
{% block titletag %}{% blocktrans trimmed %}Editing {{ setting_type_name}} - {{ instance }}{% endblocktrans %}{% endblock %}
{% block bodyclass %}menu-settings{% endblock %}
{% block content %}
<header class="nice-padding {% if tabbed %}merged tab-merged{% endif %}">
<header class="nice-padding merged">
<div class="row">
<div class="left">
<div class="col">

View File

@ -685,26 +685,21 @@
<section id="tabs">
<h2>Tabs</h2>
<ul class="tab-nav" data-tab-nav>
<li class="active"><a href="#tab1">Tab 1</a></li>
<li><a href="#tab2">Tab 2</a></li>
</ul>
<p>Tabs are currently only used following headers, where they often appear merged with the bottom of the header:</p>
{% include "wagtailadmin/shared/header.html" with title=title_trans merged=1 %}
<ul class="tab-nav merged" data-tab-nav>
<li class="active"><a href="#">Tab1</a></li>
<li><a href="#">Tab2</a></li>
</ul>
<div class="w-tabs" data-tabs data-tabs-animate>
<div role="tablist" class="w-tabs__list">
{% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='tab-1' title='Tab 1' %}
{% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='tab-2' title='Tab 2' %}
</div>
</div>
<p>Tabs can also indicate errors:</p>
{% include "wagtailadmin/shared/header.html" with title=title_trans merged=1 %}
<ul class="tab-nav merged" data-tab-nav>
<li class="active"><a href="#" class="errors" data-count="123">Tab1</a></li>
<li><a href="#" class="errors" data-count="1">Tab2</a></li>
</ul>
<div class="w-tabs" data-tabs data-tabs-animate>
<div role="tablist" class="w-tabs__list">
{% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='tab-errors-1' title='Tab 1' errors_count='5' %}
{% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='tab-errors-2' title='Tab 2' errors_count='55' %}
</div>
</div>
</section>
<section id="breadcrumbs">

View File

@ -1,34 +1,46 @@
{% load i18n wagtailadmin_tags %}
{% trans "Choose a document" as choose_str %}
{% include "wagtailadmin/shared/header.html" with title=choose_str tabbed=1 merged=1 icon="doc-full-inverse" %}
{% include "wagtailadmin/shared/header.html" with title=choose_str merged=1 icon="doc-full-inverse" %}
{{ uploadform.media.js }}
{{ uploadform.media.css }}
{% if uploadform %}
<ul class="tab-nav merged" data-tab-nav>
<li class="{% if not uploadform.errors %}active {% endif %}"><a href="#search">{% trans "Search" %}</a></li>
<li class="{% if uploadform.errors %}active {% endif %}"><a href="#upload">{% trans "Upload" %}</a></li>
</ul>
{% endif %}
<div class="tab-content">
<section id="search" class="{% if not uploadform.errors %}active {% endif %}nice-padding">
<form class="document-search search-bar" action="{% url 'wagtaildocs:chooser_results' %}" method="GET" novalidate>
<ul class="fields">
{% for field in searchform %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field %}
{% endfor %}
{% if collections %}
{% include "wagtailadmin/shared/collection_chooser.html" %}
{% endif %}
</ul>
</form>
<div id="search-results" class="listing documents">
{% include "wagtaildocs/chooser/results.html" %}
</div>
</section>
<div class="w-tabs" data-tabs data-tabs-disable-url>
{% if uploadform %}
{% include "wagtaildocs/chooser/upload_form.html" with form=uploadform %}
<div class="w-tabs__wrapper w-overflow-hidden">
{# Using nice-padding and full width class until the modal header is restyled #}
<div role="tablist" class="w-tabs__list w-w-full nice-padding">
{% trans "Search" as search_text %}
{% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='search' title=search_text %}
{% trans "Upload" as upload_text %}
{% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='upload' title=upload_text %}
</div>
</div>
{% endif %}
<div class="tab-content nice-padding">
<section
id="tab-search"
class="w-tabs__panel"
role="tabpanel"
aria-labelledby="tab-label-search"
>
<form class="document-search search-bar" action="{% url 'wagtaildocs:chooser_results' %}" method="GET" novalidate>
<ul class="fields">
{% for field in searchform %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field %}
{% endfor %}
{% if collections %}
{% include "wagtailadmin/shared/collection_chooser.html" %}
{% endif %}
</ul>
</form>
<div id="search-results" class="listing documents">
{% include "wagtaildocs/chooser/results.html" %}
</div>
</section>
{% if uploadform %}
{% include "wagtaildocs/chooser/upload_form.html" with form=uploadform %}
{% endif %}
</div>
</div>

View File

@ -25,9 +25,8 @@
{% trans "You haven't uploaded any documents." %}
{% endif %}
{% if uploadform %}
{% url 'wagtaildocs:add_multiple' as wagtaildocs_add_document_url %}
{% blocktrans trimmed %}
Why not <a class="upload-one-now" href="{{ wagtaildocs_add_document_url }}">upload one now</a>?
Why not <a class="upload-one-now" href="#tab-upload" role="tab">upload one now</a>?
{% endblocktrans %}
{% endif %}
</p>

View File

@ -1,5 +1,11 @@
{% load i18n wagtailadmin_tags %}
<section id="upload" class="{% if form.errors %}active {% endif %}nice-padding">
<section
id="tab-upload"
class="w-tabs__panel"
role="tabpanel"
hidden
aria-labelledby="tab-label-upload"
>
{% include "wagtailadmin/shared/non_field_errors.html" with form=form %}
<form class="document-upload" action="{% url 'wagtaildocs:chooser_upload' %}" method="POST" enctype="multipart/form-data" novalidate>
{% csrf_token %}

View File

@ -1,43 +1,53 @@
{% load wagtailimages_tags wagtailadmin_tags %}
{% load i18n %}
{% trans "Choose an image" as choose_str %}
{% include "wagtailadmin/shared/header.html" with title=choose_str merged=1 tabbed=1 icon="image" %}
{% include "wagtailadmin/shared/header.html" with title=choose_str merged=1 icon="image" %}
{{ uploadform.media.js }}
{{ uploadform.media.css }}
{% if uploadform %}
<ul class="tab-nav merged" data-tab-nav>
<li class="{% if not uploadform.errors %}active{% endif %}"><a href="#search" >{% trans "Search" %}</a></li>
<li class="{% if uploadform.errors %}active{% endif %}"><a href="#upload">{% trans "Upload" %}</a></li>
</ul>
{% endif %}
<div class="tab-content">
<section id="search" class="{% if not uploadform.errors %}active{% endif %} nice-padding">
<form class="image-search search-bar" action="{% url 'wagtailimages:chooser_results' %}{% if will_select_format %}?select_format=true{% endif %}" method="GET" autocomplete="off" novalidate>
<ul class="fields">
{% for field in searchform %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field %}
{% endfor %}
{% if collections %}
{% include "wagtailadmin/shared/collection_chooser.html" %}
{% endif %}
{% if popular_tags %}
<li class="taglist">
<h3>{% trans 'Popular tags' %}</h3>
{% for tag in popular_tags %}
<a class="suggested-tag tag" href="{% url 'wagtailimages:index' %}?tag={{ tag.name|urlencode }}">{{ tag.name }}</a>
{% endfor %}
</li>
{% endif %}
</ul>
</form>
<div id="image-results">
{% include "wagtailimages/chooser/results.html" %}
</div>
</section>
<div class="w-tabs" data-tabs data-tabs-disable-url>
{% if uploadform %}
{% include "wagtailimages/chooser/upload_form.html" with form=uploadform will_select_format=will_select_format %}
{# Using nice-padding and full width class until the modal header is restyled #}
<div role="tablist" class="w-tabs__list w-w-full nice-padding">
{% trans "Search" as search_text %}
{% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='search' title=search_text active=uploadform.errors %}
{% trans "Upload" as upload_text %}
{% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='upload' title=upload_text active=uploadform.errors %}
</div>
{% endif %}
<div class="tab-content nice-padding">
<section
id="tab-search"
class="w-tabs__panel"
role="tabpanel"
aria-labelledby="tab-label-search"
>
<form class="image-search search-bar" action="{% url 'wagtailimages:chooser_results' %}{% if will_select_format %}?select_format=true{% endif %}" method="GET" autocomplete="off" novalidate>
<ul class="fields">
{% for field in searchform %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field %}
{% endfor %}
{% if collections %}
{% include "wagtailadmin/shared/collection_chooser.html" %}
{% endif %}
{% if popular_tags %}
<li class="taglist">
<h3>{% trans 'Popular tags' %}</h3>
{% for tag in popular_tags %}
<a class="suggested-tag tag" href="{% url 'wagtailimages:index' %}?tag={{ tag.name|urlencode }}">{{ tag.name }}</a>
{% endfor %}
</li>
{% endif %}
</ul>
</form>
<div id="image-results">
{% include "wagtailimages/chooser/results.html" %}
</div>
</section>
{% if uploadform %}
{% include "wagtailimages/chooser/upload_form.html" with form=uploadform will_select_format=will_select_format %}
{% endif %}
</div>
</div>

View File

@ -1,6 +1,11 @@
{% load i18n wagtailadmin_tags %}
<section id="upload" class="{% if form.errors %}active{% endif %} nice-padding">
<section
id="tab-upload"
class="w-tabs__panel"
role="tabpanel"
hidden
aria-labelledby="tab-label-upload"
>
{% include "wagtailadmin/shared/non_field_errors.html" with form=form %}
<form class="image-upload" action="{% url 'wagtailimages:chooser_upload' %}{% if will_select_format %}?select_format=true{% endif %}" method="POST" enctype="multipart/form-data" novalidate>
{% csrf_token %}

View File

@ -3,7 +3,7 @@
{% block titletag %}{% blocktrans trimmed with snippet_type_name=model_opts.verbose_name %}New {{ snippet_type_name }}{% endblocktrans %}{% endblock %}
{% block content %}
{% trans "New" as new_str %}
{% include "wagtailadmin/shared/header_with_locale_selector.html" with title=new_str subtitle=model_opts.verbose_name icon="snippet" tabbed=1 merged=1 locale=locale translations=translations only %}
{% include "wagtailadmin/shared/header_with_locale_selector.html" with title=new_str subtitle=model_opts.verbose_name icon="snippet" merged=1 locale=locale translations=translations only %}
<form action="{{ action_url }}" method="POST" novalidate{% if form.is_multipart %} enctype="multipart/form-data"{% endif %}>
{% csrf_token %}

View File

@ -3,7 +3,7 @@
{% block titletag %}{% blocktrans trimmed with snippet_type_name=model_opts.verbose_name %}Editing {{ snippet_type_name }} - {{ instance }}{% endblocktrans %}{% endblock %}
{% block content %}
{% trans "Editing" as editing_str %}
{% include "wagtailsnippets/snippets/_header_with_history.html" with title=editing_str subtitle=instance icon="snippet" tabbed=1 merged=1 %}
{% include "wagtailsnippets/snippets/_header_with_history.html" with title=editing_str subtitle=instance icon="snippet" merged=1 %}
<div class="row row-flush">

View File

@ -372,19 +372,7 @@ class TestSnippetCreateView(TestCase, WagtailTestUtils):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailsnippets/snippets/create.html")
self.assertNotContains(
response, '<ul data-tab-nav role="tablist" data-current-tab="advert">'
)
self.assertNotContains(
response,
'<a href="#tab-advert" class="active" data-tab="advert">Advert</a>',
html=True,
)
self.assertNotContains(
response,
'<a href="#tab-other" class="" data-tab="other">Other</a>',
html=True,
)
self.assertNotContains(response, 'role="tablist"', html=True)
def test_snippet_with_tabbed_interface(self):
response = self.client.get(
@ -393,18 +381,14 @@ class TestSnippetCreateView(TestCase, WagtailTestUtils):
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailsnippets/snippets/create.html")
self.assertContains(response, 'role="tablist"')
self.assertContains(
response, '<ul data-tab-nav role="tablist" data-current-tab="advert">'
response,
'<a id="tab-label-advert" href="#tab-advert" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
)
self.assertContains(
response,
'<a href="#tab-advert" class="active" data-tab="advert">Advert</a>',
html=True,
)
self.assertContains(
response,
'<a href="#tab-other" class="" data-tab="other">Other</a>',
html=True,
'<a id="tab-label-other" href="#tab-other" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
)
def test_create_with_limited_permissions(self):
@ -698,17 +682,7 @@ class TestSnippetEditView(BaseTestSnippetEditView):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html")
self.assertNotContains(
response, '<ul data-tab-nav role="tablist" data-current-tab="advert">'
)
self.assertNotContains(
response,
'<a href="#advert" class="active" data-tab="advert">Advert</a>',
html=True,
)
self.assertNotContains(
response, '<a href="#other" class="" data-tab="other">Other</a>', html=True
)
self.assertNotContains(response, 'role="tablist"')
# "Last updated" timestamp should be present
self.assertContains(
@ -914,18 +888,14 @@ class TestEditTabbedSnippet(BaseTestSnippetEditView):
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html")
self.assertContains(response, 'role="tablist"')
self.assertContains(
response, '<ul data-tab-nav role="tablist" data-current-tab="advert">'
response,
'<a id="tab-label-advert" href="#tab-advert" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
)
self.assertContains(
response,
'<a href="#tab-advert" class="active" data-tab="advert">Advert</a>',
html=True,
)
self.assertContains(
response,
'<a href="#tab-other" class="" data-tab="other">Other</a>',
html=True,
'<a id="tab-label-other" href="#tab-other" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
)

View File

@ -5,46 +5,64 @@
{% block content %}
{% trans "Add user" as add_user_str %}
{% include "wagtailadmin/shared/header.html" with title=add_user_str merged=1 tabbed=1 icon="user" %}
{% include "wagtailadmin/shared/header.html" with title=add_user_str merged=1 icon="user" %}
<ul class="tab-nav merged" data-tab-nav>
<li class="active"><a href="#account">{% trans "Account" %}</a></li>
<li><a href="#roles">{% trans "Roles" %}</a></li>
</ul>
<form action="{% url 'wagtailusers_users:add' %}" method="POST" novalidate{% if form.is_multipart %} enctype="multipart/form-data"{% endif %}>
<div class="tab-content">
{% csrf_token %}
<section id="account" class="active nice-padding">
<ul class="fields">
{% block fields %}
{% if form.separate_username_field %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.username_field %}
{% endif %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.email %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.first_name %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.last_name %}
{% block extra_fields %}{% endblock extra_fields %}
{% if form.password1 %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.password1 %}
{% endif %}
{% if form.password2 %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.password2 %}
{% endif %}
{% endblock fields %}
<li><a href="#roles" class="button lowpriority tab-toggle icon icon-arrow-right-after">{% trans "Roles" %}</a></li>
</ul>
</section>
<section id="roles" class="nice-padding">
<ul class="fields">
{% include "wagtailadmin/shared/field_as_li.html" with field=form.is_superuser %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.groups %}
<li><button class="button">{% trans "Add user" %}</button></li>
</ul>
</section>
<div class="w-tabs" data-tabs data-tabs-animate>
<div class="w-tabs__wrapper">
<div role="tablist" class="w-tabs__list nice-padding">
{% trans "Account" as account_text %}
{% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='account' title=account_text %}
{% trans "Roles" as roles_text %}
{% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='roles' title=roles_text %}
</div>
</div>
</form>
<form action="{% url 'wagtailusers_users:add' %}" method="POST" novalidate{% if form.is_multipart %} enctype="multipart/form-data"{% endif %}>
<div class="tab-content nice-padding">
{% csrf_token %}
<section
id="tab-account"
class="w-tabs__panel"
role="tabpanel"
hidden
aria-labelledby="tab-label-account"
>
<ul class="fields">
{% block fields %}
{% if form.separate_username_field %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.username_field %}
{% endif %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.email %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.first_name %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.last_name %}
{% block extra_fields %}{% endblock extra_fields %}
{% if form.password1 %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.password1 %}
{% endif %}
{% if form.password2 %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.password2 %}
{% endif %}
{% endblock fields %}
<li><a href="#tab-roles" role="tab" class="button lowpriority icon icon-arrow-right-after">{% trans "Roles" %}</a></li>
</ul>
</section>
<section
id="tab-roles"
class="w-tabs__panel"
role="tabpanel"
hidden
aria-labelledby="tab-label-roles"
>
<ul class="fields">
{% include "wagtailadmin/shared/field_as_li.html" with field=form.is_superuser %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.groups %}
<li><button class="button">{% trans "Add user" %}</button></li>
</ul>
</section>
</div>
</form>
</div>
{% endblock %}
{% block extra_css %}

View File

@ -1,67 +1,85 @@
{% extends "wagtailadmin/base.html" %}
{% load wagtailimages_tags %}
{% load i18n %}
{% block titletag %}{% trans "Editing" %} {{ user.get_username}}{% endblock %}
{% block titletag %}{% trans "Editing" %} {{ user.get_username }}{% endblock %}
{% block content %}
{% trans "Editing" as editing_str %}
{% include "wagtailadmin/shared/header.html" with title=editing_str subtitle=user.get_username merged=1 tabbed=1 icon="user" %}
{% include "wagtailadmin/shared/header.html" with title=editing_str subtitle=user.get_username merged=1 icon="user" %}
<ul class="tab-nav merged" data-tab-nav>
<li class="active"><a href="#account">{% trans "Account" %}</a></li>
<li><a href="#roles">{% trans "Roles" %}</a></li>
</ul>
<form action="{% url 'wagtailusers_users:edit' user.pk %}" method="POST" novalidate{% if form.is_multipart %} enctype="multipart/form-data"{% endif %}>
<div class="tab-content">
{% csrf_token %}
<section id="account" class="active nice-padding">
<ul class="fields">
{% block fields %}
{% if form.separate_username_field %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.username_field %}
{% endif %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.email %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.first_name %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.last_name %}
{% block extra_fields %}{% endblock extra_fields %}
{% if form.password1 %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.password1 %}
{% endif %}
{% if form.password2 %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.password2 %}
{% endif %}
{% if form.is_active %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.is_active %}
{% endif %}
{% endblock fields %}
<li>
<input type="submit" value="{% trans 'Save' %}" class="button" />
{% if can_delete %}
<a href="{% url 'wagtailusers_users:delete' user.pk %}" class="button button-secondary no">{% trans "Delete user" %}</a>
{% endif %}
</li>
</ul>
</section>
<section id="roles" class="nice-padding">
<ul class="fields">
{% if form.is_superuser %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.is_superuser %}
{% endif %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.groups %}
<li>
<input type="submit" value="{% trans 'Save' %}" class="button" />
{% if can_delete %}
<a href="{% url 'wagtailusers_users:delete' user.pk %}" class="button button-secondary no">{% trans "Delete user" %}</a>
{% endif %}
</li>
</ul>
</section>
<div class="w-tabs" data-tabs data-tabs-animate>
<div class="w-tabs__wrapper">
<div role="tablist" class="w-tabs__list nice-padding">
{% trans "Account" as account_text %}
{% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='account' title=account_text %}
{% trans "Roles" as roles_text %}
{% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='roles' title=roles_text %}
</div>
</div>
</form>
<form action="{% url 'wagtailusers_users:edit' user.pk %}" method="POST" novalidate{% if form.is_multipart %} enctype="multipart/form-data"{% endif %}>
<div class="tab-content nice-padding">
{% csrf_token %}
<section
id="tab-account"
class="w-tabs__panel"
role="tabpanel"
hidden
aria-labelledby="tab-label-account"
>
<ul class="fields">
{% block fields %}
{% if form.separate_username_field %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.username_field %}
{% endif %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.email %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.first_name %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.last_name %}
{% block extra_fields %}{% endblock extra_fields %}
{% if form.password1 %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.password1 %}
{% endif %}
{% if form.password2 %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.password2 %}
{% endif %}
{% if form.is_active %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.is_active %}
{% endif %}
{% endblock fields %}
<li>
<input type="submit" value="{% trans 'Save' %}" class="button"/>
{% if can_delete %}
<a href="{% url 'wagtailusers_users:delete' user.pk %}" class="button button-secondary no">{% trans "Delete user" %}</a>
{% endif %}
</li>
</ul>
</section>
<section
id="tab-roles"
class="w-tabs__panel"
role="tabpanel"
hidden
aria-labelledby="tab-label-roles"
>
<ul class="fields">
{% if form.is_superuser %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.is_superuser %}
{% endif %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.groups %}
<li>
<input type="submit" value="{% trans 'Save' %}" class="button"/>
{% if can_delete %}
<a href="{% url 'wagtailusers_users:delete' user.pk %}" class="button button-secondary no">{% trans "Delete user" %}</a>
{% endif %}
</li>
</ul>
</section>
</div>
</form>
</div>
{% endblock %}
{% block extra_css %}