superdesk-ui-framework 4.1.3 → 4.1.5
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/.github/workflows/publish-to-npm.yml +1 -0
- package/.mocharc.json +1 -1
- package/app-typescript/components/DateTimePicker.spec.tsx +59 -0
- package/app-typescript/components/DateTimePicker.tsx +14 -6
- package/app-typescript/components/TimePicker/TimePickerPopover.spec.ts +163 -0
- package/app-typescript/components/TimePicker/TimePickerPopover.tsx +286 -0
- package/app-typescript/components/TimePicker/TimeValueHolder.tsx +36 -0
- package/app-typescript/components/{TimePicker.tsx → TimePicker/index.tsx} +6 -17
- package/dist/components/DateTimePicker.tsx +10 -0
- package/dist/components/TimePicker.tsx +7 -2
- package/dist/examples.bundle.js +1023 -944
- package/dist/superdesk-ui.bundle.js +732 -654
- package/dist/vendor.bundle.js +15 -15
- package/package.json +1 -1
- package/react/components/DateTimePicker.js +8 -6
- package/react/components/TimePicker/TimePickerPopover.d.ts +27 -0
- package/react/components/TimePicker/TimePickerPopover.js +231 -0
- package/react/components/TimePicker/TimeValueHolder.d.ts +13 -0
- package/react/components/TimePicker/TimeValueHolder.js +73 -0
- package/react/components/{TimePicker.d.ts → TimePicker/index.d.ts} +1 -1
- package/react/components/{TimePicker.js → TimePicker/index.js} +6 -14
- package/app-typescript/components/TimePickerPopover.tsx +0 -294
- package/react/components/TimePickerPopover.d.ts +0 -19
- package/react/components/TimePickerPopover.js +0 -227
package/.mocharc.json
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {describe, it} from 'mocha';
|
|
2
|
+
import * as assert from 'assert';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import {DateTimePicker} from './DateTimePicker';
|
|
5
|
+
|
|
6
|
+
function makePicker(overrides: Partial<React.ComponentProps<typeof DateTimePicker>> = {}) {
|
|
7
|
+
return new DateTimePicker({
|
|
8
|
+
valueType: 'date',
|
|
9
|
+
dateFormat: 'MM/DD/YYYY',
|
|
10
|
+
value: new Date(2024, 0, 1, 13, 30, 45),
|
|
11
|
+
onChange: () => undefined,
|
|
12
|
+
allowSeconds: true,
|
|
13
|
+
...overrides,
|
|
14
|
+
} as React.ComponentProps<typeof DateTimePicker>);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('DateTimePicker', () => {
|
|
18
|
+
it('keeps seconds when formatting date values', () => {
|
|
19
|
+
const picker = makePicker();
|
|
20
|
+
|
|
21
|
+
assert.strictEqual(picker.getTimeValue(), '13:30:45');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('keeps seconds when changing time values', () => {
|
|
25
|
+
let nextValue: Date | null = null;
|
|
26
|
+
const picker = makePicker({
|
|
27
|
+
onChange: (value: Date | null) => {
|
|
28
|
+
nextValue = value;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
picker.handleTimeChange('09:05:12');
|
|
33
|
+
|
|
34
|
+
assert.notStrictEqual(nextValue, null);
|
|
35
|
+
const timeValue = nextValue as unknown as Date;
|
|
36
|
+
|
|
37
|
+
assert.strictEqual(timeValue.getHours(), 9);
|
|
38
|
+
assert.strictEqual(timeValue.getMinutes(), 5);
|
|
39
|
+
assert.strictEqual(timeValue.getSeconds(), 12);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('keeps seconds when changing the date', () => {
|
|
43
|
+
let nextValue: Date | null = null;
|
|
44
|
+
const picker = makePicker({
|
|
45
|
+
onChange: (value: Date | null) => {
|
|
46
|
+
nextValue = value;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
picker.handleDateChange(new Date(2024, 0, 2));
|
|
51
|
+
|
|
52
|
+
assert.notStrictEqual(nextValue, null);
|
|
53
|
+
const dateValue = nextValue as unknown as Date;
|
|
54
|
+
|
|
55
|
+
assert.strictEqual(dateValue.getHours(), 13);
|
|
56
|
+
assert.strictEqual(dateValue.getMinutes(), 30);
|
|
57
|
+
assert.strictEqual(dateValue.getSeconds(), 45);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -46,10 +46,10 @@ export class DateTimePicker extends React.PureComponent<IProps> {
|
|
|
46
46
|
|
|
47
47
|
handleTimeChange = (time: string) => {
|
|
48
48
|
if (this.props.valueType === 'date') {
|
|
49
|
-
const [hours, minutes] = time.split(':').map((x) => defaultTo(parseInt(x, 10), 0));
|
|
49
|
+
const [hours, minutes, seconds] = time.split(':').map((x) => defaultTo(parseInt(x, 10), 0));
|
|
50
50
|
const origDate = this.props.value ?? new Date();
|
|
51
51
|
|
|
52
|
-
origDate.setHours(hours, minutes);
|
|
52
|
+
origDate.setHours(hours, minutes, this.props.allowSeconds ? seconds : 0);
|
|
53
53
|
|
|
54
54
|
this.props.onChange(origDate);
|
|
55
55
|
} else if (this.props.valueType === 'object') {
|
|
@@ -72,7 +72,11 @@ export class DateTimePicker extends React.PureComponent<IProps> {
|
|
|
72
72
|
const origDate = this.props.value ?? new Date();
|
|
73
73
|
const selectedDate = new Date(date);
|
|
74
74
|
|
|
75
|
-
selectedDate.setHours(
|
|
75
|
+
selectedDate.setHours(
|
|
76
|
+
origDate.getHours(),
|
|
77
|
+
origDate.getMinutes(),
|
|
78
|
+
this.props.allowSeconds ? origDate.getSeconds() : 0,
|
|
79
|
+
);
|
|
76
80
|
|
|
77
81
|
this.props.onChange(selectedDate);
|
|
78
82
|
} else if (this.props.valueType === 'object') {
|
|
@@ -91,9 +95,13 @@ export class DateTimePicker extends React.PureComponent<IProps> {
|
|
|
91
95
|
|
|
92
96
|
getTimeValue(): string | null {
|
|
93
97
|
if (this.props.valueType === 'date') {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
if (this.props.value == null) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const seconds = this.props.allowSeconds ? `:${this.prepareFormat(this.props.value.getSeconds())}` : '';
|
|
103
|
+
|
|
104
|
+
return `${this.prepareFormat(this.props.value.getHours())}:${this.prepareFormat(this.props.value.getMinutes())}${seconds}`;
|
|
97
105
|
} else if (this.props.valueType === 'object') {
|
|
98
106
|
return this.props.value.time ?? null;
|
|
99
107
|
} else {
|
|
@@ -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 '
|
|
5
|
-
import {IInputWrapper} from '
|
|
4
|
+
import {InputWrapper} from '../Form';
|
|
5
|
+
import {IInputWrapper} from '../Form/InputWrapper';
|
|
6
6
|
import {TimePickerPopover} from './TimePickerPopover';
|
|
7
|
-
import {PopupPositioner} from '
|
|
8
|
-
import {Icon} from '
|
|
9
|
-
import {IconButton} from '
|
|
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
|
|
@@ -71,11 +71,6 @@ export class TimePicker extends React.PureComponent<IProps, IState> {
|
|
|
71
71
|
<TimePickerPopover
|
|
72
72
|
value={this.props.value}
|
|
73
73
|
onChange={this.props.onChange}
|
|
74
|
-
closePopup={() => {
|
|
75
|
-
this.setState({
|
|
76
|
-
popupOpen: false,
|
|
77
|
-
});
|
|
78
|
-
}}
|
|
79
74
|
allowSeconds={this.props.allowSeconds}
|
|
80
75
|
headerTemplate={this.props.headerTemplate}
|
|
81
76
|
footerTemplate={this.props.footerTemplate}
|
|
@@ -90,18 +85,12 @@ export class TimePicker extends React.PureComponent<IProps, IState> {
|
|
|
90
85
|
ref={this.timeInputRef}
|
|
91
86
|
value={this.props.value ?? ''}
|
|
92
87
|
type="time"
|
|
93
|
-
onClick={(
|
|
94
|
-
// don't show default popup
|
|
95
|
-
e.preventDefault();
|
|
96
|
-
|
|
88
|
+
onClick={() => {
|
|
97
89
|
this.setState({
|
|
98
90
|
popupOpen: true,
|
|
99
91
|
});
|
|
100
92
|
}}
|
|
101
93
|
onKeyDown={(event) => {
|
|
102
|
-
// don't show default popup
|
|
103
|
-
event.preventDefault();
|
|
104
|
-
|
|
105
94
|
if (event.key === ' ') {
|
|
106
95
|
this.setState({
|
|
107
96
|
popupOpen: !this.state.popupOpen,
|