superdesk-ui-framework 5.0.2 → 5.0.4

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.
Files changed (48) hide show
  1. package/.mocharc.json +1 -1
  2. package/app/scripts/helpers/dropdown.helper.js +9 -0
  3. package/app/styles/_buttons.scss +4 -1
  4. package/app/styles/_helpers.scss +25 -6
  5. package/app/styles/_popover.scss +8 -8
  6. package/app/styles/_tooltips.scss +83 -0
  7. package/app/styles/design-tokens/_design-tokens-general.scss +6 -0
  8. package/app/styles/form-elements/_checkbox.scss +9 -5
  9. package/app/styles/form-elements/_inputs.scss +7 -1
  10. package/app-typescript/components/Autocomplete.tsx +1 -1
  11. package/app-typescript/components/Form/InputWrapper.tsx +1 -1
  12. package/app-typescript/components/Popover.tsx +2 -0
  13. package/app-typescript/components/SelectGrid.tsx +3 -1
  14. package/app-typescript/components/TimePicker/TimePickerPopover.spec.ts +163 -0
  15. package/app-typescript/components/TimePicker/TimePickerPopover.tsx +286 -0
  16. package/app-typescript/components/TimePicker/TimeValueHolder.tsx +36 -0
  17. package/app-typescript/components/{TimePicker.tsx → TimePicker/index.tsx} +6 -12
  18. package/app-typescript/components/Tooltip.tsx +1 -1
  19. package/app-typescript/components/TooltipV2.tsx +10 -21
  20. package/app-typescript/components/_Positioner.tsx +2 -0
  21. package/dist/components/Popover.tsx +31 -8
  22. package/dist/components/TimePicker.tsx +7 -2
  23. package/dist/components/Tooltips.tsx +15 -13
  24. package/dist/components/utilities/SpacingUtilities.tsx +126 -126
  25. package/dist/components/utilities/TextUtilities.tsx +51 -37
  26. package/dist/examples.bundle.js +1354 -1199
  27. package/dist/superdesk-ui.bundle.css +146 -41
  28. package/dist/superdesk-ui.bundle.js +829 -741
  29. package/dist/vendor.bundle.js +15 -15
  30. package/package.json +1 -1
  31. package/react/components/Autocomplete.js +2 -1
  32. package/react/components/Form/InputWrapper.js +2 -1
  33. package/react/components/Popover.d.ts +1 -0
  34. package/react/components/Popover.js +1 -1
  35. package/react/components/SelectGrid.js +2 -1
  36. package/react/components/TimePicker/TimePickerPopover.d.ts +27 -0
  37. package/react/components/TimePicker/TimePickerPopover.js +231 -0
  38. package/react/components/TimePicker/TimeValueHolder.d.ts +13 -0
  39. package/react/components/TimePicker/TimeValueHolder.js +73 -0
  40. package/react/components/{TimePicker.d.ts → TimePicker/index.d.ts} +1 -1
  41. package/react/components/{TimePicker.js → TimePicker/index.js} +5 -9
  42. package/react/components/Tooltip.js +1 -1
  43. package/react/components/TooltipV2.js +10 -18
  44. package/react/components/_Positioner.d.ts +1 -0
  45. package/react/components/_Positioner.js +1 -1
  46. package/app-typescript/components/TimePickerPopover.tsx +0 -283
  47. package/react/components/TimePickerPopover.d.ts +0 -18
  48. package/react/components/TimePickerPopover.js +0 -222
