Skip to content

Commit 807bcf3

Browse files
authored
Merge pull request #5840 from HSLdevcom/AB#595
AB:595: HSL crisis banner
2 parents e98b314 + ab3f408 commit 807bcf3

9 files changed

Lines changed: 391 additions & 17 deletions

File tree

app/component/AppBarContainer.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import withBreakpoint from '../util/withBreakpoint';
77
import { favouriteShape, userShape } from '../util/shapes';
88
import AppBar from './AppBar';
99
import AppBarHsl from './AppBarHsl';
10+
import CrisisBannerHsl from './CrisisBannerHsl';
1011
import MessageBar from './MessageBar';
1112

1213
const AppBarContainer = (
@@ -27,6 +28,7 @@ const AppBarContainer = (
2728
</a>
2829
{style === 'hsl' ? (
2930
<div className="hsl-header-container" style={{ display: 'block' }}>
31+
<CrisisBannerHsl />
3032
<AppBarHsl user={user} lang={lang} favourites={favourites} />
3133
<MessageBar breakpoint={breakpoint} />
3234
</div>

app/component/CrisisBannerHsl.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React, { useState, useEffect } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { useIntl } from 'react-intl';
4+
import { classList } from '@hsl-fi/utilities';
5+
import { Alert } from '@hsl-fi/icons';
6+
import { CrisisPriority } from '@hsl-fi/content-delivery-api-types';
7+
import { useConfigContext } from '../configurations/ConfigContext';
8+
import { getJson } from '../util/xhrPromise';
9+
import './crisis-banner-hsl.scss';
10+
11+
const CrisisBannerHsl = ({ initialBanners = null }) => {
12+
const config = useConfigContext();
13+
const { locale } = useIntl();
14+
const [banners, setBanners] = useState(() => {
15+
if (initialBanners) {
16+
return initialBanners;
17+
}
18+
if (config.showStaticCrisisBanners) {
19+
return config.staticCrisisBanners || [];
20+
}
21+
return [];
22+
});
23+
24+
useEffect(() => {
25+
if (
26+
initialBanners ||
27+
config.showStaticCrisisBanners ||
28+
!config.URL.BANNERS
29+
) {
30+
return;
31+
}
32+
getJson(`${config.URL.BANNERS}&language=${locale}`)
33+
.then(data => setBanners(data))
34+
.catch(() => setBanners([]));
35+
}, []);
36+
37+
if (!banners.length) {
38+
return null;
39+
}
40+
41+
return (
42+
<div className="crisis-banners">
43+
{banners.map(({ body, priority }) => (
44+
<div
45+
key={body}
46+
className={classList(
47+
'crisis-banners-banner',
48+
priority === CrisisPriority.Primary
49+
? 'crisis-banners-banner-primary'
50+
: 'crisis-banners-banner-secondary',
51+
)}
52+
>
53+
{priority === CrisisPriority.Primary && (
54+
<div className="crisis-banners-banner-primary-icon">
55+
<Alert width="19" fill="#ffffff" />
56+
</div>
57+
)}
58+
{/* eslint-disable-next-line react/no-danger */}
59+
<div dangerouslySetInnerHTML={{ __html: body }} />
60+
</div>
61+
))}
62+
</div>
63+
);
64+
};
65+
66+
CrisisBannerHsl.propTypes = {
67+
initialBanners: PropTypes.arrayOf(
68+
PropTypes.shape({
69+
body: PropTypes.string,
70+
priority: PropTypes.string,
71+
}),
72+
),
73+
};
74+
75+
export default CrisisBannerHsl;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
@import '~@hsl-fi/sass/fonts';
2+
@import '~@hsl-fi/sass/mixins/text';
3+
4+
$banner-major-level-color: #dc0451;
5+
$banner-neutral-level-color: #333;
6+
7+
.crisis-banners {
8+
position: relative;
9+
10+
.crisis-banners-banner {
11+
min-height: 40px;
12+
padding: 5px 15px;
13+
display: flex;
14+
justify-content: center;
15+
align-items: center;
16+
17+
@include text-typography(small);
18+
19+
font-family: $font-stack;
20+
font-weight: 500;
21+
color: #fff;
22+
margin: 0;
23+
24+
a,
25+
p {
26+
@include text-typography(small);
27+
28+
font-family: $font-stack;
29+
font-weight: 500;
30+
color: #fff;
31+
margin: 0;
32+
}
33+
34+
&.crisis-banners-banner-primary {
35+
background-color: $banner-major-level-color;
36+
}
37+
38+
&.crisis-banners-banner-secondary {
39+
background-color: $banner-neutral-level-color;
40+
41+
*:focus {
42+
outline-color: white;
43+
}
44+
}
45+
}
46+
47+
.crisis-banners-banner-primary-icon {
48+
margin-top: 3px;
49+
margin-right: 10px;
50+
51+
svg {
52+
width: 19px;
53+
}
54+
}
55+
56+
.crisis-banners-banner-primary + .crisis-banners-banner-primary,
57+
.crisis-banners-banner-secondary + .crisis-banners-banner-secondary {
58+
border-top: 1px solid #fff;
59+
}
60+
}

app/configurations/config.hsl.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,4 +757,15 @@ export default {
757757
showRouteDescNotification: IS_DEV,
758758
personalization: false,
759759
showNewRoutePage: true,
760+
staticCrisisBanners: [
761+
{
762+
body: 'Dummy crisis alert — primary',
763+
priority: 'Primary',
764+
},
765+
{
766+
body: 'Dummy crisis alert — secondary',
767+
priority: 'Secondary',
768+
},
769+
],
770+
showStaticCrisisBanners: false,
760771
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@
227227
"@rollup/plugin-commonjs": "^28.0.8",
228228
"@rollup/plugin-json": "4.1.0",
229229
"@rollup/plugin-node-resolve": "^16.0.3",
230+
"@testing-library/react": "^12",
230231
"@testing-library/react-hooks": "^8.0.1",
231232
"async": "^3.2.6",
232233
"autoprefixer": "^10.4.21",

test/unit/CrisisBannerHsl.test.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react';
3+
import { IntlProvider } from 'react-intl';
4+
import { expect } from 'chai';
5+
import { describe, it } from 'mocha';
6+
7+
import { ConfigProvider } from '../../app/configurations/ConfigContext';
8+
import CrisisBannerHsl from '../../app/component/CrisisBannerHsl';
9+
10+
const primaryBanner = { body: 'Primary alert', priority: 'Primary' };
11+
const secondaryBanner = { body: 'Secondary alert', priority: 'Secondary' };
12+
13+
const baseConfig = {
14+
CONFIG: 'hsl',
15+
URL: { BANNERS: null },
16+
};
17+
18+
const renderWithBanners = (banners = []) => {
19+
const { container } = render(
20+
<IntlProvider locale="fi" messages={{}}>
21+
<ConfigProvider value={baseConfig}>
22+
<CrisisBannerHsl initialBanners={banners} />
23+
</ConfigProvider>
24+
</IntlProvider>,
25+
);
26+
return container;
27+
};
28+
29+
describe('<CrisisBannerHsl />', () => {
30+
it('renders nothing when no banners are provided', () => {
31+
const container = renderWithBanners([]);
32+
expect(container.firstChild).to.equal(null);
33+
});
34+
35+
it('renders the correct number of banners', () => {
36+
const container = renderWithBanners([primaryBanner, secondaryBanner]);
37+
expect(
38+
container.querySelectorAll('.crisis-banners-banner'),
39+
).to.have.lengthOf(2);
40+
});
41+
42+
it('renders primary banner with the alert icon', () => {
43+
const container = renderWithBanners([primaryBanner]);
44+
expect(
45+
container.querySelector('.crisis-banners-banner-primary'),
46+
).to.not.equal(null);
47+
expect(
48+
container.querySelector('.crisis-banners-banner-primary-icon'),
49+
).to.not.equal(null);
50+
});
51+
52+
it('renders secondary banner without the alert icon', () => {
53+
const container = renderWithBanners([secondaryBanner]);
54+
expect(
55+
container.querySelector('.crisis-banners-banner-secondary'),
56+
).to.not.equal(null);
57+
expect(
58+
container.querySelector('.crisis-banners-banner-primary-icon'),
59+
).to.equal(null);
60+
});
61+
62+
it('renders banner body as HTML', () => {
63+
const container = renderWithBanners([
64+
{ body: '<b>Alert!</b>', priority: 'Secondary' },
65+
]);
66+
expect(container.querySelector('b')).to.not.equal(null);
67+
expect(container.querySelector('b').textContent).to.equal('Alert!');
68+
});
69+
});

test/unit/helpers/babel-register.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ require('@babel/register')({
77
],
88
});
99

10-
// Prevent Node.js from trying to parse CSS files as JavaScript
10+
// Prevent Node.js from trying to parse CSS/SCSS files as JavaScript
1111
require.extensions['.css'] = () => {};
12+
require.extensions['.scss'] = () => {};
1213

1314
// @hsl-fi/* packages are ESM-only ("type": "module") and cannot be require()'d
1415
// in the CommonJS test environment. Node throws ERR_REQUIRE_ESM before Babel can
@@ -48,8 +49,13 @@ Module._load = function interceptEsmPackages(request, ...args) {
4849
},
4950
);
5051
}
51-
// Fallback: stub any other @hsl-fi/* package generically
52-
if (request.startsWith('@hsl-fi/')) {
52+
// Fallback: stub any other @hsl-fi/* package generically.
53+
// Exceptions: CJS packages that can be loaded normally.
54+
if (
55+
request.startsWith('@hsl-fi/') &&
56+
request !== '@hsl-fi/utilities' &&
57+
request !== '@hsl-fi/content-delivery-api-types'
58+
) {
5359
return new Proxy(
5460
function StubComponent() {
5561
return null;

test/unit/helpers/init.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { JSDOM } from 'jsdom';
88
import { after, afterEach, before } from 'mocha';
99
import { stub } from 'sinon';
1010
import { Settings } from 'luxon';
11+
import { cleanup } from '@testing-library/react';
1112
import { initAnalyticsClientSide } from '../../../app/util/analyticsUtils';
1213
import {
1314
restoreOwnedIntlStub,
@@ -99,6 +100,7 @@ after('resetting the environment', () => {
99100

100101
// make sure the local and session storage stays clear for each test
101102
afterEach(() => {
103+
cleanup();
102104
restoreOwnedIntlStub();
103105
restoreOwnedContextStubs();
104106
window.localStorage.clear();

0 commit comments

Comments
 (0)