Change slim sidebar to be fully usable in slim mode. Fix #7918 (#8197)

Co-authored-by: Thibaud Colas <thibaudcolas@gmail.com>

- Removing the peeking attribute so the sidebar only opens when intentionally set to expanded mode by using expand or search or account functionalities
- Adding tooltips on link item hovers
- Expanding of slim sidebar when search is clicked and when account options are clicked
pull/8239/head
Steve Stein 2022-03-28 08:43:35 -06:00 zatwierdzone przez GitHub
rodzic 2fb2629ba3
commit af4c4d0653
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
28 zmienionych plików z 558 dodań i 409 usunięć

Wyświetl plik

@ -148,6 +148,7 @@ These are classes that provide overrides.
// VENDOR: overrides of vendor styles.
@import 'overrides/vendor.datetimepicker';
@import 'overrides/vendor.tagit';
@import 'overrides/vendor.tippy';
// UTILITIES: classes that do one simple thing.
@import 'overrides/utilities.hidden';

Wyświetl plik

@ -0,0 +1,21 @@
@import '../../../node_modules/tippy.js/dist/tippy';
.tippy-box {
@apply w-bg-primary w-text-white w-text-14;
}
.tippy-box[data-placement^='top'] > .tippy-arrow::before {
@apply w-border-t-primary;
}
.tippy-box[data-placement^='bottom'] > .tippy-arrow::before {
@apply w-border-b-primary;
}
.tippy-box[data-placement^='left'] > .tippy-arrow::before {
@apply w-border-l-primary;
}
.tippy-box[data-placement^='right'] > .tippy-arrow::before {
@apply w-border-r-primary;
}

Wyświetl plik

@ -127,7 +127,7 @@ $font-wagtail-icons: wagtail;
// misc sizing
$thumbnail-width: 130px;
$menu-width: 200px;
$menu-width-slim: 60px;
$menu-width-slim: 65px;
$menu-width-max: 320px;

Wyświetl plik

@ -3,7 +3,7 @@
}
.c-page-explorer__item__link {
@apply w-inline-flex w-items-start sm:w-items-center w-flex-wrap w-grow w-cursor-pointer w-gap-1;
@apply w-inline-flex w-items-start sm:w-items-center w-flex-wrap w-grow w-cursor-pointer w-gap-1 w-transition;
padding: 1.45em 1em;
&:focus,
@ -35,7 +35,7 @@
}
.c-page-explorer__item__action {
@apply w-text-white/85;
@apply w-text-white/85 w-transition;
display: inline-flex;
align-items: center;
justify-content: center;

Wyświetl plik

@ -23,6 +23,7 @@
}
@include media-breakpoint-up(sm) {
position: static;
inset-inline-end: $sidebar-toggle-spacing;
// Remove once we drop support for Safari 13.
@ -79,23 +80,13 @@
&__collapse-toggle {
@include sidebar-toggle;
display: grid;
// All other styling is done with utility classes on this element
}
// When in mobile mode, hide the collapse-toggle and show the nav-toggle (which is defined in the .sidebar-nav-toggle class below)
&--mobile &__collapse-toggle {
display: none;
}
// This element should cover all the area beneath the collapse toggle
// It's only used to attach mouse enter/exit event handlers to control peeking
&__peek-hover-area {
margin-top: $sidebar-toggle-size;
display: grid;
grid-template-columns: 1fr;
overflow-y: auto;
overflow-x: hidden;
}
}
// This is a separate component as it needs to display in the header
@ -118,5 +109,4 @@
@import 'menu/MenuItem';
@import 'menu/SubMenuItem';
@import 'modules/MainMenu';
@import 'modules/Search';
@import 'modules/WagtailBranding';

Wyświetl plik

