diff --git a/Gemfile.lock b/Gemfile.lock index 0e6cbf8889..0a5af509dc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -219,7 +219,7 @@ GEM addressable (~> 2.7) letter_opener (1.7.0) launchy (~> 2.2) - loofah (2.19.0) + loofah (2.19.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -241,7 +241,7 @@ GEM ruby2_keywords (~> 0.0.1) netrc (0.11.0) nio4r (2.5.8) - nokogiri (1.13.9) + nokogiri (1.13.10) mini_portile2 (~> 2.8.0) racc (~> 1.4) oauth2 (1.4.9) @@ -338,8 +338,8 @@ GEM activesupport (>= 4.2) choice (~> 0.2.0) ruby-graphviz (~> 1.2) - rails-html-sanitizer (1.4.3) - loofah (~> 2.3) + rails-html-sanitizer (1.4.4) + loofah (~> 2.19, >= 2.19.1) rails-i18n (6.0.0) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 7) diff --git a/app/assets/stylesheets/core/alerts.scss b/app/assets/stylesheets/core/alerts.scss index eb312afd9c..35fbf1b248 100644 --- a/app/assets/stylesheets/core/alerts.scss +++ b/app/assets/stylesheets/core/alerts.scss @@ -5,10 +5,6 @@ text-align: center; border-radius: $size-4; - &Dashboard { - @include setMargin($size-0, $size-0, $size-26, $size-0); - } - &Text { color: $carmine; } diff --git a/app/assets/stylesheets/core/notices.scss b/app/assets/stylesheets/core/notices.scss index 31765c35cb..3a84605893 100644 --- a/app/assets/stylesheets/core/notices.scss +++ b/app/assets/stylesheets/core/notices.scss @@ -4,8 +4,4 @@ padding: $size-10; text-align: center; border-radius: $size-4; - - &Dashboard { - @include setMargin($size-0, $size-0, $size-26, $size-0); - } } diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 86bf310f19..3cb3f70de5 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -52,7 +52,10 @@ <%= react_component('SkipToContent', props: { id: 'mainContent' })%> <%= react_component('Header', props: header_props) %>
- <%= render partial: 'shared/alerts' %> + <%= react_component('Toast', props: { + notice: notice.present? ? notice : '', + alert: alert.present? ? alert : '' , + appendDashboardClass: (user_signed_in? && !static_page?) || secret_share_path? }) %> <% if user_signed_in? && !static_page? %>
{displayInfo(info)}
diff --git a/client/app/components/Story/StoryActions.jsx b/client/app/components/Story/StoryActions.jsx index 86b2614c14..55b19d79f6 100644 --- a/client/app/components/Story/StoryActions.jsx +++ b/client/app/components/Story/StoryActions.jsx @@ -132,7 +132,9 @@ const tooltipElement = ( ) => { const { link, dataMethod, dataConfirm, name, onClick, commentBy, - } = actions[item]; + } = actions[ + item + ]; const ariaLabel = commentBy || `${name} ${storyName || ''}`; diff --git a/client/app/components/Toast/Toast.scss b/client/app/components/Toast/Toast.scss new file mode 100644 index 0000000000..05b4e36b11 --- /dev/null +++ b/client/app/components/Toast/Toast.scss @@ -0,0 +1,28 @@ +@import "~styles/_global.scss"; + +.toast { + display: flex; + justify-content: space-between; + width: fit-content; + margin: 0 auto; + + button { + background: transparent; + color: $white; + opacity: 1; + border: none; + + &:hover { + border: none; + opacity: 0.8; + } + } +} + +.toastElementHidden { + visibility: hidden; +} + +.toastElementVisible { + visibility: visible; +} diff --git a/client/app/components/Toast/__tests__/Toast.spec.jsx b/client/app/components/Toast/__tests__/Toast.spec.jsx new file mode 100644 index 0000000000..64aa833636 --- /dev/null +++ b/client/app/components/Toast/__tests__/Toast.spec.jsx @@ -0,0 +1,96 @@ +// @flow +import React from 'react'; +import { + render, screen, fireEvent, waitFor, +} from '@testing-library/react'; +import { Toast } from 'components/Toast'; + +describe('Toast', () => { + describe('Toast Type: Alert', () => { + it('renders correctly', () => { + render( + , + ); + expect(screen).not.toBeNull(); + }); + + it('closes correctly on button click', () => { + const { getByRole, container } = render( + , + ); + + const toastContent = getByRole('alert'); + const toastBtn = container.querySelector('#btn-close-toast-alert'); + + expect(toastContent).toHaveClass('toastElementVisible'); + fireEvent.click(toastBtn); + expect(toastContent).toHaveClass('toastElementHidden'); + }); + + it('closes automatically after 7 seconds', async () => { + const { getByRole } = render( + , + ); + + const toastContent = getByRole('alert'); + expect(toastContent).toHaveClass('toastElementVisible'); + await waitFor( + () => { + expect(toastContent).toHaveClass('toastElementHidden'); + }, + { + timeout: 7000, + }, + ); + }, 30000); + }); + + describe('Toast Type: Notice', () => { + it('renders correctly', () => { + render(); + expect(screen).not.toBeNull(); + }); + + it('closes correctly on button click', () => { + const { getByRole, container } = render(); + + const toastContent = getByRole('region'); + const toastBtn = container.querySelector('#btn-close-toast-notice'); + + expect(toastContent).toHaveClass('toastElementVisible'); + fireEvent.click(toastBtn); + expect(toastContent).toHaveClass('toastElementHidden'); + }); + + it('closes automatically after 7 seconds', async () => { + const { getByRole } = render(); + + const toastContent = getByRole('region'); + expect(toastContent).toHaveClass('toastElementVisible'); + await waitFor( + () => { + expect(toastContent).toHaveClass('toastElementHidden'); + }, + { + timeout: 7000, + }, + ); + }, 30000); + }); + + describe('Toast Type: Notice', () => { + it('renders correctly', () => { + render(); + expect(screen).not.toBeNull(); + }); + }); +}); diff --git a/client/app/components/Toast/index.jsx b/client/app/components/Toast/index.jsx new file mode 100644 index 0000000000..d4f476b506 --- /dev/null +++ b/client/app/components/Toast/index.jsx @@ -0,0 +1,103 @@ +// @flow +import React, { useState } from 'react'; +import type { Node } from 'react'; +import { I18n } from 'libs/i18n'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import css from './Toast.scss'; + +type Props = { + alert?: string, + notice?: string, + appendDashboardClass?: boolean, +}; +export type State = { + showToast: boolean, +}; + +export const Toast = ({ alert, notice, appendDashboardClass }: Props): Node => { + const [showAlert, setShowAlert] = useState( + alert !== null + && alert !== '' + && !document.documentElement?.hasAttribute('data-turbolinks-preview'), + ); + const [showNotice, setShowNotice] = useState( + notice !== null + && notice !== '' + && !document.documentElement?.hasAttribute('data-turbolinks-preview'), + ); + const hideNotice = () => { + setShowNotice(false); + }; + const hideAlert = () => { + setShowAlert(false); + }; + if (showAlert || showNotice) { + setTimeout(() => { + hideNotice(); + hideAlert(); + }, 7000); + } + return ( + <> +
+ {showNotice && ( + <> +
+ {notice} +
+ + + )} +
+ + + ); +}; + +export default ({ alert, notice, appendDashboardClass }: Props): Node => ( + +); diff --git a/client/app/startup/registration.js b/client/app/startup/registration.js index 1512de974b..551524506c 100644 --- a/client/app/startup/registration.js +++ b/client/app/startup/registration.js @@ -27,6 +27,7 @@ import { Tag } from 'components/Tag'; import { Tooltip } from 'components/Tooltip'; import Input from 'components/Input'; import OAuthButton from 'components/OAuthButton'; +import Toast from 'components/Toast'; import Comments from 'widgets/Comments'; import { ToggleLocale } from 'widgets/ToggleLocale'; import Resources from 'widgets/Resources'; @@ -65,6 +66,7 @@ ReactOnRails.register({ Tag, ToggleLocale, Tooltip, + Toast, CrisisPrevention, CarePlanContacts, OAuthButton, diff --git a/client/app/stories/Toast.stories.jsx b/client/app/stories/Toast.stories.jsx new file mode 100644 index 0000000000..b2f23b00f0 --- /dev/null +++ b/client/app/stories/Toast.stories.jsx @@ -0,0 +1,25 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import React from 'react'; +import { Toast } from 'components/Toast'; + +export default { + title: 'Components/Toast', + component: Toast, +}; + +const Template = (args) => ; + +export const noticeToast = Template.bind({}); + +noticeToast.args = { + notice: 'Login successful.', + appendDashboardClass: 'true', +}; +noticeToast.storyName = 'Toast Type: Notice'; + +export const alertToast = Template.bind({}); + +alertToast.args = { + alert: 'Login failed.', +}; +alertToast.storyName = 'Toast Type: Alert'; diff --git a/client/app/utils/index.js b/client/app/utils/index.js index c6e3275d1e..57a75d18e7 100644 --- a/client/app/utils/index.js +++ b/client/app/utils/index.js @@ -3,8 +3,12 @@ import axios from 'axios'; import renderHTML from 'react-render-html'; import { sanitize } from 'dompurify'; -const randomString = (): string => Math.random().toString(36).substring(2, 15) - + Math.random().toString(36).substring(2, 15); +const randomString = (): string => Math.random() + .toString(36) + .substring(2, 15) + + Math.random() + .toString(36) + .substring(2, 15); const setCsrfToken = (): void => { const tokenDom = document.querySelector('meta[name=csrf-token]'); diff --git a/client/app/widgets/CarePlanContacts/__tests__/index.spec.jsx b/client/app/widgets/CarePlanContacts/__tests__/index.spec.jsx index 0031bfcbb5..ece3f0fd40 100644 --- a/client/app/widgets/CarePlanContacts/__tests__/index.spec.jsx +++ b/client/app/widgets/CarePlanContacts/__tests__/index.spec.jsx @@ -45,7 +45,9 @@ describe('CarePlanContacts', () => { }); const { container } = render(component); - const editLink = container.querySelector('a[aria-label="Edit Test1 Lastname"]'); + const editLink = container.querySelector( + 'a[aria-label="Edit Test1 Lastname"]', + ); expect(screen.queryByText('Edit Contact')).not.toBeInTheDocument(); await userEvent.click(editLink); @@ -72,7 +74,9 @@ describe('CarePlanContacts', () => { const axiosPostSpy = jest.spyOn(axios, 'patch').mockRejectedValue(error); const { container } = render(component); - const editLink = container.querySelector('a[aria-label="Edit Test1 Lastname"]'); + const editLink = container.querySelector( + 'a[aria-label="Edit Test1 Lastname"]', + ); expect(screen.queryByText('Edit Contact')).not.toBeInTheDocument(); await userEvent.click(editLink); diff --git a/client/app/widgets/Comments/__tests__/Comments.spec.jsx b/client/app/widgets/Comments/__tests__/Comments.spec.jsx index f73724a361..43a79295ab 100644 --- a/client/app/widgets/Comments/__tests__/Comments.spec.jsx +++ b/client/app/widgets/Comments/__tests__/Comments.spec.jsx @@ -86,7 +86,11 @@ describe('Comments', () => { expect( screen.getByRole('button', { name: 'Submit' }), ).toBeInTheDocument(); - expect(screen.getByRole('link', { name: `Report comment by ${getComment().commentByName}` })).toBeInTheDocument(); + expect( + screen.getByRole('link', { + name: `Report comment by ${getComment().commentByName}`, + }), + ).toBeInTheDocument(); }); it('add and delete a comment', async () => { @@ -106,7 +110,11 @@ describe('Comments', () => { await waitFor(() => expect(screen.getByRole('article')).toBeInTheDocument()); expect(screen.getByRole('article')).toHaveTextContent('Hey'); - await userEvent.click(screen.getByRole('link', { name: `Delete comment by ${getComment().commentByName}` })); + await userEvent.click( + screen.getByRole('link', { + name: `Delete comment by ${getComment().commentByName}`, + }), + ); await waitFor(() => expect(screen.queryByRole('article')).not.toBeInTheDocument()); }); @@ -147,7 +155,11 @@ describe('Comments', () => { await waitFor(() => expect(screen.getByRole('article')).toBeInTheDocument()); expect(screen.getByRole('article')).toHaveTextContent('Hey'); - await userEvent.click(screen.getByRole('link', { name: `Delete comment by ${getComment().commentByName}` })); + await userEvent.click( + screen.getByRole('link', { + name: `Delete comment by ${getComment().commentByName}`, + }), + ); await waitFor(() => expect(screen.queryByRole('article')).not.toBeInTheDocument()); }); diff --git a/config/locales/de.yml b/config/locales/de.yml index cdf89327ef..bc059d67d0 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -13,6 +13,7 @@ de: no_text: nein expand_menu: Menü aufklappen close: schließen + alert_auto_hide: Deze waarschuwing wordt automatisch verborgen load_more: Mehr laden of: von languages: diff --git a/config/locales/en.yml b/config/locales/en.yml index c84b8c6a25..8537d50f7e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -13,6 +13,7 @@ en: no_text: 'no' expand_menu: expand menu close: close + alert_auto_hide: This alert will be hidden automatically load_more: Load more of: of languages: diff --git a/config/locales/es.yml b/config/locales/es.yml index 7f1e2f12f4..763505fbcc 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -13,6 +13,7 @@ es: no_text: 'no' expand_menu: ampliar menú close: cerca + alert_auto_hide: Esta alerta se ocultará automáticamente load_more: Carga más of: de languages: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 2454db3227..a737221e68 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -13,6 +13,7 @@ fr: no_text: non expand_menu: dérouler le menu close: fermer + alert_auto_hide: Cette alerte sera masquée automatiquement load_more: Charger plus of: de languages: diff --git a/config/locales/hi.yml b/config/locales/hi.yml index 1cb664373e..4af2b71f38 100644 --- a/config/locales/hi.yml +++ b/config/locales/hi.yml @@ -13,6 +13,7 @@ hi: no_text: नहीं expand_menu: मेनू का विस्तार करें close: बंद करे + alert_auto_hide: यह अलर्ट अपने आप छिप जाएगा load_more: और लोड करें of: का languages: diff --git a/config/locales/id.yml b/config/locales/id.yml index 91e52507b9..b869276a02 100644 --- a/config/locales/id.yml +++ b/config/locales/id.yml @@ -13,6 +13,7 @@ id: no_text: Tidak expand_menu: Perluas menu close: Tutup + alert_auto_hide: Lansiran ini akan disembunyikan secara otomatis load_more: Muat lebih banyak of: dari languages: diff --git a/config/locales/it.yml b/config/locales/it.yml index 8db8bebd2c..4b92118819 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -13,6 +13,7 @@ it: no_text: 'no' expand_menu: espandere il menu close: vicino + alert_auto_hide: Questo avviso verrà nascosto automaticamente load_more: Carica altro of: di languages: diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 861b2b1cff..42d1e6a78f 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -13,6 +13,7 @@ ko: no_text: 아니오 expand_menu: 추가 메뉴 close: 종료 + alert_auto_hide: 이 알림은 자동으로 숨겨집니다. load_more: 더 보기 of: of languages: diff --git a/config/locales/nb.yml b/config/locales/nb.yml index dbe0a04337..44a9620f85 100644 --- a/config/locales/nb.yml +++ b/config/locales/nb.yml @@ -13,6 +13,7 @@ nb: no_text: nei expand_menu: utvide menyen close: lukk + alert_auto_hide: Dette varselet vil bli skjult automatisk load_more: Last mer of: av languages: diff --git a/config/locales/nl.yml b/config/locales/nl.yml index d42bbb67fd..8b6e66bec0 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -13,6 +13,7 @@ nl: no_text: nee expand_menu: vouw menu uit close: sluiten + alert_auto_hide: Deze waarschuwing wordt automatisch verborgen load_more: Meer laden of: van languages: diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 492eb66c51..68588ba6f5 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -13,6 +13,7 @@ pt-BR: no_text: não expand_menu: expandir menu close: fechar + alert_auto_hide: Este alerta será ocultado automaticamente load_more: Carregue mais of: do languages: diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 2dc05ca4cc..5fb285e65d 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -13,6 +13,7 @@ sv: no_text: nej expand_menu: expandera menyn close: stänga + alert_auto_hide: Denna varning kommer att döljas automatiskt load_more: Ladda mer of: av languages: diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 65a8166896..cc2813da2c 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -13,6 +13,7 @@ vi: no_text: không expand_menu: mở rộng menu close: gần + alert_auto_hide: Cảnh báo này sẽ tự động bị ẩn load_more: Tải thêm of: của languages: diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index be55af9e63..94e540390c 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -13,6 +13,7 @@ zh-CN: no_text: 没有 expand_menu: 展开菜单 close: 关闭 + alert_auto_hide: 此警报将自动隐藏 load_more: 更多 of: 的 languages: