superdesk-ui-framework 4.0.48 → 4.0.50

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.
@@ -0,0 +1,28 @@
1
+ .time-unit-highlight {
2
+ background-color: var(--sd-colour-interactive);
3
+ color: $white;
4
+ }
5
+
6
+ .time-unit {
7
+ transition: all ease 0.2s;
8
+
9
+ &:hover {
10
+ background-color: var(--sd-colour-interactive--alpha-10);
11
+ box-shadow: inset 0 0 0 1px var(--sd-colour-interactive);
12
+ cursor: pointer;
13
+ }
14
+
15
+ &:active {
16
+ background-color: var(--sd-colour-interactive--alpha-20);
17
+ }
18
+
19
+ display: flex;
20
+ justify-content: center;
21
+ width: 32px;
22
+ height: 32px;
23
+ border-radius: var(--b-radius--x-small);
24
+ font-weight: normal;
25
+ margin-inline-start: var(--gap--x-small);
26
+ margin-inline-end: var(--gap--x-small);
27
+ transition: var(--transition__menu-item);
28
+ }
@@ -118,3 +118,4 @@
118
118
 
119
119
  // React Components
120
120
  @import 'avatar';
121
+ @import 'time';
@@ -31,7 +31,7 @@
31
31
  --gap-4: calc(4 * var(--base-increment)); // 32px;
32
32
  --gap--xx-large: calc(5 * var(--base-increment)); // 40px;
33
33
  --gap-5: calc(5 * var(--base-increment)); // 40px;
34
-
34
+
35
35
  --gap--auto: auto;
36
36
 
37
37
  // BORDER RADIUS
@@ -85,10 +85,13 @@
85
85
  --sd-drop-shadow--none: drop-shadow(0 0 0 transparent);
86
86
 
87
87
  // FORM ELEMENTS
88
- // Size
88
+ // Size
89
89
  --form-element-height-small: var(--space--3);
90
90
  --form-element-height-medium: var(--space--4);
91
91
  --form-element-height-large: var(--space--5);
92
+
93
+ // TRANSITIONS
94
+ --transition__menu-item: background-color ease 0.1s
92
95
  }
93
96
 
94
97
 
@@ -123,7 +126,7 @@
123
126
  --sd-drop-shadow--z2: drop-shadow(0 0 0 var(--sd-shadow-outline)) drop-shadow(0 1px 4px hsla(0, 0%, 0%, 0.38)) drop-shadow(0 2px 6px hsla(0, 0%, 0%, 0.28)) drop-shadow(0 0 1px hsla(0, 0%, 0%, 0.2));
124
127
  --sd-drop-shadow--z3: drop-shadow(0 0 0 var(--sd-shadow-outline)) drop-shadow(0 1px 6px hsla(0, 0%, 0%, 0.38)) drop-shadow(0 3px 8px hsla(0, 0%, 0%, 0.50)) drop-shadow(0 0 1px hsla(0, 0%, 0%, 0.2));
125
128
  --sd-drop-shadow--z4: drop-shadow(0 0 0 var(--sd-shadow-outline)) drop-shadow(0 2px 10px hsla(0, 0%, 0%, 0.44)) drop-shadow(0 6px 16px hsla(0, 0%, 0%, 0.64)) drop-shadow(0 0 1px hsla(0, 0%, 0%, 0.2));
126
-
129
+
127
130
  }
128
131
 
129
132
  .sd-top-menu,
@@ -61,7 +61,7 @@ $checkButtonBorderRadius: $border-radius__base--small;
61
61
  background-color: var(--sd-colour-interactive--hover);
62
62
  }
63
63
  }
64
-
64
+
65
65
  }
66
66
 
67
67
  // Modifier for .sd-checkbox to create a radio button
