Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions app/component/Snackbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import PropTypes from 'prop-types';
import React from 'react';
import cx from 'classnames';
import { FormattedMessage } from 'react-intl';
import Icon from './Icon';
import { useTranslationsContext } from '../util/useTranslationsContext';

/**
* A generic snackbar notification.
* The caller is responsible for managing the `show` state transitions and
* for providing a `liveRegionMessage` for screen-reader announcements.
*/
const Snackbar = ({
show,
messageId,
defaultMessage,
liveRegionMessage,
onClose,
iconImg,
className,
}) => {
const intl = useTranslationsContext();
return (
<>
<div
className={cx('snackbar', className, {
hide: show === null,
show: show === true,
'slide-out': show === false,
})}
aria-hidden="true"
>
<Icon img={iconImg} omitViewBox />
<span className="snackbar-text">
<FormattedMessage id={messageId} defaultMessage={defaultMessage} />
</span>
<button
type="button"
className="close-button"
aria-label={intl.formatMessage({
id: 'close',
defaultMessage: 'Close notification',
})}
onClick={onClose}
tabIndex="-1"
>
<Icon id="close-icon" img="notification-close" omitViewBox />
</button>
</div>
<div className="sr-only" aria-live="polite" role="status">
{liveRegionMessage}
</div>
</>
);
};

Snackbar.propTypes = {
/** null = hidden, true = slide in, false = slide out */
show: PropTypes.oneOf([null, true, false]),
messageId: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired,
liveRegionMessage: PropTypes.string,
onClose: PropTypes.func.isRequired,
iconImg: PropTypes.string,
className: PropTypes.string,
};

Snackbar.defaultProps = {
show: null,
liveRegionMessage: '',
iconImg: 'icon_checkmark-circled',
className: undefined,
};

export default Snackbar;
81 changes: 0 additions & 81 deletions app/component/itinerary/customize-search.scss
Original file line number Diff line number Diff line change
@@ -1,22 +1,3 @@
.mobile {
.restore-settings-success-snackbar {
width: calc(100% - 32px);
left: 16px;
top: unset;
bottom: 16px;

&.show {
animation: slideUpFromBottomWithMargin 0.5s $slide-up-down-animation;
transform: translateY(calc(100% + 16px));
}

&.slide-out {
animation: slideDownWithMargin 1s ease-in-out forwards;
transform: translateY(0);
}
}
}

.sr-only {
position: 'absolute';
left: '-9999px';
Expand All @@ -25,68 +6,6 @@
overflow: 'hidden';
}

.restore-settings-success-snackbar {
position: fixed;
left: 50%;
top: 78px;
z-index: 9999;
color: #fff;
display: flex;
width: 480px;
min-width: 320px;
max-height: 96px;
padding: 16px;
gap: var(--space-xs, 8px);
border-radius: var(--border-radius-bigger, 8px);
background: $success-bar-color;
box-shadow: 0 4px 4px 0 var(--color-shadow-weak, rgba(0, 52, 86, 0.25));
overflow: hidden;
text-overflow: ellipsis;
font-size: $font-size-medium-large;
font-weight: $font-weight-book;
line-height: 27px;
letter-spacing: $letter-spacing;
font-family: $font-family;
align-items: center;

&.show {
animation: slideDownFromTopCenter 0.5s $slide-up-down-animation;
transform: translateY(calc(-100% - 78px)) translateX(-50%);
}

&.slide-out {
animation: slideUpToTopCenter 1s ease-in-out forwards;
transform: translateY(0) translateX(-50%);
}

.icon-container {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
align-self: center;

.icon {
width: 24px;
height: 24px;
color: #fff;
}
}

.close-button {
width: 32px;
height: 32px;
margin-left: auto;

.icon {
width: 32px;
height: 32px;
color: #fff;
}
}
}

