improve keyboard accessibility of modals 12427 10610 (#12511)
* adds focusTrap wrapper to preact Modal component * add view specific code for focus trap in admin add nav link * add script to return a focustrap toggle, use in add nav link modal partial * add trap to edit nav link modal * add handlers for sign up modal * update modal controller for admin section, update nav link modals to use * update other admin modals with new data values for trap * remove unneeded erb script file * remove unneeded target * refactor to remove extra unneeded param * remove duplicate code, store getFocusTrapToggle in window * trap focus in comment and bookmark showModal instances for not logged in user * remove need for activator id * clean up id refs no longer needed * remove custom code and re-use focsu-trap lib * update storybook docs * update default export in focusTrap * prevent close button click triggering a modal toggle twice * ensure if user navigates from a modal the trap is deactivated * add jsdoc comments and add dynamic import * ensure admin controller modal traps are cleaned up on disconnect * update sign up modal to use crayons * update modal controller and admin nav links modals to use preact modal * update profile fields modals for new controller * tweak styling of sign up and admin modals to match previous * update listings modal to use crayons modal, adapt focus trap to work with click outside * memoize deactivate callback to ensure modal can be presented on first page load * add missed focustrap changes * fix focus trap issues in onboarding flow * refactor onboarding focus trap, remove getFocusTrapToggle * tweaks for styling and article modal toggle * add click outside tests to modal * add cypress tests for the login modal * update liquid tag tests affected by change * refactors to address review comments * fix issue with login modal presented twice on comment add * change ids to selectors in admin modals * small pr comment refactors * add listings e2e tests * add nav link modal tests * fix issue with help modal * tweak to fix ui bug from merge * remove context from showLoginModal * rename toggleModal * rename state property for clarity Co-authored-by: Nick Taylor <nick@dev.to>
This commit is contained in:
parent
2bba2a6ee4
commit
5bed8f56d4
41 changed files with 694 additions and 479 deletions
|
|
@ -1,4 +1,4 @@
|
|||
/* global showModal */
|
||||
/* global showLoginModal */
|
||||
|
||||
function initializeAllFollowButts() {
|
||||
var followButts = document.getElementsByClassName('follow-action-button');
|
||||
|
|
@ -88,7 +88,7 @@ function addModalEventListener(butt) {
|
|||
assignState(butt, 'login');
|
||||
butt.onclick = function (e) {
|
||||
e.preventDefault();
|
||||
showModal('follow-button');
|
||||
showLoginModal();
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
|
@ -158,7 +158,7 @@ function handleOptimisticButtRender(butt) {
|
|||
if (butt.dataset.verb === 'self') {
|
||||
window.location.href = '/settings';
|
||||
} else if (butt.dataset.verb === 'login') {
|
||||
showModal('follow-button');
|
||||
showLoginModal();
|
||||
} else {
|
||||
// Handles actual following of tags/users
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* global sendHapticMessage, showModal */
|
||||
/* global sendHapticMessage, showLoginModal */
|
||||
|
||||
// Set reaction count to correct number
|
||||
function setReactionCount(reactionName, newCount) {
|
||||
|
|
@ -63,7 +63,7 @@ function reactToArticle(articleId, reaction) {
|
|||
var userStatus = document.body.getAttribute('data-user-status');
|
||||
sendHapticMessage('medium');
|
||||
if (userStatus === 'logged-out') {
|
||||
showModal('react-to-article');
|
||||
showLoginModal();
|
||||
return;
|
||||
}
|
||||
toggleReaction();
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ function initializeCommentsPage() {
|
|||
sendHapticMessage('medium');
|
||||
var userStatus = document.body.getAttribute('data-user-status');
|
||||
if (userStatus === 'logged-out') {
|
||||
showModal('react-to-comment');
|
||||
showLoginModal();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -155,7 +155,7 @@ function initializeCommentsPage() {
|
|||
} else {
|
||||
var userStatus = document.body.getAttribute('data-user-status');
|
||||
if (userStatus == 'logged-out') {
|
||||
showModal('reply-to-comment');
|
||||
showLoginModal();
|
||||
return;
|
||||
}
|
||||
var parentId = event.target.closest('.comment').dataset.commentId;
|
||||
|
|
@ -302,12 +302,8 @@ function handleFocus(event) {
|
|||
var area = event.target;
|
||||
if (userStatus == 'logged-out') {
|
||||
event.preventDefault();
|
||||
showModal('reply-to-comment');
|
||||
showLoginModal();
|
||||
area.blur();
|
||||
setTimeout(function () {
|
||||
area.blur();
|
||||
showModal('reply-to-comment');
|
||||
}, 100);
|
||||
} else {
|
||||
var form = event.target.closest(".comment-form");
|
||||
form.classList.add("comment-form--initiated");
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ function reactToReadingListButtonClick(event) {
|
|||
sendHapticMessage('medium');
|
||||
userStatus = document.body.getAttribute('data-user-status');
|
||||
if (userStatus === 'logged-out') {
|
||||
showModal('add-to-readinglist-from-index');
|
||||
showLoginModal();
|
||||
return;
|
||||
}
|
||||
button = properButtonFromEvent(event);
|
||||
|
|
|
|||
7
app/assets/javascripts/utilities/showLoginModal.js
Normal file
7
app/assets/javascripts/utilities/showLoginModal.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
function showLoginModal() {
|
||||
window.showModal({
|
||||
title: 'Log in to continue',
|
||||
contentSelector: '#global-signup-modal',
|
||||
overlay: true,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
function initSignupModal() {
|
||||
if (document.getElementById('global-signup-modal')) {
|
||||
document.getElementsByClassName(
|
||||
'authentication-modal__close-btn',
|
||||
)[0].onclick = () => {
|
||||
document.getElementById('global-signup-modal').classList.add('hidden');
|
||||
document.body.classList.remove('modal-open');
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function showModal(context) {
|
||||
document.getElementById('global-signup-modal').classList.remove('hidden');
|
||||
document.body.classList.add('modal-open');
|
||||
initSignupModal();
|
||||
}
|
||||
|
|
@ -43,6 +43,10 @@
|
|||
top: 0;
|
||||
bottom: 0;
|
||||
pointer-events: auto;
|
||||
|
||||
&.background-clickable {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__box {
|
||||
|
|
|
|||
|
|
@ -91,3 +91,9 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.listings-modal {
|
||||
.crayons-modal__box__body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +1,11 @@
|
|||
@import '../config/import';
|
||||
|
||||
.authentication-modal {
|
||||
&__box {
|
||||
position: relative;
|
||||
margin-left: var(--su-4);
|
||||
margin-right: var(--su-4);
|
||||
height: auto;
|
||||
width: auto;
|
||||
|
||||
@include generate-box(
|
||||
$level: 2,
|
||||
$bg: var(--base-inverted),
|
||||
$border: var(--base),
|
||||
$color: var(--card-color)
|
||||
);
|
||||
|
||||
@media (min-width: $breakpoint-s) {
|
||||
margin: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&__close-btn {
|
||||
position: absolute;
|
||||
top: var(--su-3);
|
||||
right: var(--su-3);
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: grid;
|
||||
grid-gap: var(--su-2);
|
||||
padding: var(--su-4);
|
||||
|
||||
@media (min-width: $breakpoint-s) {
|
||||
padding: var(--su-8) var(--su-8) var(--su-6);
|
||||
}
|
||||
grid-gap: var(--su-4);
|
||||
}
|
||||
|
||||
&__image-container {
|
||||
width: var(--su-8);
|
||||
height: var(--su-8);
|
||||
|
|
@ -58,16 +29,6 @@
|
|||
grid-gap: var(--su-1);
|
||||
}
|
||||
|
||||
&__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);
|
||||
}
|
||||
|
|
@ -83,16 +44,18 @@
|
|||
width: 100%;
|
||||
margin: 0 auto;
|
||||
justify-self: center;
|
||||
padding: 0 var(--su-8) var(--su-7);
|
||||
padding: 0 var(--su-8) var(--su-4);
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
border-top: 2px solid var(--base-100);
|
||||
padding: var(--su-2) var(--su-4);
|
||||
padding: var(--su-2) var(--su-4) 0 var(--su-4);
|
||||
margin: 0 calc(-1 * var(--modal-body-padding)) 0
|
||||
calc(-1 * var(--modal-body-padding));
|
||||
|
||||
@media (min-width: $breakpoint-s) {
|
||||
padding: var(--su-5) var(--su-8);
|
||||
padding: var(--su-5) var(--su-8) 0 var(--su-8);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,37 @@ import { Controller } from 'stimulus';
|
|||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
export default class ModalController extends Controller {
|
||||
static classes = ['hidden'];
|
||||
static targets = ['toggle'];
|
||||
static values = {
|
||||
rootSelector: String,
|
||||
contentSelector: String,
|
||||
title: String,
|
||||
size: String,
|
||||
};
|
||||
|
||||
toggleModal() {
|
||||
if (this.hasToggleTarget) {
|
||||
this.toggleTarget.classList.toggle(this.hiddenClass);
|
||||
}
|
||||
async toggleModal() {
|
||||
const [{ Modal }, { render, h }] = await Promise.all([
|
||||
import('@crayons/Modal'),
|
||||
import('preact'),
|
||||
]);
|
||||
|
||||
const modalRoot = document.querySelector(this.rootSelectorValue);
|
||||
|
||||
render(
|
||||
<Modal
|
||||
title={this.titleValue}
|
||||
onClose={() => {
|
||||
render(null, modalRoot);
|
||||
}}
|
||||
size={this.sizeValue}
|
||||
>
|
||||
<div
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: document.querySelector(this.contentSelectorValue).innerHTML,
|
||||
}}
|
||||
/>
|
||||
</Modal>,
|
||||
modalRoot,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ export class ArticleForm extends Component {
|
|||
siteLogo,
|
||||
helpFor: null,
|
||||
helpPosition: null,
|
||||
isModalOpen: false,
|
||||
...previousContentState,
|
||||
};
|
||||
}
|
||||
|
|
@ -240,7 +241,7 @@ export class ArticleForm extends Component {
|
|||
edited: false,
|
||||
helpFor: null,
|
||||
helpPosition: 0,
|
||||
showModal: false,
|
||||
isModalOpen: false,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -264,10 +265,10 @@ export class ArticleForm extends Component {
|
|||
});
|
||||
};
|
||||
|
||||
toggleModal = () => {
|
||||
showModal = (isModalOpen) => {
|
||||
if (this.state.edited) {
|
||||
this.setState({
|
||||
showModal: !this.state.showModal,
|
||||
isModalOpen,
|
||||
});
|
||||
} else {
|
||||
// If the user has not edited the body we send them home
|
||||
|
|
@ -319,7 +320,7 @@ export class ArticleForm extends Component {
|
|||
organizationId={organizationId}
|
||||
onToggle={this.handleOrgIdChange}
|
||||
siteLogo={siteLogo}
|
||||
displayModal={this.toggleModal}
|
||||
displayModal={() => this.showModal(true)}
|
||||
/>
|
||||
|
||||
{previewShowing ? (
|
||||
|
|
@ -351,11 +352,11 @@ export class ArticleForm extends Component {
|
|||
helpPosition={helpPosition}
|
||||
version={version}
|
||||
/>
|
||||
{this.state.showModal && (
|
||||
{this.state.isModalOpen && (
|
||||
<Modal
|
||||
size="s"
|
||||
title="You have unsaved changes"
|
||||
onClose={this.toggleModal}
|
||||
onClose={() => this.showModal(false)}
|
||||
>
|
||||
<p>
|
||||
You've made changes to your post. Do you want to navigate to leave
|
||||
|
|
@ -365,7 +366,7 @@ export class ArticleForm extends Component {
|
|||
<Button className="mr-2" variant="danger" url="/" tagName="a">
|
||||
Yes, leave the page
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={this.toggleModal}>
|
||||
<Button variant="secondary" onClick={() => this.showModal(false)}>
|
||||
No, keep editing
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,27 +15,14 @@ export class Help extends Component {
|
|||
frontmatterHelpHTML:
|
||||
document.getElementById('editor-frontmatter-help') &&
|
||||
document.getElementById('editor-frontmatter-help').innerHTML,
|
||||
liquidShowing: false,
|
||||
markdownShowing: false,
|
||||
frontmatterShowing: false,
|
||||
};
|
||||
}
|
||||
|
||||
setCommonProps = ({
|
||||
liquidShowing = false,
|
||||
markdownShowing = false,
|
||||
frontmatterShowing = false,
|
||||
}) => {
|
||||
return {
|
||||
liquidShowing,
|
||||
markdownShowing,
|
||||
frontmatterShowing,
|
||||
};
|
||||
};
|
||||
|
||||
toggleModal = (varShowing) => () => {
|
||||
this.setState((prevState) => ({
|
||||
...this.setCommonProps({
|
||||
[varShowing]: !prevState[varShowing],
|
||||
}),
|
||||
}));
|
||||
showModal = (sectionShowing, isOpen) => () => {
|
||||
this.setState({ [sectionShowing]: isOpen });
|
||||
};
|
||||
|
||||
renderArticleFormTitleHelp = () => {
|
||||
|
|
@ -94,7 +81,10 @@ export class Help extends Component {
|
|||
className="crayons-card crayons-card--secondary p-4 mb-6"
|
||||
>
|
||||
You are currently using the basic markdown editor that uses{' '}
|
||||
<a href="#frontmatter" onClick={this.toggleModal('frontmatterShowing')}>
|
||||
<a
|
||||
href="#frontmatter"
|
||||
onClick={this.showModal('frontmatterShowing', true)}
|
||||
>
|
||||
Jekyll front matter
|
||||
</a>
|
||||
. You can also use the <em>rich+markdown</em> editor you can find in{' '}
|
||||
|
|
@ -130,7 +120,10 @@ export class Help extends Component {
|
|||
<ul className="list-disc pl-6 color-base-70">
|
||||
<li>
|
||||
Use{' '}
|
||||
<a href="#markdown" onClick={this.toggleModal('markdownShowing')}>
|
||||
<a
|
||||
href="#markdown"
|
||||
onClick={this.showModal('markdownShowing', true)}
|
||||
>
|
||||
Markdown
|
||||
</a>{' '}
|
||||
to write and format posts.
|
||||
|
|
@ -229,7 +222,7 @@ export class Help extends Component {
|
|||
</li>
|
||||
<li>
|
||||
You can use{' '}
|
||||
<a href="#liquid" onClick={this.toggleModal('liquidShowing')}>
|
||||
<a href="#liquid" onClick={this.showModal('liquidShowing', true)}>
|
||||
Liquid tags
|
||||
</a>{' '}
|
||||
to add rich content such as Tweets, YouTube videos, etc.
|
||||
|
|
@ -285,21 +278,21 @@ export class Help extends Component {
|
|||
|
||||
{liquidShowing &&
|
||||
this.renderModal(
|
||||
this.toggleModal('liquidShowing'),
|
||||
this.showModal('liquidShowing', false),
|
||||
'🌊 Liquid Tags',
|
||||
liquidHelpHTML,
|
||||
)}
|
||||
|
||||
{markdownShowing &&
|
||||
this.renderModal(
|
||||
this.toggleModal('markdownShowing'),
|
||||
this.showModal('markdownShowing', false),
|
||||
'✍️ Markdown',
|
||||
markdownHelpHTML,
|
||||
)}
|
||||
|
||||
{frontmatterShowing &&
|
||||
this.renderModal(
|
||||
this.toggleModal('frontmatterShowing'),
|
||||
this.showModal('frontmatterShowing', false),
|
||||
'Jekyll Front Matter',
|
||||
frontmatterHelpHTML,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
|||
import { useListNavigation } from '../shared/components/useListNavigation';
|
||||
import { useKeyboardShortcuts } from '../shared/components/useKeyboardShortcuts';
|
||||
|
||||
/* global userData sendHapticMessage showModal buttonFormData renderNewSidebarCount */
|
||||
/* global userData sendHapticMessage showLoginModal buttonFormData renderNewSidebarCount */
|
||||
|
||||
export const Feed = ({ timeFrame, renderFeed }) => {
|
||||
const { reading_list_ids = [] } = userData(); // eslint-disable-line camelcase
|
||||
|
|
@ -102,7 +102,7 @@ export const Feed = ({ timeFrame, renderFeed }) => {
|
|||
sendHapticMessage('medium');
|
||||
|
||||
if (userStatus === 'logged-out') {
|
||||
showModal('add-to-readinglist-from-index');
|
||||
showLoginModal();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { KeyboardShortcuts } from '../../shared/components/useKeyboardShortcuts';
|
||||
import { FocusTrap } from '../../shared/components/focusTrap';
|
||||
import { defaultChildrenPropTypes } from '../../common-prop-types';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
|
|
@ -40,44 +40,50 @@ export const Modal = ({
|
|||
title,
|
||||
overlay,
|
||||
onClose,
|
||||
closeOnClickOutside = false,
|
||||
}) => {
|
||||
const shortcuts = {
|
||||
escape: onClose,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="modal-container"
|
||||
className={`crayons-modal${getAdditionalClassNames({
|
||||
size,
|
||||
className,
|
||||
})}`}
|
||||
<FocusTrap
|
||||
onDeactivate={onClose}
|
||||
clickOutsideDeactivates={closeOnClickOutside}
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="modal"
|
||||
className="crayons-modal__box"
|
||||
data-testid="modal-container"
|
||||
className={`crayons-modal${getAdditionalClassNames({
|
||||
size,
|
||||
className,
|
||||
})}`}
|
||||
>
|
||||
{title && (
|
||||
<div className="crayons-modal__box__header">
|
||||
<h2>{title}</h2>
|
||||
<Button
|
||||
icon={CloseIcon}
|
||||
variant="ghost"
|
||||
contentType="icon"
|
||||
aria-label="Close"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<KeyboardShortcuts shortcuts={shortcuts} />
|
||||
</div>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="modal"
|
||||
className="crayons-modal__box"
|
||||
>
|
||||
{title && (
|
||||
<div className="crayons-modal__box__header">
|
||||
<h2>{title}</h2>
|
||||
<Button
|
||||
icon={CloseIcon}
|
||||
variant="ghost"
|
||||
contentType="icon"
|
||||
aria-label="Close"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="crayons-modal__box__body">{children}</div>
|
||||
</div>
|
||||
{overlay && (
|
||||
<div
|
||||
data-testid="modal-overlay"
|
||||
className={`crayons-modal__overlay ${
|
||||
closeOnClickOutside ? 'background-clickable' : ''
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
<div className="crayons-modal__box__body">{children}</div>
|
||||
</div>
|
||||
{overlay && (
|
||||
<div data-testid="modal-overlay" className="crayons-modal__overlay" />
|
||||
)}
|
||||
</div>
|
||||
</FocusTrap>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { h } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { withKnobs, text, boolean, select } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import notes from './modals.md';
|
||||
import { Modal, Button } from '@crayons';
|
||||
import '../../storybook-utilities/designSystem.scss';
|
||||
|
|
@ -11,37 +11,38 @@ export default {
|
|||
parameters: { notes },
|
||||
};
|
||||
|
||||
const commonProps = {
|
||||
onClose: action('close fired'),
|
||||
};
|
||||
export const Default = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
export const Default = () => (
|
||||
<div>
|
||||
{/* TODO: add a trigger that will change component state... */}
|
||||
<Button>Open modal</Button>
|
||||
<Modal
|
||||
size={select(
|
||||
'size',
|
||||
{
|
||||
Small: 's',
|
||||
Medium: 'm',
|
||||
Default: 'default',
|
||||
},
|
||||
'default',
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={() => setIsModalOpen(true)}>Open modal</Button>
|
||||
{isModalOpen && (
|
||||
<Modal
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
size={select(
|
||||
'size',
|
||||
{
|
||||
Small: 's',
|
||||
Medium: 'm',
|
||||
Default: 'default',
|
||||
},
|
||||
'default',
|
||||
)}
|
||||
className={text('className')}
|
||||
title={text('title', 'This is my Modal title')}
|
||||
overlay={boolean('overlay', true)}
|
||||
>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
|
||||
odio est, ultricies vel euismod ut, fringilla quis tellus. Sed at
|
||||
dui mi. Fusce cursus nibh lectus, vitae lobortis orci volutpat quis.{' '}
|
||||
</p>
|
||||
</Modal>
|
||||
)}
|
||||
className={text('className')}
|
||||
title={text('title', 'This is my Modal title')}
|
||||
overlay={boolean('overlay', true)}
|
||||
{...commonProps}
|
||||
>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
|
||||
odio est, ultricies vel euismod ut, fringilla quis tellus. Sed at dui
|
||||
mi. Fusce cursus nibh lectus, vitae lobortis orci volutpat quis.{' '}
|
||||
</p>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Default.story = {
|
||||
name: 'Modals',
|
||||
|
|
|
|||
|
|
@ -4,3 +4,15 @@ Modals should be positioned centered in relation to entire viewport. So relation
|
|||
to its trigger doesn’t really matter.
|
||||
|
||||
Use your best judgements when choosing the right size.
|
||||
|
||||
### Modal accessibility
|
||||
|
||||
The Preact Modal component utilises the `focus-trap` library to ensure that:
|
||||
|
||||
- When the modal is opened, focus is transferred to the first interactive item
|
||||
in that modal
|
||||
- When the modal is closed, focus is transferred back to the button that
|
||||
activated the modal
|
||||
- While the modal is open, focus is trapped inside so that when a user presses
|
||||
the Tab key, interactive items behind the modal are not focused
|
||||
- When the modal is open, pressing the Escape key will close it
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { h } from 'preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { render } from '@testing-library/preact';
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, waitFor } from '@testing-library/preact';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Modal } from '../Modal';
|
||||
|
||||
it('should have no a11y violations', async () => {
|
||||
|
|
@ -12,6 +14,26 @@ it('should have no a11y violations', async () => {
|
|||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should trap focus', async () => {
|
||||
const { getByText, getByLabelText } = render(
|
||||
<div>
|
||||
<button>Outside modal button</button>
|
||||
<Modal title="This is a modal title">
|
||||
<button>Modal content button</button>
|
||||
</Modal>
|
||||
</div>,
|
||||
);
|
||||
|
||||
const closeButton = getByLabelText('Close', { selector: 'button' });
|
||||
await waitFor(() => expect(closeButton).toHaveFocus());
|
||||
|
||||
userEvent.tab();
|
||||
expect(getByText('Modal content button')).toHaveFocus();
|
||||
|
||||
userEvent.tab();
|
||||
expect(closeButton).toHaveFocus();
|
||||
});
|
||||
|
||||
it('should close when the close button is clicked', async () => {
|
||||
const onClose = jest.fn();
|
||||
const { getByLabelText } = render(
|
||||
|
|
@ -27,6 +49,52 @@ it('should close when the close button is clicked', async () => {
|
|||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should close when Escape is pressed', () => {
|
||||
const onClose = jest.fn();
|
||||
const { container } = render(
|
||||
<Modal title="This is a modal title" onClose={onClose}>
|
||||
This is the modal body content
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
userEvent.type(container, '{esc}');
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("shouldn't close on outside click by default", () => {
|
||||
const onClose = jest.fn();
|
||||
const { getByText } = render(
|
||||
<div>
|
||||
<p>Outside content</p>
|
||||
<Modal title="This is a modal title" onClose={onClose}>
|
||||
This is the modal body content
|
||||
</Modal>
|
||||
</div>,
|
||||
);
|
||||
|
||||
userEvent.click(getByText('Outside content'));
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should close on click outside, if enabled', () => {
|
||||
const onClose = jest.fn();
|
||||
const { getByText } = render(
|
||||
<div>
|
||||
<p>Outside content</p>
|
||||
<Modal
|
||||
title="This is a modal title"
|
||||
onClose={onClose}
|
||||
closeOnClickOutside
|
||||
>
|
||||
This is the modal body content
|
||||
</Modal>
|
||||
</div>,
|
||||
);
|
||||
|
||||
userEvent.click(getByText('Outside content'));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should render with additional class names', async () => {
|
||||
const { getByTestId } = render(
|
||||
<Modal
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { h } from 'preact';
|
|||
import PropTypes from 'prop-types';
|
||||
import { SingleListing } from '../singleListing/SingleListing';
|
||||
import { MessageModal } from './MessageModal';
|
||||
import { Button } from '@crayons';
|
||||
import { Modal as CrayonsModal } from '@crayons';
|
||||
|
||||
export const Modal = ({
|
||||
currentUserId,
|
||||
|
|
@ -17,41 +17,13 @@ export const Modal = ({
|
|||
}) => {
|
||||
const shouldRenderMessageModal = listing && listing.contact_via_connect;
|
||||
|
||||
const Icon = () => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
className="crayons-icon pointer-events-none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636l4.95 4.95z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// TODO: Why are we not using the crayons modal component and instead recreating it here?
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="listing modal"
|
||||
id="single-listing-container__inner"
|
||||
className="single-listing-container__inner crayons-modal__box"
|
||||
>
|
||||
<div className="crayons-modal__box__header flex s:hidden">
|
||||
<Button
|
||||
type="button"
|
||||
id="close-listing-modal"
|
||||
tagName="button"
|
||||
contentType="icon"
|
||||
variant="ghost"
|
||||
className="ml-auto"
|
||||
icon={Icon}
|
||||
onClick={onClick}
|
||||
aria-label="Close listing"
|
||||
/>
|
||||
</div>
|
||||
<div className="crayons-modal__box__body p-0">
|
||||
<div className="listings-modal" data-testid="listings-modal">
|
||||
<CrayonsModal
|
||||
onClose={onClick}
|
||||
closeOnClickOutside={true}
|
||||
title="Listing"
|
||||
>
|
||||
<div className="p-3 m:p-6 l:p-8">
|
||||
<SingleListing
|
||||
onAddTag={onAddTag}
|
||||
|
|
@ -72,7 +44,7 @@ export const Modal = ({
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CrayonsModal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import { h, Component, Fragment } from 'preact';
|
||||
import { h, Component } from 'preact';
|
||||
import { debounceAction } from '../utilities/debounceAction';
|
||||
import { fetchSearch } from '../utilities/search';
|
||||
import { KeyboardShortcuts } from '../shared/components/useKeyboardShortcuts';
|
||||
import { ModalBackground } from './components/ModalBackground';
|
||||
import { Modal } from './components/Modal';
|
||||
import { AllListings } from './components/AllListings';
|
||||
import { ListingFilters } from './components/ListingFilters';
|
||||
import {
|
||||
LISTING_PAGE_SIZE,
|
||||
MATCH_LISTING,
|
||||
updateListings,
|
||||
getQueryParams,
|
||||
resizeAllMasonryItems,
|
||||
|
|
@ -29,6 +26,7 @@ export class Listings extends Component {
|
|||
slug: null,
|
||||
page: 0,
|
||||
showNextPageButton: false,
|
||||
isModalOpen: false,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
|
|
@ -40,6 +38,7 @@ export class Listings extends Component {
|
|||
let openedListing = null;
|
||||
let slug = null;
|
||||
let listings = [];
|
||||
let isModalOpen = false;
|
||||
|
||||
if (params.t) {
|
||||
tags = params.t.split(',');
|
||||
|
|
@ -54,7 +53,7 @@ export class Listings extends Component {
|
|||
if (container.dataset.displayedlisting) {
|
||||
openedListing = JSON.parse(container.dataset.displayedlisting);
|
||||
({ slug } = openedListing);
|
||||
document.body.classList.add('modal-open');
|
||||
isModalOpen = true;
|
||||
}
|
||||
|
||||
this.debouncedListingSearch = debounceAction(this.handleQuery.bind(this), {
|
||||
|
|
@ -70,6 +69,7 @@ export class Listings extends Component {
|
|||
listings,
|
||||
openedListing,
|
||||
slug,
|
||||
isModalOpen,
|
||||
});
|
||||
this.listingSearch(query, tags, category, slug);
|
||||
this.setUser();
|
||||
|
|
@ -88,15 +88,12 @@ export class Listings extends Component {
|
|||
|
||||
addTag = (e, tag) => {
|
||||
e.preventDefault();
|
||||
if (document.body.classList.contains('modal-open')) {
|
||||
this.handleCloseModal('close-modal');
|
||||
}
|
||||
const { query, tags, category } = this.state;
|
||||
const newTags = tags;
|
||||
if (newTags.indexOf(tag) === -1) {
|
||||
newTags.push(tag);
|
||||
}
|
||||
this.setState({ tags: newTags, page: 0 });
|
||||
this.setState({ tags: newTags, page: 0, isModalOpen: false });
|
||||
this.listingSearch(query, newTags, category, null);
|
||||
window.scroll(0, 0);
|
||||
};
|
||||
|
|
@ -126,30 +123,21 @@ export class Listings extends Component {
|
|||
this.listingSearch(query, tags, cat, null);
|
||||
};
|
||||
|
||||
handleCloseModal = (e) => {
|
||||
const { openedListing } = this.state;
|
||||
if (
|
||||
e === 'close-modal' ||
|
||||
(openedListing !== null && e.key === 'Escape') ||
|
||||
MATCH_LISTING.includes(e.target.id)
|
||||
) {
|
||||
const { query, tags, category } = this.state;
|
||||
this.setState({ openedListing: null, page: 0 });
|
||||
this.setLocation(query, tags, category, null);
|
||||
document.body.classList.remove('modal-open');
|
||||
}
|
||||
handleCloseModal = () => {
|
||||
const { query, tags, category } = this.state;
|
||||
this.setState({ openedListing: null, page: 0, isModalOpen: false });
|
||||
this.setLocation(query, tags, category, null);
|
||||
};
|
||||
|
||||
handleOpenModal = (e, listing) => {
|
||||
e.preventDefault();
|
||||
this.setState({ openedListing: listing });
|
||||
this.setState({ openedListing: listing, isModalOpen: true });
|
||||
window.history.replaceState(
|
||||
null,
|
||||
null,
|
||||
`/listings/${listing.category}/${listing.slug}`,
|
||||
);
|
||||
this.setLocation(null, null, listing.category, listing.slug);
|
||||
document.body.classList.add('modal-open');
|
||||
};
|
||||
|
||||
handleDraftingMessage = (e) => {
|
||||
|
|
@ -269,10 +257,11 @@ export class Listings extends Component {
|
|||
showNextPageButton,
|
||||
initialFetch,
|
||||
message,
|
||||
isModalOpen,
|
||||
} = this.state;
|
||||
|
||||
const shouldRenderModal =
|
||||
openedListing !== null && openedListing !== undefined;
|
||||
isModalOpen && openedListing !== null && openedListing !== undefined;
|
||||
|
||||
if (initialFetch) {
|
||||
this.triggerMasonry();
|
||||
|
|
@ -301,27 +290,17 @@ export class Listings extends Component {
|
|||
loadNextPage={this.loadNextPage}
|
||||
/>
|
||||
{shouldRenderModal && (
|
||||
<Fragment>
|
||||
<div className="crayons-modal">
|
||||
<Modal
|
||||
currentUserId={currentUserId}
|
||||
onAddTag={this.addTag}
|
||||
onChangeDraftingMessage={this.handleDraftingMessage}
|
||||
onClick={this.handleCloseModal}
|
||||
onChangeCategory={this.selectCategory}
|
||||
onOpenModal={this.handleOpenModal}
|
||||
onSubmit={this.handleSubmitMessage}
|
||||
listing={openedListing}
|
||||
message={message}
|
||||
/>
|
||||
<ModalBackground onClick={this.handleCloseModal} />
|
||||
</div>
|
||||
<KeyboardShortcuts
|
||||
shortcuts={{
|
||||
Escape: this.handleCloseModal,
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
<Modal
|
||||
currentUserId={currentUserId}
|
||||
onAddTag={this.addTag}
|
||||
onChangeDraftingMessage={this.handleDraftingMessage}
|
||||
onClick={this.handleCloseModal}
|
||||
onChangeCategory={this.selectCategory}
|
||||
onOpenModal={this.handleOpenModal}
|
||||
onSubmit={this.handleSubmitMessage}
|
||||
listing={openedListing}
|
||||
message={message}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,13 +4,6 @@
|
|||
*/
|
||||
export const LISTING_PAGE_SIZE = 75;
|
||||
|
||||
export const MATCH_LISTING = [
|
||||
'single-listing-container__inner',
|
||||
'listing-filters',
|
||||
'listings-modal-background',
|
||||
'close-listing-modal',
|
||||
];
|
||||
|
||||
export function updateListings(listings) {
|
||||
const fullListings = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FocusTrap } from '../shared/components/focusTrap';
|
||||
import { IntroSlide } from './components/IntroSlide';
|
||||
import { EmailPreferencesForm } from './components/EmailPreferencesForm';
|
||||
|
|
@ -66,22 +65,25 @@ export class Onboarding extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Update main element id to enable skip link. See issue #1153.
|
||||
render() {
|
||||
const { currentSlide } = this.state;
|
||||
const { communityConfig } = this.props;
|
||||
return (
|
||||
<FocusTrap>
|
||||
<main
|
||||
className="onboarding-body"
|
||||
style={
|
||||
communityConfig.communityBackground && {
|
||||
backgroundImage: `url(${communityConfig.communityBackground})`,
|
||||
}
|
||||
}
|
||||
>
|
||||
<main
|
||||
className="onboarding-body"
|
||||
style={
|
||||
communityConfig.communityBackground
|
||||
? {
|
||||
backgroundImage: `url(${communityConfig.communityBackground})`,
|
||||
}
|
||||
: null
|
||||
}
|
||||
>
|
||||
<FocusTrap key={`onboarding-${currentSlide}`}>
|
||||
{this.slides[currentSlide]}
|
||||
</main>
|
||||
</FocusTrap>
|
||||
</FocusTrap>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getContentOfToken, updateOnboarding } from '../utilities';
|
||||
import { Navigation } from './Navigation';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import he from 'he';
|
||||
|
||||
import { getContentOfToken } from '../utilities';
|
||||
import { Navigation } from './Navigation';
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,40 @@ import {
|
|||
initializeTouchDevice,
|
||||
} from '../topNavigation/utilities';
|
||||
|
||||
window.showModal = async ({
|
||||
title,
|
||||
contentSelector,
|
||||
overlay = false,
|
||||
size = 's',
|
||||
}) => {
|
||||
const [{ Modal }, { render, h }] = await Promise.all([
|
||||
import('@crayons/Modal'),
|
||||
import('preact'),
|
||||
]);
|
||||
|
||||
const modalRoot = document.createElement('div');
|
||||
document.body.appendChild(modalRoot);
|
||||
|
||||
render(
|
||||
<Modal
|
||||
overlay={overlay}
|
||||
title={title}
|
||||
onClose={() => {
|
||||
render(null, modalRoot);
|
||||
}}
|
||||
size={size}
|
||||
>
|
||||
<div
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: document.querySelector(contentSelector).innerHTML,
|
||||
}}
|
||||
/>
|
||||
</Modal>,
|
||||
modalRoot,
|
||||
);
|
||||
};
|
||||
|
||||
function getPageEntries() {
|
||||
return Object.entries({
|
||||
'notifications-index': document.getElementById('notifications-link'),
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
/* global showLoginModal */
|
||||
|
||||
function loadFunctionality() {
|
||||
if (!document.getElementById('notification-subscriptions-area')) {
|
||||
return;
|
||||
|
|
@ -33,9 +35,7 @@ function loadFunctionality() {
|
|||
|
||||
if (userStatus === 'logged-out') {
|
||||
updateStatus = () => {
|
||||
// Disabled because showModal() is globally defined in asset pipeline
|
||||
// eslint-disable-next-line no-undef
|
||||
showModal('notification-subscription');
|
||||
showLoginModal();
|
||||
};
|
||||
} else {
|
||||
updateStatus = (target) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-disable no-alert */
|
||||
/* eslint-disable no-restricted-globals */
|
||||
/* global showLoginModal */
|
||||
|
||||
function toggleTemplateTypeButton(form, e) {
|
||||
const { targetType } = e.target.dataset;
|
||||
|
|
@ -296,10 +297,13 @@ function observeForReplyClick() {
|
|||
}
|
||||
|
||||
function handleLoggedOut() {
|
||||
// global method from app/assets/javascripts/utilities/showModal.js
|
||||
document
|
||||
.getElementsByClassName('response-templates-button')[0]
|
||||
?.addEventListener('click', showModal); /* eslint-disable-line no-undef */
|
||||
?.addEventListener(
|
||||
'click',
|
||||
// eslint-disable-next-line no-undef
|
||||
showLoginModal,
|
||||
);
|
||||
}
|
||||
/* eslint-enable no-alert */
|
||||
/* eslint-enable no-restricted-globals */
|
||||
|
|
|
|||
|
|
@ -1,27 +1,88 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { useLayoutEffect, useRef } from 'preact/hooks';
|
||||
import { h, Fragment } from 'preact';
|
||||
import { useLayoutEffect, useRef, useCallback } from 'preact/hooks';
|
||||
import { createFocusTrap } from 'focus-trap';
|
||||
import { defaultChildrenPropTypes } from '../../common-prop-types';
|
||||
import { KeyboardShortcuts } from './useKeyboardShortcuts';
|
||||
|
||||
export const FocusTrap = ({ selector, children }) => {
|
||||
/**
|
||||
* Wrapper component to create a focus trap within the HTML element found by the given selector
|
||||
*
|
||||
* @example
|
||||
* import { FocusTrap } from "shared/components/FocusTrap";
|
||||
*
|
||||
* const ExampleComponent = ({ onClose }) => (
|
||||
* <FocusTrap selector=".component-with-focus-trap" onDeactivate={onClose}>
|
||||
* <div class="component-with-focus-trap">
|
||||
* <button onClick={onClose}>Close</button>
|
||||
* </div>
|
||||
* </FocusTrap>
|
||||
* )
|
||||
*
|
||||
* @param {string} selector The CSS selector for the element where focus is to be trapped
|
||||
* @param {Array} children Child element(s) passed via composition
|
||||
* @param {Function} onDeactivate Callback function to be called when the focus trap is deactivated by navigation or Escape press
|
||||
* @param {boolean} clickOutsideDeactivates If true, the focus trap will deactivate when a user clicks outside of the trap area
|
||||
*/
|
||||
export const FocusTrap = ({
|
||||
selector,
|
||||
children,
|
||||
onDeactivate,
|
||||
clickOutsideDeactivates = false,
|
||||
}) => {
|
||||
const focusTrap = useRef(null);
|
||||
const deactivate = useCallback(() => onDeactivate(), [onDeactivate]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
focusTrap.current = createFocusTrap(selector);
|
||||
const currentLocationHref = document.location.href;
|
||||
const routeChangeObserver = new MutationObserver((mutations) => {
|
||||
const hasRouteChanged = mutations.some(
|
||||
() => currentLocationHref !== document.location.href,
|
||||
);
|
||||
|
||||
// Ensure trap deactivates if user navigates from the page
|
||||
if (hasRouteChanged) {
|
||||
focusTrap.current?.deactivate();
|
||||
routeChangeObserver.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
focusTrap.current = createFocusTrap(selector, {
|
||||
escapeDeactivates: false,
|
||||
clickOutsideDeactivates,
|
||||
onDeactivate: deactivate,
|
||||
});
|
||||
|
||||
focusTrap.current.activate();
|
||||
routeChangeObserver.observe(document.querySelector('body'), {
|
||||
childList: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
focusTrap.current.deactivate();
|
||||
routeChangeObserver.disconnect();
|
||||
};
|
||||
});
|
||||
}, [clickOutsideDeactivates, selector, deactivate]);
|
||||
|
||||
return children;
|
||||
const shortcuts = {
|
||||
escape: onDeactivate,
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{children}
|
||||
<KeyboardShortcuts shortcuts={shortcuts} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
FocusTrap.defaultProps = {
|
||||
selector: '.crayons-modal',
|
||||
onDeactivate: () => {},
|
||||
};
|
||||
|
||||
FocusTrap.propTypes = {
|
||||
selector: PropTypes.string,
|
||||
children: defaultChildrenPropTypes.isRequired,
|
||||
onDeactivate: PropTypes.func,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -92,8 +92,8 @@ class PollTag < LiquidTagBase
|
|||
var els = document.getElementsByClassName('ltag-poll')
|
||||
for (i = 0; i < els.length; i += 1) {
|
||||
els[i].onclick = function(e) {
|
||||
if (typeof showModal !== "undefined") {
|
||||
showModal('poll');
|
||||
if (typeof showLoginModal !== "undefined") {
|
||||
showLoginModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,8 +175,8 @@ class UserSubscriptionTag < LiquidTagBase
|
|||
const signInBtn = document.getElementById('sign-in-btn');
|
||||
if (signInBtn) {
|
||||
signInBtn.addEventListener('click', function(e) {
|
||||
if (typeof showModal !== 'undefined') {
|
||||
showModal('user_subscription');
|
||||
if (typeof showLoginModal !== 'undefined') {
|
||||
showLoginModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,18 @@
|
|||
<button class="btn btn-primary ml-7" type="button" onclick="toggleNavigationLinkModal()">
|
||||
Add Navigation Link
|
||||
</button>
|
||||
<span data-controller="modal" data-modal-root-selector-value="#add-navigation-link-modal-root" data-modal-content-selector-value="#add-navigation-link-modal" data-modal-title-value="Add Navigation Link" data-modal-size-value="l">
|
||||
<button class="btn btn-primary ml-7" type="button" data-action="modal#toggleModal" id="nav-link-button">
|
||||
Add Navigation Link
|
||||
</button>
|
||||
|
||||
<div id="add-navigation-link-modal" class="hidden">
|
||||
<div class="crayons-modal crayons-modal--l">
|
||||
<div class="crayons-modal__box">
|
||||
<header class="crayons-modal__box__header">
|
||||
<h2>Add Navigation Link</h2>
|
||||
<button type="button" onclick="toggleNavigationLinkModal()" class="crayons-btn crayons-btn--icon crayons-btn--ghost">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" class="crayons-icon" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636l4.95 4.95z" />
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
<div class="crayons-modal__box__body">
|
||||
<div class="form-group grid p-6 mb-6 gap-1">
|
||||
<div class="form-group grid p-6 mb-6 gap-1">
|
||||
<%= form_for(NavigationLink.new, url: admin_navigation_links_path) do |form| %>
|
||||
<%= render "form", form: form %>
|
||||
<%= form.submit "Add link", class: "crayons-btn" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div id="add-navigation-link-modal" class="hidden">
|
||||
<div class="form-group grid p-6 mb-6 gap-1">
|
||||
<div class="form-group grid p-6 mb-6 gap-1">
|
||||
<%= form_for(NavigationLink.new, url: admin_navigation_links_path) do |form| %>
|
||||
<%= render "form", form: form %>
|
||||
<%= form.submit "Add link", class: "crayons-btn" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="crayons-modal__overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleNavigationLinkModal() {
|
||||
const confirmationModal = document.getElementById(`add-navigation-link-modal`);
|
||||
if (confirmationModal) {
|
||||
confirmationModal.classList.toggle("hidden");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<div id="add-navigation-link-modal-root"></div>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,36 +1,16 @@
|
|||
<div onclick="toggleEditNavigationLinkModal('<%= link[:id] %>')" class="color-accent-brand cursor-pointer">
|
||||
Edit Link
|
||||
</div>
|
||||
<span data-controller="modal" data-modal-root-selector-value="#edit-navigation-link-<%= link[:id] %>-modal-root" data-modal-content-selector-value="#edit-navigation-link-<%= link[:id] %>-modal" data-modal-title-value="Edit Link" data-modal-size-value="l">
|
||||
<button data-action="modal#toggleModal" class="color-accent-brand crayons-btn crayons-btn--ghost-brand">
|
||||
Edit Link
|
||||
</button>
|
||||
|
||||
<div id="edit-navigation-link-<%= link[:id] %>-modal" class="hidden">
|
||||
<div class="crayons-modal crayons-modal--l">
|
||||
<div class="crayons-modal__box">
|
||||
<header class="crayons-modal__box__header">
|
||||
<h2>Edit Link</h2>
|
||||
<button type="button" onclick="toggleEditNavigationLinkModal('<%= link[:id] %>')" class="crayons-btn crayons-btn--icon crayons-btn--ghost">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" class="crayons-icon" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636l4.95 4.95z" />
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
<div class="crayons-modal__box__body">
|
||||
<div class="form-group grid p-6 mb-6 gap-1">
|
||||
<%= form_for [:admin, link] do |form| %>
|
||||
<%= render "form", form: form %>
|
||||
<%= form.submit class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div id="edit-navigation-link-<%= link[:id] %>-modal" class="hidden">
|
||||
<div class="form-group grid p-6 mb-6 gap-1">
|
||||
<%= form_for [:admin, link] do |form| %>
|
||||
<%= render "form", form: form %>
|
||||
<%= form.submit class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="crayons-modal__overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleEditNavigationLinkModal(link_id) {
|
||||
const confirmationModal = document.getElementById(`edit-navigation-link-${link_id}-modal`);
|
||||
if (confirmationModal) {
|
||||
confirmationModal.classList.toggle("hidden");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<div id="edit-navigation-link-<%= link[:id] %>-modal-root"></div>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,36 +1,23 @@
|
|||
<span data-controller="modal" data-modal-hidden-class="hidden">
|
||||
<span data-controller="modal" data-modal-root-selector-value="#add-group-modal-root" data-modal-content-selector-value="#add-group-modal" data-modal-title-value="Add Group" data-modal-size-value="s">
|
||||
<button class="crayons-btn" type="button" data-action="modal#toggleModal">
|
||||
Add group
|
||||
</button>
|
||||
|
||||
<div id="add-group-modal" class="hidden" data-modal-target="toggle">
|
||||
<div class="crayons-modal crayons-modal--s absolute">
|
||||
<div class="crayons-modal__box">
|
||||
<header class="crayons-modal__box__header">
|
||||
<h2>Add Group</h2>
|
||||
<button type="button" data-action="modal#toggleModal" class="crayons-btn crayons-btn--icon crayons-btn--ghost">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" class="crayons-icon" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636l4.95 4.95z" />
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
<div class="crayons-modal__box__body">
|
||||
<div class="form-group grid p-6 mb-6 gap-1">
|
||||
<%= form_for [:admin, ProfileFieldGroup.new] do |form| %>
|
||||
<div class="form-group">
|
||||
<%= form.label :name %>
|
||||
<%= form.text_field :name, class: "form-control" %>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<%= form.label :description %>
|
||||
<%= form.text_field :description, class: "form-control" %>
|
||||
</div>
|
||||
<%= form.submit class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div id="add-group-modal" class="hidden">
|
||||
<div class="form-group grid p-6 mb-6 gap-1">
|
||||
<%= form_for [:admin, ProfileFieldGroup.new] do |form| %>
|
||||
<div class="form-group">
|
||||
<%= form.label :name %>
|
||||
<%= form.text_field :name, class: "form-control" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="crayons-modal__overlay"></div>
|
||||
<div class="form-group">
|
||||
<%= form.label :description %>
|
||||
<%= form.text_field :description, class: "form-control" %>
|
||||
</div>
|
||||
<%= form.submit class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="add-group-modal-root"></div>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,16 @@
|
|||
<span data-controller="modal" class="mx-1" data-modal-hidden-class="hidden">
|
||||
<span data-controller="modal" data-modal-root-selector-value="#add-<%= group_name %>-profile-field-modal-root" data-modal-content-selector-value="#add-<%= group_name %>-profile-field-modal" data-modal-title-value="Add Profile Field" data-modal-size-value="s" class="mx-1">
|
||||
<button data-action="modal#toggleModal" class="crayons-btn">
|
||||
Add Field
|
||||
</button>
|
||||
|
||||
<div id="add-<%= group_name %>-profile-field-modal" class="hidden" data-modal-target="toggle">
|
||||
<div class="crayons-modal crayons-modal--s">
|
||||
<div class="crayons-modal__box">
|
||||
<header class="crayons-modal__box__header">
|
||||
<h2>Add Profile Field</h2>
|
||||
<button type="button" data-action="modal#toggleModal" class="crayons-btn crayons-btn--icon crayons-btn--ghost">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" class="crayons-icon" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636l4.95 4.95z" />
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
<div class="crayons-modal__box__body">
|
||||
<div class="form-group grid p-6 mb-6 gap-1">
|
||||
<%= form_for [:admin, ProfileField.new] do |form| %>
|
||||
<%= render "profile_field_form", form: form, group: group, group_name: group_name %>
|
||||
<%= form.submit class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="crayons-modal__overlay"></div>
|
||||
<div id="add-<%= group_name %>-profile-field-modal" class="hidden">
|
||||
<div class="form-group grid p-6 mb-6 gap-1">
|
||||
<%= form_for [:admin, ProfileField.new] do |form| %>
|
||||
<%= render "profile_field_form", form: form, group: group, group_name: group_name %>
|
||||
<%= form.submit class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="add-<%= group_name %>-profile-field-modal-root"></div>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,36 +1,23 @@
|
|||
<span data-controller="modal" class="mx-1" data-modal-hidden-class="hidden">
|
||||
<span data-controller="modal" data-modal-root-selector-value="#edit-group-<%= group_name %>-modal-root" data-modal-content-selector-value="#edit-group-<%= group_name %>-modal" data-modal-title-value="Edit Group <%= group_name %>" data-modal-size-value="s" class="mx-1">
|
||||
<button data-action="modal#toggleModal" class="crayons-btn crayons-btn--secondary">
|
||||
Edit group
|
||||
</button>
|
||||
|
||||
<div id="edit-group-<%= group_name %>-modal" class="hidden" data-modal-target="toggle">
|
||||
<div class="crayons-modal crayons-modal--s">
|
||||
<div class="crayons-modal__box">
|
||||
<header class="crayons-modal__box__header">
|
||||
<h2>Edit Group <%= group_name %></h2>
|
||||
<button type="button" data-action="modal#toggleModal" class="crayons-btn crayons-btn--icon crayons-btn--ghost">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" class="crayons-icon" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636l4.95 4.95z" />
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
<div class="crayons-modal__box__body">
|
||||
<div class="form-group grid p-6 mb-6 gap-1">
|
||||
<%= form_for [:admin, group] do |form| %>
|
||||
<div class="form-group">
|
||||
<%= form.label :name %>
|
||||
<%= form.text_field :name, class: "form-control" %>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<%= form.label :description %>
|
||||
<%= form.text_field :description, class: "form-control" %>
|
||||
</div>
|
||||
<%= form.submit class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div id="edit-group-<%= group_name %>-modal" class="hidden">
|
||||
<div class="form-group grid p-6 mb-6 gap-1">
|
||||
<%= form_for [:admin, group] do |form| %>
|
||||
<div class="form-group">
|
||||
<%= form.label :name %>
|
||||
<%= form.text_field :name, class: "form-control" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="crayons-modal__overlay"></div>
|
||||
<div class="form-group">
|
||||
<%= form.label :description %>
|
||||
<%= form.text_field :description, class: "form-control" %>
|
||||
</div>
|
||||
<%= form.submit class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="edit-group-<%= group_name %>-modal-root"></div>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
var sortString = buildSortString(sortBy, sortDirection);
|
||||
|
||||
if (e.target.classList.contains("my-posts-query-button") && !checkUserLoggedIn()) {
|
||||
showModal("search-my-posts");
|
||||
showLoginModal();
|
||||
return;
|
||||
}
|
||||
var filters = e.target.dataset.filter;
|
||||
|
|
|
|||
|
|
@ -1,46 +1,32 @@
|
|||
<div id="global-signup-modal" class="authentication-modal crayons-modal crayons-modal--s fixed hidden">
|
||||
<div class="authentication-modal__box crayons-modal__box">
|
||||
<button type="button" class="authentication-modal__close-btn close-modal-button crayons-btn crayons-btn--icon crayons-btn--ghost">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" class="crayons-icon" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636l4.95 4.95z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="crayons-modal__box__body p-0">
|
||||
<div class="authentication-modal__container">
|
||||
<figure class="authentication-modal__image-container">
|
||||
<img class="authentication-modal__image" src="<%= optimized_image_url(safe_logo_url(SiteConfig.secondary_logo_url), width: 190) %>"
|
||||
alt="Secondary <%= community_name %> logo" />
|
||||
</figure>
|
||||
<div class="authentication-modal__content">
|
||||
<h1 class="authentication-modal__title">
|
||||
Log in to continue
|
||||
</h1>
|
||||
<p class="authentication-modal__description">
|
||||
<% if SiteConfig.tagline.present? %>
|
||||
<%= SiteConfig.tagline %>
|
||||
<% else %>
|
||||
We’d love for you to be a part of this community.
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="authentication-modal__actions">
|
||||
<a href="<%= sign_up_path %>" class="crayons-btn" aria-label="Log in" data-no-instant>
|
||||
Log in
|
||||
</a>
|
||||
<a href="<%= sign_up_path(state: "new-user") %>" class="crayons-btn crayons-btn--ghost-brand" aria-label="Create new account" data-no-instant>
|
||||
Create new account
|
||||
</a>
|
||||
</div>
|
||||
<footer class="authentication-modal__footer">
|
||||
<p class="authentication-modal__footer-label">
|
||||
We strive for transparency and don't collect excess data.
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
<div id="global-signup-modal" class="authentication-modal hidden">
|
||||
<div class="authentication-modal__container">
|
||||
<figure class="authentication-modal__image-container">
|
||||
<img class="authentication-modal__image" src="<%= optimized_image_url(safe_logo_url(SiteConfig.secondary_logo_url), width: 190) %>"
|
||||
alt="<%= community_name %>" />
|
||||
</figure>
|
||||
<div class="authentication-modal__content">
|
||||
<p class="authentication-modal__description">
|
||||
<% if SiteConfig.tagline.present? %>
|
||||
<%= SiteConfig.tagline %>
|
||||
<% else %>
|
||||
We’d love for you to be a part of this community.
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="authentication-modal__actions">
|
||||
<a href="<%= sign_up_path %>" class="crayons-btn" aria-label="Log in" data-no-instant>
|
||||
Log in
|
||||
</a>
|
||||
<a href="<%= sign_up_path(state: "new-user") %>" class="crayons-btn crayons-btn--ghost-brand" aria-label="Create new account" data-no-instant>
|
||||
Create new account
|
||||
</a>
|
||||
</div>
|
||||
<footer class="authentication-modal__footer">
|
||||
<p class="authentication-modal__footer-label">
|
||||
We strive for transparency and don't collect excess data.
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
<div class="crayons-modal__overlay"></div>
|
||||
</div>
|
||||
|
||||
<%= javascript_packs_with_chunks_tag "signupModalShortcuts", defer: true %>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
describe('Navigation links', () => {
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.fixture('users/adminUser.json').as('user');
|
||||
|
||||
cy.get('@user').then((user) => {
|
||||
cy.loginUser(user).then(() => {
|
||||
cy.visit('/admin/navigation_links');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should open the add navigation link modal', () => {
|
||||
cy.findByText('Add Navigation Link').as('addLinkButton');
|
||||
|
||||
cy.get('@addLinkButton').click();
|
||||
cy.findByTestId('modal-container').as('addLinkModal');
|
||||
|
||||
cy.get('@addLinkModal').findByText('Add Navigation Link').should('exist');
|
||||
cy.get('@addLinkModal')
|
||||
.findAllByRole('button')
|
||||
.first()
|
||||
.should('have.focus');
|
||||
|
||||
cy.get('@addLinkModal').findByRole('button', { name: /Close/ }).click();
|
||||
cy.get('@addLinkButton').should('have.focus');
|
||||
});
|
||||
|
||||
it('should open the edit navigation link modal', () => {
|
||||
cy.findByText('Edit Link').as('editLinkButton');
|
||||
|
||||
cy.get('@editLinkButton').click();
|
||||
cy.findByTestId('modal-container').as('editLinkModal');
|
||||
|
||||
cy.get('@editLinkModal').findByText('Edit Link').should('exist');
|
||||
cy.get('@editLinkModal')
|
||||
.findAllByRole('button')
|
||||
.first()
|
||||
.should('have.focus');
|
||||
|
||||
cy.get('@editLinkModal').findByRole('button', { name: /Close/ }).click();
|
||||
cy.get('@editLinkButton').should('have.focus');
|
||||
});
|
||||
});
|
||||
40
cypress/integration/listingFlows/viewListing.spec.js
Normal file
40
cypress/integration/listingFlows/viewListing.spec.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
describe('View listing', () => {
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.viewport('macbook-16');
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
it('opens a listing from the Feed page', () => {
|
||||
cy.findByText('Listing title').click();
|
||||
cy.findByTestId('listings-modal').as('listingsModal');
|
||||
|
||||
cy.get('@listingsModal').findByText('Listing').should('exist');
|
||||
cy.get('@listingsModal').findByText('Listing title').should('exist');
|
||||
cy.get('@listingsModal')
|
||||
.findAllByRole('button')
|
||||
.first()
|
||||
.should('have.focus');
|
||||
|
||||
cy.get('@listingsModal').findByRole('button', { name: /Close/ }).click();
|
||||
cy.get('@listingsModal').should('not.exist');
|
||||
});
|
||||
|
||||
it('opens a listing from the listings page', () => {
|
||||
cy.visit('/listings');
|
||||
|
||||
cy.findByText('Listing title').as('listingTitle');
|
||||
cy.get('@listingTitle').click();
|
||||
|
||||
cy.findByTestId('listings-modal').as('listingsModal');
|
||||
cy.get('@listingsModal').findByText('Listing').should('exist');
|
||||
cy.get('@listingsModal').findByText('Listing title').should('exist');
|
||||
cy.get('@listingsModal')
|
||||
.findAllByRole('button')
|
||||
.first()
|
||||
.should('have.focus');
|
||||
|
||||
cy.get('@listingsModal').findByRole('button', { name: /Close/ }).click();
|
||||
cy.get('@listingTitle').should('have.focus');
|
||||
});
|
||||
});
|
||||
46
cypress/integration/loggedOutFlows/showLoginModal.spec.js
Normal file
46
cypress/integration/loggedOutFlows/showLoginModal.spec.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
describe('Show log in modal', () => {
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
it('should show a log in modal on Feed bookmark click', () => {
|
||||
cy.findAllByRole('button', { name: /Save/ }).first().as('bookmarkButton');
|
||||
|
||||
cy.get('@bookmarkButton').click();
|
||||
|
||||
cy.findByTestId('modal-container').as('modal');
|
||||
cy.get('@modal').findByText('Log in to continue').should('exist');
|
||||
cy.get('@modal').findByLabelText('Log in').should('exist');
|
||||
cy.get('@modal').findByLabelText('Create new account').should('exist');
|
||||
cy.get('@modal').findByRole('button').first().should('have.focus');
|
||||
|
||||
cy.get('@modal').findByRole('button', { name: /Close/ }).click();
|
||||
cy.get('@bookmarkButton').should('have.focus');
|
||||
cy.findByTestId('modal-container').should('not.exist');
|
||||
});
|
||||
|
||||
it('should show login modal for article reaction clicks', () => {
|
||||
cy.findAllByText('Test article').last().click();
|
||||
|
||||
cy.findByRole('checkbox', { name: 'Heart' }).as('heartReaction');
|
||||
cy.findByRole('checkbox', { name: 'Unicorn' }).as('unicornReaction');
|
||||
cy.findByRole('checkbox', { name: 'Saved' }).as('bookmarkReaction');
|
||||
|
||||
['@heartReaction', '@unicornReaction', '@bookmarkReaction'].forEach(
|
||||
(reaction) => {
|
||||
cy.get(reaction).click();
|
||||
|
||||
cy.findByTestId('modal-container').as('modal');
|
||||
cy.get('@modal').findByText('Log in to continue').should('exist');
|
||||
cy.get('@modal').findByLabelText('Log in').should('exist');
|
||||
cy.get('@modal').findByLabelText('Create new account').should('exist');
|
||||
cy.get('@modal').findByRole('button').first().should('have.focus');
|
||||
|
||||
cy.get('@modal').findByRole('button', { name: /Close/ }).click();
|
||||
cy.get(reaction).should('have.focus');
|
||||
cy.findByTestId('modal-container').should('not.exist');
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -137,9 +137,9 @@ RSpec.describe UserSubscriptionTag, type: :liquid_tag do
|
|||
end
|
||||
|
||||
it "allows a user to sign in", type: :system, js: true do
|
||||
expect(page).to have_css("#global-signup-modal", visible: :hidden)
|
||||
expect(page).not_to have_text("Log in to continue")
|
||||
click_button("Sign In", id: "sign-in-btn")
|
||||
expect(page).to have_css("#global-signup-modal", visible: :visible)
|
||||
expect(page).to have_text("Log in to continue")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -111,3 +111,72 @@ seeder.create_if_doesnt_exist(User, "email", "article-editor-v2-user@forem.com")
|
|||
end
|
||||
|
||||
##############################################################################
|
||||
|
||||
seeder.create_if_doesnt_exist(Article, "title", "Test article") do
|
||||
markdown = <<~MARKDOWN
|
||||
---
|
||||
title: Test article
|
||||
published: true
|
||||
cover_image: #{Faker::Company.logo}
|
||||
---
|
||||
|
||||
#{Faker::Hipster.paragraph(sentence_count: 2)}
|
||||
#{Faker::Markdown.random}
|
||||
#{Faker::Hipster.paragraph(sentence_count: 2)}
|
||||
MARKDOWN
|
||||
Article.create(
|
||||
body_markdown: markdown,
|
||||
featured: true,
|
||||
show_comments: true,
|
||||
user_id: User.order(Arel.sql("RANDOM()")).first.id,
|
||||
)
|
||||
end
|
||||
|
||||
##############################################################################
|
||||
|
||||
seeder.create_if_none(ListingCategory) do
|
||||
ListingCategory.create!(
|
||||
slug: "cfp",
|
||||
cost: 1,
|
||||
name: "Conference CFP",
|
||||
rules: "Currently open for proposals, with link to form.",
|
||||
)
|
||||
end
|
||||
|
||||
##############################################################################
|
||||
|
||||
seeder.create_if_none(Listing) do
|
||||
user = User.first
|
||||
Credit.add_to(user, rand(100))
|
||||
|
||||
Listing.create!(
|
||||
user: user,
|
||||
title: "Listing title",
|
||||
body_markdown: Faker::Markdown.random,
|
||||
location: Faker::Address.city,
|
||||
organization_id: user.organizations.first&.id,
|
||||
listing_category_id: ListingCategory.first&.id,
|
||||
contact_via_connect: true,
|
||||
published: true,
|
||||
originally_published_at: Time.current,
|
||||
bumped_at: Time.current,
|
||||
tag_list: Tag.order(Arel.sql("RANDOM()")).first(2).pluck(:name),
|
||||
)
|
||||
end
|
||||
|
||||
##############################################################################
|
||||
|
||||
seeder.create_if_none(NavigationLink) do
|
||||
protocol = ApplicationConfig["APP_PROTOCOL"].freeze
|
||||
domain = Rails.application&.initialized? ? SiteConfig.app_domain : ApplicationConfig["APP_DOMAIN"]
|
||||
base_url = "#{protocol}#{domain}".freeze
|
||||
reading_icon = File.read(Rails.root.join("app/assets/images/twemoji/drawer.svg")).freeze
|
||||
|
||||
NavigationLink.create!(
|
||||
name: "Reading List",
|
||||
url: "#{base_url}/readinglist",
|
||||
icon: reading_icon,
|
||||
display_only_when_signed_in: true,
|
||||
position: 0,
|
||||
)
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue