superdesk-ui-framework 5.0.2 → 5.0.3

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.
package/.mocharc.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "extension": ["ts", ".tsx"],
3
- "spec": "app-typescript/components/*.spec.*",
3
+ "spec": "app-typescript/**/*.spec.*",
4
4
  "require": ["ts-node/register", "./mocha-setup.ts"]
5
5
  }
@@ -0,0 +1,163 @@
1
+ import {describe, it} from 'mocha';
2
+ import * as assert from 'assert';
3
+ import {convert12HourTo24Hour, convert24HourTo12Hour, toInternalState} from './TimePickerPopover';
4
+
5
+ it('should convert all 12-hour times to 24-hour format', () => {
6
+ // AM times
7
+ assert.strictEqual(convert12HourTo24Hour(12, 'am'), 0);
8
+ assert.strictEqual(convert12HourTo24Hour(1, 'am'), 1);
9
+ assert.strictEqual(convert12HourTo24Hour(2, 'am'), 2);
10
+ assert.strictEqual(convert12HourTo24Hour(3, 'am'), 3);
11
+ assert.strictEqual(convert12HourTo24Hour(4, 'am'), 4);
12
+ assert.strictEqual(convert12HourTo24Hour(5, 'am'), 5);
13
+ assert.strictEqual(convert12HourTo24Hour(6, 'am'), 6);
14
+ assert.strictEqual(convert12HourTo24Hour(7, 'am'), 7);
15
+ assert.strictEqual(convert12HourTo24Hour(8, 'am'), 8);
16
+ assert.strictEqual(convert12HourTo24Hour(9, 'am'), 9);
17
+ assert.strictEqual(convert12HourTo24Hour(10, 'am'), 10);
18
+ assert.strictEqual(convert12HourTo24Hour(11, 'am'), 11);
19
+
20
+ // PM times
21
+ assert.strictEqual(convert12HourTo24Hour(12, 'pm'), 12);
22
+ assert.strictEqual(convert12HourTo24Hour(1, 'pm'), 13);
23
+ assert.strictEqual(convert12HourTo24Hour(2, 'pm'), 14);
24
+ assert.strictEqual(convert12HourTo24Hour(3, 'pm'), 15);
25
+ assert.strictEqual(convert12HourTo24Hour(4, 'pm'), 16);
26
+ assert.strictEqual(convert12HourTo24Hour(5, 'pm'), 17);
27
+ assert.strictEqual(convert12HourTo24Hour(6, 'pm'), 18);
28
+ assert.strictEqual(convert12HourTo24Hour(7, 'pm'), 19);
29
+ assert.strictEqual(convert12HourTo24Hour(8, 'pm'), 20);
30
+ assert.strictEqual(convert12HourTo24Hour(9, 'pm'), 21);
31
+ assert.strictEqual(convert12HourTo24Hour(10, 'pm'), 22);
32
+ assert.strictEqual(convert12HourTo24Hour(11, 'pm'), 23);
33
+ });
34
+
35
+ it('should convert all 24-hour times to 12-hour format', () => {
36
+ assert.strictEqual(convert24HourTo12Hour(0), 12);
37
+ assert.strictEqual(convert24HourTo12Hour(1), 1);
38
+ assert.strictEqual(convert24HourTo12Hour(2), 2);
39
+ assert.strictEqual(convert24HourTo12Hour(3), 3);
40
+ assert.strictEqual(convert24HourTo12Hour(4), 4);
41
+ assert.strictEqual(convert24HourTo12Hour(5), 5);
42
+ assert.strictEqual(convert24HourTo12Hour(6), 6);
43
+ assert.strictEqual(convert24HourTo12Hour(7), 7);
44
+ assert.strictEqual(convert24HourTo12Hour(8), 8);
45
+ assert.strictEqual(convert24HourTo12Hour(9), 9);
46
+ assert.strictEqual(convert24HourTo12Hour(10), 10);
47
+ assert.strictEqual(convert24HourTo12Hour(11), 11);
48
+ assert.strictEqual(convert24HourTo12Hour(12), 12);
49
+ assert.strictEqual(convert24HourTo12Hour(13), 1);
50
+ assert.strictEqual(convert24HourTo12Hour(14), 2);
51
+ assert.strictEqual(convert24HourTo12Hour(15), 3);
52
+ assert.strictEqual(convert24HourTo12Hour(16), 4);
53
+ assert.strictEqual(convert24HourTo12Hour(17), 5);
54
+ assert.strictEqual(convert24HourTo12Hour(18), 6);
55
+ assert.strictEqual(convert24HourTo12Hour(19), 7);
56
+ assert.strictEqual(convert24HourTo12Hour(20), 8);
57
+ assert.strictEqual(convert24HourTo12Hour(21), 9);
58
+ assert.strictEqual(convert24HourTo12Hour(22), 10);
59
+ assert.strictEqual(convert24HourTo12Hour(23), 11);
60
+ });
61
+
62
+ describe('toInternalState', () => {
63
+ describe('null/undefined/empty input', () => {
64
+ it('should return null state for null input', () => {
65
+ assert.deepStrictEqual(toInternalState(null), {
66
+ hours: null,
67
+ minutes: null,
68
+ seconds: null,
69
+ period: null,
70
+ });
71
+ });
72
+
73
+ it('should return null state for undefined input', () => {
74
+ assert.deepStrictEqual(toInternalState(undefined), {
75
+ hours: null,
76
+ minutes: null,
77
+ seconds: null,
78
+ period: null,
79
+ });
80
+ });
81
+
82
+ it('should return null state for empty string', () => {
83
+ assert.deepStrictEqual(toInternalState(' '), {
84
+ hours: null,
85
+ minutes: null,
86
+ seconds: null,
87
+ period: null,
88
+ });
89
+ });
90
+ });
91
+
92
+ it('should convert 00:00:00 to 12:00:00 am', () => {
93
+ assert.deepStrictEqual(toInternalState('00:00:00'), {
94
+ hours: '12',
95
+ minutes: '00',
96
+ seconds: '00',
97
+ period: 'am',
98
+ });
99
+ });
100
+
101
+ it('should convert 01:30:45 to 01:30:45 am', () => {
102
+ assert.deepStrictEqual(toInternalState('01:30:45'), {
103
+ hours: '01',
104
+ minutes: '30',
105
+ seconds: '45',
106
+ period: 'am',
107
+ });
108
+ });
109
+
110
+ it('should convert 11:59:59 to 11:59:59 am', () => {
111
+ assert.deepStrictEqual(toInternalState('11:59:59'), {
112
+ hours: '11',
113
+ minutes: '59',
114
+ seconds: '59',
115
+ period: 'am',
116
+ });
117
+ });
118
+
119
+ it('should convert 12:00:00 to 12:00:00 pm', () => {
120
+ assert.deepStrictEqual(toInternalState('12:00:00'), {
121
+ hours: '12',
122
+ minutes: '00',
123
+ seconds: '00',
124
+ period: 'pm',
125
+ });
126
+ });
127
+
128
+ it('should convert 13:30:15 to 01:30:15 pm', () => {
129
+ assert.deepStrictEqual(toInternalState('13:30:15'), {
130
+ hours: '01',
131
+ minutes: '30',
132
+ seconds: '15',
133
+ period: 'pm',
134
+ });
135
+ });
136
+
137
+ it('should convert 23:59:59 to 11:59:59 pm', () => {
138
+ assert.deepStrictEqual(toInternalState('23:59:59'), {
139
+ hours: '11',
140
+ minutes: '59',
141
+ seconds: '59',
142
+ period: 'pm',
143
+ });
144
+ });
145
+
146
+ it('should handle time without seconds and default to 00', () => {
147
+ assert.deepStrictEqual(toInternalState('14:30'), {
148
+ hours: '02',
149
+ minutes: '30',
150
+ seconds: '00',
151
+ period: 'pm',
152
+ });
153
+ });
154
+
155
+ it('should handle time without seconds in morning', () => {
156
+ assert.deepStrictEqual(toInternalState('09:15'), {
157
+ hours: '09',
158
+ minutes: '15',
159
+ seconds: '00',
160
+ period: 'am',
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,286 @@
1
+ import * as React from 'react';
2
+ import {Spacer} from '@sourcefabric/common';
3
+ import {ContentDivider} from '../ContentDivider';
4
+ import {RadioButtonGroup} from '../RadioButtonGroup';
5
+ import {getOptionsForTimeUnit} from '../../utils/time';
6
+ import {TimeValueHolder} from './TimeValueHolder';
7
+
8
+ export function convert12HourTo24Hour(hour: number, period: 'am' | 'pm'): number {
9
+ if (period === 'am' && hour === 12) {
10
+ return 0; // midnight
11
+ }
12
+
13
+ if (period === 'pm' && hour !== 12) {
14
+ return hour + 12; // PM and not 12
15
+ }
16
+
17
+ return hour; // For 12PM, 1-11AM
18
+ }
19
+
20
+ export function convert24HourTo12Hour(hour: number) {
21
+ const remainder = hour % 12;
22
+
23
+ return remainder === 0 ? 12 : remainder;
24
+ }
25
+
26
+ function isAm(hours: number) {
27
+ return hours < 12;
28
+ }
29
+
30
+ export function toInternalState(
31
+ timeStr: string | undefined | null, // will always be in 24h format
32
+ ): IState {
33
+ if (timeStr == null || (timeStr ?? '').trim().length < 1) {
34
+ return {
35
+ hours: null,
36
+ minutes: null,
37
+ seconds: null,
38
+ period: null,
39
+ };
40
+ }
41
+
42
+ const [hours, minutes, seconds] = timeStr.split(':');
43
+ const secondsDefault = hours != null && minutes != null ? '00' : null;
44
+
45
+ return {
46
+ hours: (() => {
47
+ if (hours == null) {
48
+ return null;
49
+ }
50
+
51
+ if (is12HourFormat) {
52
+ return convert24HourTo12Hour(parseInt(hours, 10)).toString().padStart(2, '0');
53
+ } else {
54
+ return hours;
55
+ }
56
+ })(),
57
+ minutes: minutes ?? null,
58
+ seconds: seconds ?? secondsDefault,
59
+ period: hours == null ? null : isAm(parseInt(hours, 10)) ? 'am' : 'pm',
60
+ };
61
+ }
62
+
63
+ const is12HourFormat: boolean = (() => {
64
+ const hour = new Date().toLocaleTimeString([]);
65
+
66
+ return hour.includes('AM') || hour.includes('PM');
67
+ })();
68
+
69
+ interface IProps {
70
+ headerTemplate?: React.ReactNode;
71
+ footerTemplate?: React.ReactNode;
72
+ allowSeconds?: boolean;
73
+ onChange: (nextValue: string) => void;
74
+ value: string | null;
75
+ }
76
+
77
+ // internal state is needed in order to be able to wait for all inputs to be filled before triggering `props.onChange`
78
+ interface IState {
79
+ hours: string | null;
80
+ minutes: string | null;
81
+ seconds: string | null;
82
+ period: 'am' | 'pm' | null;
83
+ }
84
+
85
+ export class TimePickerPopover extends React.PureComponent<IProps, IState> {
86
+ // hour, minutes, seconds
87
+ private inputRefs: Array<React.RefObject<TimeValueHolder>>;
88
+
89
+ constructor(props: IProps) {
90
+ super(props);
91
+
92
+ this.inputRefs = [React.createRef(), React.createRef(), React.createRef()];
93
+ this.handleChange = this.handleChange.bind(this);
94
+ this.scrollToValues = this.scrollToValues.bind(this);
95
+
96
+ this.state = toInternalState(props.value);
97
+ }
98
+
99
+ private handleChange(nextState: IState) {
100
+ this.setState(nextState, () => {
101
+ let timeParts: Array<string> = [];
102
+
103
+ if (this.state.hours == null) {
104
+ return;
105
+ }
106
+
107
+ if (is12HourFormat) {
108
+ if (this.state.period == null) {
109
+ return;
110
+ }
111
+
112
+ timeParts.push(
113
+ convert12HourTo24Hour(parseInt(this.state.hours, 10), this.state.period)
114
+ .toString()
115
+ .padStart(2, '0'),
116
+ );
117
+ } else {
118
+ timeParts.push(this.state.hours);
119
+ }
120
+
121
+ if (this.state.minutes == null) {
122
+ return;
123
+ } else {
124
+ timeParts.push(this.state.minutes);
125
+ }
126
+
127
+ if (this.props.allowSeconds) {
128
+ if (this.state.seconds == null) {
129
+ return;
130
+ } else {
131
+ timeParts.push(this.state.seconds);
132
+ }
133
+ }
134
+
135
+ this.props.onChange(timeParts.join(':'));
136
+ });
137
+ }
138
+
139
+ scrollToValues() {
140
+ this.inputRefs.forEach((unitOfTime) => unitOfTime?.current?.scrollToValue?.());
141
+ }
142
+
143
+ componentDidMount(): void {
144
+ this.scrollToValues();
145
+ }
146
+
147
+ componentDidUpdate(prevProps: Readonly<IProps>): void {
148
+ if (this.props.value !== prevProps.value) {
149
+ this.setState(toInternalState(this.props.value), () => {
150
+ this.scrollToValues();
151
+ });
152
+ }
153
+ }
154
+
155
+ render(): React.ReactNode {
156
+ const styleForColumnOfUnit: React.CSSProperties = {
157
+ maxHeight: 190,
158
+ overflowY: 'auto',
159
+ scrollbarWidth: 'thin',
160
+ marginTop: 'var(--gap-1)',
161
+ scrollBehavior: 'smooth',
162
+ };
163
+
164
+ return (
165
+ <div className="sd-shadow--z2 radius-md">
166
+ <Spacer
167
+ v
168
+ gap="0"
169
+ style={{
170
+ minWidth: 200,
171
+ maxWidth: 'max-content',
172
+ backgroundColor: 'var(--color-dropdown-menu-Bg)',
173
+ borderRadius: 'var(--b-radius--small)',
174
+ }}
175
+ >
176
+ {this.props.headerTemplate && (
177
+ <div className="px-1-5 py-1" style={{borderBottom: '1px solid var(--color-line-x-light)'}}>
178
+ {this.props.headerTemplate}
179
+ </div>
180
+ )}
181
+
182
+ <Spacer
183
+ h
184
+ gap="4"
185
+ noWrap
186
+ justifyContent="center"
187
+ alignItems="start"
188
+ style={{paddingInline: 'var(--gap-1)'}}
189
+ >
190
+ <Spacer v gap="4" style={styleForColumnOfUnit} alignItems="center" noWrap>
191
+ {getOptionsForTimeUnit('hours', is12HourFormat).map((hour) => {
192
+ const isActiveHour = hour === this.state.hours;
193
+
194
+ return (
195
+ <TimeValueHolder
196
+ key={hour}
197
+ ref={isActiveHour ? this.inputRefs[0] : undefined}
198
+ onClick={() => {
199
+ this.handleChange({...this.state, hours: hour});
200
+ }}
201
+ isActive={isActiveHour}
202
+ value={hour}
203
+ />
204
+ );
205
+ })}
206
+ </Spacer>
207
+
208
+ <ContentDivider align="center" border type="solid" orientation="vertical" margin="none" />
209
+
210
+ <Spacer v gap="4" style={styleForColumnOfUnit} alignItems="center" noWrap>
211
+ {getOptionsForTimeUnit('minutes', is12HourFormat).map((minute) => {
212
+ const isActiveMinute = minute === this.state.minutes;
213
+
214
+ return (
215
+ <TimeValueHolder
216
+ key={minute}
217
+ ref={isActiveMinute ? this.inputRefs[1] : undefined}
218
+ isActive={isActiveMinute}
219
+ value={minute}
220
+ onClick={() => {
221
+ this.handleChange({...this.state, minutes: minute});
222
+ }}
223
+ />
224
+ );
225
+ })}
226
+ </Spacer>
227
+
228
+ {this.props.allowSeconds && (
229
+ <>
230
+ <ContentDivider
231
+ align="center"
232
+ border
233
+ type="solid"
234
+ orientation="vertical"
235
+ margin="none"
236
+ />
237
+ <Spacer v gap="4" style={styleForColumnOfUnit} alignItems="center" noWrap>
238
+ {getOptionsForTimeUnit('seconds', is12HourFormat).map((second) => {
239
+ const isActiveSecond = second === this.state.seconds;
240
+
241
+ return (
242
+ <TimeValueHolder
243
+ key={second}
244
+ ref={isActiveSecond ? this.inputRefs[2] : undefined}
245
+ onClick={() => {
246
+ this.handleChange({...this.state, seconds: second});
247
+ }}
248
+ isActive={isActiveSecond}
249
+ value={second}
250
+ />
251
+ );
252
+ })}
253
+ </Spacer>
254
+ </>
255
+ )}
256
+
257
+ {is12HourFormat && (
258
+ <div
259
+ style={{
260
+ marginTop: 'var(--gap-1)',
261
+ }}
262
+ >
263
+ <RadioButtonGroup
264
+ onChange={(nextValue) => {
265
+ this.handleChange({...this.state, period: nextValue as 'am' | 'pm'});
266
+ }}
267
+ options={[
268
+ {label: 'AM', value: 'am'},
269
+ {label: 'PM', value: 'pm'},
270
+ ]}
271
+ value={this.state.period == null ? '' : this.state.period}
272
+ />
273
+ </div>
274
+ )}
275
+ </Spacer>
276
+
277
+ {this.props.footerTemplate && (
278
+ <div className="px-1-5 py-1" style={{borderTop: '1px solid var(--color-line-x-light)'}}>
279
+ {this.props.footerTemplate}
280
+ </div>
281
+ )}
282
+ </Spacer>
283
+ </div>
284
+ );
285
+ }
286
+ }
@@ -0,0 +1,36 @@
1
+ import * as React from 'react';
2
+ import {classnames} from '@sourcefabric/common';
3
+
4
+ interface IProps {
5
+ isActive?: boolean;
6
+ value: string;
7
+ onClick(event: React.MouseEvent<HTMLSpanElement>): void;
8
+ }
9
+
10
+ export class TimeValueHolder extends React.PureComponent<IProps> {
11
+ private spanEl: React.RefObject<HTMLSpanElement>;
12
+
13
+ constructor(props: IProps) {
14
+ super(props);
15
+
16
+ this.spanEl = React.createRef();
17
+ }
18
+
19
+ public scrollToValue() {
20
+ this.spanEl.current?.scrollIntoView({block: 'start', behavior: 'smooth'});
21
+ }
22
+
23
+ render() {
24
+ return (
25
+ <span
26
+ ref={this.props.isActive ? this.spanEl : undefined}
27
+ onClick={this.props.onClick}
28
+ className={classnames('p-1 time-unit', {
29
+ 'time-unit-highlight': this.props.isActive ?? false,
30
+ })}
31
+ >
32
+ {this.props.value}
33
+ </span>
34
+ );
35
+ }
36
+ }
@@ -1,12 +1,12 @@
1
1
  import * as React from 'react';
2
2
  import nextId from 'react-id-generator';
3
3
  import classNames from 'classnames';
4
- import {InputWrapper} from './Form';
5
- import {IInputWrapper} from './Form/InputWrapper';
4
+ import {InputWrapper} from '../Form';
5
+ import {IInputWrapper} from '../Form/InputWrapper';
6
6
  import {TimePickerPopover} from './TimePickerPopover';
7
- import {PopupPositioner} from './ShowPopup';
8
- import {Icon} from './Icon';
9
- import {IconButton} from './IconButton';
7
+ import {PopupPositioner} from '../ShowPopup';
8
+ import {Icon} from '../Icon';
9
+ import {IconButton} from '../IconButton';
10
10
 
11
11
  interface IProps extends IInputWrapper {
12
12
  value: string | null; // ISO8601 time string(e.g. 16:55) or null if there's no value
@@ -120,18 +120,12 @@ export class TimePicker extends React.PureComponent<IProps, IState> {
120
120
  ref={this.timeInputRef}
121
121
  value={this.props.value ?? ''}
122
122
  type="time"
123
- onClick={(e) => {
124
- // don't show default popup
125
- e.preventDefault();
126
-
123
+ onClick={() => {
127
124
  this.setState({
128
125
  popupOpen: !this.state.popupOpen,
129
126
  });
130
127
  }}
131
128
  onKeyDown={(event) => {
132
- // don't show default popup
133
- event.preventDefault();
134
-
135
129
  if (event.key === ' ') {
136
130
  this.setState({
137
131
  popupOpen: !this.state.popupOpen,
@@ -36,7 +36,12 @@ class TimePickerExample extends React.PureComponent<{}, {time: string | null}> {
36
36
  <Button size="small" text="In 30 min" style="hollow" onClick={() => false} />
37
37
  <Button size="small" text="In 1 hr" style="hollow" onClick={() => false} />
38
38
  <Button size="small" text="In 2 hr" style="hollow" onClick={() => false} />
39
- <Button size="small" text="In 5 hr" style="hollow" onClick={() => false} />
39
+ <Button
40
+ size="small"
41
+ text="16:32"
42
+ style="hollow"
43
+ onClick={() => this.setState({time: '16:32'})}
44
+ />
40
45
  </ButtonGroup>
41
46
  }
42
47
  footerTemplate={<div>Footer</div>}
@@ -65,7 +70,7 @@ export default class TimePickerDoc extends React.Component<{}, {time: string}> {
65
70
  render() {
66
71
  return (
67
72
  <section className="docs-page__container">
68
- <h2 className="docs-page__h2">Time picker</h2>
73
+ <h2 className="docs-page__h2">Time picker 1</h2>
69
74
  <Markup.ReactMarkupCodePreview>{`
70
75
  <TimePicker
71
76
  value={this.state.time}