.offcanvas {
width: 400px;
height: 100%;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import PropTypes from 'prop-types';
import React, { useState, useEffect, useRef } from 'react';
import cx from 'classnames';
import { FormattedMessage, intlShape } from 'react-intl';
import { isKeyboardSelectionEvent } from '../../../util/browser';
import { saveRoutingSettings } from '../../../action/SearchSettingsActions';
import {
getDefaultSettings,
Expand All @@ -12,28 +10,29 @@ import {
import { configShape } from '../../../util/shapes';
import { getCustomizedSettings } from '../../../store/localStorage';
import Icon from '../../Icon';
import Snackbar from '../../Snackbar';

const RestoreDefaultSettingSection = ({ config }, { executeAction, intl }) => {
const [showSnackbar, setShowSnackbar] = useState(null);
const [slideOutRestoreSettingsButton, setSlideOutRestoreSettingsButton] =
useState(null);
const [snackBarLiveRegionMessage, setSnackBarLiveRegionMessage] =
const [snackbarLiveRegionMessage, setSnackBarLiveRegionMessage] =
useState('');
const snackBarLiveRegionRef = useRef(null);
const [restoreButtonLiveRegionMessage, setRestoreButtonLiveRegionMessage] =
useState('');
const restoreButtonLiveRegionRef = useRef(null);
const hasBeenShownButtonRef = useRef(false);
const userHasCustomizedSettings = hasCustomizedSettings(config);
const snackBarTimeout = useRef(null);
const snackbarTimeout = useRef(null);

useEffect(() => {
return () => {
clearTimeout(snackBarTimeout.current);
clearTimeout(snackbarTimeout.current);
};
}, []);

useEffect(() => {
if (userHasCustomizedSettings) {
hasBeenShownButtonRef.current = true;
setRestoreButtonLiveRegionMessage(
intl.formatMessage({
id: 'settings-changed-by-you',
Expand All @@ -46,10 +45,7 @@ const RestoreDefaultSettingSection = ({ config }, { executeAction, intl }) => {
);
return () => clearTimeout(liveRegionTimeoutId);
}
if (
userHasCustomizedSettings === false &&
slideOutRestoreSettingsButton !== null
) {
if (userHasCustomizedSettings === false && hasBeenShownButtonRef.current) {
setSlideOutRestoreSettingsButton(true);
const timeoutId = setTimeout(() => {
setSlideOutRestoreSettingsButton(false);
Expand Down Expand Up @@ -81,12 +77,18 @@ const RestoreDefaultSettingSection = ({ config }, { executeAction, intl }) => {
defaultMessage: 'Settings restored to default.',
}),
);
snackBarTimeout.current = setTimeout(() => {
snackbarTimeout.current = setTimeout(() => {
setSnackBarLiveRegionMessage('');
setShowSnackbar(false);
}, 4000);
};

const handleSnackbarClose = () => {
clearTimeout(snackbarTimeout.current);
setSnackBarLiveRegionMessage('');
setShowSnackbar(false);
};

const noChangesSRContainer = (
<span className="sr-only" aria-live="polite" role="status">
<FormattedMessage
Expand All @@ -98,48 +100,14 @@ const RestoreDefaultSettingSection = ({ config }, { executeAction, intl }) => {

return (
<>
<div
className={cx('restore-settings-success-snackbar', {
hide: showSnackbar === null,
show: showSnackbar === true,
'slide-out': showSnackbar === false,
})}
aria-hidden="true"
>
<Icon img="icon_checkmark-circled" omitViewBox />
<span className="snackbar-text">
<FormattedMessage
id="restore-default-settings-success"
defaultMessage="Settings restored to default."
/>
</span>
<button
type="button"
className="close-button"
aria-label={intl.formatMessage({
id: 'close',
defaultMessage: 'Close notification',
})}
onClick={() => setShowSnackbar(false)}
tabIndex="-1"
>
<Icon id="close-icon" img="notification-close" omitViewBox />
</button>
</div>
<div
className="sr-only"
aria-live="polite"
role="status"
ref={snackBarLiveRegionRef}
>
{snackBarLiveRegionMessage}
</div>
<div
className="sr-only"
aria-live="polite"
role="status"
ref={restoreButtonLiveRegionRef}
>
<Snackbar
show={showSnackbar}
messageId="restore-default-settings-success"
defaultMessage="Settings restored to default."
liveRegionMessage={snackbarLiveRegionMessage}
onClose={handleSnackbarClose}
/>
<div className="sr-only" aria-live="polite" role="status">
{restoreButtonLiveRegionMessage}
</div>
{userHasCustomizedSettings || slideOutRestoreSettingsButton ? (
Expand All @@ -159,11 +127,7 @@ const RestoreDefaultSettingSection = ({ config }, { executeAction, intl }) => {
/>
<button
type="button"
tabIndex="0"
onClick={restoreDefaultSettings}
onKeyPress={e =>
isKeyboardSelectionEvent(e) && restoreDefaultSettings()
}
className="noborder cursor-pointer restore-settings-button"
aria-label={intl.formatMessage({
id: 'restore-default-settings-aria-label',
Expand Down
80 changes: 80 additions & 0 deletions app/component/snackbar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
.mobile {
.snackbar {
width: calc(100% - 32px);
left: 16px;
top: unset;
bottom: 16px;

&.show {
animation: slideUpFromBottomWithMargin 0.5s $slide-up-down-animation;
transform: translateY(calc(100% + 16px));
}

&.slide-out {
animation: slideDownWithMargin 1s ease-in-out forwards;
transform: translateY(0);
}
}
}

.snackbar {
position: fixed;
left: 50%;
top: 78px;
z-index: 9999;
color: #fff;
display: flex;
width: 480px;
min-width: 320px;
max-height: 96px;
padding: 16px;
gap: var(--space-xs, 8px);
border-radius: var(--border-radius-bigger, 8px);
background: $success-bar-color;
box-shadow: 0 4px 4px 0 var(--color-shadow-weak, rgba(0, 52, 86, 0.25));
overflow: hidden;
text-overflow: ellipsis;
font-size: $font-size-medium-large;
font-weight: $font-weight-book;
line-height: 27px;
letter-spacing: $letter-spacing;
font-family: $font-family;
align-items: center;

&.show {
animation: slideDownFromTopCenter 0.5s $slide-up-down-animation;
transform: translateY(calc(-100% - 78px)) translateX(-50%);
}

&.slide-out {
animation: slideUpToTopCenter 1s ease-in-out forwards;
transform: translateY(0) translateX(-50%);
}

.icon-container {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
align-self: center;

.icon {
width: 24px;
height: 24px;
color: #fff;
}
}

.close-button {
width: 32px;
height: 32px;
margin-left: auto;

.icon {
width: 32px;
height: 32px;
color: #fff;
}
}
}
Loading