Updating header with new Crayons buttons (#15653)

* init

* whoops

* components update

* components update

* classes

* variables

* focus

* test

* spec

* test

* adjust colors and variables

* buttons colors

* Update app/javascript/crayons/CTAs/CTA.jsx

Co-authored-by: Suzanne Aitchison <suzanne@forem.com>

* proptype

* remove duplicated tests now in Cypress, enhance Cypress nav menu tests

* premove unneeded viewport in test

* remove focus check for now

* classnames

* Update app/javascript/crayons/CTAs/__stories__/CTAs.mdx

Co-authored-by: Nick Taylor <nick@iamdeveloper.com>

* Update app/javascript/crayons/CTAs/__stories__/CTAs.mdx

Co-authored-by: Nick Taylor <nick@iamdeveloper.com>

* better contrast

* classname

* radius-full

Co-authored-by: Suzanne Aitchison <suzanne@forem.com>
Co-authored-by: Nick Taylor <nick@iamdeveloper.com>
This commit is contained in:
ludwiczakpawel 2021-12-09 16:41:18 +01:00 committed by GitHub
parent ac5934e63f
commit 11cabac518
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 333 additions and 481 deletions

View file

@ -1,26 +0,0 @@
function initializeSpecialNavigationFunctionality() {
var connectLink = document.getElementById('connect-link');
var notificationsLink = document.getElementById('notifications-link');
var moderationLink = document.getElementById('moderation-link');
if (connectLink) {
if (document.getElementById('notifications-container')) {
notificationsLink.blur();
notificationsLink.classList.add('crayons-header__link--current');
} else {
notificationsLink.classList.remove('crayons-header__link--current');
}
if (document.getElementById('chat')) {
connectLink.blur();
connectLink.classList.add('crayons-header__link--current');
} else {
connectLink.classList.remove('crayons-header__link--current');
}
if (document.getElementById('moderation-page')) {
moderationLink.blur();
moderationLink.classList.add('crayons-header__link--current');
} else {
moderationLink.classList.remove('crayons-header__link--current');
}
}
}

View file

@ -427,7 +427,6 @@ label[class*='crayons-btn']:focus-within // label for input[type="file"] made to
font: inherit;
text-align: center;
cursor: pointer;
transition: all var(--transition-props);
border: none;
overflow-wrap: normal;
outline: 0;
@ -451,6 +450,7 @@ label[class*='crayons-btn']:focus-within // label for input[type="file"] made to
color: var(--color-hover);
z-index: var(--z-elevate);
box-shadow: var(--focus-ring);
outline: 0;
}
.js-focus-visible &.focus-visible:focus {
@ -458,6 +458,14 @@ label[class*='crayons-btn']:focus-within // label for input[type="file"] made to
color: var(--color-hover);
z-index: var(--z-elevate);
box-shadow: var(--focus-ring);
outline: 0;
}
&[aria-expanded='true'],
&[aria-pressed='true'] {
--bg: var(--btn-current-bg);
--color: var(--btn-current-color);
font-weight: var(--fw-bold);
}
&[disabled] {

View file

@ -4,37 +4,79 @@
// .c-cta //////////////////////////////////////////////////
////////////////////////////////////////////////////////////
.c-link {
&.c-cta {
--border-width: 1px;
padding: calc(var(--su-2) - var(--border-width))
calc(var(--su-4) - var(--border-width));
border: var(--border-width) solid var(--border);
text-decoration: none;
background-color: var(--bg);
color: var(--color);
text-align: center;
display: inline-block;
.c-cta {
--border-width: 1px;
padding: calc(var(--su-2) - var(--border-width))
calc(var(--su-4) - var(--border-width));
border: var(--border-width) solid;
border-color: var(--border);
text-decoration: none;
border-radius: var(--radius);
font: inherit;
background-color: var(--bg);
color: var(--color);
outline: 0;
text-align: center;
display: inline-flex;
position: relative;
overflow-wrap: normal;
&:hover {
background-color: var(--bg-hover);
border-color: var(--border-hover);
color: var(--color-hover);
z-index: var(--z-elevate);
text-decoration: underline;
}
&.c-cta {
--bg: transparent;
--bg-hover: var(--base-20);
--color: var(--base-80);
--color-hover: var(--base-100);
--border: var(--base-60);
--border-hover: var(--base-80);
// Both focus declarations below are the same but we unfortunately can't
// combine them because Safari doesn't recognize it properly.
// First declaration is for all browsers that support :focus-visible
// which is basically almost all of them
// Second declaration is for browsers that do not support :focus-visible
// which basically is only Safari right now.
&:focus-visible {
background-color: var(--bg-hover);
border-color: var(--border-hover);
color: var(--color-hover);
z-index: var(--z-elevate);
box-shadow: var(--focus-ring);
text-decoration: underline;
}
&.c-cta--branded {
--bg: transparent;
--bg-hover: var(--accent-brand-a10);
--color: var(--accent-brand);
--color-hover: var(--accent-brand-darker);
--border: var(--accent-brand);
--border-hover: var(--accent-brand-darker);
.js-focus-visible &.focus-visible:focus {
background-color: var(--bg-hover);
border-color: var(--border-hover);
color: var(--color-hover);
z-index: var(--z-elevate);
box-shadow: var(--focus-ring);
text-decoration: underline;
}
}
.c-cta {
--bg: var(--cta-bg);
--bg-hover: var(--cta-bg-hover);
--color: var(--cta-color);
--color-hover: var(--cta-color-hover);
--border: var(--cta-border);
--border-hover: var(--cta-border-hover);
&--branded {
--bg: var(--cta-branded-bg);
--bg-hover: var(--cta-branded-bg-hover);
--color: var(--cta-branded-color);
--color-hover: var(--cta-branded-color-hover);
--border: var(--cta-branded-border);
--border-hover: var(--cta-branded-border-hover);
font-weight: var(--fw-medium);
}
}
.c-cta--icon-left {
.c-cta__icon {
margin-right: var(--su-2);
margin-left: calc(var(--su-1) * -1);
}
}

View file

@ -16,8 +16,8 @@
&-focus {
background-color: var(--form-bg-focus);
border-color: var(--form-border-focus);
box-shadow: 0 0 0 1px var(--form-border-focus);
border-color: var(--focus);
box-shadow: 0 0 0 1px var(--focus);
}
&-disabled {

View file

@ -2,8 +2,6 @@
@import './skip-link';
.crayons-header {
--indicator-outline: var(--header-bg);
height: var(--header-height);
background: var(--header-bg);
box-shadow: 0 1px 1px var(--header-shadow);
@ -30,7 +28,7 @@
position: absolute;
top: calc(var(--su-1) * -1);
right: 0;
box-shadow: 0 0 0 2px var(--indicator-outline);
box-shadow: 0 0 0 2px var(--header-bg);
}
}
@ -45,64 +43,18 @@
margin: 0 var(--su-4);
display: block;
}
.crayons-btn:focus {
outline: revert;
}
}
.crayons-header__link {
margin: 0 var(--su-1);
&:hover,
&:focus {
--indicator-outline: var(--header-bg-hover);
color: var(--header-icons-color-hover);
background: var(--header-bg-hover);
}
&--current {
--indicator-outline: var(--header-bg-current);
color: var(--header-icons-color-hover);
background: var(--header-bg-current) !important;
.crayons-indicator {
display: none;
}
}
}
// Dropdown under profile picture / hamburger icon
.crayons-header__menu {
--trigger-color: var(--header-icons-color);
--trigger-border-color: transparent;
height: 100%;
display: flex;
align-items: center;
@media (min-width: $breakpoint-s) {
perspective: 1000px;
}
&__trigger {
all: unset;
padding: calc(var(--su-1) / 2);
margin: 0 var(--su-2);
border-radius: 50%;
color: var(--trigger-color);
cursor: pointer;
// hold space for interaction style
border: 2px solid var(--trigger-border-color);
}
&__dropdown {
transform-style: preserve-3d;
top: var(--header-height);
transform-origin: top right;
transition: all var(--transition-props);
transition-delay: var(--dropdown-delay, 0.25s);
transform: var(
--dropdown-transform,
translateY(calc(1 * var(--su-1))) rotateX(-10deg)
@ -111,20 +63,11 @@
visibility: var(--dropdown-visibility, hidden);
}
&.showing,
&.desktop:hover {
--trigger-border-color: var(--header-bg-hover);
--trigger-color: var(--header-icons-color-hover);
&.showing {
--dropdown-transform: translateY(0) rotateX(0);
--dropdown-delay: 0;
--dropdown-opacity: 1;
--dropdown-visibility: visible;
}
.focus-visible:focus {
--trigger-border-color: var(--header-button-focus-color);
--trigger-color: var(--header-button-focus-color);
}
}
.pwa-nav-buttons {
@ -147,52 +90,6 @@
}
}
.authentication-header {
&__container {
padding: var(--su-6);
}
&__image-container {
width: var(--su-8);
height: var(--su-8);
margin-bottom: var(--su-2);
}
&__image {
border-radius: var(--radius);
transform: rotate(-10deg);
height: auto;
width: 100%;
}
&__title {
color: var(--card-color);
font-size: var(--fs-2xl);
line-height: var(--lh-tight);
@media (min-width: $breakpoint-s) {
font-size: var(--fs-3xl);
}
}
&__description {
color: var(--card-color-secondary);
margin-bottom: var(--su-4);
}
&__actions {
display: grid;
grid-gap: var(--su-1);
@media (min-width: $breakpoint-s) {
display: flex;
flex-direction: column;
width: max-content;
justify-self: center;
}
}
}
.navigation-progress {
--duration: 3600ms;
--delay: 250ms;

View file

@ -10,7 +10,6 @@
border-radius: var(--radius);
font: inherit;
cursor: pointer;
transition: all var(--transition-props);
outline: 0;
text-decoration: none;
color: var(--color);
@ -31,7 +30,7 @@
z-index: var(--z-elevate);
text-decoration: underline;
}
// Both focus declarations below are the same but we unfortunately can't
// combine them because Safari doesn't recognize it properly.
// First declaration is for all browsers that support :focus-visible
@ -61,20 +60,26 @@
background-color: var(--bg-hover);
}
}
&[aria-current='page'] {
--bg: var(--link-current-bg);
--color: var(--link-current-color);
font-weight: var(--fw-bold);
}
}
.c-link {
--bg: transparent;
--bg-hover: var(--base-20);
--color: var(--base-80);
--color-hover: var(--accent-brand-darker);
--bg: var(--link-bg);
--bg-hover: var(--link-bg-hover);
--color: var(--link-color);
--color-hover: var(--link-branded-color-hover);
}
.c-link--branded {
--bg: transparent;
--bg-hover: var(--accent-brand-a10);
--color: var(--accent-brand);
--color-hover: var(--accent-brand-darker);
--bg: var(--link-branded-bg);
--bg-hover: var(--link-branded-bg-hover);
--color: var(--link-branded-color);
--color-hover: var(--link-branded-color-hover);
}
.c-link--icon-alone {

View file

@ -116,9 +116,6 @@
--footer-link-color-hover: var(--base-100);
// Links
--link-color: var(--base-90);
--link-color-hover: var(--accent-brand-darker);
--link-bg-hover: var(--base-a5);
--link-bg-hover-alt: var(--base-inverted);
--link-brand-color: var(--accent-brand);
--link-color-current: var(--base-100);
@ -253,9 +250,12 @@
// Buttons: Default
--btn-bg: transparent;
--btn-bg-hover: var(--base-20);
--btn-bg-hover: var(--accent-brand-a10);
--btn-color: var(--base-80);
--btn-color-hover: var(--base-100);
--btn-color-hover: var(--accent-brand-darker);
--btn-current-bg: var(--base-20);
--btn-current-color: var(--base-100);
// Buttons: Default Destructive
--btn-destructive-bg: transparent;
@ -274,4 +274,49 @@
--btn-primary-destructive-bg-hover: var(--accent-danger-darker);
--btn-primary-destructive-color: var(--base-inverted);
--btn-primary-destructive-color-hover: var(--base-inverted);
////////////////////////////////////////////////
// Links ///////////////////////////////////////
////////////////////////////////////////////////
// Links: Default
--link-bg: transparent;
--link-bg-hover: var(--accent-brand-a10);
--link-color: var(--base-80);
--link-color-hover: var(--accent-brand-darker);
--link-current-bg: var(--base-20);
--link-current-color: var(--base-100);
// Links: Branded
--link-branded-bg: transparent;
--link-branded-bg-hover: var(--accent-brand-a10);
--link-branded-color: var(--accent-brand);
--link-branded-color-hover: var(--accent-brand-darker);
////////////////////////////////////////////////
// CTAs ////////////////////////////////////////
////////////////////////////////////////////////
// CTAs: Default
--cta-bg: transparent;
--cta-bg-hover: var(--accent-brand-a10);
--cta-color: var(--base-80);
--cta-color-hover: var(--accent-brand-darker);
--cta-border: var(--base-60);
--cta-border-hover: var(--accent-brand-darker);
// CTAs: Branded
--cta-branded-bg: transparent;
--cta-branded-bg-hover: var(--accent-brand);
--cta-branded-color: var(--accent-brand);
--cta-branded-color-hover: var(--base-inverted);
--cta-branded-border: var(--accent-brand);
--cta-branded-border-hover: var(--accent-brand-darker);
////////////////////////////////////////////////
// Utilities ///////////////////////////////////
////////////////////////////////////////////////
--focus: var(--accent-brand);
}

View file

@ -184,6 +184,13 @@
(),
false
),
(
'inset',
('top', 'left', 'right', 'bottom'),
('px': 1px, 'unset': unset, '0': 0),
(),
false
),
(
'top',
'top',

View file

@ -56,8 +56,7 @@
// Focus ring //////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////
--focus-ring: 0 0 0 2px var(--base-inverted),
0 0 0 4px var(--button-primary-bg-hover); // TODO: replace with appropriate variable from _colors.scss
--focus-ring: 0 0 0 2px var(--base-inverted), 0 0 0 4px var(--focus);
////////////////////////////////////////////////////////////////////////////////////
// Typography. /////////////////////////////////////////////////////////////////////

View file

@ -109,9 +109,6 @@
--footer-link-color-hover: var(--base-100);
// Links
--link-color: var(--base-90);
--link-color-hover: var(--accent-brand-lighter);
--link-bg-hover: var(--base-a5);
--link-bg-hover-alt: var(--base-inverted);
--link-brand-color: #17a1f6;
--link-color-current: var(--base-100);
@ -240,9 +237,40 @@
// Buttons /////////////////////////////////////
////////////////////////////////////////////////
// Buttons: Default
--btn-color-hover: var(--accent-brand-lighter);
// Buttons: Primary
--btn-primary-bg-hover: var(--accent-brand-lighter);
// Buttons: Primary Destructive
--btn-primary-destructive-bg-hover: var(--accent-danger-lighter);
////////////////////////////////////////////////
// Links ///////////////////////////////////////
////////////////////////////////////////////////
// Links: Default
--link-color-hover: var(--accent-brand-lighter);
// Links: Branded
--link-branded-color-hover: var(--accent-brand-lighter);
////////////////////////////////////////////////
// CTAs ////////////////////////////////////////
////////////////////////////////////////////////
// CTAs: Default
--cta-color-hover: var(--accent-brand-lighter);
--cta-border-hover: var(--accent-brand-lighter);
// CTAs: Branded
--cta-branded-bg-hover: var(--accent-brand);
--cta-branded-border: var(--accent-brand);
--cta-branded-border-hover: var(--accent-brand-lighter);
////////////////////////////////////////////////
// Utilities ///////////////////////////////////
////////////////////////////////////////////////
--focus: var(--accent-brand-lighter);
}

