@times-components/ts-components 1.18.3 → 1.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,14 +7,16 @@ exports[`LiveArticleFlag should render the base live article flag 1`] = `
7
7
  class="sc-bwzfXH WFEid"
8
8
  >
9
9
  <div
10
- class="sc-bxivhb hmIbmT"
10
+ class="sc-bxivhb fnsGfe"
11
11
  >
12
-
12
+
13
13
  </div>
14
- <div
15
- class="sc-EHOje jboioV"
16
- >
17
- BASE
14
+ <div>
15
+ <span
16
+ class="sc-EHOje jboioV"
17
+ >
18
+ BASE
19
+ </span>
18
20
  </div>
19
21
  </div>
20
22
  </div>
@@ -28,14 +30,16 @@ exports[`LiveArticleFlag should render the breaking article flag 1`] = `
28
30
  class="sc-bwzfXH WFEid"
29
31
  >
30
32
  <div
31
- class="sc-bxivhb hmIbmT"
33
+ class="sc-bxivhb fnsGfe"
32
34
  >
33
-
35
+
34
36
  </div>
35
- <div
36
- class="sc-EHOje jboioV"
37
- >
38
- BREAKING
37
+ <div>
38
+ <span
39
+ class="sc-EHOje jboioV"
40
+ >
41
+ BREAKING
42
+ </span>
39
43
  </div>
40
44
  </div>
41
45
  </div>
@@ -49,14 +53,16 @@ exports[`LiveArticleFlag should render the live article flag 1`] = `
49
53
  class="sc-bwzfXH WFEid"
50
54
  >
51
55
  <div
52
- class="sc-bxivhb hmIbmT"
56
+ class="sc-bxivhb fnsGfe"
53
57
  >
54
-
58
+
55
59
  </div>
56
- <div
57
- class="sc-EHOje jboioV"
58
- >
59
- LIVE
60
+ <div>
61
+ <span
62
+ class="sc-EHOje jboioV"
63
+ >
64
+ LIVE
65
+ </span>
60
66
  </div>
61
67
  </div>
62
68
  </div>
@@ -23,10 +23,10 @@ export const ArticleFlagBullet = styled.div`
23
23
  background-color: ${({ color }) => gqlRgbaToStyle(color) || color};
24
24
  `;
25
25
 
26
- export const LiveDiamondContainer = styled.div`
27
- margin-right: 4px;
26
+ export const LiveIconContainer = styled.div`
27
+ margin-right: 8px;
28
28
  color: #ffffff;
29
- line-height: 16px;
29
+ align-self: self-start;
30
30
  `;
31
31
 
32
32
  export const ArticleFlagTextContainer = styled.div`
@@ -38,7 +38,8 @@ export const ArticleFlagTextContainer = styled.div`
38
38
  margin-left: 5px;
39
39
  color: ${({ color }) => gqlRgbaToStyle(color) || color};