@ -17,6 +17,8 @@ export interface ModuleRenderContext {
key: number;
slim: boolean;
expandingOrCollapsing: boolean;
onAccountExpand: () => void;
onSearchClick: () => void;
currentPath: string;
strings: Strings;
navigate(url: string): Promise<void>;
@ -46,7 +48,6 @@ export const Sidebar: React.FunctionComponent<SidebarProps> = ({
// 'collapsed' is a persistent state that is controlled by the arrow icon at the top
// It records the user's general preference for a collapsed/uncollapsed menu
// This is just a hint though, and we may still collapse the menu if the screen is too small
// Also, we may display the full menu temporarily in collapsed mode (see 'peeking' below)
const [collapsed, setCollapsed] = React.useState(collapsedOnLoad);
// Call onExpandCollapse(true) if menu is initialised in collapsed state
@ -56,11 +57,6 @@ export const Sidebar: React.FunctionComponent<SidebarProps> = ({
}
}, []);
// 'peeking' is a temporary state to allow the user to peek in the menu while it is collapsed, or hidden.
// When peeking is true, the menu renders as if it's not collapsed, but as an overlay instead of occupying
// space next to the content
const [peeking, setPeeking] = React.useState(false);
// 'visibleOnMobile' indicates whether the sidebar is currently visible on mobile
// On mobile, the sidebar is completely hidden by default and must be opened manually
const [visibleOnMobile, setVisibleOnMobile] = React.useState(false);
@ -80,6 +76,7 @@ export const Sidebar: React.FunctionComponent<SidebarProps> = ({
setVisibleOnMobile(false);
}
}
window.addEventListener('resize', handleResize);
handleResize();
return () => window.removeEventListener('resize', handleResize);
@ -88,7 +85,7 @@ export const Sidebar: React.FunctionComponent<SidebarProps> = ({
// Whether or not to display the menu with slim layout.
// Separate from 'collapsed' as the menu can still be displayed with an expanded
// layout while in 'collapsed' mode if the user is 'peeking' into it (see above)
const slim = collapsed && !peeking && !isMobile;
const slim = collapsed && !isMobile;
// 'expandingOrCollapsing' is set to true whilst the the menu is transitioning between slim and expanded layouts
const [expandingOrCollapsing, setExpandingOrCollapsing] =
@ -124,40 +121,33 @@ export const Sidebar: React.FunctionComponent<SidebarProps> = ({
};
};
// Switch peeking on/off when the mouse cursor hovers the sidebar or focus is on the sidebar
const [mouseHover, setMouseHover] = React.useState(false);
const [focused, setFocused] = React.useState(false);
const onMouseEnterHandler = () => {
setMouseHover(true);
};
const onMouseLeaveHandler = () => {
setMouseHover(false);
const onBlurHandler = () => {
if (focused) {
setFocused(false);
setCollapsed(true);
}
};
const onFocusHandler = () => {
setFocused(true);
};
const onBlurHandler = () => {
setFocused(false);
};
// We need a stop peeking timeout to stop the sidebar moving as someone tab's though the menu
const stopPeekingTimeout = React.useRef<any>(null);
React.useEffect(() => {
if (mouseHover || focused) {
clearTimeout(stopPeekingTimeout.current);
setPeeking(true);
} else {
clearTimeout(stopPeekingTimeout.current);
stopPeekingTimeout.current = setTimeout(() => {
setPeeking(false);
}, SIDEBAR_TRANSITION_DURATION);
if (focused) {
setCollapsed(false);
setFocused(true);
}
}, [mouseHover, focused]);
};
const onSearchClick = () => {
if (slim) {
onClickCollapseToggle();
}
};
const onAccountExpand = () => {
if (slim) {
onClickCollapseToggle();
}
};
// Render modules
const renderedModules = modules.map((module, index) =>
@ -165,6 +155,8 @@ export const Sidebar: React.FunctionComponent<SidebarProps> = ({
key: index,
slim,
expandingOrCollapsing,
onAccountExpand,
onSearchClick,
currentPath,
strings,
navigate,
@ -181,31 +173,43 @@ export const Sidebar: React.FunctionComponent<SidebarProps> = ({
(isMobile && !visibleOnMobile ? ' sidebar--hidden' : '')
}
>
<div className="sidebar__inner">
<button
onClick={onClickCollapseToggle}
aria-label={strings.TOGGLE_SIDEBAR}
aria-expanded={slim ? 'false' : 'true'}
type="button"
className="button sidebar__collapse-toggle hover:w-bg-primary-200 hover:text-white hover:opacity-100"
<div
className="sidebar__inner"
onFocus={onFocusHandler}
onBlur={onBlurHandler}
>
<div
className={`${
slim ? 'w-justify-center' : 'w-justify-end'
} w-flex w-items-center`}
>
<Icon
name="expand-right"
className={`w-transition motion-reduce:w-transition-none
<button
onClick={onClickCollapseToggle}
aria-label={strings.TOGGLE_SIDEBAR}
aria-expanded={slim ? 'false' : 'true'}
type="button"
className={`
${!slim ? 'w-mr-4' : ''}
button
sidebar__collapse-toggle
w-flex
w-justify-center
w-items-center
sm:w-mt-4
hover:w-bg-primary-200
hover:text-white
hover:opacity-100`}
>
<Icon
name="expand-right"
className={`w-transition motion-reduce:w-transition-none
${!collapsed ? '-w-rotate-180' : ''}
`}
/>
</button>
<div
className="sidebar__peek-hover-area"
onMouseEnter={onMouseEnterHandler}
onMouseLeave={onMouseLeaveHandler}
onFocus={onFocusHandler}
onBlur={onBlurHandler}
>
{renderedModules}
/>
</button>
</div>
{renderedModules}
</div>
</div>
<button

Wyświetl plik

@ -1,9 +1,10 @@
.sidebar-panel {
@apply w-transition w-duration-150;
// With CSS variable allows panels with different widths to animate properly
--width: #{$menu-width};
visibility: hidden;
transform: translate3d(0, 0, 0);
transform: translateX(-100%);
position: fixed;
height: 100vh;
padding: 0;
@ -17,20 +18,9 @@
flex-direction: column;
overflow: hidden;
@include transition(
// Remove once we drop support for Safari 13.
// stylelint-disable-next-line property-disallowed-list
left $menu-transition-duration ease,
inset-inline-start $menu-transition-duration ease
);
@include media-breakpoint-up(sm) {
z-index: var(--z-index);
width: var(--width);
// Remove once we drop support for Safari 13.
// stylelint-disable-next-line property-disallowed-list
left: calc(#{$menu-width} - var(--width));
inset-inline-start: calc(#{$menu-width} - var(--width));
}
@media (forced-colors: $media-forced-colours) {
@ -43,12 +33,18 @@
box-shadow: 2px 0 2px rgba(0, 0, 0, 0.35);
}
// Showing the submenu options panel in mobile mode
.sidebar--mobile .sidebar-sub-menu-item--open &,
.sidebar--mobile .sidebar-page-explorer-item.sidebar-menu-item--active & {
transform: translateX(0);
}
@include media-breakpoint-up(sm) {
@at-root .sidebar--slim #{&} {
// Remove once we drop support for Safari 13.
// stylelint-disable-next-line property-disallowed-list
left: calc(#{$menu-width-slim} - var(--width));
inset-inline-start: calc(#{$menu-width-slim} - var(--width));
left: $menu-width-slim;
inset-inline-start: $menu-width-slim;
}
// Don't apply this to nested submenus though
@at-root .sidebar--slim .sidebar-panel #{&} {
@ -63,13 +59,15 @@
// stylelint-disable-next-line property-disallowed-list
left: $menu-width;
inset-inline-start: $menu-width;
transform: translateX(0);
// Don't apply this to nested submenus though
@at-root .sidebar--slim .sidebar-panel #{&} {
// Remove once we drop support for Safari 13.
// stylelint-disable-next-line property-disallowed-list
left: $menu-width;
inset-inline-start: $menu-width;
left: $menu-width-slim;
inset-inline-start: $menu-width-slim;
transform: translateX(0);
}
}
}

Wyświetl plik

@ -7,27 +7,36 @@ exports[`Sidebar should render with the minimum required props 1`] = `
>
<div
className="sidebar__inner"
onBlur={[Function]}
onFocus={[Function]}
>
<button
aria-expanded="true"
className="button sidebar__collapse-toggle hover:w-bg-primary-200 hover:text-white hover:opacity-100"
onClick={[Function]}
type="button"
<div
className="w-justify-end w-flex w-items-center"
>
<Icon
className="w-transition motion-reduce:w-transition-none
<button
aria-expanded="true"
className="
w-mr-4
button
sidebar__collapse-toggle
w-flex
w-justify-center
w-items-center
sm:w-mt-4
hover:w-bg-primary-200
hover:text-white
hover:opacity-100"
onClick={[Function]}
type="button"
>
<Icon
className="w-transition motion-reduce:w-transition-none
-w-rotate-180
"
name="expand-right"
/>
</button>
<div
className="sidebar__peek-hover-area"
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
/>
name="expand-right"
/>
</button>
</div>
</div>
</div>
<button

Wyświetl plik

@ -2,10 +2,11 @@ import * as React from 'react';
import Icon from '../../Icon/Icon';
import { MenuItemDefinition, MenuItemProps } from './MenuItem';
import Tippy from '@tippyjs/react';
export const LinkMenuItem: React.FunctionComponent<
MenuItemProps<LinkMenuItemDefinition>
> = ({ item, path, state, dispatch, navigate }) => {
> = ({ item, slim, path, state, dispatch, navigate }) => {
const isCurrent = state.activePath === path;
const isActive = state.activePath.startsWith(path);
const isInSubMenu = path.split('.').length > 2;
@ -40,17 +41,23 @@ export const LinkMenuItem: React.FunctionComponent<
return (
<li className={className}>
<a
href={item.url}
aria-current={isCurrent ? 'page' : undefined}
onClick={onClick}
className={`sidebar-menu-item__link ${item.classNames}`}
<Tippy
disabled={!slim || isInSubMenu}
content={item.label}
placement="right"
>
{item.iconName && (
<Icon name={item.iconName} className="icon--menuitem" />
)}
<span className="menuitem-label">{item.label}</span>
</a>
<a
href={item.url}
aria-current={isCurrent ? 'page' : undefined}
onClick={onClick}
className={`sidebar-menu-item__link ${item.classNames}`}
>
{item.iconName && (
<Icon name={item.iconName} className="icon--menuitem" />
)}
<span className="menuitem-label">{item.label}</span>
</a>
</Tippy>
</li>
);
};
@ -76,12 +83,13 @@ export class LinkMenuItemDefinition implements MenuItemDefinition {
this.classNames = classnames;
}
render({ path, state, dispatch, navigate }) {
render({ path, slim, state, dispatch, navigate }) {
return (
<LinkMenuItem
key={this.name}
item={this}
path={path}
slim={slim}
state={state}
dispatch={dispatch}
navigate={navigate}

Wyświetl plik

@ -5,6 +5,7 @@
position: relative;
&__link {
@apply w-text-14;
@include transition(
border-color $menu-transition-duration ease,
background-color $menu-transition-duration ease
@ -15,14 +16,12 @@
box-sizing: border-box;
white-space: nowrap;
border-inline-start: 3px solid transparent;
-webkit-font-smoothing: auto;
border: 0;
background: transparent;
text-align: start;
color: $color-menu-text;
padding: 11px 20px;
font-size: 13px;
font-weight: 400;
// Note, font-weights lower than normal,
@ -88,7 +87,15 @@
}
.sidebar--slim {
.menuitem-label {
opacity: 0;
.sidebar-menu-item {
.menuitem-label {
opacity: 0;
}
}
.sidebar-menu-item--in-sub-menu {
.menuitem-label {
opacity: 1;
}
}
}

Wyświetl plik

@ -18,6 +18,7 @@ export interface MenuItemDefinition {
export interface MenuItemProps<T> {
path: string;
slim: boolean;
state: MenuState;
item: T;
dispatch(action: MenuAction): void;

Wyświetl plik

@ -11,10 +11,11 @@ import {
} from '../../PageExplorer/actions';
import { SidebarPanel } from '../SidebarPanel';
import { SIDEBAR_TRANSITION_DURATION } from '../Sidebar';
import Tippy from '@tippyjs/react';
export const PageExplorerMenuItem: React.FunctionComponent<
MenuItemProps<PageExplorerMenuItemDefinition>
> = ({ path, item, state, dispatch, navigate }) => {
> = ({ path, slim, item, state, dispatch, navigate }) => {
const isOpen = state.navigationPath.startsWith(path);
const isActive = isOpen || state.activePath.startsWith(path);
const depth = path.split('.').length;
@ -72,17 +73,19 @@ export const PageExplorerMenuItem: React.FunctionComponent<
return (
<li className={className}>
<button
onClick={onClick}
className="sidebar-menu-item__link"
aria-haspopup="menu"
aria-expanded={isOpen ? 'true' : 'false'}
type="button"
>
<Icon name="folder-open-inverse" className="icon--menuitem" />
<span className="menuitem-label">{item.label}</span>
<Icon className={sidebarTriggerIconClassName} name="arrow-right" />
</button>
<Tippy disabled={isOpen || !slim} content={item.label} placement="right">
<button
onClick={onClick}
className="sidebar-menu-item__link"
aria-haspopup="menu"
aria-expanded={isOpen ? 'true' : 'false'}
type="button"
>
<Icon name="folder-open-inverse" className="icon--menuitem" />
<span className="menuitem-label">{item.label}</span>
<Icon className={sidebarTriggerIconClassName} name="arrow-right" />
</button>
</Tippy>
<div>
<SidebarPanel
isVisible={isVisible}
@ -112,12 +115,13 @@ export class PageExplorerMenuItemDefinition extends LinkMenuItemDefinition {
this.startPageId = startPageId;
}
render({ path, state, dispatch, navigate }) {
render({ path, slim, state, dispatch, navigate }) {
return (
<PageExplorerMenuItem
key={this.name}
item={this}
path={path}
slim={slim}
state={state}
dispatch={dispatch}
navigate={navigate}

Wyświetl plik

@ -1,4 +1,5 @@
.sidebar-sub-menu-trigger-icon {
$root: &;
display: block;
width: 20px;
height: 20px;
@ -27,6 +28,10 @@
height: 16px;
transform: translate3d(13px, 0, 0);
}
.sidebar--slim &--open {
transform: translate3d(13px, 0, 0) rotate(180deg);
}
}
.sidebar-sub-menu-panel {
@ -40,7 +45,7 @@
> h2 {
// w-min-h-[160px] and w-mt-[35px] classes are to vertically align the title and icon combination to the search input on the left
@apply w-min-h-[160px] w-mt-[35px] w-text-white w-mb-0 w-inline-flex w-flex-col w-justify-center w-items-center;
@apply w-min-h-[160px] w-mt-[45px] w-px-4 w-box-border w-text-center w-text-white w-mb-0 w-inline-flex w-flex-col w-justify-center w-items-center;
&:before {
font-size: 4em;
@ -50,6 +55,10 @@
width: 100%;
opacity: 0.15;
}
@at-root .sidebar--slim & {
@apply w-mt-3;
}
}
ul > li {
@ -81,9 +90,6 @@
box-shadow: 2px 0 2px rgba(0, 0, 0, 0.35);
}
@at-root .sidebar--slim #{&} {
transform: translate3d($menu-width-slim - $menu-width, 0, 0);
}
// Don't apply this to nested submenus though
@at-root .sidebar--slim .sidebar-sub-menu-panel #{&} {
transform: translate3d(0, 0, 0);

Wyświetl plik

@ -6,6 +6,7 @@ import { renderMenu } from '../modules/MainMenu';
import { SidebarPanel } from '../SidebarPanel';
import { SIDEBAR_TRANSITION_DURATION } from '../Sidebar';
import { MenuItemDefinition, MenuItemProps } from './MenuItem';
import Tippy from '@tippyjs/react';
interface SubMenuItemProps extends MenuItemProps<SubMenuItemDefinition> {
slim: boolean;
@ -65,19 +66,21 @@ export const SubMenuItem: React.FunctionComponent<SubMenuItemProps> = ({
return (
<li className={className}>
<button
onClick={onClick}
className={`sidebar-menu-item__link ${item.classNames}`}
aria-haspopup="menu"
aria-expanded={isOpen ? 'true' : 'false'}
type="button"
>
{item.iconName && (
<Icon name={item.iconName} className="icon--menuitem" />
)}
<span className="menuitem-label">{item.label}</span>
<Icon className={sidebarTriggerIconClassName} name="arrow-right" />
</button>
<Tippy disabled={isOpen || !slim} content={item.label} placement="right">
<button
onClick={onClick}
className={`sidebar-menu-item__link ${item.classNames}`}
aria-haspopup="menu"
aria-expanded={isOpen ? 'true' : 'false'}
type="button"
>
{item.iconName && (
<Icon name={item.iconName} className="icon--menuitem" />
)}
<span className="menuitem-label">{item.label}</span>
<Icon className={sidebarTriggerIconClassName} name="arrow-right" />
</button>
</Tippy>
<SidebarPanel isVisible={isVisible} isOpen={isOpen} depth={depth}>
<div className="sidebar-sub-menu-panel">
<h2

Wyświetl plik

@ -4,25 +4,30 @@ exports[`PageExplorerMenuItem should render with the minimum required props 1`]
<li
className="sidebar-menu-item sidebar-page-explorer-item"
>
<button
aria-expanded="false"
aria-haspopup="menu"
className="sidebar-menu-item__link"
onClick={[Function]}
type="button"
<ForwardRef(TippyWrapper)
disabled={true}
placement="right"
>
<Icon
className="icon--menuitem"
name="folder-open-inverse"
/>
<span
className="menuitem-label"
/>
<Icon
className="sidebar-sub-menu-trigger-icon"
name="arrow-right"
/>
</button>
<button
aria-expanded="false"
aria-haspopup="menu"
className="sidebar-menu-item__link"
onClick={[Function]}
type="button"
>
<Icon
className="icon--menuitem"
name="folder-open-inverse"
/>
<span
className="menuitem-label"
/>
<Icon
className="sidebar-sub-menu-trigger-icon"
name="arrow-right"
/>
</button>
</ForwardRef(TippyWrapper)>
<div>
<SidebarPanel
depth={2}

Wyświetl plik

@ -4,21 +4,26 @@ exports[`SubMenuItem should render with the minimum required props 1`] = `
<li
className="sidebar-menu-item sidebar-sub-menu-item sidebar-menu-item--active"
>
<button
aria-expanded="false"
aria-haspopup="menu"
className="sidebar-menu-item__link "
onClick={[Function]}
type="button"
<ForwardRef(TippyWrapper)
disabled={true}
placement="right"
>
<span
className="menuitem-label"
/>
<Icon
className="sidebar-sub-menu-trigger-icon"
name="arrow-right"
/>
</button>
<button
aria-expanded="false"
aria-haspopup="menu"
className="sidebar-menu-item__link "
onClick={[Function]}
type="button"
>
<span
className="menuitem-label"
/>
<Icon
className="sidebar-sub-menu-trigger-icon"
name="arrow-right"
/>
</button>
</ForwardRef(TippyWrapper)>
<SidebarPanel
depth={2}
isOpen={false}

Wyświetl plik

@ -2,9 +2,32 @@
.sidebar-main-menu {
overflow: auto;
overflow-x: hidden;
margin-bottom: 60px;
// So the last items in the menu will be seen when the menu is vertically scrollable
margin-bottom: 52px;
// Scrollbar styling for firefox
scrollbar-color: theme('colors.grey.200');
scrollbar-width: thin;
@include transition(margin-bottom $menu-transition-duration ease);
//Custom scrollbar styling for windows/mac and slim mode
&::-webkit-scrollbar {
width: 4px;
}
&::-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-transparent;
}
&--open-footer {
margin-bottom: 127px;
}
@ -84,6 +107,8 @@
}
&__account {
@include show-focus-outline-inside();
&-toggle {
@apply w-pl-2 w-inline-flex w-justify-between w-w-full w-translate-x-0 w-transition w-duration-150;
}

Wyświetl plik

@ -5,6 +5,7 @@ import { Menu } from './MainMenu';
describe('Menu', () => {
const strings = {};
const user = { avatarUrl: 'https://gravatar/profile' };
const onAccountExpand = jest.fn();
it('should render with the minimum required props', () => {
const wrapper = shallow(
@ -13,6 +14,7 @@ describe('Menu', () => {
menuItems={[]}
strings={strings}
user={user}
onAccountExpand={onAccountExpand}
/>,
);
@ -26,6 +28,7 @@ describe('Menu', () => {
menuItems={[]}
strings={strings}
user={user}
onAccountExpand={onAccountExpand}
/>,
);

Wyświetl plik

@ -5,6 +5,7 @@ import { LinkMenuItemDefinition } from '../menu/LinkMenuItem';
import { MenuItemDefinition } from '../menu/MenuItem';
import { SubMenuItemDefinition } from '../menu/SubMenuItem';
import { ModuleDefinition, Strings } from '../Sidebar';
import Tippy from '@tippyjs/react';
export function renderMenu(
path: string,
@ -64,6 +65,7 @@ interface MenuProps {
user: MainMenuModuleDefinition['user'];
slim: boolean;
expandingOrCollapsing: boolean;
onAccountExpand: () => void;
currentPath: string;
strings: Strings;
@ -75,6 +77,7 @@ export const Menu: React.FunctionComponent<MenuProps> = ({
accountMenuItems,
user,
expandingOrCollapsing,
onAccountExpand,
slim,
currentPath,
strings,
@ -90,6 +93,7 @@ export const Menu: React.FunctionComponent<MenuProps> = ({
activePath: '',
});
const accountSettingsOpen = state.navigationPath.startsWith('.account');
const isVisible = !slim || expandingOrCollapsing;
// Whenever currentPath or menu changes, work out new activePath
React.useEffect(() => {
@ -161,17 +165,30 @@ export const Menu: React.FunctionComponent<MenuProps> = ({
};
}, []);
// Determine if the sidebar is expanded from account button click
const [expandedFromAccountClick, setExpandedFromAccountClick] =
React.useState<boolean>(false);
// Whenever the parent Sidebar component collapses or expands, close any open menus
React.useEffect(() => {
if (expandingOrCollapsing) {
if (expandingOrCollapsing && !expandedFromAccountClick) {
dispatch({
type: 'set-navigation-path',
path: '',
});
}
if (expandedFromAccountClick) {
setExpandedFromAccountClick(false);
}
}, [expandingOrCollapsing]);
const onClickAccountSettings = () => {
// Pass account expand information to Sidebar component
onAccountExpand();
if (slim) {
setExpandedFromAccountClick(true);
}
if (accountSettingsOpen) {
dispatch({
type: 'set-navigation-path',
@ -199,47 +216,53 @@ export const Menu: React.FunctionComponent<MenuProps> = ({
<div
className={
'sidebar-footer' +
(accountSettingsOpen ? ' sidebar-footer--open' : '')
(accountSettingsOpen ? ' sidebar-footer--open' : '') +
(isVisible ? ' sidebar-footer--visible' : '')
}
>
<button
className="
sidebar-footer__account
w-bg-primary
w-text-white
w-flex
w-items-center
w-relative
w-p-0
w-w-full
w-appearance-none
w-border-0
w-overflow-hidden
w-px-5
w-py-3
hover:w-bg-primary-200
focus:w-bg-primary-200
w-transition"
title={strings.EDIT_YOUR_ACCOUNT}
onClick={onClickAccountSettings}
aria-label={strings.EDIT_YOUR_ACCOUNT}
aria-haspopup="menu"
aria-expanded={accountSettingsOpen ? 'true' : 'false'}
type="button"
<Tippy
disabled={!slim}
content={strings.EDIT_YOUR_ACCOUNT}
placement="right"
>
<div className="avatar avatar-on-dark w-flex-shrink-0 !w-w-[28px] !w-h-[28px]">
<img src={user.avatarUrl} alt="" />
</div>
<div className="sidebar-footer__account-toggle">
<div className="sidebar-footer__account-label w-label-3">
{user.name}
<button
className="
sidebar-footer__account
w-bg-primary
w-text-white
w-flex
w-items-center
w-relative
w-w-full
w-appearance-none
w-border-0
w-overflow-hidden
w-px-5
w-py-3
hover:w-bg-primary-200
focus:w-bg-primary-200
w-transition"
title={strings.EDIT_YOUR_ACCOUNT}
onClick={onClickAccountSettings}
aria-label={strings.EDIT_YOUR_ACCOUNT}
aria-haspopup="menu"
aria-expanded={accountSettingsOpen ? 'true' : 'false'}
type="button"
>
<div className="avatar avatar-on-dark w-flex-shrink-0 !w-w-[28px] !w-h-[28px]">
<img src={user.avatarUrl} alt="" />
</div>
<Icon
className="w-w-4 w-h-4 w-text-white"
name={accountSettingsOpen ? 'arrow-down' : 'arrow-up'}
/>
</div>
</button>
<div className="sidebar-footer__account-toggle">
<div className="sidebar-footer__account-label w-label-3">
{user.name}
</div>
<Icon
className="w-w-4 w-h-4 w-text-white"
name={accountSettingsOpen ? 'arrow-down' : 'arrow-up'}
/>
</div>
</button>
</Tippy>
<ul>
{renderMenu('', accountMenuItems, slim, state, dispatch, navigate)}
@ -267,7 +290,15 @@ export class MainMenuModuleDefinition implements ModuleDefinition {
this.user = user;
}
render({ slim, expandingOrCollapsing, key, currentPath, strings, navigate }) {
render({
slim,
expandingOrCollapsing,
onAccountExpand,
key,
currentPath,
strings,
navigate,
}) {
return (
<Menu
menuItems={this.menuItems}
@ -275,6 +306,7 @@ export class MainMenuModuleDefinition implements ModuleDefinition {
user={this.user}
slim={slim}
expandingOrCollapsing={expandingOrCollapsing}
onAccountExpand={onAccountExpand}
key={key}
currentPath={currentPath}
strings={strings}

Wyświetl plik

@ -1,68 +0,0 @@
// stylelint-disable declaration-no-important
.sidebar-search {
@apply w-relative w-box-border w-flex w-items-center w-flex-row w-h-[42px] w-px-5;
$root: &;
.sidebar--slim & {
@apply w-justify-center w-p-0;
}
&__label {
@include visuallyhidden;
}
// Beat specificity
input:not([type='submit']) {
@apply w-pl-[45px];
@include show-focus-outline-inside();
position: absolute;
// Remove once we drop support for Safari 13.
// stylelint-disable-next-line property-disallowed-list
left: 0;
inset-inline-start: 0;
top: 0;
font-size: 13px;
font-weight: 400;
background-color: transparent;
border: 0;
border-radius: 0;
color: $color-menu-text;
-webkit-font-smoothing: auto;
.sidebar--slim & {
opacity: 0;
}
&::placeholder {
color: $color-menu-text;
}
}
&__submit {
@include show-focus-outline-inside();
background-color: transparent;
border: 0;
border-radius: 0;
color: #ccc;
padding: 0;
width: 35px;
height: 35px;
transition: opacity $menu-transition-duration ease,
width $menu-transition-duration ease;
svg {
margin-inline-end: 20px;
transition: margin-inline-end $menu-transition-duration ease;
}
.sidebar--slim & {
svg {
margin-inline-end: 0;
}
}
&:hover {
background-color: transparent;
}
}
}

Wyświetl plik

@ -1,11 +1,18 @@
import * as React from 'react';
import Icon from '../../Icon/Icon';
import { ModuleDefinition, Strings } from '../Sidebar';
import {
ModuleDefinition,
Strings,
SIDEBAR_TRANSITION_DURATION,
} from '../Sidebar';
import Tippy from '@tippyjs/react';
interface SearchInputProps {
slim: boolean;
expandingOrCollapsing: boolean;
onSearchClick: () => void;
searchUrl: string;
strings: Strings;
@ -15,11 +22,13 @@ interface SearchInputProps {
export const SearchInput: React.FunctionComponent<SearchInputProps> = ({
slim,
expandingOrCollapsing,
onSearchClick,
searchUrl,
strings,
navigate,
}) => {
const isVisible = !slim || expandingOrCollapsing;
const searchInput = React.useRef<HTMLInputElement>(null);
const onSubmitForm = (e: React.FormEvent<HTMLFormElement>) => {
if (e.target instanceof HTMLFormElement) {
@ -36,36 +45,84 @@ export const SearchInput: React.FunctionComponent<SearchInputProps> = ({
}
};
const className =
'sidebar-search' +
(slim ? ' sidebar-search--slim' : '') +
(isVisible ? ' sidebar-search--visible' : '');
return (
<form
role="search"
className={className}
className={`w-h-[42px] w-relative w-box-border w-flex w-items-center w-justify-start w-flex-row w-flex-shrink-0`}
action={searchUrl}
method="get"
onSubmit={onSubmitForm}
>
<button
className="button sidebar-search__submit"
type="submit"
aria-label={strings.SEARCH}
>
<Icon className="icon--menuitem" name="search" />
</button>
<label className="sidebar-search__label" htmlFor="menu-search-q">
{strings.SEARCH}
</label>
<input
className="sidebar-search__input"
type="text"
id="menu-search-q"
name="q"
placeholder={strings.SEARCH}
/>
<div className="w-flex w-flex-row w-items-center w-h-full">
<Tippy
disabled={isVisible || !slim}
content={strings.SEARCH}
placement="right"
>
{/* Use padding left 23px to align icon in slim mode and padding right 18px to ensure focus is full width */}
<button
className={`
${slim ? 'w-pr-[18px]' : 'w-pr-0'}
w-w-full
w-pl-[23px]
w-h-[35px]
w-bg-transparent
w-outline-offset-inside
w-border-0
w-rounded-none
w-text-white/80
w-z-10
hover:w-text-white
focus:w-text-white
hover:w-bg-transparent`}
type="submit"
aria-label={strings.SEARCH}
onClick={(e) => {
if (slim) {
e.preventDefault();
onSearchClick();
// Focus search input after transition when button is clicked in slim mode
setTimeout(() => {
if (searchInput.current) {
searchInput.current.focus();
}
}, SIDEBAR_TRANSITION_DURATION);
}
}}
>
<Icon className="icon--menuitem" name="search" />
</button>
</Tippy>
<label className="w-sr-only" htmlFor="menu-search-q">
{strings.SEARCH}
</label>
{/* Classes marked important to trump the base input styling set in _forms.scss */}
<input
className={`
${slim || !isVisible ? 'w-hidden' : ''}
!w-pl-[45px]
!w-subpixel-antialiased
!w-absolute
!w-left-0
!w-font-normal
!w-top-0
!w-text-14
!w-bg-transparent
!w-border-0
!w-rounded-none
!w-text-white/80
!w-outline-offset-inside
placeholder:!w-text-white/80`}
type="text"
id="menu-search-q"
name="q"
placeholder={strings.SEARCH}
ref={searchInput}
/>
</div>
</form>
);
};
@ -77,13 +134,21 @@ export class SearchModuleDefinition implements ModuleDefinition {
this.searchUrl = searchUrl;
}
render({ slim, key, expandingOrCollapsing, strings, navigate }) {
render({
slim,
key,
expandingOrCollapsing,
onSearchClick,
strings,
navigate,
}) {
return (
<SearchInput
searchUrl={this.searchUrl}
slim={slim}
key={key}
expandingOrCollapsing={expandingOrCollapsing}
onSearchClick={onSearchClick}
strings={strings}
navigate={navigate}
/>

Wyświetl plik

@ -18,7 +18,7 @@
align-items: center;
color: #aaa;
-webkit-font-smoothing: auto;
margin: 1.8em auto 2.5em;
margin: 4em auto 1.8em;
text-align: center;
width: 100px;
height: 100px;
@ -28,10 +28,15 @@
box-sizing: border-box;
border-radius: 100%;
@include media-breakpoint-up(sm) {
margin: 1.8em auto 2.5em;
}
// Reduce overall size when in slim mode
.sidebar--slim & {
@include show-focus-outline-inside();
width: 60px;
transform: none;
height: 60px;
}
// Remove background on 404 page
@ -63,30 +68,13 @@
// Bird wrapper
&__icon-wrapper {
@apply w-bg-white/15 w-overflow-hidden hover:w-overflow-visible;
@apply w-bg-white/15 w-relative w-overflow-hidden hover:w-overflow-visible;
margin: auto;
position: absolute;
// Remove once we drop support for Safari 13.
// stylelint-disable-next-line property-disallowed-list
left: 0;
inset-inline-start: 0;
top: 0;
width: 100px;
height: 100px;
border-radius: 50%;
// Remove once we drop support for Safari 13.
// stylelint-disable-next-line property-disallowed-list
transition: left $menu-transition-duration ease,
inset-inline-start $menu-transition-duration ease,
top $menu-transition-duration ease, width $menu-transition-duration ease,
height $menu-transition-duration ease;
.sidebar--slim & {
// Remove once we drop support for Safari 13.
// stylelint-disable-next-line property-disallowed-list
left: 10px;
inset-inline-start: 10px;
top: 10px;
width: 40px;
height: 40px;
}
@ -97,28 +85,6 @@
position: static;
}
}
// Bird icons
&__icon {
.sidebar--slim & {
width: 42px;
height: 51px;
top: 10px;
// Remove once we drop support for Safari 13.
// stylelint-disable-next-line property-disallowed-list
left: -9px;
inset-inline-start: -9px;
}
// TODO: Fix legacy specificity issues
&[data-part='eye--open'] {
display: inline !important;
}
&[data-part='eye--closed'] {
display: none !important;
}
}
}
.sidebar-custom-branding {

Wyświetl plik

@ -5,6 +5,7 @@ import WagtailLogo from './WagtailLogo';
interface WagtailBrandingProps {
homeUrl: string;
strings: Strings;
slim: boolean;
currentPath: string;
navigate(url: string): void;
}
@ -12,6 +13,7 @@ interface WagtailBrandingProps {
const WagtailBranding: React.FunctionComponent<WagtailBrandingProps> = ({
homeUrl,
strings,
slim,
currentPath,
navigate,
}) => {
@ -79,7 +81,7 @@ const WagtailBranding: React.FunctionComponent<WagtailBrandingProps> = ({
};
const desktopClassName =
'sidebar-wagtail-branding' +
'sidebar-wagtail-branding w-transition-all w-duration-150' +
(isWagging ? ' sidebar-wagtail-branding--wagging' : '');
return (
@ -92,8 +94,8 @@ const WagtailBranding: React.FunctionComponent<WagtailBrandingProps> = ({
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
>
<div className="sidebar-wagtail-branding__icon-wrapper">
<WagtailLogo />
<div className="sidebar-wagtail-branding__icon-wrapper w-transition-all w-duration-150">
<WagtailLogo slim={slim} />
</div>
</a>
);
@ -106,11 +108,12 @@ export class WagtailBrandingModuleDefinition implements ModuleDefinition {
this.homeUrl = homeUrl;
}
render({ strings, key, navigate, currentPath }) {
render({ strings, slim, key, navigate, currentPath }) {
return (
<WagtailBranding
key={key}
homeUrl={this.homeUrl}
slim={slim}
strings={strings}
navigate={navigate}
currentPath={currentPath}

Wyświetl plik

@ -2,16 +2,32 @@ import React from 'react';
interface WagtailLogoProps {
className?: string;
slim: boolean;
}
const WagtailLogo = ({ className }: WagtailLogoProps) => {
const feathersClasses = 'group-hover:w-text-black';
const WagtailLogo = ({ className, slim }: WagtailLogoProps) => {
const feathersClasses =
'group-hover:w-text-black w-transition-all w-duration-150';
return (
<svg
className={`
${className || ''}
sidebar-wagtail-branding__icon !w-overflow-visible w-group w-text-primary w-transition w-delay-150 w-duration-150 hover:w-scale-75 hover:w-rotate-6 hover:w-translate-y-[-20px] hover:w-translate-x-[10px] w-z-10 w-absolute w-w-[100px] w-h-[125px] w-top-[25px] w-left-[-20px]
sidebar-wagtail-branding__icon
!w-overflow-visible
w-group
w-text-primary
w-z-10
w-absolute
w-transition-all
w-duration-150
hover:w-scale-75
hover:w-rotate-6
${className || ''}
${
slim
? 'w-w-[42px] w-h-[51px] w-top-2.5 w-left-[-9px] hover:-w-translate-y-1.5 hover:w-translate-x-1'
: 'w-w-[100px] w-h-[125px] w-top-[25px] -w-left-5 hover:w-translate-x-2.5 hover:-w-translate-y-5'
}
`}
width="430"
height="537"

Wyświetl plik

@ -10,51 +10,55 @@ exports[`Menu should render with the minimum required props 1`] = `
/>
</nav>
<div
className="sidebar-footer"
className="sidebar-footer sidebar-footer--visible"
>
<button
aria-expanded="false"
aria-haspopup="menu"
className="
sidebar-footer__account
w-bg-primary
w-text-white
w-flex
w-items-center
w-relative
w-p-0
w-w-full
w-appearance-none
w-border-0
w-overflow-hidden
w-px-5
w-py-3
hover:w-bg-primary-200
focus:w-bg-primary-200
w-transition"
onClick={[Function]}
type="button"
<ForwardRef(TippyWrapper)
disabled={true}
placement="right"
>
<div
className="avatar avatar-on-dark w-flex-shrink-0 !w-w-[28px] !w-h-[28px]"
>
<img
alt=""
src="https://gravatar/profile"
/>
</div>
<div
className="sidebar-footer__account-toggle"
<button
aria-expanded="false"
aria-haspopup="menu"
className="
sidebar-footer__account
w-bg-primary
w-text-white
w-flex
w-items-center
w-relative
w-w-full
w-appearance-none
w-border-0
w-overflow-hidden
w-px-5
w-py-3
hover:w-bg-primary-200
focus:w-bg-primary-200
w-transition"
onClick={[Function]}
type="button"
>
<div
className="sidebar-footer__account-label w-label-3"
/>
<Icon
className="w-w-4 w-h-4 w-text-white"
name="arrow-up"
/>
</div>
</button>
className="avatar avatar-on-dark w-flex-shrink-0 !w-w-[28px] !w-h-[28px]"
>
<img
alt=""
src="https://gravatar/profile"
/>
</div>
<div
className="sidebar-footer__account-toggle"
>
<div
className="sidebar-footer__account-label w-label-3"
/>
<Icon
className="w-w-4 w-h-4 w-text-white"
name="arrow-up"
/>
</div>
</button>
</ForwardRef(TippyWrapper)>
<ul />
</div>
</Fragment>

Wyświetl plik

@ -73,6 +73,9 @@ module.exports = {
15: '0.15',
85: '0.85',
},
outlineOffset: {
inside: '-3px',
},
},
},
plugins: [

42
package-lock.json wygenerowano
Wyświetl plik

@ -8,6 +8,7 @@
"name": "wagtail",
"version": "1.0.0",
"dependencies": {
"@tippyjs/react": "^4.2.6",
"draft-js": "^0.10.5",
"draftail": "^1.4.1",
"draftjs-filters": "^2.5.0",
@ -23,6 +24,7 @@
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"telepath-unpack": "^0.0.3",
"tippy.js": "^6.3.7",
"uuid": "^8.3.2"
},
"devDependencies": {
@ -3112,7 +3114,6 @@
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz",
"integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==",
"dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@ -9436,6 +9437,18 @@
"react-dom": "^16.8.0 || ^17.0.0"
}
},
"node_modules/@tippyjs/react": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz",
"integrity": "sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==",
"dependencies": {
"tippy.js": "^6.3.1"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/@tootallnate/once": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@ -27477,6 +27490,14 @@
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
"dev": true
},
"node_modules/tippy.js": {
"version": "6.3.7",
"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz",
"integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
"dependencies": {
"@popperjs/core": "^2.9.0"
}
},
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@ -31768,8 +31789,7 @@
"@popperjs/core": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz",
"integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==",
"dev": true
"integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA=="
},
"@sinonjs/commons": {
"version": "1.8.3",
@ -36771,6 +36791,14 @@
"store2": "^2.12.0"
}
},
"@tippyjs/react": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz",
"integrity": "sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==",
"requires": {
"tippy.js": "^6.3.1"
}
},
"@tootallnate/once": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@ -50810,6 +50838,14 @@
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
"dev": true
},
"tippy.js": {
"version": "6.3.7",
"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz",
"integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
"requires": {
"@popperjs/core": "^2.9.0"
}
},
"tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",

Wyświetl plik

@ -95,6 +95,7 @@
"webpack-cli": "^4.9.1"
},
"dependencies": {
"@tippyjs/react": "^4.2.6",
"draft-js": "^0.10.5",
"draftail": "^1.4.1",
"draftjs-filters": "^2.5.0",
@ -110,6 +111,7 @@
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"telepath-unpack": "^0.0.3",
"tippy.js": "^6.3.7",
"uuid": "^8.3.2"
},
"scripts": {