@@ -295,7 +295,7 @@ $checkButtonBorderRadius: $border-radius__base--small;
295
295
  .sd-check-new--disabled, .sd-check-new[disabled="disabled"] {
296
296
  opacity: 0.40;
297
297
  cursor: not-allowed !important;
298
-
298
+
299
299
  &:hover {
300
300
  color: $checkButtonTextColor;
301
301
  border-color: $checkButtonBorderColor;
@@ -675,8 +675,8 @@ $checkButtonBorderRadius: $border-radius__base--small;
675
675
  & ~ label {
676
676
  opacity: 1;
677
677
  color: $white;
678
- background-color: var(--sd-colour-interactive--active);
679
- border-color: var(--sd-colour-interactive--active);
678
+ background-color: var(--sd-colour-interactive);
679
+ border-color: var(--sd-colour-interactive);
680
680
  border-top-color: var(--sd-colour-interactive--darken-20);
681
681
  box-shadow: inset 0 2px 0 0 rgba(0, 0, 0, 0.2);
682
682
  }
@@ -748,7 +748,7 @@ $checkButtonBorderRadius: $border-radius__base--small;
748
748
  border-top-color: var(--sd-colour-interactive--darken-20);
749
749
  box-shadow: inset 0 2px 0 0 hsla(0, 0%, 0%, 0.2);
750
750
  color: $white;
751
-
751
+
752
752
  &:hover {
753
753
  color: $white;
754
754
  border-color: var(--sd-colour-interactive--active);
@@ -22,6 +22,8 @@ interface IPropsValueDate extends IInputWrapper {
22
22
  disabled?: boolean;
23
23
  ref?: React.LegacyRef<InputWrapper>;
24
24
  'data-test-id'?: string;
25
+ timeHeaderTemplate?: React.ReactNode;
26
+ timeFooterTemplate?: React.ReactNode;
25
27
  }
26
28
 
27
29
  type IValue = {date?: string; time?: string};
@@ -38,6 +40,8 @@ interface IPropsValueObject extends IInputWrapper {
38
40
  disabled?: boolean;
39
41
  ref?: React.LegacyRef<InputWrapper>;
40
42
  'data-test-id'?: string;
43
+ timeHeaderTemplate?: React.ReactNode;
44
+ timeFooterTemplate?: React.ReactNode;
41
45
  }
42
46
 
43
47
  type IProps = IPropsValueDate | IPropsValueObject;
@@ -90,13 +94,13 @@ export class DateTimePicker extends React.PureComponent<IProps> {
90
94
  return unitOfTime.toString().padStart(2, '0');
91
95
  }
92
96
 
93
- getTimeValue(): string {
97
+ getTimeValue(): string | null {
94
98
  if (this.props.valueType === 'date') {
95
99
  return this.props.value != null
96
100
  ? `${this.prepareFormat(this.props.value.getHours())}:${this.prepareFormat(this.props.value.getMinutes())}`
97
- : '';
101
+ : null;
98
102
  } else if (this.props.valueType === 'object') {
99
- return this.props.value.time ?? '';
103
+ return this.props.value.time ?? null;
100
104
  } else {
101
105
  assertNever(this.props);
102
106
  }
@@ -169,6 +173,8 @@ export class DateTimePicker extends React.PureComponent<IProps> {
169
173
  allowSeconds={this.props.allowSeconds}
170
174
  fullWidth={this.props.fullWidth}
171
175
  required={this.props.required}
176
+ headerTemplate={this.props.timeHeaderTemplate}
177
+ footerTemplate={this.props.timeFooterTemplate}
172
178
  />
173
179
  </div>
174
180
  {this.props.preview !== true && (
@@ -10,6 +10,7 @@ interface IPropsPopupPositioner {
10
10
  placement: Placement;
11
11
  onClose(): void;
12
12
  closeOnHoverEnd?: boolean;
13
+ 'data-test-id'?: string;
13
14
  }
14
15
 
15
16
  export class PopupPositioner extends React.PureComponent<IPropsPopupPositioner> {
@@ -124,6 +125,7 @@ export class PopupPositioner extends React.PureComponent<IPropsPopupPositioner>
124
125
  display: 'flex',
125
126
  zIndex: this.zIndex,
126
127
  }}
128
+ data-test-id={this.props['data-test-id']}
127
129
  >
128
130
  {this.props.children}
129
131
  </div>,
@@ -2,16 +2,34 @@ import * as React from 'react';
2
2
  import nextId from 'react-id-generator';
3
3
  import {InputWrapper} from './Form';
4
4
  import {IInputWrapper} from './Form/InputWrapper';
5
+ import {TimePickerPopover} from './TimePickerPopover';
6
+ import {PopupPositioner} from './ShowPopup';
5
7
 
6
8
  interface IProps extends IInputWrapper {
7
- value: string; // will output time as ISO8601 time string(e.g. 16:55) or an empty string if there's no value
9
+ value: string | null; // ISO8601 time string(e.g. 16:55) or null if there's no value
8
10
  onChange(valueNext: string): void;
9
11
  allowSeconds?: boolean;
12
+ headerTemplate?: React.ReactNode;
13
+ footerTemplate?: React.ReactNode;
10
14
  'data-test-id'?: string;
11
15
  }
12
16
 
13
- export class TimePicker extends React.PureComponent<IProps> {
17
+ interface IState {
18
+ popupOpen: boolean;
19
+ }
20
+
21
+ export class TimePicker extends React.PureComponent<IProps, IState> {
14
22
  private htmlId = nextId();
23
+ private timeInputRef: React.RefObject<HTMLInputElement>;
24
+
25
+ constructor(props: IProps) {
26
+ super(props);
27
+
28
+ this.timeInputRef = React.createRef();
29
+ this.state = {
30
+ popupOpen: false,
31
+ };
32
+ }
15
33
 
16
34
  render() {
17
35
  if (this.props.preview) {
@@ -34,11 +52,47 @@ export class TimePicker extends React.PureComponent<IProps> {
34
52
  labelHidden={this.props.labelHidden}
35
53
  htmlId={this.htmlId}
36
54
  tabindex={this.props.tabindex}
37
- inputWrapper={this.props.inputWrapper}
38
55
  >
56
+ {this.state.popupOpen && (
57
+ <PopupPositioner
58
+ getReferenceElement={() => this.timeInputRef.current as HTMLElement}
59
+ placement="bottom-start"
60
+ onClose={() => {
61
+ this.setState({
62
+ popupOpen: false,
63
+ });
64
+ }}
65
+ data-test-id="time-picker-popover"
66
+ >
67
+ <TimePickerPopover
68
+ value={this.props.value}
69
+ onChange={this.props.onChange}
70
+ closePopup={() => {
71
+ this.setState({
72
+ popupOpen: false,
73
+ });
74
+ }}
75
+ allowSeconds={this.props.allowSeconds}
76
+ headerTemplate={this.props.headerTemplate}
77
+ footerTemplate={this.props.footerTemplate}
78
+ />
79
+ </PopupPositioner>
80
+ )}
39
81
  <input
40
- value={this.props.value}
82
+ style={{
83
+ cursor: 'pointer',
84
+ }}
85
+ ref={this.timeInputRef}
86
+ value={this.props.value ?? ''}
41
87
  type="time"
88
+ onClick={(e) => {
89
+ // don't show default popup
90
+ e.preventDefault();
91
+
92
+ this.setState({
93
+ popupOpen: true,
94
+ });
95
+ }}
42
96
  className="sd-input__input"
43
97
  id={this.htmlId}
44
98
  aria-labelledby={this.htmlId + 'label'}
@@ -0,0 +1,274 @@
1
+ import * as React from 'react';
2
+ import {classnames, Spacer} from '@sourcefabric/common';
3
+ import {ContentDivider} from './ContentDivider';
4
+ import {RadioButtonGroup} from './RadioButtonGroup';
5
+ import {getOptionsForTimeUnit, ITimeUnit, padValue} from '../utils/time';
6
+ import {assertNever} from '../helpers';
7
+
8
+ interface IProps {
9
+ closePopup: () => void;
10
+ headerTemplate?: React.ReactNode;
11
+ footerTemplate?: React.ReactNode;
12
+ allowSeconds?: boolean;
13
+ onChange: (nextValue: string) => void;
14
+ value: string | null;
15
+ }
16
+
17
+ interface IPropsTimeValueHolder {
18
+ isActive?: boolean;
19
+ value: string;
20
+ onClick(event: React.MouseEvent<HTMLSpanElement>): void;
21
+ }
22
+
23
+ class TimeValueHolder extends React.PureComponent<IPropsTimeValueHolder> {
24
+ spanEl: React.RefObject<HTMLSpanElement>;
25
+
26
+ constructor(props: IPropsTimeValueHolder) {
27
+ super(props);
28
+
29
+ this.spanEl = React.createRef();
30
+ }
31
+
32
+ public scrollToValue() {
33
+ this.spanEl.current?.scrollIntoView();
34
+ }
35
+
36
+ render() {
37
+ return (
38
+ <span
39
+ ref={this.props.isActive ? this.spanEl : undefined}
40
+ onClick={this.props.onClick}
41
+ className={classnames('p-1 time-unit', {
42
+ 'time-unit-highlight': this.props.isActive ?? false,
43
+ })}
44
+ >
45
+ {this.props.value}
46
+ </span>
47
+ );
48
+ }
49
+ }
50
+
51
+ function parseUnitOfTime(unit: ITimeUnit, value: string | null, is12HourFormat?: boolean): string {
52
+ const [hour, minutes, seconds] = (value ?? '').split(':');
53
+ const valueForUnit = (() => {
54
+ if (unit === 'hours') {
55
+ /**
56
+ * Hour value is always in 24-hour format, so we need to adjust it
57
+ * to 12-hour if needed.
58
+ */
59
+ if (is12HourFormat) {
60
+ return hour === '00' ? '12' : hour;
61
+ } else {
62
+ return hour;
63
+ }
64
+ } else if (unit === 'minutes') {
65
+ return minutes;
66
+ } else if (unit === 'seconds') {
67
+ return seconds;
68
+ } else {
69
+ assertNever(unit);
70
+ }
71
+ })();
72
+
73
+ const valueParsed =
74
+ is12HourFormat && unit === 'hours' && valueForUnit !== '12'
75
+ ? parseInt(valueForUnit, 10) % 12
76
+ : parseInt(valueForUnit, 10);
77
+
78
+ return padValue(valueParsed);
79
+ }
80
+
81
+ export class TimePickerPopover extends React.PureComponent<IProps> {
82
+ private is12HourFormat: boolean;
83
+
84
+ // hour, minutes, seconds
85
+ private inputRefs: Array<React.RefObject<TimeValueHolder>>;
86
+
87
+ constructor(props: IProps) {
88
+ super(props);
89
+
90
+ this.inputRefs = [React.createRef(), React.createRef(), React.createRef()];
91
+ this.handleChange = this.handleChange.bind(this);
92
+
93
+ const hour = new Date().toLocaleTimeString([]);
94
+ this.is12HourFormat = hour.includes('AM') || hour.includes('PM');
95
+ }
96
+
97
+ handleChange(unit: ITimeUnit, value: string) {
98
+ const fallbackDate = new Date();
99
+ const [hour, minutes, seconds] =
100
+ this.props.value == null
101
+ ? [
102
+ padValue(fallbackDate.getHours()),
103
+ padValue(fallbackDate.getMinutes()),
104
+ padValue(fallbackDate.getSeconds()),
105
+ ]
106
+ : this.props.value.split(':');
107
+ let nextValue = '';
108
+
109
+ if (unit === 'hours') {
110
+ nextValue = `${value}:${minutes}`;
111
+ } else if (unit === 'minutes') {
112
+ nextValue = `${hour}:${value}`;
113
+ } else if (unit === 'seconds') {
114
+ nextValue = `${hour}:${minutes}:${value}`;
115
+ } else {
116
+ assertNever(unit);
117
+ }
118
+
119
+ if (this.props.allowSeconds && unit !== 'seconds') {
120
+ nextValue += `:${seconds}`;
121
+ }
122
+
123
+ this.props.onChange(nextValue);
124
+ }
125
+
126
+ componentDidMount(): void {
127
+ this.inputRefs.forEach((unitOfTime) => unitOfTime?.current?.scrollToValue?.());
128
+ }
129
+
130
+ render(): React.ReactNode {
131
+ const styleForColumnOfUnit: React.CSSProperties = {
132
+ maxHeight: 190,
133
+ overflowY: 'auto',
134
+ scrollbarWidth: 'none',
135
+ marginTop: 'var(--gap-1)',
136
+ };
137
+
138
+ return (
139
+ <div className="sd-shadow--z2 radius-md" onBlur={this.props.closePopup}>
140
+ <Spacer
141
+ v
142
+ gap="0"
143
+ style={{
144
+ width: 200,
145
+ padding: 'var(--gap-1)',
146
+ backgroundColor: 'var(--color-bg-00)',
147
+ borderRadius: 'var(--b-radius--small)',
148
+ }}
149
+ >
150
+ {this.props.headerTemplate && (
151
+ <>
152
+ {this.props.headerTemplate}
153
+ <ContentDivider border type="solid" orientation="horizontal" margin="none" />
154
+ </>
155
+ )}
156
+ <Spacer h gap="4" noWrap justifyContent="center" alignItems="start">
157
+ <Spacer v gap="4" style={styleForColumnOfUnit} alignItems="center" noWrap>
158
+ {getOptionsForTimeUnit('hours', this.is12HourFormat).map((hour) => {
159
+ const isActiveHour =
160
+ hour === parseUnitOfTime('hours', this.props.value, this.is12HourFormat);
161
+
162
+ return (
163
+ <TimeValueHolder
164
+ ref={isActiveHour ? this.inputRefs[0] : undefined}
165
+ onClick={() => {
166
+ this.handleChange('hours', hour);
167
+ }}
168
+ isActive={isActiveHour}
169
+ value={hour}
170
+ />
171
+ );
172
+ })}
173
+ </Spacer>
174
+ <ContentDivider align="center" border type="solid" orientation="vertical" margin="none" />
175
+ <Spacer v gap="4" style={styleForColumnOfUnit} alignItems="center" noWrap>
176
+ {getOptionsForTimeUnit('minutes', this.is12HourFormat).map((minute) => {
177
+ const isActiveMinute =
178
+ minute === parseUnitOfTime('minutes', this.props.value, this.is12HourFormat);
179
+
180
+ return (
181
+ <TimeValueHolder
182
+ ref={isActiveMinute ? this.inputRefs[1] : undefined}
183
+ isActive={isActiveMinute}
184
+ value={minute}
185
+ onClick={() => {
186
+ this.handleChange('minutes', minute);
187
+ }}
188
+ />
189
+ );
190
+ })}
191
+ </Spacer>
192
+ {this.props.allowSeconds && (
193
+ <>
194
+ <ContentDivider
195
+ align="center"
196
+ border
197
+ type="solid"
198
+ orientation="vertical"
199
+ margin="none"
200
+ />
201
+ <Spacer v gap="4" style={styleForColumnOfUnit} alignItems="center" noWrap>
202
+ {getOptionsForTimeUnit('seconds', this.is12HourFormat).map((second) => {
203
+ const isActiveMinute =
204
+ second ===
205
+ parseUnitOfTime('seconds', this.props.value, this.is12HourFormat);
206
+
207
+ return (
208
+ <TimeValueHolder
209
+ ref={isActiveMinute ? this.inputRefs[2] : undefined}
210
+ onClick={() => {
211
+ this.handleChange('seconds', second);
212
+ }}
213
+ isActive={isActiveMinute}
214
+ value={second}
215
+ />
216
+ );
217
+ })}
218
+ </Spacer>
219
+ </>
220
+ )}
221
+ {this.is12HourFormat && (
222
+ <div
223
+ style={{
224
+ marginTop: 'var(--gap-1)',
225
+ }}
226
+ >
227
+ <RadioButtonGroup
228
+ onChange={(nextValue) => {
229
+ const [hour, minutes, seconds] = (this.props.value ?? '').split(':');
230
+
231
+ if (nextValue === 'PM') {
232
+ let newValue = `${padValue(parseInt(hour, 10) + 12)}:${minutes}`;
233
+
234
+ if (this.props.allowSeconds) {
235
+ newValue += `:${seconds}`;
236
+ }
237
+
238
+ this.props.onChange(newValue);
239
+ } else {
240
+ let newValue = `${padValue(parseInt(hour, 10) - 12)}:${minutes}`;
241
+
242
+ if (this.props.allowSeconds) {
243
+ newValue += `:${seconds}`;
244
+ }
245
+
246
+ this.props.onChange(newValue);
247
+ }
248
+ }}
249
+ options={[
250
+ {
251
+ label: 'AM',
252
+ value: 'AM',
253
+ },
254
+ {
255
+ label: 'PM',
256
+ value: 'PM',
257
+ },
258
+ ]}
259
+ value={parseInt((this.props.value ?? '').split(':')[0], 10) < 12 ? 'AM' : 'PM'}
260
+ />
261
+ </div>
262
+ )}
263
+ </Spacer>
264
+ {this.props.footerTemplate && (
265
+ <>
266
+ <ContentDivider border type="solid" orientation="horizontal" margin="none" />
267
+ {this.props.footerTemplate}
268
+ </>
269
+ )}
270
+ </Spacer>
271
+ </div>
272
+ );
273
+ }
274
+ }
@@ -0,0 +1,31 @@
1
+ import {range} from 'lodash';
2
+
3
+ export type ITimeUnit = 'hours' | 'minutes' | 'seconds';
4
+
5
+ export function getOptionsForTimeUnit(
6
+ timeUnit: ITimeUnit,
7
+ is12HourFormat?: boolean,
8
+ disabledOptions?: {[key: string]: Array<number>},
9
+ ): Array<string> {
10
+ const format12HourArr = [12, ...range(1, 12)];
11
+
12
+ const timeUnitArray = (() => {
13
+ if (timeUnit === 'hours') {
14
+ if (is12HourFormat) {
15
+ return format12HourArr;
16
+ } else {
17
+ return range(24);
18
+ }
19
+ } else {
20
+ return range(60);
21
+ }
22
+ })();
23
+
24
+ return timeUnitArray
25
+ .filter((item) => !(disabledOptions?.[timeUnit] ?? []).includes(item))
26
+ .map((value) => value.toString().padStart(2, '0'));
27
+ }
28
+
29
+ export function padValue(value: number) {
30
+ return value.toString().padStart(2, '0');
31
+ }
@@ -7,7 +7,7 @@ class DateTimePickerExample extends React.PureComponent<{}, {dateTime: Date | nu
7
7
  super(props);
8
8
 
9
9
  this.state = {
10
- dateTime: new Date(),
10
+ dateTime: null,
11
11
  };
12
12
  }
13
13
 
@@ -17,6 +17,8 @@ class DateTimePickerExample extends React.PureComponent<{}, {dateTime: Date | nu
17
17
  label="Planning date"
18
18
  labelHidden
19
19
  inlineLabel
20
+ fullWidth
21
+ valueType="date"
20
22
  value={this.state.dateTime}
21
23
  dateFormat="YYYY-MM-DD"
22
24
  fullWidth
@@ -7,12 +7,12 @@ import {TimePickerV2} from '../../../app-typescript/components/TimePickerV2';
7
7
  let minutes = Array.from(Array(60).keys());
8
8
  let changedMinutes = minutes.filter((num) => num % 15 !== 0);
9
9
 
10
- class TimePickerExample extends React.PureComponent<{}, {time: string}> {
10
+ class TimePickerExample extends React.PureComponent<{}, {time: string | null}> {
11
11
  constructor(props) {
12
12
  super(props);
13
13
 
14
14
  this.state = {
15
- time: '',
15
+ time: null,
16
16
  };
17
17
  }
18
18