@@ -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,
@@ -37,7 +37,7 @@ export class Tooltip extends React.PureComponent<IProps> {
37
37
  return (
38
38
  <TooltipV2 content={this.props.text ?? ''} placement={flowToPlacement(this.props.flow)}>
39
39
  {({attributes}) => (
40
- <div {...attributes} style={{display: 'inline-flex'}}>
40
+ <div {...attributes} style={{display: 'contents'}}>
41
41
  {this.props.children}
42
42
  </div>
43
43
  )}
@@ -24,27 +24,16 @@ export class TooltipV2 extends React.PureComponent<IPropsTooltipV2> {
24
24
  placement={this.props.placement ?? 'top'}
25
25
  component={() => {
26
26
  return (
27
- <div data-theme="dark-ui">
28
- <div
29
- style={{
30
- background: 'var(--color-bg-100)',
31
- color: 'var(--color-text)',
32
- borderRadius: 'var(--b-radius--medium)',
33
- paddingInline: 'var(--space--0-5)',
34
- fontSize: 'var(--text-size-x-small)',
35
- margin: 2,
36
- }}
37
- >
38
- {(() => {
39
- if (typeof this.props.content === 'string') {
40
- return <span>{this.props.content}</span>;
41
- } else {
42
- const Component = this.props.content;
27
+ <div className="tooltip">
28
+ {(() => {
29
+ if (typeof this.props.content === 'string') {
30
+ return <span>{this.props.content}</span>;
31
+ } else {
32
+ const Component = this.props.content;
43
33
 
44
- return <Component />;
45
- }
46
- })()}
47
- </div>
34
+ return <Component />;
35
+ }
36
+ })()}
48
37
  </div>
49
38
  );
50
39
  }}
@@ -63,7 +52,7 @@ export class TooltipV2 extends React.PureComponent<IPropsTooltipV2> {
63
52
  return this.props.children({attributes});
64
53
  } else {
65
54
  return (
66
- <span {...attributes} style={{display: 'inline-flex'}}>
55
+ <span {...attributes} style={{display: 'contents'}}>
67
56
  {this.props.children}
68
57
  </span>
69
58
  );
@@ -93,6 +93,7 @@ class PopperWrapper extends React.Component<IPropsPopperWrapper> {
93
93
  }}
94
94
  tabIndex={0}
95
95
  role="dialog"
96
+ data-theme={this.props.theme}
96
97
  aria-labelledby="popoverTitle"
97
98
  onKeyDown={(event) => {
98
99
  if (event.key === 'Escape') {
@@ -111,6 +112,7 @@ interface IPropsPositioner {
111
112
  triggerSelector: string;
112
113
  placement: PopperOptions['placement'];
113
114
  className?: string;
115
+ theme?: 'light-ui' | 'dark-ui';
114
116
  }
115
117
 
116
118
  interface IStatePositioner {
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import {Popover, PropsList, Prop} from '../../../app-typescript';
2
+ import {Popover, PropsList, Prop, Button, ButtonGroup} from '../../../app-typescript';
3
3
 
4
4
  import * as Markup from '../../js/react';
5
5
 
@@ -23,13 +23,20 @@ export class PopoverDoc extends React.Component {
23
23
  <Markup.ReactMarkupPreview>
24
24
  <div className="docs-page__content-row docs-page__content-row--no-margin">
25
25
  <div className="form__row">
26
- <button
27
- className="btn btn-default btn--small"
28
- aria-haspopup="true"
29
- id="button-view-content"
30
- >
31
- Open popover
32
- </button>
26
+ <ButtonGroup>
27
+ <Button
28
+ aria-haspopup="true"
29
+ id="button-view-content"
30
+ text="Open popover"
31
+ onClick={() => false}
32
+ />
33
+ <Button
34
+ aria-haspopup="true"
35
+ id="button-view-content-dark"
36
+ text="Open popover"
37
+ onClick={() => false}
38
+ />
39
+ </ButtonGroup>
33
40
 
34
41
  <Popover
35
42
  triggerSelector="#button-view-content"
@@ -38,6 +45,15 @@ export class PopoverDoc extends React.Component {
38
45
  >
39
46
  Donec sed odio dui. Aenean lacinia bibendum nulla sed consectetur.
40
47
  </Popover>
48
+ <Popover
49
+ triggerSelector="#button-view-content-dark"
50
+ title="Popover test"
51
+ placement="top-end"
52
+ theme="dark-ui"
53
+ >
54
+ Enforce a dark theme. Donec sed odio dui. Aenean lacinia bibendum nulla sed
55
+ consectetur.
56
+ </Popover>
41
57
  </div>
42
58
  </div>
43
59
  </Markup.ReactMarkupPreview>
@@ -83,6 +99,13 @@ export class PopoverDoc extends React.Component {
83
99
  default="auto"
84
100
  description="Define the placement of the Popover."
85
101
  />
102
+ <Prop
103
+ name="theme"
104
+ isRequired={false}
105
+ type="light-ui | dark-ui"
106
+ default="/"
107
+ description="Define the theme of the Popover. It will inherit the default theme if not set."
108
+ />
86
109
  </PropsList>
87
110
  </section>
88
111
  );
@@ -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}
@@ -2,7 +2,7 @@ import * as React from 'react';
2
2
 
3
3
  import * as Markup from '../../js/react';
4
4
 
5
- import {Tooltip, Prop, PropsList, Button} from '../../../app-typescript';
5
+ import {Tooltip, Prop, PropsList, Button, ButtonGroup} from '../../../app-typescript';
6
6
 
7
7
  export default class TooltipDoc extends React.Component {
8
8
  render() {
@@ -20,18 +20,20 @@ export default class TooltipDoc extends React.Component {
20
20
  <Markup.ReactMarkup>
21
21
  <Markup.ReactMarkupPreview>
22
22
  <div className="docs-page__content-row docs-page__content-row--no-margin">
23
- <Tooltip content="I'm on top">
24
- <Button text="top" onClick={() => false} />
25
- </Tooltip>
26
- <Tooltip content="I'm at the bottom" placement="bottom">
27
- <Button text="bottom" onClick={() => false} />
28
- </Tooltip>
29
- <Tooltip content="I open on the left" placement="left">
30
- <Button text="left" onClick={() => false} />
31
- </Tooltip>
32
- <Tooltip content="Right on!" placement="right">
33
- <Button text="right" onClick={() => false} />
34
- </Tooltip>
23
+ <ButtonGroup>
24
+ <Tooltip content="I'm on top">
25
+ <Button text="top" onClick={() => false} />
26
+ </Tooltip>
27
+ <Tooltip content="I'm at the bottom" placement="bottom">
28
+ <Button text="bottom" onClick={() => false} />
29
+ </Tooltip>
30
+ <Tooltip content="I open on the left" placement="left">
31
+ <Button text="left" onClick={() => false} />
32
+ </Tooltip>
33
+ <Tooltip content="Right on!" placement="right">
34
+ <Button text="right" onClick={() => false} />
35
+ </Tooltip>
36
+ </ButtonGroup>
35
37
  </div>
36
38
  </Markup.ReactMarkupPreview>
37
39
  <Markup.ReactMarkupCode>