View file

@ -2,20 +2,8 @@ import PropTypes from 'prop-types';
import { h } from 'preact';
import { forwardRef } from 'preact/compat';
import { locale } from '../utilities/locale';
import { Button } from '@crayons';
const SearchIcon = () => (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
className="crayons-icon"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M18.031 16.617l4.283 4.282-1.415 1.415-4.282-4.283A8.96 8.96 0 0111 20c-4.968 0-9-4.032-9-9s4.032-9 9-9 9 4.032 9 9a8.96 8.96 0 01-1.969 5.617zm-2.006-.742A6.977 6.977 0 0018 11c0-3.868-3.133-7-7-7-3.868 0-7 3.132-7 7 0 3.867 3.132 7 7 7a6.977 6.977 0 004.875-1.975l.15-.15z" />
</svg>
);
import { ButtonNew as Button } from '@crayons';
import SearchIcon from '@images/search.svg';
export const SearchForm = forwardRef(({ searchTerm, onSubmitSearch }, ref) => (
<form
@ -40,11 +28,8 @@ export const SearchForm = forwardRef(({ searchTerm, onSubmitSearch }, ref) => (
/>
<Button
type="submit"
variant="ghost"
contentType="icon-rounded"
icon={SearchIcon}
size="s"
className="absolute right-2 bottom-0 top-0 m-1"
className="absolute inset-px left-auto mt-0 py-0"
aria-label="Search"
/>
</div>

View file

@ -51,7 +51,7 @@ export const ButtonNew = (props) => {
aria-hidden="true"
focusable="false"
src={icon}
className={classNames('c-btn__icon')}
className="c-btn__icon"
/>
)}
{children}
@ -80,5 +80,5 @@ ButtonNew.propTypes = {
className: PropTypes.string,
tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
onKeyUp: PropTypes.func,
icon: PropTypes.ReactNode,
icon: PropTypes.elementType,
};

View file

@ -1,22 +1,47 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
import classNames from 'classnames/bind';
import { Link } from '@crayons';
import { defaultChildrenPropTypes } from '../../common-prop-types/default-children-prop-types';
import { Icon } from '@crayons';
export const CTA = (props) => {
const { variant = 'default', className, ...otherProps } = props;
const {
children,
href = '#',
variant,
icon,
className,
...otherProps
} = props;
const classes = classNames('c-cta', {
[`c-cta--${variant}`]: variant,
'c-cta--icon-left': icon && children,
[className]: className,
});
return <Link block className={classes} {...otherProps} />;
return (
<a href={href} className={classes} {...otherProps}>
{icon && (
<Icon
src={icon}
aria-hidden="true"
focusable="false"
className="c-cta__icon"
/>
)}
{children}
</a>
);
};
CTA.displayName = 'CTA';
CTA.propTypes = {
variant: PropTypes.oneOf(['default', 'branded']),
variant: PropTypes.oneOf([undefined, 'branded']),
rounded: PropTypes.bool,
href: PropTypes.string.isRequired,
className: PropTypes.string,
children: defaultChildrenPropTypes.isRequired,
icon: PropTypes.elementType,
};

View file

@ -1,7 +1,6 @@
# CTAs (Call To Actions)
Call To Action is an extension for `<Link block />` component therefore it inherits Link's props and configuration.
CTA is meant to be more prominent and visually heavy so use these cautiously.
CTA is meant to be more prominent and visually heavy than regular links and should deliver clear... well... *call to action* :).
## CTAs vs Links vs Buttons
@ -14,10 +13,9 @@ Do not use `<Button />s` as navigational elements. Instead, use Links (or CTAs)
## Usage
Under the hood, CTA outputs `<a>` element.
A CTA renders an anchor element, therefore standard link markup must be used,
including a `href` attribute, otherwise, it is not focusable by the keyboard.
Call To Actions has slightly different UI than regular `<Link block />` as our intention here was to make these more prominent.
`<CTA />` is basically a `<Link />` therefore standard link markup should be used, including an `href` (links omitting `href` attributes are not focusable by keyboard).
### Icons
@ -27,13 +25,8 @@ By default, the icon itself won't be customizable. Icons within CTAs are hidden
#### Icon-only
There are certain instances where an icon will suffice in place of a text label, but use these cautiously.
Icon-only CTAs should always use clear and obvious icon, ideally accompanied by a title. Icon-only CTAs will require an `aria-label` to ensure they are understood by users of assistive technologies.
##### Fully rounded
In most cases, whenever we display icon-only CTAs we tend to make them fully rounded - use `rounded` prop for that.
Other than that please avoid customizing border-radius on CTAs.
A call to action requires a label, therefore it is forbidden to create a CTA with an icon only.
For such a scenario, use a `<Link />` or `<Button />` component (depending on the use case).
## Design

View file

@ -15,7 +15,10 @@ export default {
variant: {
control: {
type: 'select',
options: ['default', 'branded'],
options: {
default: undefined,
branded: 'branded',
},
},
description:
'There are two available variants (styles) to pick from: _default_ and _branded_. The primary difference is color: _default_ uses grey color and _branded_ uses accent color.',
@ -23,21 +26,12 @@ export default {
defaultValue: { summary: 'default' },
},
},
rounded: {
description:
'By enabling this setting you can turn all corners of the CTA to be fully rounded. We usually use it **only** for CTAs containing **icon without label**.',
table: {
defaultValue: { summary: false },
},
},
},
};
export const Default = (args) => <CTA {...args} />;
Default.args = {
children: 'Call to action',
variant: 'default',
rounded: false,
};
export const Branded = (args) => <CTA {...args} />;

View file

@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<CTA /> renders branded CTA 1`] = `"<a href=\\"#\\" class=\\"c-link c-link--block c-cta c-cta--branded\\">Hello world!</a>"`;
exports[`<CTA /> renders branded CTA 1`] = `"<a href=\\"#\\" class=\\"c-cta c-cta--branded\\">Hello world!</a>"`;
exports[`<CTA /> renders default CTA 1`] = `"<a href=\\"#\\" class=\\"c-link c-link--block c-cta c-cta--default\\">Hello world!</a>"`;
exports[`<CTA /> renders default CTA 1`] = `"<a href=\\"#\\" class=\\"c-cta\\">Hello world!</a>"`;

View file

@ -24,5 +24,5 @@ Icon.displayName = 'Icon';
Icon.propTypes = {
native: PropTypes.bool,
className: PropTypes.string,
src: PropTypes.node.isRequired,
src: PropTypes.elementType.isRequired,
};

View file

@ -32,7 +32,7 @@ export const Link = (props) => {
src={icon}
aria-hidden="true"
focusable="false"
className={classNames('c-link__icon')}
className="c-link__icon"
/>
)}
{children}
@ -43,11 +43,11 @@ export const Link = (props) => {
Link.displayName = 'Link';
Link.propTypes = {
variant: PropTypes.oneOf(['default', 'branded']),
variant: PropTypes.oneOf([undefined, 'branded']),
block: PropTypes.bool,
rounded: PropTypes.bool,
href: PropTypes.string.isRequired,
className: PropTypes.string,
children: defaultChildrenPropTypes,
icon: PropTypes.ReactNode,
icon: PropTypes.elementType,
};

View file

@ -15,7 +15,10 @@ export default {
variant: {
control: {
type: 'select',
options: ['default', 'branded'],
options: {
default: undefined,
branded: 'branded',
},
},
description:
'There are two available variants (styles) to pick from: _default_ and _branded_. The primary difference is color: _default_ uses grey color and _branded_ uses accent color.',
@ -60,7 +63,7 @@ export const Block = (args) => <Link {...args} />;
Block.args = {
...Inline.args,
block: true,
variant: 'default',
variant: undefined,
children: 'Block link',
};

View file

@ -114,7 +114,7 @@ function getPageEntries() {
return Object.entries({
'notifications-index': document.getElementById('notifications-link'),
'moderations-index': document.getElementById('moderation-link'),
'stories-search': document.getElementById('search-link'),
'articles_search-index': document.getElementById('search-link'),
});
}

View file

@ -1,17 +1,10 @@
// TODO: Once this is merged, PR up the removal of this and in other tests and move it to testSetup.js
import '@testing-library/jest-dom';
import {
getByTestId,
getByLabelText,
getByText,
fireEvent,
waitFor,
} from '@testing-library/dom';
import { getByLabelText } from '@testing-library/dom';
import {
getInstantClick,
initializeMobileMenu,
setCurrentPageIconLink,
initializeMemberMenu,
} from '../utilities';
// TODO: ★★★ These tests should be promoted to E2E tests once we have that in place. ★★★
@ -41,7 +34,7 @@ describe('top navigation utilities', () => {
describe('initializeMobileMenu', () => {
it('should open the hamburger menu', () => {
document.body.innerHTML = `
<button aria-label="nav-button-left" class="crayons-btn crayons-btn--ghost crayons-btn--icon-rounded js-hamburger-trigger inline-block m:hidden mx-2">
<button aria-label="nav-button-left" class="c-btn c-btn--icon-alone radius-full js-hamburger-trigger mx-2">
<svg><title>Navigation menu</title></svg>
</button>
`;
@ -57,7 +50,7 @@ describe('top navigation utilities', () => {
it('should close the hamburger menu', () => {
document.body.innerHTML = `
<button aria-label="nav-button-left" class="crayons-btn crayons-btn--ghost crayons-btn--icon-rounded js-hamburger-trigger inline-block m:hidden mx-2">
<button aria-label="nav-button-left" class="c-btn c-btn--icon-alone radius-full js-hamburger-trigger mx-2">
<svg><title>Navigation menu</title></svg>
</button>
`;
@ -72,176 +65,23 @@ describe('top navigation utilities', () => {
navButton.click();
expect(document.body.dataset.leftNavState).toEqual('closed');
});
it('should open the more menu', () => {
document.body.innerHTML = `
<a href="javascript:void(0)" class="crayons-link crayons-link--secondary crayons-link--block crayons-link--block--indented fs-s js-nav-more-trigger">More...</a>
<div class="hidden js-nav-more spec-nav-more">
<div class="flex justify-around p-4 mt-4 border-solid border-0 border-t-1 border-base-10">
</div>
</div>
`;
const navMoreLink = getByText(document.body, 'More...');
initializeMobileMenu([], [navMoreLink]);
navMoreLink.click();
expect(navMoreLink).toHaveClass('hidden');
});
});
describe('setCurrentPageIconLink', () => {
it('should set the current page icon', () => {
document.body.innerHTML = `
<a href="/connect" id="connect-link" class="crayons-header__link crayons-btn crayons-btn--ghost crayons-btn--icon-rounded" aria-label="Connect"></a>
<a href="/notifications" id="notifications-link" class="crayons-header__link crayons-btn crayons-btn--ghost crayons-btn--icon-rounded" aria-label="Notifications"></a>
<a href="/notifications" id="notifications-link" class="c-link c-link--icon-alone c-link--block radius-full mx-1" aria-current="page" aria-label="Notifications"></a>
`;
const connectLink = getByLabelText(document.body, 'Connect');
const notificationsLink = getByLabelText(document.body, 'Notifications');
const pageEntries = Object.entries({
'page-1': connectLink,
'page-2': notificationsLink,
'page-1': notificationsLink,
});
const page = 'page-1';
setCurrentPageIconLink('page-1', pageEntries);
setCurrentPageIconLink(page, pageEntries);
expect(connectLink).toHaveClass('crayons-header__link--current');
expect(notificationsLink).not.toHaveClass(
'crayons-header__link--current',
);
});
it('should set the current page icon and remove the previous current page icon styling', () => {
document.body.innerHTML = `
<a href="/connect" id="connect-link" class="crayons-header__link crayons-btn crayons-btn--ghost crayons-btn--icon-rounded" aria-label="Connect"></a>
<a href="/notifications" id="notifications-link" class="crayons-header__link crayons-header__link--current crayons-btn crayons-btn--ghost crayons-btn--icon-rounded" aria-label="Notifications"></a>
`;
const connectLink = getByLabelText(document.body, 'Connect');
const notificationsLink = getByLabelText(document.body, 'Notifications');
const pageEntries = Object.entries({
'page-1': connectLink,
'page-2': notificationsLink,
});
const page = 'page-1';
setCurrentPageIconLink(page, pageEntries);
expect(connectLink).toHaveClass('crayons-header__link--current');
expect(notificationsLink).not.toHaveClass(
'crayons-header__link--current',
);
});
});
describe('initializeMemberMenu', () => {
const initialMenuHTML = `
<div class="crayons-header__menu" id="crayons-header__menu" data-testid="menu-dropdown">
<button type="button" class="crayons-header__menu__trigger" id="member-menu-button" aria-expanded="false" aria-label="Navigation menu">
<span class="crayons-avatar crayons-avatar--l"><img class="crayons-avatar__image" alt="" id="nav-profile-image" src=""></span>
</button>
<div class="crayons-dropdown left-2 right-2 s:right-4 s:left-auto p-0 crayons-header__menu__dropdown inline-block">
<ul class="p-0" id="crayons-header__menu__dropdown__list">
<li id="user-profile-link-placeholder" class="border-0 border-b-1 border-solid border-base-20 p-1 mb-1">
<a id="first-nav-link" class="crayons-link crayons-link--block" href="">
</a>
</li>
<li class="px-1 py-1">
<a href="/signout_confirm" class="crayons-link crayons-link--block" id="last-nav-link">Sign Out</a>
</li>
</ul>
</div>
</div>
`;
it('toggles dropdown menu expanded state', async () => {
document.body.innerHTML = initialMenuHTML;
const menuNavButton = getByLabelText(document.body, 'Navigation menu');
const memberMenu = getByTestId(document.body, 'menu-dropdown');
initializeMemberMenu(memberMenu, menuNavButton);
await waitFor(() =>
expect(menuNavButton.getAttribute('aria-expanded')).toEqual('false'),
);
expect(menuNavButton.getAttribute('aria-expanded')).toEqual('false');
expect(memberMenu).not.toHaveClass('showing');
fireEvent.click(menuNavButton);
expect(menuNavButton.getAttribute('aria-expanded')).toEqual('true');
expect(memberMenu).toHaveClass('showing');
fireEvent.click(menuNavButton);
expect(menuNavButton.getAttribute('aria-expanded')).toEqual('false');
expect(memberMenu).not.toHaveClass('showing');
});
it('closes menu on Escape key', async () => {
document.body.innerHTML = initialMenuHTML;
const menuNavButton = getByLabelText(document.body, 'Navigation menu');
const memberMenu = getByTestId(document.body, 'menu-dropdown');
initializeMemberMenu(memberMenu, menuNavButton);
await waitFor(() =>
expect(menuNavButton.getAttribute('aria-expanded')).toEqual('false'),
);
fireEvent.click(menuNavButton);
expect(menuNavButton.getAttribute('aria-expanded')).toEqual('true');
expect(memberMenu).toHaveClass('showing');
fireEvent.keyUp(memberMenu, { key: 'Escape' });
await waitFor(() =>
expect(menuNavButton.getAttribute('aria-expanded')).toEqual('false'),
);
expect(memberMenu).not.toHaveClass('showing');
});
it('closes the menu on click outside', async () => {
document.body.innerHTML = `
<div data-testid="container">
${initialMenuHTML}
</div>
`;
const menuNavButton = getByLabelText(document.body, 'Navigation menu');
const memberMenu = getByTestId(document.body, 'menu-dropdown');
initializeMemberMenu(memberMenu, menuNavButton);
await waitFor(() =>
expect(menuNavButton.getAttribute('aria-expanded')).toEqual('false'),
);
fireEvent.click(menuNavButton);
expect(menuNavButton.getAttribute('aria-expanded')).toEqual('true');
expect(memberMenu).toHaveClass('showing');
// Click on a link in the menu to close load a page and close the menu.
memberMenu.querySelector('a').click();
await waitFor(() =>
expect(menuNavButton.getAttribute('aria-expanded')).toEqual('false'),
);
expect(memberMenu).not.toHaveClass('showing');
expect(notificationsLink).toHaveAttribute('aria-current', 'page');
});
});
});

View file

@ -69,16 +69,6 @@ export function initializeMemberMenu(memberTopMenu, menuNavButton) {
menuNavButton.setAttribute('aria-expanded', 'true');
});
} else {
memberTopMenu.addEventListener('mouseover', (_event) => {
classList.add('desktop');
openHeaderMenu(memberTopMenu, menuNavButton);
});
memberTopMenu.addEventListener('mouseout', (_event) => {
if (!memberTopMenu.dataset.clicked) {
closeHeaderMenu(memberTopMenu, menuNavButton);
}
});
memberTopMenu.addEventListener('keyup', (e) => {
if (e.key === 'Escape' && classList.contains('showing')) {
closeHeaderMenu(memberTopMenu, menuNavButton);
@ -192,9 +182,9 @@ export function setCurrentPageIconLink(currentPage, pageEntries) {
.forEach(([page, iconLink]) => {
if (currentPage === page) {
iconLink.blur();
iconLink.classList.add('crayons-header__link--current');
iconLink.setAttribute('aria-current', 'page');
} else {
iconLink.classList.remove('crayons-header__link--current');
iconLink.removeAttribute('aria-current');
}
});
}

View file

@ -1,27 +1,35 @@
<div class="crayons-dropdown left-2 right-2 s:right-4 s:left-auto crayons-header__menu__dropdown inline-block m:-mr-2 top-100 -mt-1">
<div class="crayons-dropdown left-2 right-2 s:left-auto crayons-header__menu__dropdown inline-block m:-mr-2 top-100">
<ul class="m-0" id="crayons-header__menu__dropdown__list">
<li id="user-profile-link-placeholder" class="border-0 border-b-1 border-solid border-base-20 pb-2 mb-2">
<a id="first-nav-link" class="crayons-link crayons-link--block">
<a href="#" id="first-nav-link" class="c-link c-link--default c-link--block lh-tight">
<div>
<span class="fw-medium block"><%= t("views.main.nav.placeholder") %></span>
<small class="fs-s color-base-50"></small>
<small class="fs-s opacity-75"></small>
</div>
</a>
</li>
<li class="js-header-menu-admin-link hidden">
<a href="<%= admin_path %>" class="crayons-link crayons-link--block" data-no-instant><%= t("views.main.nav.admin") %></a>
<a href="<%= admin_path %>" class="c-link c-link--block" data-no-instant><%= t("views.main.nav.admin") %></a>
</li>
<li>
<a href="<%= dashboard_path %>" class="c-link c-link--block"><%= t("views.main.nav.dashboard") %></a>
</li>
<li>
<a href="<%= mod_path %>" class="c-link c-link--block trusted-visible-block"><%= t("views.main.nav.moderator_center") %></a>
</li>
<li>
<a href="<%= new_path %>" class="c-link c-link--block"><%= t("views.main.create_post") %></a>
</li>
<li>
<a href="<%= readinglist_path %>" class="c-link c-link--block"><%= t("views.main.nav.list") %></a>
</li>
<li><a href="<%= dashboard_path %>" class="crayons-link crayons-link--block"><%= t("views.main.nav.dashboard") %></a></li>
<li><a href="<%= mod_path %>" class="crayons-link crayons-link--block trusted-visible-block"><%= t("views.main.nav.moderator_center") %></a></li>
<li><a href="<%= new_path %>" class="crayons-link crayons-link--block"><%= t("views.main.create_post") %></a></li>
<li><a href="<%= readinglist_path %>" class="crayons-link crayons-link--block"><%= t("views.main.nav.list") %></a></li>
<li class="border-0 border-b-1 border-solid border-base-20 pb-2">
<a href="<%= user_settings_path %>" class="crayons-link crayons-link--block" id="second-last-nav-link"><%= t("views.main.nav.settings") %></a>
<a href="<%= user_settings_path %>" class="c-link c-link--block" id="second-last-nav-link"><%= t("views.main.nav.settings") %></a>
</li>
<li class="pt-2">
<a href="<%= signout_confirm_path %>" class="crayons-link crayons-link--block" id="last-nav-link"><%= t("views.main.signout") %></a>
<a href="<%= signout_confirm_path %>" class="c-link c-link--block" id="last-nav-link"><%= t("views.main.signout") %></a>
</li>
</ul>
</div>

View file

@ -3,9 +3,11 @@
<span id="route-change-target" tabindex="-1"></span>
<a href="#main-content" class="skip-content-link"><%= t("views.main.header.skip") %></a>
<div class="crayons-header__container">
<button class="crayons-btn crayons-btn--ghost crayons-btn--icon-rounded js-hamburger-trigger inline-block m:hidden mx-2">
<%= inline_svg_tag("hamburger.svg", aria: true, class: "crayons-icon", title: t("views.hamburger.icon")) %>
</button>
<span class="inline-block m:hidden ">
<button class="c-btn c-btn--icon-alone js-hamburger-trigger mx-2">
<%= inline_svg_tag("hamburger.svg", aria: true, class: "crayons-icon", title: t("views.hamburger.icon")) %>
</button>
</span>
<%= render "layouts/logo" %>
<div id="pwa-nav-buttons" class="pwa-nav-buttons">
@ -25,7 +27,7 @@
<div class="crayons-fields crayons-fields--horizontal">
<div class="crayons-field flex-1 relative">
<input class="crayons-header--search-input crayons-textfield js-search-input" type="text" id="nav-search" name="q" placeholder="<%= t("views.search.placeholder") %>" autocomplete="off" />
<button type="submit" aria-label="<%= t("views.search.icon.aria_label") %>" class="crayons-btn crayons-btn--ghost crayons-btn--s crayons-btn--icon-rounded absolute right-2 bottom-0 top-0 m-1">
<button type="submit" aria-label="<%= t("views.search.icon.aria_label") %>" class="c-btn c-btn--icon-alone absolute inset-px left-auto mt-0 py-0">
<%= inline_svg_tag("search.svg", aria_hidden: true, width: 24, height: 24, class: "crayons-icon", title: t("views.search.icon.title")) %>
</button>
</div>
@ -34,35 +36,39 @@
</div>
<div class="flex items-center h-100 ml-auto">
<a class="crayons-header__link crayons-btn crayons-btn--ghost crayons-btn--icon-rounded m:hidden <% unless user_signed_in? %>mx-2<% end %>" id="search-link" aria-label="<%= t("views.search.icon.aria_label") %>" href="<%= search_path %>">
<a class="c-link c-link--icon-alone c-link--block m:hidden <% unless user_signed_in? %>mx-1<% end %>" id="search-link" aria-label="<%= t("views.search.icon.aria_label") %>" href="<%= search_path %>">
<%= inline_svg_tag("search.svg", aria: true, class: "crayons-icon", title: t("views.search.icon.title")) %>
</a>
<% if user_signed_in? %>
<a href="<%= new_path %>" class="crayons-btn hidden mr-2 whitespace-nowrap m:block ml-auto"><%= t("views.main.create_post") %></a>
<span class="hidden m:flex ml-auto">
<a href="<%= new_path %>" class="c-cta c-cta--branded mr-2 whitespace-nowrap"><%= t("views.main.create_post") %></a>
<span class="trusted-visible-block">
<a id="moderation-link" class="crayons-header__link crayons-btn crayons-btn--ghost crayons-btn--icon-rounded m:flex hidden" aria-label="<%= t("views.main.header.moderation.aria_label") %>" href="<%= mod_path %>">
<%= inline_svg_tag("mod.svg", aria: true, class: "crayons-icon", title: t("views.main.header.moderation.icon")) %>
</a>
<span class="trusted-visible-block">
<a id="moderation-link" class="c-link c-link--icon-alone c-link--block mx-1" aria-label="<%= t("views.main.header.moderation.aria_label") %>" href="<%= mod_path %>">
<%= inline_svg_tag("mod.svg", aria: true, class: "crayons-icon", title: t("views.main.header.moderation.icon")) %>
</a>
</span>
</span>
<a href="<%= notifications_path %>" id="notifications-link" class="crayons-header__link crayons-btn crayons-btn--ghost crayons-btn--icon-rounded" aria-label="<%= t("views.main.header.notifications.aria_label") %>">
<a href="<%= notifications_path %>" id="notifications-link" class="c-link c-link--icon-alone c-link--block mx-1" aria-label="<%= t("views.main.header.notifications.aria_label") %>">
<%= inline_svg_tag("bell.svg", aria: true, class: "crayons-icon", title: t("views.main.header.notifications.icon")) %>
<span class="crayons-indicator crayons-indicator--critical hidden" id="notifications-number"></span>
</a>
<div class="crayons-header__menu" id="crayons-header__menu">
<button type="button" class="crayons-header__menu__trigger" id="member-menu-button" aria-label="<%= t("views.main.header.nav.aria_label") %>" aria-expanded="false">
<div class="crayons-header__menu mx-1" id="crayons-header__menu">
<button type="button" class="c-btn c-btn--icon-alone radius-full p-1" id="member-menu-button" aria-label="<%= t("views.main.header.nav.aria_label") %>" aria-expanded="false">
<span class="crayons-avatar crayons-avatar--l"><img class="crayons-avatar__image" alt="" id="nav-profile-image" /></span>
</button>
<%= render "layouts/nav_menu" %>
</div>
<% else %>
<a href="<%= sign_up_path %>" class="crayons-btn crayons-btn--ghost-brand mr-2 hidden m:block whitespace-nowrap ml-auto" data-no-instant>
<%= t("views.main.header.login") %>
</a>
<span class="hidden m:block">
<a href="<%= sign_up_path %>" class="c-link c-link--block mr-2 whitespace-nowrap ml-auto" data-no-instant>
<%= t("views.main.header.login") %>
</a>
</span>
<a href="<%= sign_up_path(state: "new-user") %>" class="crayons-btn mr-2 whitespace-nowrap fs-s s:fs-base" data-no-instant>
<a href="<%= sign_up_path(state: "new-user") %>" class="c-cta c-cta--branded whitespace-nowrap mr-2" data-no-instant>
<%= t("views.main.header.create_account") %>
</a>
<% end %>

View file

@ -3,69 +3,72 @@ describe('Member Menu Dropdown', () => {
cy.testSetup();
cy.fixture('users/articleEditorV1User.json').as('user');
// Explicitly set the viewport to make sure we're in the full desktop view for these tests
cy.viewport('macbook-15');
cy.get('@user').then((user) => {
cy.loginAndVisit(user, '/');
});
});
it('hovering on User Avatar should show User Dropdown menu', () => {
cy.findByRole('button', { name: 'Navigation menu' }).trigger('mouseover');
it('clicking on User Avatar should open and close User Dropdown menu', () => {
cy.findByRole('button', { name: 'Navigation menu' }).as('menuButton');
cy.get('@menuButton')
.should('have.attr', 'aria-expanded', 'false')
.click()
.should('have.attr', 'aria-expanded', 'true');
cy.get('#crayons-header__menu__dropdown__list').within(() => {
cy.findByRole('link', {
name: 'Article Editor v1 User @article_editor_v1_user',
}).should('have.focus');
cy.findByRole('link', { name: 'Dashboard' });
cy.findByRole('link', { name: 'Create Post' });
cy.findByRole('link', { name: 'Reading list' });
cy.findByRole('link', { name: 'Settings' });
cy.findByRole('link', { name: 'Sign Out' });
});
cy.get('@menuButton')
.click()
.should('have.attr', 'aria-expanded', 'false')
.should('have.focus');
});
it('pressing escape should close the User Dropdown menu', () => {
cy.findByRole('button', { name: 'Navigation menu' }).trigger('mouseover');
cy.findByRole('button', { name: 'Navigation menu' }).as('menuButton');
cy.get('@menuButton')
.should('have.attr', 'aria-expanded', 'false')
.click()
.should('have.attr', 'aria-expanded', 'true');
cy.findByRole('link', { name: 'Dashboard' })
.as('dashboard')
.should('be.visible');
cy.get('body').type('{esc}');
cy.get('@dashboard').should('not.be.visible');
cy.get('@menuButton').should('have.attr', 'aria-expanded', 'false');
});
it('close menu on clicking', () => {
cy.findByRole('button', { name: 'Navigation menu' }).trigger('mouseover');
it('closes menu on click outside', () => {
cy.findByRole('button', { name: 'Navigation menu' }).as('menuButton');
cy.get('@menuButton')
.should('have.attr', 'aria-expanded', 'false')
.click()
.should('have.attr', 'aria-expanded', 'true');
cy.findByRole('link', { name: 'Dashboard' })
.as('dashboard')
.should('be.visible');
cy.get('body').click('topLeft');
cy.get('@dashboard').should('not.be.visible');
cy.get('@menuButton').should('have.attr', 'aria-expanded', 'false');
});
it('closes menu on selecting a link from the menu', () => {
cy.findByRole('button', { name: 'Navigation menu' }).click();
cy.findByRole('link', { name: 'Dashboard' }).as('dashboard').click();
cy.get('@dashboard').should('not.be.visible');
});
it('if User profile is already hovered, clicking on it again should not close it', () => {
cy.findByRole('button', { name: 'Navigation menu' }).as(
'memberDropDownButton',
);
cy.get('@memberDropDownButton').trigger('mouseover');
cy.findByRole('link', { name: 'Dashboard' }).as('dashboard');
cy.get('@memberDropDownButton').click();
cy.get('@dashboard').should('be.visible');
});
it('hovering out should close menu', () => {
cy.findByRole('button', { name: 'Navigation menu' }).as(
'memberDropDownButton',
);
cy.get('@memberDropDownButton').trigger('mouseover');
cy.findByRole('link', { name: 'Dashboard' }).as('dashboard');
cy.get('@memberDropDownButton').trigger('mouseout');
cy.get('@dashboard').should('not.be.visible');
});
it('Dropdown menu opened by click will not close on hovering out', () => {
cy.findByRole('button', { name: 'Navigation menu' }).as(
'memberDropDownButton',
);
cy.get('@memberDropDownButton').trigger('mouseover').click();
cy.findByRole('link', { name: 'Dashboard' })
.as('dashboard')
.should('be.visible');
cy.get('@memberDropDownButton').trigger('mouseout');
cy.get('@dashboard').should('be.visible');
});
});