40
40
  `;
41
- export const LiveArticleFlagTextContainer = styled.div`
41
+
42
+ export const LiveArticleFlagText = styled.span`
42
43
  font-family: ${fonts.supporting};
43
44
  color: #ffffff;
44
45
  font-weight: 500;
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import { storiesOf } from '@storybook/react';
3
+ import { date, select, text } from '@storybook/addon-knobs';
4
+
5
+ import { ArticleHarness } from '../../fixtures/article-harness/ArticleHarness';
6
+ import ArticleHeader from './ArticleHeader';
7
+
8
+ storiesOf('Typescript Component/Article Header', module)
9
+ .addDecorator((storyFn: () => React.ReactNode) => (
10
+ <ArticleHarness>{storyFn()}</ArticleHarness>
11
+ ))
12
+ .add('Article Header with headline', () => {
13
+ const label = 'Updated Date/Time';
14
+ const defaultValue = new Date();
15
+ const groupId = 'Options';
16
+ const value = date(label, defaultValue, groupId);
17
+ const breakingOptions = {
18
+ True: 'true',
19
+ False: undefined
20
+ };
21
+ const updated = new Date(value).toISOString();
22
+
23
+ const headline = text('Headline', 'This is the headline', groupId);
24
+
25
+ return (
26
+ <ArticleHeader
27
+ updated={updated}
28
+ breaking={select('Breaking', breakingOptions, undefined, groupId)}
29
+ headline={encodeURIComponent(headline)}
30
+ />
31
+ );
32
+ })
33
+ .add('Article Header without headline', () => {
34
+ const label = 'Updated Date/Time';
35
+ const defaultValue = new Date();
36
+ const groupId = 'Options';
37
+ const value = date(label, defaultValue, groupId);
38
+ const breakingOptions = {
39
+ True: 'true',
40
+ False: undefined
41
+ };
42
+ const updated = new Date(value).toISOString();
43
+
44
+ return (
45
+ <ArticleHeader
46
+ updated={updated}
47
+ breaking={select('Breaking', breakingOptions, undefined, groupId)}
48
+ />
49
+ );
50
+ });
@@ -0,0 +1,83 @@
1
+ import React from 'react';
2
+ import {
3
+ format,
4
+ differenceInSeconds,
5
+ differenceInCalendarDays,
6
+ formatDistanceStrict
7
+ } from 'date-fns';
8
+
9
+ import {
10
+ Container,
11
+ Divider,
12
+ Headline,
13
+ TimeSincePublishing,
14
+ TimeSincePublishingContainer,
15
+ UpdatedDate,
16
+ UpdatedTime,
17
+ UpdatedTimeItems,
18
+ UpdatesContainer,
19
+ FlagContainer
20
+ } from './styles';
21
+ import { BreakingArticleFlag } from '../article-flag/LiveArticleFlag';
22
+
23
+ const ArticleHeader: React.FC<{
24
+ updated: string;
25
+ breaking?: string;
26
+ headline?: string;
27
+ }> = ({ updated, breaking, headline }) => {
28
+ const currentDateTime = new Date();
29
+ const updatedDate = new Date(updated);
30
+ const timeSincePublishing =
31
+ formatDistanceStrict(updatedDate, currentDateTime, {
32
+ roundingMethod: 'floor'
33
+ }) + ' ago';
34
+ const diffInSeconds = differenceInSeconds(currentDateTime, updatedDate);
35
+
36
+ const isLessThan1Minute = diffInSeconds < 60;
37
+ const isLessThan1Hour = diffInSeconds < 60 * 60;
38
+ const isLessThan13Hours = diffInSeconds < 60 * 60 * 13;
39
+ const isDaysAgo = differenceInCalendarDays(currentDateTime, updatedDate) >= 1;
40
+
41
+ const isBreaking = breaking
42
+ ? Boolean(breaking.toLowerCase() === 'true')
43
+ : false;
44
+
45
+ return (
46
+ <Container isBreaking={isBreaking && isLessThan1Hour}>
47
+ <UpdatesContainer>
48
+ <UpdatedTimeItems>
49
+ {isBreaking && isLessThan1Hour ? (
50
+ <FlagContainer>
51
+ <BreakingArticleFlag />
52
+ </FlagContainer>
53
+ ) : null}
54
+ {!isLessThan1Minute && isLessThan13Hours ? (
55
+ <TimeSincePublishingContainer>
56
+ <TimeSincePublishing
57
+ isBreaking={isBreaking}
58
+ data-testId="TimeSincePublishing"
59
+ >
60
+ {timeSincePublishing}
61
+ </TimeSincePublishing>
62
+ <Divider />
63
+ </TimeSincePublishingContainer>
64
+ ) : null}
65
+ <UpdatedTime
66
+ isLessThan13Hours={!isLessThan1Minute && isLessThan13Hours}
67
+ data-testId="UpdatedTime"
68
+ >
69
+ {format(updatedDate, 'h.mmaaa')}
70
+ </UpdatedTime>
71
+ </UpdatedTimeItems>
72
+ {isDaysAgo ? (
73
+ <UpdatedDate data-testid="UpdatedDate">
74
+ {format(updatedDate, 'MMMM d yyyy')}
75
+ </UpdatedDate>
76
+ ) : null}
77
+ </UpdatesContainer>
78
+ {headline && <Headline>{decodeURI(headline)}</Headline>}
79
+ </Container>
80
+ );
81
+ };
82
+
83
+ export default ArticleHeader;
@@ -0,0 +1,183 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+
5
+ import ArticleHeader from '../ArticleHeader';
6
+ import MockDate from 'mockdate';
7
+
8
+ describe('ArticleHeader', () => {
9
+ describe('In one calendar day', () => {
10
+ const updated = '2021-12-31T06:30:00Z';
11
+ afterEach(() => MockDate.reset());
12
+ it('Within the first minute of update - Breaking', () => {
13
+ MockDate.set('2021-12-31T06:30:00Z');
14
+ const { baseElement, getByText, queryByTestId } = render(
15
+ <ArticleHeader
16
+ updated={updated}
17
+ breaking="true"
18
+ headline="This%20is%20the%20headline"
19
+ />
20
+ );
21
+ expect(baseElement).toMatchSnapshot();
22
+ expect(getByText('BREAKING')).toBeVisible();
23
+ expect(queryByTestId('UpdatedTime')).toBeTruthy();
24
+ expect(getByText('6.30am')).toBeVisible();
25
+ expect(queryByTestId('TimeSincePublishing')).toBeFalsy();
26
+ expect(queryByTestId('UpdatedDate')).toBeFalsy();
27
+ expect(getByText('This is the headline')).toBeVisible();
28
+ });
29
+ it('Within the first minute of update - Not Breaking', () => {
30
+ MockDate.set('2021-12-31T06:30:00Z');
31
+ const { baseElement, getByText, queryByTestId, queryByText } = render(
32
+ <ArticleHeader
33
+ updated={updated}
34
+ breaking="false"
35
+ headline="This%20is%20the%20headline"
36
+ />
37
+ );
38
+ expect(baseElement).toMatchSnapshot();
39
+ expect(queryByText('BREAKING')).toBeFalsy();
40
+ expect(queryByTestId('UpdatedTime')).toBeTruthy();
41
+ expect(getByText('6.30am')).toBeVisible();
42
+ expect(queryByTestId('TimeSincePublishing')).toBeFalsy();
43
+ expect(queryByTestId('UpdatedDate')).toBeFalsy();
44
+ expect(getByText('This is the headline')).toBeVisible();
45
+ });
46
+ it('Within the first minute of update - No headline not breaking', () => {
47
+ MockDate.set('2021-12-31T06:30:00Z');
48
+ const { baseElement, getByText, queryByTestId, queryByText } = render(
49
+ <ArticleHeader updated={updated} />
50
+ );
51
+ expect(baseElement).toMatchSnapshot();
52
+ expect(queryByText('This is the headline')).toBeFalsy();
53
+ expect(queryByText('BREAKING')).toBeFalsy();
54
+ expect(queryByTestId('UpdatedTime')).toBeTruthy();
55
+ expect(getByText('6.30am')).toBeVisible();
56
+ expect(queryByTestId('TimeSincePublishing')).toBeFalsy();
57
+ expect(queryByTestId('UpdatedDate')).toBeFalsy();
58
+ });
59
+ it('within an hour of updating', () => {
60
+ MockDate.set('2021-12-31T07:00:00Z');
61
+ const { getByText, queryByTestId } = render(
62
+ <ArticleHeader
63
+ updated={updated}
64
+ breaking="true"
65
+ headline="This%20is%20the%20headline"
66
+ />
67
+ );
68
+ expect(getByText('BREAKING')).toBeVisible();
69
+ expect(queryByTestId('UpdatedTime')).toBeTruthy();
70
+ expect(getByText('6.30am')).toBeVisible();
71
+ expect(queryByTestId('TimeSincePublishing')).toBeTruthy();
72
+ expect(getByText('30 minutes ago')).toBeVisible();
73
+ expect(queryByTestId('UpdatedDate')).toBeFalsy();
74
+ expect(getByText('This is the headline')).toBeVisible();
75
+ });
76
+ it('between 1 and 12 hours after update time', () => {
77
+ MockDate.set('2021-12-31T08:30:00Z');
78
+ const { getByText, queryByTestId, queryByText } = render(
79
+ <ArticleHeader
80
+ updated={updated}
81
+ breaking="true"
82
+ headline="This%20is%20the%20headline"
83
+ />
84
+ );
85
+ expect(queryByText('BREAKING')).toBeFalsy();
86
+ expect(queryByTestId('UpdatedTime')).toBeTruthy();
87
+ expect(getByText('6.30am')).toBeVisible();
88
+ expect(queryByTestId('TimeSincePublishing')).toBeTruthy();
89
+ expect(getByText('2 hours ago')).toBeVisible();
90
+ expect(queryByTestId('UpdatedDate')).toBeFalsy();
91
+ expect(getByText('This is the headline')).toBeVisible();
92
+ });
93
+ it('after 12 hours but on the same calendar day', () => {
94
+ MockDate.set('2021-12-31T19:30:00Z');
95
+ const { getByText, queryByTestId, queryByText } = render(
96
+ <ArticleHeader
97
+ updated={updated}
98
+ breaking="true"
99
+ headline="This%20is%20the%20headline"
100
+ />
101
+ );
102
+ expect(queryByText('BREAKING')).toBeFalsy();
103
+ expect(queryByTestId('UpdatedTime')).toBeTruthy();
104
+ expect(getByText('6.30am')).toBeVisible();
105
+ expect(queryByTestId('TimeSincePublishing')).toBeFalsy();
106
+ expect(queryByTestId('UpdatedDate')).toBeFalsy();
107
+ expect(getByText('This is the headline')).toBeVisible();
108
+ });
109
+ });
110
+ describe('Across calendar days', () => {
111
+ const updated = '2021-12-31T23:30:00Z';
112
+ afterEach(() => MockDate.reset());
113
+ it('within an hour of updating but on a different calendar day', () => {
114
+ MockDate.set('2022-01-01T00:29:00Z');
115
+ const { getByTestId, queryByTestId, getByText } = render(
116
+ <ArticleHeader
117
+ updated={updated}
118
+ breaking="true"
119
+ headline="This%20is%20the%20headline"
120
+ />
121
+ );
122
+ expect(getByText('BREAKING')).toBeVisible();
123
+ expect(queryByTestId('UpdatedTime')).toBeTruthy();
124
+ expect(getByText('11.30pm')).toBeVisible();
125
+ expect(getByTestId('TimeSincePublishing')).toBeTruthy();
126
+ expect(getByTestId('UpdatedDate')).toBeVisible();
127
+ expect(getByText('December 31 2021')).toBeVisible();
128
+ expect(getByText('This is the headline')).toBeVisible();
129
+ });
130
+ it('between 1-12 hours of updating but on a different calendar day', () => {
131
+ MockDate.set('2022-01-01T02:00:00Z');
132
+ const { getByTestId, queryByText, queryByTestId, getByText } = render(
133
+ <ArticleHeader
134
+ updated={updated}
135
+ breaking="true"
136
+ headline="This%20is%20the%20headline"
137
+ />
138
+ );
139
+ expect(queryByText('BREAKING')).toBeFalsy();
140
+ expect(queryByTestId('UpdatedTime')).toBeTruthy();
141
+ expect(getByText('11.30pm')).toBeVisible();
142
+ expect(getByTestId('TimeSincePublishing')).toBeTruthy();
143
+ expect(getByText('2 hours ago')).toBeVisible();
144
+ expect(getByTestId('UpdatedDate')).toBeVisible();
145
+ expect(getByText('December 31 2021')).toBeVisible();
146
+ expect(getByText('This is the headline')).toBeVisible();
147
+ });
148
+ it('after 12 hours but on a different calendar day', () => {
149
+ MockDate.set('2022-01-01T14:30:00Z');
150
+ const { getByTestId, queryByText, queryByTestId, getByText } = render(
151
+ <ArticleHeader
152
+ updated={updated}
153
+ breaking="true"
154
+ headline="This%20is%20the%20headline"
155
+ />
156
+ );
157
+ expect(queryByText('BREAKING')).toBeFalsy();
158
+ expect(queryByTestId('UpdatedTime')).toBeTruthy();
159
+ expect(getByText('11.30pm')).toBeVisible();
160
+ expect(queryByTestId('TimeSincePublishing')).toBeFalsy();
161
+ expect(getByTestId('UpdatedDate')).toBeVisible();
162
+ expect(getByText('December 31 2021')).toBeVisible();
163
+ expect(getByText('This is the headline')).toBeVisible();
164
+ });
165
+ it('after multiple calendar days', () => {
166
+ MockDate.set('2022-01-03T14:30:00Z');
167
+ const { getByTestId, queryByText, queryByTestId, getByText } = render(
168
+ <ArticleHeader
169
+ updated={updated}
170
+ breaking="true"
171
+ headline="This%20is%20the%20headline"
172
+ />
173
+ );
174
+ expect(queryByText('BREAKING')).toBeFalsy();
175
+ expect(queryByTestId('UpdatedTime')).toBeTruthy();
176
+ expect(getByText('11.30pm')).toBeVisible();
177
+ expect(queryByTestId('TimeSincePublishing')).toBeFalsy();
178
+ expect(getByTestId('UpdatedDate')).toBeVisible();
179
+ expect(getByText('December 31 2021')).toBeVisible();
180
+ expect(getByText('This is the headline')).toBeVisible();
181
+ });
182
+ });
183
+ });
@@ -0,0 +1,106 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`ArticleHeader In one calendar day Within the first minute of update - Breaking 1`] = `
4
+ <body>
5
+ <div>
6
+ <div
7
+ class="sc-bdVaJa cQBRMp"
8
+ >
9
+ <div
10
+ class="sc-bwzfXH htoLMg"
11
+ >
12
+ <div
13
+ class="sc-ifAKCX iDsMnF"
14
+ >
15
+ <div
16
+ class="sc-dnqmqq bGasAc"
17
+ >
18
+ <div
19
+ class="sc-gZMcBi bcfOTp"
20
+ >
21
+ <div
22
+ class="sc-VigVT bvOrBW"
23
+ >
24
+
25
+ </div>
26
+ <div>
27
+ <span
28
+ class="sc-fjdhpX cvtGYv"
29
+ >
30
+ BREAKING
31
+ </span>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ <div
36
+ class="sc-EHOje cDoFHw"
37
+ data-testid="UpdatedTime"
38
+ >
39
+ 6.30am
40
+ </div>
41
+ </div>
42
+ </div>
43
+ <h2
44
+ class="sc-htoDjs ccbbfo"
45
+ >
46
+ This is the headline
47
+ </h2>
48
+ </div>
49
+ </div>
50
+ </body>
51
+ `;
52
+
53
+ exports[`ArticleHeader In one calendar day Within the first minute of update - No headline not breaking 1`] = `
54
+ <body>
55
+ <div>
56
+ <div
57
+ class="sc-bdVaJa fLcQHz"
58
+ >
59
+ <div
60
+ class="sc-bwzfXH htoLMg"
61
+ >
62
+ <div
63
+ class="sc-ifAKCX iDsMnF"
64
+ >
65
+ <div
66
+ class="sc-EHOje cDoFHw"
67
+ data-testid="UpdatedTime"
68
+ >
69
+ 6.30am
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </body>
76
+ `;
77
+
78
+ exports[`ArticleHeader In one calendar day Within the first minute of update - Not Breaking 1`] = `
79
+ <body>
80
+ <div>
81
+ <div
82
+ class="sc-bdVaJa fLcQHz"
83
+ >
84
+ <div
85
+ class="sc-bwzfXH htoLMg"
86
+ >
87
+ <div
88
+ class="sc-ifAKCX iDsMnF"
89
+ >
90
+ <div
91
+ class="sc-EHOje cDoFHw"
92
+ data-testid="UpdatedTime"
93
+ >
94
+ 6.30am
95
+ </div>
96
+ </div>
97
+ </div>
98
+ <h2
99
+ class="sc-htoDjs ccbbfo"
100
+ >
101
+ This is the headline
102
+ </h2>
103
+ </div>
104
+ </div>
105
+ </body>
106
+ `;
@@ -0,0 +1,88 @@
1
+ import styled, { css } from 'styled-components';
2
+ import { breakpoints, colours, fonts } from '@times-components/styleguide';
3
+
4
+ export const Container = styled.div<{ isBreaking: boolean }>`
5
+ display: flex;
6
+ flex-direction: column;
7
+ justify-content: center;
8
+ margin: 48px 10px 24px 10px;
9
+ padding-top: ${({ isBreaking }) => (isBreaking ? '8px' : '5px')};
10
+ border-top: 2px solid #9f0000;
11
+
12
+ @media (min-width: ${breakpoints.medium}px) {
13
+ width: 80.8%;
14
+ margin: 64px 0 24px 10%;
15
+ }
16
+
17
+ @media (min-width: ${breakpoints.wide}px) {
18
+ width: 56.2%;
19
+ margin: 64px 0 24px 22%;
20
+ }
21
+ `;
22
+
23
+ export const UpdatesContainer = styled.div`
24
+ display: flex;
25
+ flex-direction: row;
26
+ justify-content: space-between;
27
+ `;
28
+
29
+ export const TimeSincePublishingContainer = styled.div`
30
+ display: flex;
31
+ flex-direction: row;
32
+ `;
33
+
34
+ export const TimeSincePublishing = styled.div<{ isBreaking?: boolean }>`
35
+ color: ${colours.functional.brandColour};
36
+ font-family: ${fonts.supporting};
37
+ font-size: 14px;
38
+ line-height: 18px;
39
+ `;
40
+
41
+ const updatedStyle = css`
42
+ color: ${colours.functional.secondary};
43
+ font-family: ${fonts.supporting};
44
+ font-size: 14px;
45
+ line-height: 18px;
46
+ `;
47
+
48
+ export const UpdatedTimeItems = styled.div`
49
+ display: flex;
50
+ justify-content: space-between;
51
+ align-items: baseline;
52
+ `;
53
+
54
+ export const UpdatedTime = styled.div<{ isLessThan13Hours?: boolean }>`
55
+ ${updatedStyle};
56
+ `;
57
+
58
+ export const UpdatedDate = styled.div`
59
+ ${updatedStyle} justify-content: end;
60
+ padding: 0 4px 0 0;
61
+ `;
62
+
63
+ export const Divider = styled.div`
64
+ background-color: ${colours.functional.greyLabel};
65
+ width: 1px;
66
+ margin: 2px 8px 6px 12px;
67
+ `;
68
+
69
+ export const Headline = styled.h2`
70
+ color: ${colours.functional.brandColour};
71
+ font-family: ${fonts.headline};
72
+ font-size: 28px;
73
+ line-height: 28px;
74
+ margin-top: 22px;
75
+ margin-bottom: 0px;
76
+ font-weight: 400;
77
+
78
+ @media (min-width: ${breakpoints.medium}px) {
79
+ font-size: 36px;
80
+ line-height: 36px;
81
+ margin-top: 20px;
82
+ margin-bottom: 0px;
83
+ }
84
+ `;
85
+
86
+ export const FlagContainer = styled.div`
87
+ margin-right: 8px;
88
+ `;
package/src/index.ts CHANGED
@@ -83,3 +83,7 @@ export { HiddenDiv } from './components/common-styles';
83
83
  export { InlineMessage } from './components/inline-message/InlineMessage';
84
84
 
85
85
  export { InlineDialog } from './components/inline-dialog/InlineDialog';
86
+
87
+ export {
88
+ default as ArticleHeader
89
+ } from './components/article-header/ArticleHeader';