@itwin/itwinui-react 2.1.1 → 2.2.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.2.0](https://www.github.com/iTwin/iTwinUI-react/compare/v2.1.0...v2.2.0) (2022-12-19)
4
+
5
+ ### What's new
6
+
7
+ * **NotificationMarker:** Add new `NotificationMarker` component ([#829](https://www.github.com/iTwin/iTwinUI-react/issues/829)) ([6a0a6c3](https://www.github.com/iTwin/iTwinUI-react/commit/6a0a6c3913a0d5ca51f9835f517bdac36236e8c9))
8
+ * **Table:** `onFilter` gets filtered rows ([#958](https://www.github.com/iTwin/iTwinUI-react/issues/958)) ([84bc9e9](https://www.github.com/iTwin/iTwinUI-react/commit/84bc9e9dd957ed7e87ef9f3fc617c83c74127a5f))
9
+ * **Timepicker:** Add combined time column ([#844](https://www.github.com/iTwin/iTwinUI-react/issues/844)) ([86050eb](https://www.github.com/iTwin/iTwinUI-react/commit/86050eb6d971437f287d13dbf9935d8dafbbb281))
10
+
11
+ ### Fixes
12
+
13
+ * **Table:** Reset `columnOrder` when `columns` changes ([#983](https://www.github.com/iTwin/iTwinUI-react/issues/983)) ([4f184f6](https://www.github.com/iTwin/iTwinUI-react/commit/4f184f6caf37545a0008e6b9cd5d218bd218fdbd))
14
+
3
15
  ### 2.1.1 (2022-12-16)
4
16
 
5
17
  ### Fixes
@@ -130,7 +130,7 @@ exports.generateLocalizedStrings = generateLocalizedStrings;
130
130
  */
131
131
  const DatePicker = (props) => {
132
132
  var _a, _b, _c, _d, _e, _f, _g, _h;
133
- const { date, onChange, localizedNames, className, style, setFocus = false, showTime = false, use12Hours = false, precision, hourStep, minuteStep, secondStep, showYearSelection = false, enableRangeSelect = false, startDate, endDate, ...rest } = props;
133
+ const { date, onChange, localizedNames, className, style, setFocus = false, showTime = false, use12Hours = false, precision, hourStep, minuteStep, secondStep, useCombinedRenderer, combinedRenderer, hourRenderer, minuteRenderer, secondRenderer, meridiemRenderer, showYearSelection = false, enableRangeSelect = false, startDate, endDate, ...rest } = props;
134
134
  (0, utils_1.useTheme)();
135
135
  const monthNames = (_a = localizedNames === null || localizedNames === void 0 ? void 0 : localizedNames.months) !== null && _a !== void 0 ? _a : defaultMonths;
136
136
  const shortDays = (_b = localizedNames === null || localizedNames === void 0 ? void 0 : localizedNames.shortDays) !== null && _b !== void 0 ? _b : defaultShortDays;
@@ -368,7 +368,7 @@ const DatePicker = (props) => {
368
368
  (element === null || element === void 0 ? void 0 : element.focus()) }, dateValue));
369
369
  })));
370
370
  }))),
371
- showTime && (react_1.default.createElement(TimePicker_1.TimePicker, { date: selectedStartDay !== null && selectedStartDay !== void 0 ? selectedStartDay : selectedDay, use12Hours: use12Hours, precision: precision, hourStep: hourStep, minuteStep: minuteStep, secondStep: secondStep, onChange: (date) => {
371
+ showTime && (react_1.default.createElement(TimePicker_1.TimePicker, { date: selectedStartDay !== null && selectedStartDay !== void 0 ? selectedStartDay : selectedDay, use12Hours: use12Hours, precision: precision, hourStep: hourStep, minuteStep: minuteStep, secondStep: secondStep, useCombinedRenderer: useCombinedRenderer, combinedRenderer: combinedRenderer, hourRenderer: hourRenderer, minuteRenderer: minuteRenderer, secondRenderer: secondRenderer, meridiemRenderer: meridiemRenderer, onChange: (date) => {
372
372
  var _a, _b, _c, _d, _e, _f;
373
373
  return isSingleOnChange(onChange, enableRangeSelect)
374
374
  ? onChange === null || onChange === void 0 ? void 0 : onChange(date)
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import '@itwin/itwinui-css/css/utils.css';
3
+ export declare type NotificationMarkerProps = {
4
+ /**
5
+ * Content of the NotificationMarker.
6
+ */
7
+ children: React.ReactNode;
8
+ /**
9
+ * Status of notification
10
+ *
11
+ * - 'primary' = blue,
12
+ * - 'positive' = green,
13
+ * - 'warning' = orange,
14
+ * - 'negative' = red,
15
+ * - 'white' = white (useful for notifications in buttons with `style` = `high-visibility` | `cta`),
16
+ * @default 'primary'
17
+ */
18
+ status?: 'primary' | 'positive' | 'warning' | 'negative' | 'white';
19
+ /**
20
+ * Adds a pulse effect to the notification.
21
+ *
22
+ * **WARNING**: Avoid overuse of this prop.
23
+ * @default false
24
+ */
25
+ pulsing?: boolean;
26
+ /**
27
+ * Set this programmatically to false when you just want to render the passed children without the notification
28
+ * @default true
29
+ * @example
30
+ * let [newMessagesCount, ...] = useState(0);
31
+ * ...
32
+ * <NotificationMarker enabled={newMessagesCount > 0}>
33
+ * <SvgNotification />
34
+ * </NotificationMarker>
35
+ */
36
+ enabled?: boolean;
37
+ } & React.ComponentProps<'span'>;
38
+ /**
39
+ * A small notification circle to the top-right of the passed children prop.
40
+ * This can be applied to almost anything but mostly intended for icons within buttons with `styleType = default / borderless`.
41
+ * @example
42
+ * <IconButton styleType='borderless'>
43
+ * <NotificationMarker>
44
+ * <SvgNotification />
45
+ * </NotificationMarker>
46
+ * </IconButton>
47
+ * @example
48
+ * <NotificationMarker status='positive' pulsing={true}>Live</NotificationMarker>
49
+ */
50
+ export declare const NotificationMarker: React.ForwardRefExoticComponent<Pick<NotificationMarkerProps, "key" | "status" | keyof React.HTMLAttributes<HTMLSpanElement> | "enabled" | "pulsing"> & React.RefAttributes<HTMLSpanElement>>;
51
+ export default NotificationMarker;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.NotificationMarker = void 0;
7
+ /*---------------------------------------------------------------------------------------------
8
+ * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
9
+ * See LICENSE.md in the project root for license terms and full copyright notice.
10
+ *--------------------------------------------------------------------------------------------*/
11
+ const react_1 = __importDefault(require("react"));
12
+ const utils_1 = require("../utils");
13
+ const classnames_1 = __importDefault(require("classnames"));
14
+ require("@itwin/itwinui-css/css/utils.css");
15
+ /**
16
+ * A small notification circle to the top-right of the passed children prop.
17
+ * This can be applied to almost anything but mostly intended for icons within buttons with `styleType = default / borderless`.
18
+ * @example
19
+ * <IconButton styleType='borderless'>
20
+ * <NotificationMarker>
21
+ * <SvgNotification />
22
+ * </NotificationMarker>
23
+ * </IconButton>
24
+ * @example
25
+ * <NotificationMarker status='positive' pulsing={true}>Live</NotificationMarker>
26
+ */
27
+ exports.NotificationMarker = react_1.default.forwardRef((props, ref) => {
28
+ const { className, children, status = 'primary', pulsing = false, enabled = true, ...rest } = props;
29
+ (0, utils_1.useTheme)();
30
+ return (react_1.default.createElement("span", { ref: ref, className: (0, classnames_1.default)({ 'iui-notification-marker': enabled }, className), "data-iui-variant": enabled ? status : null, "data-iui-urgent": enabled ? pulsing : null, ...rest }, children));
31
+ });
32
+ exports.default = exports.NotificationMarker;
@@ -0,0 +1,4 @@
1
+ export { NotificationMarker } from './NotificationMarker';
2
+ export type { NotificationMarkerProps } from './NotificationMarker';
3
+ declare const _default: "./NotificationMarker";
4
+ export default _default;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NotificationMarker = void 0;
4
+ /*---------------------------------------------------------------------------------------------
5
+ * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
6
+ * See LICENSE.md in the project root for license terms and full copyright notice.
7
+ *--------------------------------------------------------------------------------------------*/
8
+ var NotificationMarker_1 = require("./NotificationMarker");
9
+ Object.defineProperty(exports, "NotificationMarker", { enumerable: true, get: function () { return NotificationMarker_1.NotificationMarker; } });
10
+ exports.default = './NotificationMarker';
@@ -117,7 +117,7 @@ export declare type TableProps<T extends Record<string, unknown> = Record<string
117
117
  * Use with `manualFilters` to handle filtering yourself e.g. filter in server-side.
118
118
  * Must be memoized.
119
119
  */
120
- onFilter?: (filters: TableFilterValue<T>[], state: TableState<T>) => void;
120
+ onFilter?: (filters: TableFilterValue<T>[], state: TableState<T>, filteredData?: Row<T>[]) => void;
121
121
  /**
122
122
  * Value used for global filtering.
123
123
  * Use with `globalFilter` and/or `manualGlobalFilter` to handle filtering yourself e.g. filter in server-side.
@@ -123,13 +123,15 @@ const Table = (props) => {
123
123
  disableUserSelect,
124
124
  enableUserSelect,
125
125
  ]);
126
+ const previousFilter = react_1.default.useRef([]);
127
+ const currentFilter = react_1.default.useRef(previousFilter.current);
126
128
  const tableStateReducer = react_1.default.useCallback((newState, action, previousState, instance) => {
127
129
  switch (action.type) {
128
130
  case react_table_1.actions.toggleSortBy:
129
131
  onSort === null || onSort === void 0 ? void 0 : onSort(newState);
130
132
  break;
131
133
  case react_table_1.actions.setFilter:
132
- (0, actionHandlers_1.onFilterHandler)(newState, action, previousState, instance, onFilter);
134
+ currentFilter.current = (0, actionHandlers_1.onFilterHandler)(newState, action, previousState, currentFilter.current, instance);
133
135
  break;
134
136
  case react_table_1.actions.toggleRowExpanded:
135
137
  case react_table_1.actions.toggleAllRowsExpanded:
@@ -173,7 +175,6 @@ const Table = (props) => {
173
175
  hasManualSelectionColumn,
174
176
  isRowDisabled,
175
177
  onExpand,
176
- onFilter,
177
178
  onSelect,
178
179
  onSort,
179
180
  stateReducer,
@@ -249,6 +250,22 @@ const Table = (props) => {
249
250
  react_1.default.useEffect(() => {
250
251
  setPageSize(pageSize);
251
252
  }, [pageSize, setPageSize]);
253
+ react_1.default.useEffect(() => {
254
+ if (previousFilter.current !== currentFilter.current) {
255
+ previousFilter.current = currentFilter.current;
256
+ onFilter === null || onFilter === void 0 ? void 0 : onFilter(currentFilter.current, state, instance.filteredRows);
257
+ }
258
+ }, [state, instance.filteredRows, onFilter]);
259
+ const lastPassedColumns = react_1.default.useRef([]);
260
+ // Reset the column order whenever new columns are passed
261
+ // This is to avoid the old columnOrder from affecting the new columns' columnOrder
262
+ react_1.default.useEffect(() => {
263
+ // Check if columns have changed (by value)
264
+ if (JSON.stringify(lastPassedColumns.current) !== JSON.stringify(columns)) {
265
+ instance.setColumnOrder([]);
266
+ }
267
+ lastPassedColumns.current = columns;
268
+ }, [columns, instance]);
252
269
  const paginatorRendererProps = react_1.default.useMemo(() => ({
253
270
  currentPage: state.pageIndex,
254
271
  pageSize: state.pageSize,
@@ -1,3 +1,3 @@
1
1
  import { ActionType, TableInstance, TableState } from 'react-table';
2
2
  import { TableFilterValue } from '../filters';
3
- export declare const onFilterHandler: <T extends Record<string, unknown>>(newState: TableState<T>, action: ActionType, previousState: TableState<T>, instance?: TableInstance<T> | undefined, onFilter?: ((filters: TableFilterValue<T>[], state: TableState<T>) => void) | undefined) => void;
3
+ export declare const onFilterHandler: <T extends Record<string, unknown>>(newState: TableState<T>, action: ActionType, previousState: TableState<T>, currentFilter: TableFilterValue<T>[], instance?: TableInstance<T> | undefined) => TableFilterValue<T>[];
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.onFilterHandler = void 0;
4
- const onFilterHandler = (newState, action, previousState, instance, onFilter) => {
4
+ const onFilterHandler = (newState, action, previousState, currentFilter, instance) => {
5
5
  const previousFilter = previousState.filters.find((f) => f.id === action.columnId);
6
6
  if ((previousFilter === null || previousFilter === void 0 ? void 0 : previousFilter.value) != action.filterValue) {
7
7
  const filters = newState.filters.map((f) => {
@@ -14,7 +14,8 @@ const onFilterHandler = (newState, action, previousState, instance, onFilter) =>
14
14
  filterType: (_b = column === null || column === void 0 ? void 0 : column.filter) !== null && _b !== void 0 ? _b : 'text',
15
15
  };
16
16
  });
17
- onFilter === null || onFilter === void 0 ? void 0 : onFilter(filters, newState);
17
+ return filters;
18
18
  }
19
+ return currentFilter;
19
20
  };
20
21
  exports.onFilterHandler = onFilterHandler;
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { StylingProps } from '../utils';
3
3
  import '@itwin/itwinui-css/css/time-picker.css';
4
4
  export declare type MeridiemType = 'AM' | 'PM';
5
+ export declare type Precision = 'hours' | 'minutes' | 'seconds';
5
6
  export declare type TimePickerProps = {
6
7
  /**
7
8
  * Selected date. Only time is used from Date object.
@@ -20,7 +21,7 @@ export declare type TimePickerProps = {
20
21
  * Precision of the time.
21
22
  * @default 'minutes'
22
23
  */
23
- precision?: 'hours' | 'minutes' | 'seconds';
24
+ precision?: Precision;
24
25
  /**
25
26
  * Change step of the hours displayed.
26
27
  * @default 1
@@ -61,6 +62,30 @@ export declare type TimePickerProps = {
61
62
  * @default (meridiem: MeridiemType) => meridiem
62
63
  */
63
64
  meridiemRenderer?: (meridiem: MeridiemType) => React.ReactNode;
65
+ /**
66
+ * Use combined time renderer. Combines hour, minute, and seconds into one column.
67
+ * **WARNING**: Using the combined renderer with a `precision` of 'seconds' along with
68
+ * small time steps (`hourStep`, `minuteStep`, and especially `secondStep`) can result in slow performance!
69
+ * @default false
70
+ */
71
+ useCombinedRenderer?: boolean;
72
+ /**
73
+ * Custom combined time renderer.
74
+ * Default returns time in `HH:MM:SS` format
75
+ * @default (date: Date, precision: Precision) => {
76
+ * let dateString = '';
77
+ * switch (precision) {
78
+ * case 'seconds':
79
+ * dateString = ':' + date.getSeconds().toLocaleString(undefined, { minimumIntegerDigits: 2 });
80
+ * case 'minutes':
81
+ * dateString = ':' + date.getMinutes().toLocaleString(undefined, { minimumIntegerDigits: 2 }) + dateString;
82
+ * case 'hours':
83
+ * dateString = date.getHours().toLocaleString(undefined, { minimumIntegerDigits: 2 }) + dateString;
84
+ * }
85
+ * return dateString;
86
+ * }
87
+ */
88
+ combinedRenderer?: (date: Date, precision: Precision) => React.ReactNode;
64
89
  } & StylingProps;
65
90
  /**
66
91
  * Time picker component
@@ -27,6 +27,24 @@ const isSameMinute = (date1, date2) => {
27
27
  const isSameSecond = (date1, date2) => {
28
28
  return !!date2 && date1.getSeconds() === date2.getSeconds();
29
29
  };
30
+ const isSameTime = (date1, date2, precision, meridiem) => {
31
+ let isSameTime = true;
32
+ switch (precision) {
33
+ case 'seconds':
34
+ isSameTime = isSameSecond(date1, date2);
35
+ if (!isSameTime) {
36
+ break;
37
+ }
38
+ case 'minutes':
39
+ isSameTime = isSameMinute(date1, date2);
40
+ if (!isSameTime) {
41
+ break;
42
+ }
43
+ case 'hours':
44
+ isSameTime = isSameHour(date1, date2, meridiem);
45
+ }
46
+ return isSameTime;
47
+ };
30
48
  const isSameMeridiem = (meridiem, date) => {
31
49
  return (!!date && (meridiem === 'AM' ? date.getHours() < 12 : date.getHours() >= 12));
32
50
  };
@@ -37,13 +55,36 @@ const formatHourFrom12 = (hour, meridiem) => {
37
55
  const setHours = (hour, date) => {
38
56
  return new Date(date.getFullYear(), date.getMonth(), date.getDate(), hour, date.getMinutes(), date.getSeconds());
39
57
  };
58
+ const defaultCombinedRenderer = (date, precision) => {
59
+ let dateString = '';
60
+ switch (precision) {
61
+ case 'seconds':
62
+ dateString =
63
+ ':' +
64
+ date
65
+ .getSeconds()
66
+ .toLocaleString(undefined, { minimumIntegerDigits: 2 });
67
+ case 'minutes':
68
+ dateString =
69
+ ':' +
70
+ date
71
+ .getMinutes()
72
+ .toLocaleString(undefined, { minimumIntegerDigits: 2 }) +
73
+ dateString;
74
+ case 'hours':
75
+ dateString =
76
+ date.getHours().toLocaleString(undefined, { minimumIntegerDigits: 2 }) +
77
+ dateString;
78
+ }
79
+ return dateString;
80
+ };
40
81
  /**
41
82
  * Time picker component
42
83
  * @example
43
84
  * <TimePicker date={new Date()} onChange={(e) => console.log('New time value: ' + e)} />
44
85
  */
45
86
  const TimePicker = (props) => {
46
- const { date, onChange, use12Hours = false, precision = 'minutes', hourStep = 1, minuteStep = 1, secondStep = 1, setFocusHour = false, hourRenderer = (date) => date.getHours().toLocaleString(undefined, { minimumIntegerDigits: 2 }), minuteRenderer = (date) => date.getMinutes().toLocaleString(undefined, { minimumIntegerDigits: 2 }), secondRenderer = (date) => date.getSeconds().toLocaleString(undefined, { minimumIntegerDigits: 2 }), meridiemRenderer = (meridiem) => meridiem, className, ...rest } = props;
87
+ const { date, onChange, use12Hours = false, precision = 'minutes', hourStep = 1, minuteStep = 1, secondStep = 1, setFocusHour = false, hourRenderer = (date) => date.getHours().toLocaleString(undefined, { minimumIntegerDigits: 2 }), minuteRenderer = (date) => date.getMinutes().toLocaleString(undefined, { minimumIntegerDigits: 2 }), secondRenderer = (date) => date.getSeconds().toLocaleString(undefined, { minimumIntegerDigits: 2 }), meridiemRenderer = (meridiem) => meridiem, useCombinedRenderer = false, combinedRenderer = defaultCombinedRenderer, className, ...rest } = props;
47
88
  (0, utils_1.useTheme)();
48
89
  const [selectedTime, setSelectedTime] = react_1.default.useState(date);
49
90
  const [focusedTime, setFocusedTime] = react_1.default.useState(selectedTime !== null && selectedTime !== void 0 ? selectedTime : new Date());
@@ -59,6 +100,13 @@ const TimePicker = (props) => {
59
100
  const adjustedSelectedTime = setHours(adjustedHour, selectedTime !== null && selectedTime !== void 0 ? selectedTime : new Date());
60
101
  updateCurrentTime(adjustedSelectedTime);
61
102
  };
103
+ const onTimeClick = (date) => {
104
+ const adjustedHour = use12Hours
105
+ ? formatHourFrom12(date.getHours(), meridiem)
106
+ : date.getHours();
107
+ const adjustedSelectedTime = setHours(adjustedHour, date);
108
+ updateCurrentTime(adjustedSelectedTime);
109
+ };
62
110
  const onMeridiemClick = (value) => {
63
111
  let adjustedSelectedTime = selectedTime !== null && selectedTime !== void 0 ? selectedTime : new Date();
64
112
  const currentHours = adjustedSelectedTime.getHours();
@@ -90,6 +138,12 @@ const TimePicker = (props) => {
90
138
  : date.getHours();
91
139
  setFocusedTime(setHours(adjustedHour, focusedTime));
92
140
  };
141
+ const onTimeFocus = (date) => {
142
+ const adjustedHour = use12Hours
143
+ ? formatHourFrom12(date.getHours(), meridiem)
144
+ : date.getHours();
145
+ setFocusedTime(setHours(adjustedHour, date));
146
+ };
93
147
  const onMeridiemFocus = (value) => {
94
148
  let adjustedSelectedTime = selectedTime !== null && selectedTime !== void 0 ? selectedTime : new Date();
95
149
  const currentHours = adjustedSelectedTime.getHours();
@@ -105,13 +159,40 @@ const TimePicker = (props) => {
105
159
  };
106
160
  const generateDataList = (size, value, step) => {
107
161
  const data = [];
108
- for (let i = 0; i < size; ++i) {
162
+ for (let i = 0; i < size; i++) {
109
163
  if (i % step === 0) {
110
164
  data.push(value(i));
111
165
  }
112
166
  }
113
167
  return data;
114
168
  };
169
+ const time = react_1.default.useMemo(() => {
170
+ const time = selectedTime !== null && selectedTime !== void 0 ? selectedTime : new Date();
171
+ const data = [];
172
+ const hoursArray = Array.from(Array(use12Hours ? 12 : 24).keys())
173
+ .filter((i) => i % hourStep === 0)
174
+ .map((i) => (use12Hours && i === 0 ? 12 : i));
175
+ const minutesArray = Array.from(Array(60).keys()).filter((i) => i % minuteStep === 0);
176
+ const secondsArray = Array.from(Array(60).keys()).filter((i) => i % secondStep === 0);
177
+ hoursArray.forEach((hour) => {
178
+ if (precision === 'hours') {
179
+ data.push(new Date(time.getFullYear(), time.getMonth(), time.getDate(), hour, time.getMinutes(), time.getSeconds()));
180
+ }
181
+ else {
182
+ minutesArray.forEach((minute) => {
183
+ if (precision === 'minutes') {
184
+ data.push(new Date(time.getFullYear(), time.getMonth(), time.getDate(), hour, minute, time.getSeconds()));
185
+ }
186
+ else {
187
+ secondsArray.forEach((second) => {
188
+ data.push(new Date(time.getFullYear(), time.getMonth(), time.getDate(), hour, minute, second));
189
+ });
190
+ }
191
+ });
192
+ }
193
+ });
194
+ return data;
195
+ }, [hourStep, minuteStep, secondStep, selectedTime, use12Hours, precision]);
115
196
  const hours = react_1.default.useMemo(() => {
116
197
  const time = selectedTime !== null && selectedTime !== void 0 ? selectedTime : new Date();
117
198
  return generateDataList(use12Hours ? 12 : 24, (i) => new Date(time.getFullYear(), time.getMonth(), time.getDate(), use12Hours && i === 0 ? 12 : i, time.getMinutes(), time.getSeconds()), hourStep);
@@ -125,14 +206,15 @@ const TimePicker = (props) => {
125
206
  return generateDataList(60, (i) => new Date(time.getFullYear(), time.getMonth(), time.getDate(), time.getHours(), time.getMinutes(), i), secondStep);
126
207
  }, [secondStep, selectedTime]);
127
208
  return (react_1.default.createElement("div", { className: (0, classnames_1.default)('iui-time-picker', className), ...rest },
128
- react_1.default.createElement(TimePickerColumn, { data: hours, isSameFocused: (val) => isSameHour(val, focusedTime, use12Hours ? meridiem : undefined), isSameSelected: (val) => isSameHour(val, selectedTime, use12Hours ? meridiem : undefined), onFocusChange: onHourFocus, onSelectChange: onHourClick, setFocus: setFocusHour, valueRenderer: hourRenderer }),
129
- precision != 'hours' && (react_1.default.createElement(TimePickerColumn, { data: minutes, isSameFocused: (val) => isSameMinute(val, focusedTime), isSameSelected: (val) => isSameMinute(val, selectedTime), onFocusChange: (date) => setFocusedTime(date), onSelectChange: (date) => updateCurrentTime(date), valueRenderer: minuteRenderer })),
130
- precision == 'seconds' && (react_1.default.createElement(TimePickerColumn, { data: seconds, isSameFocused: (val) => isSameSecond(val, focusedTime), isSameSelected: (val) => isSameSecond(val, selectedTime), onFocusChange: (date) => setFocusedTime(date), onSelectChange: (date) => updateCurrentTime(date), valueRenderer: secondRenderer })),
209
+ useCombinedRenderer ? (react_1.default.createElement(TimePickerColumn, { data: time, isSameFocused: (val) => isSameTime(val, focusedTime, precision, use12Hours ? meridiem : undefined), isSameSelected: (val) => isSameTime(val, selectedTime, precision, use12Hours ? meridiem : undefined), onFocusChange: onTimeFocus, onSelectChange: onTimeClick, setFocus: setFocusHour, precision: precision, valueRenderer: combinedRenderer })) : (react_1.default.createElement(react_1.default.Fragment, null,
210
+ react_1.default.createElement(TimePickerColumn, { data: hours, isSameFocused: (val) => isSameHour(val, focusedTime, use12Hours ? meridiem : undefined), isSameSelected: (val) => isSameHour(val, selectedTime, use12Hours ? meridiem : undefined), onFocusChange: onHourFocus, onSelectChange: onHourClick, setFocus: setFocusHour, valueRenderer: hourRenderer }),
211
+ precision !== 'hours' && (react_1.default.createElement(TimePickerColumn, { data: minutes, isSameFocused: (val) => isSameMinute(val, focusedTime), isSameSelected: (val) => isSameMinute(val, selectedTime), onFocusChange: (date) => setFocusedTime(date), onSelectChange: (date) => updateCurrentTime(date), valueRenderer: minuteRenderer })),
212
+ precision === 'seconds' && (react_1.default.createElement(TimePickerColumn, { data: seconds, isSameFocused: (val) => isSameSecond(val, focusedTime), isSameSelected: (val) => isSameSecond(val, selectedTime), onFocusChange: (date) => setFocusedTime(date), onSelectChange: (date) => updateCurrentTime(date), valueRenderer: secondRenderer })))),
131
213
  use12Hours && (react_1.default.createElement(TimePickerColumn, { data: ['AM', 'PM'], isSameFocused: (val) => isSameMeridiem(val, focusedTime), isSameSelected: (val) => isSameMeridiem(val, selectedTime), onFocusChange: (date) => onMeridiemFocus(date), onSelectChange: (value) => onMeridiemClick(value), valueRenderer: meridiemRenderer, className: 'iui-period' }))));
132
214
  };
133
215
  exports.TimePicker = TimePicker;
134
216
  const TimePickerColumn = (props) => {
135
- const { data, onFocusChange, onSelectChange, isSameFocused, isSameSelected, setFocus = false, valueRenderer, className = 'iui-time', } = props;
217
+ const { data, onFocusChange, onSelectChange, isSameFocused, isSameSelected, setFocus = false, valueRenderer, precision = 'minutes', className = 'iui-time', } = props;
136
218
  const needFocus = react_1.default.useRef(setFocus);
137
219
  // Used to focus row only when changed (keyboard navigation)
138
220
  // e.g. without this on every rerender it would be focused
@@ -182,7 +264,7 @@ const TimePickerColumn = (props) => {
182
264
  needFocus.current && isSameFocus && (ref === null || ref === void 0 ? void 0 : ref.focus());
183
265
  }, onClick: () => {
184
266
  onSelectChange(value);
185
- } }, valueRenderer(value)));
267
+ } }, valueRenderer(value, precision)));
186
268
  }))));
187
269
  };
188
270
  exports.default = exports.TimePicker;
@@ -60,6 +60,8 @@ export { Menu, MenuItem, MenuDivider, MenuExtraContent, MenuItemSkeleton, } from
60
60
  export type { MenuProps, MenuItemProps, MenuDividerProps, MenuExtraContentProps, MenuItemSkeletonProps, } from './Menu';
61
61
  export { Modal, ModalButtonBar, ModalContent } from './Modal';
62
62
  export type { ModalProps, ModalButtonBarProps, ModalContentProps, } from './Modal';
63
+ export { NotificationMarker } from './NotificationMarker';
64
+ export type { NotificationMarkerProps } from './NotificationMarker';
63
65
  export { ProgressLinear, ProgressRadial } from './ProgressIndicators';
64
66
  export type { ProgressLinearProps, ProgressRadialProps, } from './ProgressIndicators';
65
67
  export { Radio } from './Radio';
package/cjs/core/index.js CHANGED
@@ -4,8 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.LabeledInput = exports.Label = exports.Input = exports.InformationPanelContent = exports.InformationPanelBody = exports.InformationPanelHeader = exports.InformationPanelWrapper = exports.InformationPanel = exports.HorizontalTabs = exports.Tab = exports.Tabs = exports.VerticalTabs = exports.HeaderLogo = exports.HeaderButton = exports.HeaderBreadcrumbs = exports.Header = exports.defaultFooterElements = exports.Footer = exports.FileUploadTemplate = exports.FileUpload = exports.Fieldset = exports.ExpandableBlock = exports.NonIdealState = exports.ErrorPage = exports.DropdownMenu = exports.Dialog = exports.generateLocalizedStrings = exports.DatePicker = exports.ComboBox = exports.ColorPalette = exports.ColorInputPanel = exports.ColorBuilder = exports.ColorSwatch = exports.ColorPicker = exports.Checkbox = exports.Carousel = exports.ButtonGroup = exports.SplitButton = exports.IdeasButton = exports.IconButton = exports.DropdownButton = exports.Button = exports.Breadcrumbs = exports.Badge = exports.Backdrop = exports.UserIconGroup = exports.AvatarGroup = exports.UserIcon = exports.Avatar = exports.Alert = void 0;
7
- exports.Headline = exports.Body = exports.Anchor = exports.TreeNodeExpander = exports.TreeNode = exports.Tree = exports.Tooltip = exports.ToggleSwitch = exports.ThemeProvider = exports.toaster = exports.TimePicker = exports.Tile = exports.Textarea = exports.TagContainer = exports.Tag = exports.SelectionColumn = exports.ExpanderColumn = exports.ActionColumn = exports.TablePaginator = exports.EditableCell = exports.DefaultCell = exports.FilterButtonBar = exports.BaseFilter = exports.tableFilters = exports.Table = exports.Surface = exports.StatusMessage = exports.Slider = exports.SkipToContentLink = exports.SidenavSubmenuHeader = exports.SidenavSubmenu = exports.SidenavButton = exports.SideNavigation = exports.Select = exports.RadioTileGroup = exports.RadioTile = exports.Radio = exports.ProgressRadial = exports.ProgressLinear = exports.ModalContent = exports.ModalButtonBar = exports.Modal = exports.MenuItemSkeleton = exports.MenuExtraContent = exports.MenuDivider = exports.MenuItem = exports.Menu = exports.LabeledTextarea = exports.LabeledSelect = exports.InputGroup = void 0;
8
- exports.MiddleTextTruncation = exports.ColorValue = exports.useTheme = exports.getUserColor = exports.WorkflowDiagram = exports.Stepper = exports.Wizard = exports.Text = exports.KbdKeys = exports.Kbd = exports.Code = exports.Blockquote = exports.Title = exports.Subheading = exports.Small = exports.Leading = void 0;
7
+ exports.Body = exports.Anchor = exports.TreeNodeExpander = exports.TreeNode = exports.Tree = exports.Tooltip = exports.ToggleSwitch = exports.ThemeProvider = exports.toaster = exports.TimePicker = exports.Tile = exports.Textarea = exports.TagContainer = exports.Tag = exports.SelectionColumn = exports.ExpanderColumn = exports.ActionColumn = exports.TablePaginator = exports.EditableCell = exports.DefaultCell = exports.FilterButtonBar = exports.BaseFilter = exports.tableFilters = exports.Table = exports.Surface = exports.StatusMessage = exports.Slider = exports.SkipToContentLink = exports.SidenavSubmenuHeader = exports.SidenavSubmenu = exports.SidenavButton = exports.SideNavigation = exports.Select = exports.RadioTileGroup = exports.RadioTile = exports.Radio = exports.ProgressRadial = exports.ProgressLinear = exports.NotificationMarker = exports.ModalContent = exports.ModalButtonBar = exports.Modal = exports.MenuItemSkeleton = exports.MenuExtraContent = exports.MenuDivider = exports.MenuItem = exports.Menu = exports.LabeledTextarea = exports.LabeledSelect = exports.InputGroup = void 0;
8
+ exports.MiddleTextTruncation = exports.ColorValue = exports.useTheme = exports.getUserColor = exports.WorkflowDiagram = exports.Stepper = exports.Wizard = exports.Text = exports.KbdKeys = exports.Kbd = exports.Code = exports.Blockquote = exports.Title = exports.Subheading = exports.Small = exports.Leading = exports.Headline = void 0;
9
9
  /*---------------------------------------------------------------------------------------------
10
10
  * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
11
11
  * See LICENSE.md in the project root for license terms and full copyright notice.
@@ -102,6 +102,8 @@ var Modal_1 = require("./Modal");
102
102
  Object.defineProperty(exports, "Modal", { enumerable: true, get: function () { return Modal_1.Modal; } });
103
103
  Object.defineProperty(exports, "ModalButtonBar", { enumerable: true, get: function () { return Modal_1.ModalButtonBar; } });
104
104
  Object.defineProperty(exports, "ModalContent", { enumerable: true, get: function () { return Modal_1.ModalContent; } });
105
+ var NotificationMarker_1 = require("./NotificationMarker");
106
+ Object.defineProperty(exports, "NotificationMarker", { enumerable: true, get: function () { return NotificationMarker_1.NotificationMarker; } });
105
107
  var ProgressIndicators_1 = require("./ProgressIndicators");
106
108
  Object.defineProperty(exports, "ProgressLinear", { enumerable: true, get: function () { return ProgressIndicators_1.ProgressLinear; } });
107
109
  Object.defineProperty(exports, "ProgressRadial", { enumerable: true, get: function () { return ProgressIndicators_1.ProgressRadial; } });
@@ -123,7 +123,7 @@ export const generateLocalizedStrings = (locale) => {
123
123
  */
124
124
  export const DatePicker = (props) => {
125
125
  var _a, _b, _c, _d, _e, _f, _g, _h;
126
- const { date, onChange, localizedNames, className, style, setFocus = false, showTime = false, use12Hours = false, precision, hourStep, minuteStep, secondStep, showYearSelection = false, enableRangeSelect = false, startDate, endDate, ...rest } = props;
126
+ const { date, onChange, localizedNames, className, style, setFocus = false, showTime = false, use12Hours = false, precision, hourStep, minuteStep, secondStep, useCombinedRenderer, combinedRenderer, hourRenderer, minuteRenderer, secondRenderer, meridiemRenderer, showYearSelection = false, enableRangeSelect = false, startDate, endDate, ...rest } = props;
127
127
  useTheme();
128
128
  const monthNames = (_a = localizedNames === null || localizedNames === void 0 ? void 0 : localizedNames.months) !== null && _a !== void 0 ? _a : defaultMonths;
129
129
  const shortDays = (_b = localizedNames === null || localizedNames === void 0 ? void 0 : localizedNames.shortDays) !== null && _b !== void 0 ? _b : defaultShortDays;
@@ -361,7 +361,7 @@ export const DatePicker = (props) => {
361
361
  (element === null || element === void 0 ? void 0 : element.focus()) }, dateValue));
362
362
  })));
363
363
  }))),
364
- showTime && (React.createElement(TimePicker, { date: selectedStartDay !== null && selectedStartDay !== void 0 ? selectedStartDay : selectedDay, use12Hours: use12Hours, precision: precision, hourStep: hourStep, minuteStep: minuteStep, secondStep: secondStep, onChange: (date) => {
364
+ showTime && (React.createElement(TimePicker, { date: selectedStartDay !== null && selectedStartDay !== void 0 ? selectedStartDay : selectedDay, use12Hours: use12Hours, precision: precision, hourStep: hourStep, minuteStep: minuteStep, secondStep: secondStep, useCombinedRenderer: useCombinedRenderer, combinedRenderer: combinedRenderer, hourRenderer: hourRenderer, minuteRenderer: minuteRenderer, secondRenderer: secondRenderer, meridiemRenderer: meridiemRenderer, onChange: (date) => {
365
365
  var _a, _b, _c, _d, _e, _f;
366
366
  return isSingleOnChange(onChange, enableRangeSelect)
367
367
  ? onChange === null || onChange === void 0 ? void 0 : onChange(date)
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import '@itwin/itwinui-css/css/utils.css';
3
+ export declare type NotificationMarkerProps = {
4
+ /**
5
+ * Content of the NotificationMarker.
6
+ */
7
+ children: React.ReactNode;
8
+ /**
9
+ * Status of notification
10
+ *
11
+ * - 'primary' = blue,
12
+ * - 'positive' = green,
13
+ * - 'warning' = orange,
14
+ * - 'negative' = red,
15
+ * - 'white' = white (useful for notifications in buttons with `style` = `high-visibility` | `cta`),
16
+ * @default 'primary'
17
+ */
18
+ status?: 'primary' | 'positive' | 'warning' | 'negative' | 'white';
19
+ /**
20
+ * Adds a pulse effect to the notification.
21
+ *
22
+ * **WARNING**: Avoid overuse of this prop.
23
+ * @default false
24
+ */
25
+ pulsing?: boolean;
26
+ /**
27
+ * Set this programmatically to false when you just want to render the passed children without the notification
28
+ * @default true
29
+ * @example
30
+ * let [newMessagesCount, ...] = useState(0);
31
+ * ...
32
+ * <NotificationMarker enabled={newMessagesCount > 0}>
33
+ * <SvgNotification />
34
+ * </NotificationMarker>
35
+ */
36
+ enabled?: boolean;
37
+ } & React.ComponentProps<'span'>;
38
+ /**
39
+ * A small notification circle to the top-right of the passed children prop.
40
+ * This can be applied to almost anything but mostly intended for icons within buttons with `styleType = default / borderless`.
41
+ * @example
42
+ * <IconButton styleType='borderless'>
43
+ * <NotificationMarker>
44
+ * <SvgNotification />
45
+ * </NotificationMarker>
46
+ * </IconButton>
47
+ * @example
48
+ * <NotificationMarker status='positive' pulsing={true}>Live</NotificationMarker>
49
+ */
50
+ export declare const NotificationMarker: React.ForwardRefExoticComponent<Pick<NotificationMarkerProps, "key" | "status" | keyof React.HTMLAttributes<HTMLSpanElement> | "enabled" | "pulsing"> & React.RefAttributes<HTMLSpanElement>>;
51
+ export default NotificationMarker;
@@ -0,0 +1,26 @@
1
+ /*---------------------------------------------------------------------------------------------
2
+ * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
3
+ * See LICENSE.md in the project root for license terms and full copyright notice.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ import React from 'react';
6
+ import { useTheme } from '../utils';
7
+ import cx from 'classnames';
8
+ import '@itwin/itwinui-css/css/utils.css';
9
+ /**
10
+ * A small notification circle to the top-right of the passed children prop.
11
+ * This can be applied to almost anything but mostly intended for icons within buttons with `styleType = default / borderless`.
12
+ * @example
13
+ * <IconButton styleType='borderless'>
14
+ * <NotificationMarker>
15
+ * <SvgNotification />
16
+ * </NotificationMarker>
17
+ * </IconButton>
18
+ * @example
19
+ * <NotificationMarker status='positive' pulsing={true}>Live</NotificationMarker>
20
+ */
21
+ export const NotificationMarker = React.forwardRef((props, ref) => {
22
+ const { className, children, status = 'primary', pulsing = false, enabled = true, ...rest } = props;
23
+ useTheme();
24
+ return (React.createElement("span", { ref: ref, className: cx({ 'iui-notification-marker': enabled }, className), "data-iui-variant": enabled ? status : null, "data-iui-urgent": enabled ? pulsing : null, ...rest }, children));
25
+ });
26
+ export default NotificationMarker;
@@ -0,0 +1,4 @@
1
+ export { NotificationMarker } from './NotificationMarker';
2
+ export type { NotificationMarkerProps } from './NotificationMarker';
3
+ declare const _default: "./NotificationMarker";
4
+ export default _default;
@@ -0,0 +1,6 @@
1
+ /*---------------------------------------------------------------------------------------------
2
+ * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
3
+ * See LICENSE.md in the project root for license terms and full copyright notice.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ export { NotificationMarker } from './NotificationMarker';
6
+ export default './NotificationMarker';
@@ -117,7 +117,7 @@ export declare type TableProps<T extends Record<string, unknown> = Record<string
117
117
  * Use with `manualFilters` to handle filtering yourself e.g. filter in server-side.
118
118
  * Must be memoized.
119
119
  */
120
- onFilter?: (filters: TableFilterValue<T>[], state: TableState<T>) => void;
120
+ onFilter?: (filters: TableFilterValue<T>[], state: TableState<T>, filteredData?: Row<T>[]) => void;
121
121
  /**
122
122
  * Value used for global filtering.
123
123
  * Use with `globalFilter` and/or `manualGlobalFilter` to handle filtering yourself e.g. filter in server-side.
@@ -117,13 +117,15 @@ export const Table = (props) => {
117
117
  disableUserSelect,
118
118
  enableUserSelect,
119
119
  ]);
120
+ const previousFilter = React.useRef([]);
121
+ const currentFilter = React.useRef(previousFilter.current);
120
122
  const tableStateReducer = React.useCallback((newState, action, previousState, instance) => {
121
123
  switch (action.type) {
122
124
  case TableActions.toggleSortBy:
123
125
  onSort === null || onSort === void 0 ? void 0 : onSort(newState);
124
126
  break;
125
127
  case TableActions.setFilter:
126
- onFilterHandler(newState, action, previousState, instance, onFilter);
128
+ currentFilter.current = onFilterHandler(newState, action, previousState, currentFilter.current, instance);
127
129
  break;
128
130
  case TableActions.toggleRowExpanded:
129
131
  case TableActions.toggleAllRowsExpanded:
@@ -167,7 +169,6 @@ export const Table = (props) => {
167
169
  hasManualSelectionColumn,
168
170
  isRowDisabled,
169
171
  onExpand,
170
- onFilter,
171
172
  onSelect,
172
173
  onSort,
173
174
  stateReducer,
@@ -243,6 +244,22 @@ export const Table = (props) => {
243
244
  React.useEffect(() => {
244
245
  setPageSize(pageSize);
245
246
  }, [pageSize, setPageSize]);
247
+ React.useEffect(() => {
248
+ if (previousFilter.current !== currentFilter.current) {
249
+ previousFilter.current = currentFilter.current;
250
+ onFilter === null || onFilter === void 0 ? void 0 : onFilter(currentFilter.current, state, instance.filteredRows);
251
+ }
252
+ }, [state, instance.filteredRows, onFilter]);
253
+ const lastPassedColumns = React.useRef([]);
254
+ // Reset the column order whenever new columns are passed
255
+ // This is to avoid the old columnOrder from affecting the new columns' columnOrder
256
+ React.useEffect(() => {
257
+ // Check if columns have changed (by value)
258
+ if (JSON.stringify(lastPassedColumns.current) !== JSON.stringify(columns)) {
259
+ instance.setColumnOrder([]);
260
+ }
261
+ lastPassedColumns.current = columns;
262
+ }, [columns, instance]);
246
263
  const paginatorRendererProps = React.useMemo(() => ({
247
264
  currentPage: state.pageIndex,
248
265
  pageSize: state.pageSize,
@@ -1,3 +1,3 @@
1
1
  import { ActionType, TableInstance, TableState } from 'react-table';
2
2
  import { TableFilterValue } from '../filters';
3
- export declare const onFilterHandler: <T extends Record<string, unknown>>(newState: TableState<T>, action: ActionType, previousState: TableState<T>, instance?: TableInstance<T> | undefined, onFilter?: ((filters: TableFilterValue<T>[], state: TableState<T>) => void) | undefined) => void;
3
+ export declare const onFilterHandler: <T extends Record<string, unknown>>(newState: TableState<T>, action: ActionType, previousState: TableState<T>, currentFilter: TableFilterValue<T>[], instance?: TableInstance<T> | undefined) => TableFilterValue<T>[];
@@ -1,4 +1,4 @@
1
- export const onFilterHandler = (newState, action, previousState, instance, onFilter) => {
1
+ export const onFilterHandler = (newState, action, previousState, currentFilter, instance) => {
2
2
  const previousFilter = previousState.filters.find((f) => f.id === action.columnId);
3
3
  if ((previousFilter === null || previousFilter === void 0 ? void 0 : previousFilter.value) != action.filterValue) {
4
4
  const filters = newState.filters.map((f) => {
@@ -11,6 +11,7 @@ export const onFilterHandler = (newState, action, previousState, instance, onFil
11
11
  filterType: (_b = column === null || column === void 0 ? void 0 : column.filter) !== null && _b !== void 0 ? _b : 'text',
12
12
  };
13
13
  });
14
- onFilter === null || onFilter === void 0 ? void 0 : onFilter(filters, newState);
14
+ return filters;
15
15
  }
16
+ return currentFilter;
16
17
  };
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { StylingProps } from '../utils';
3
3
  import '@itwin/itwinui-css/css/time-picker.css';
4
4
  export declare type MeridiemType = 'AM' | 'PM';
5
+ export declare type Precision = 'hours' | 'minutes' | 'seconds';
5
6
  export declare type TimePickerProps = {
6
7
  /**
7
8
  * Selected date. Only time is used from Date object.
@@ -20,7 +21,7 @@ export declare type TimePickerProps = {
20
21
  * Precision of the time.
21
22
  * @default 'minutes'
22
23
  */
23
- precision?: 'hours' | 'minutes' | 'seconds';
24
+ precision?: Precision;
24
25
  /**
25
26
  * Change step of the hours displayed.
26
27
  * @default 1
@@ -61,6 +62,30 @@ export declare type TimePickerProps = {
61
62
  * @default (meridiem: MeridiemType) => meridiem
62
63
  */
63
64
  meridiemRenderer?: (meridiem: MeridiemType) => React.ReactNode;
65
+ /**
66
+ * Use combined time renderer. Combines hour, minute, and seconds into one column.
67
+ * **WARNING**: Using the combined renderer with a `precision` of 'seconds' along with
68
+ * small time steps (`hourStep`, `minuteStep`, and especially `secondStep`) can result in slow performance!
69
+ * @default false
70
+ */
71
+ useCombinedRenderer?: boolean;
72
+ /**
73
+ * Custom combined time renderer.
74
+ * Default returns time in `HH:MM:SS` format
75
+ * @default (date: Date, precision: Precision) => {
76
+ * let dateString = '';
77
+ * switch (precision) {
78
+ * case 'seconds':
79
+ * dateString = ':' + date.getSeconds().toLocaleString(undefined, { minimumIntegerDigits: 2 });
80
+ * case 'minutes':
81
+ * dateString = ':' + date.getMinutes().toLocaleString(undefined, { minimumIntegerDigits: 2 }) + dateString;
82
+ * case 'hours':
83
+ * dateString = date.getHours().toLocaleString(undefined, { minimumIntegerDigits: 2 }) + dateString;
84
+ * }
85
+ * return dateString;
86
+ * }
87
+ */
88
+ combinedRenderer?: (date: Date, precision: Precision) => React.ReactNode;
64
89
  } & StylingProps;
65
90
  /**
66
91
  * Time picker component
@@ -21,6 +21,24 @@ const isSameMinute = (date1, date2) => {
21
21
  const isSameSecond = (date1, date2) => {
22
22
  return !!date2 && date1.getSeconds() === date2.getSeconds();
23
23
  };
24
+ const isSameTime = (date1, date2, precision, meridiem) => {
25
+ let isSameTime = true;
26
+ switch (precision) {
27
+ case 'seconds':
28
+ isSameTime = isSameSecond(date1, date2);
29
+ if (!isSameTime) {
30
+ break;
31
+ }
32
+ case 'minutes':
33
+ isSameTime = isSameMinute(date1, date2);
34
+ if (!isSameTime) {
35
+ break;
36
+ }
37
+ case 'hours':
38
+ isSameTime = isSameHour(date1, date2, meridiem);
39
+ }
40
+ return isSameTime;
41
+ };
24
42
  const isSameMeridiem = (meridiem, date) => {
25
43
  return (!!date && (meridiem === 'AM' ? date.getHours() < 12 : date.getHours() >= 12));
26
44
  };
@@ -31,13 +49,36 @@ const formatHourFrom12 = (hour, meridiem) => {
31
49
  const setHours = (hour, date) => {
32
50
  return new Date(date.getFullYear(), date.getMonth(), date.getDate(), hour, date.getMinutes(), date.getSeconds());
33
51
  };
52
+ const defaultCombinedRenderer = (date, precision) => {
53
+ let dateString = '';
54
+ switch (precision) {
55
+ case 'seconds':
56
+ dateString =
57
+ ':' +
58
+ date
59
+ .getSeconds()
60
+ .toLocaleString(undefined, { minimumIntegerDigits: 2 });
61
+ case 'minutes':
62
+ dateString =
63
+ ':' +
64
+ date
65
+ .getMinutes()
66
+ .toLocaleString(undefined, { minimumIntegerDigits: 2 }) +
67
+ dateString;
68
+ case 'hours':
69
+ dateString =
70
+ date.getHours().toLocaleString(undefined, { minimumIntegerDigits: 2 }) +
71
+ dateString;
72
+ }
73
+ return dateString;
74
+ };
34
75
  /**
35
76
  * Time picker component
36
77
  * @example
37
78
  * <TimePicker date={new Date()} onChange={(e) => console.log('New time value: ' + e)} />
38
79
  */
39
80
  export const TimePicker = (props) => {
40
- const { date, onChange, use12Hours = false, precision = 'minutes', hourStep = 1, minuteStep = 1, secondStep = 1, setFocusHour = false, hourRenderer = (date) => date.getHours().toLocaleString(undefined, { minimumIntegerDigits: 2 }), minuteRenderer = (date) => date.getMinutes().toLocaleString(undefined, { minimumIntegerDigits: 2 }), secondRenderer = (date) => date.getSeconds().toLocaleString(undefined, { minimumIntegerDigits: 2 }), meridiemRenderer = (meridiem) => meridiem, className, ...rest } = props;
81
+ const { date, onChange, use12Hours = false, precision = 'minutes', hourStep = 1, minuteStep = 1, secondStep = 1, setFocusHour = false, hourRenderer = (date) => date.getHours().toLocaleString(undefined, { minimumIntegerDigits: 2 }), minuteRenderer = (date) => date.getMinutes().toLocaleString(undefined, { minimumIntegerDigits: 2 }), secondRenderer = (date) => date.getSeconds().toLocaleString(undefined, { minimumIntegerDigits: 2 }), meridiemRenderer = (meridiem) => meridiem, useCombinedRenderer = false, combinedRenderer = defaultCombinedRenderer, className, ...rest } = props;
41
82
  useTheme();
42
83
  const [selectedTime, setSelectedTime] = React.useState(date);
43
84
  const [focusedTime, setFocusedTime] = React.useState(selectedTime !== null && selectedTime !== void 0 ? selectedTime : new Date());
@@ -53,6 +94,13 @@ export const TimePicker = (props) => {
53
94
  const adjustedSelectedTime = setHours(adjustedHour, selectedTime !== null && selectedTime !== void 0 ? selectedTime : new Date());
54
95
  updateCurrentTime(adjustedSelectedTime);
55
96
  };
97
+ const onTimeClick = (date) => {
98
+ const adjustedHour = use12Hours
99
+ ? formatHourFrom12(date.getHours(), meridiem)
100
+ : date.getHours();
101
+ const adjustedSelectedTime = setHours(adjustedHour, date);
102
+ updateCurrentTime(adjustedSelectedTime);
103
+ };
56
104
  const onMeridiemClick = (value) => {
57
105
  let adjustedSelectedTime = selectedTime !== null && selectedTime !== void 0 ? selectedTime : new Date();
58
106
  const currentHours = adjustedSelectedTime.getHours();
@@ -84,6 +132,12 @@ export const TimePicker = (props) => {
84
132
  : date.getHours();
85
133
  setFocusedTime(setHours(adjustedHour, focusedTime));
86
134
  };
135
+ const onTimeFocus = (date) => {
136
+ const adjustedHour = use12Hours
137
+ ? formatHourFrom12(date.getHours(), meridiem)
138
+ : date.getHours();
139
+ setFocusedTime(setHours(adjustedHour, date));
140
+ };
87
141
  const onMeridiemFocus = (value) => {
88
142
  let adjustedSelectedTime = selectedTime !== null && selectedTime !== void 0 ? selectedTime : new Date();
89
143
  const currentHours = adjustedSelectedTime.getHours();
@@ -99,13 +153,40 @@ export const TimePicker = (props) => {
99
153
  };
100
154
  const generateDataList = (size, value, step) => {
101
155
  const data = [];
102
- for (let i = 0; i < size; ++i) {
156
+ for (let i = 0; i < size; i++) {
103
157
  if (i % step === 0) {
104
158
  data.push(value(i));
105
159
  }
106
160
  }
107
161
  return data;
108
162
  };
163
+ const time = React.useMemo(() => {
164
+ const time = selectedTime !== null && selectedTime !== void 0 ? selectedTime : new Date();
165
+ const data = [];
166
+ const hoursArray = Array.from(Array(use12Hours ? 12 : 24).keys())
167
+ .filter((i) => i % hourStep === 0)
168
+ .map((i) => (use12Hours && i === 0 ? 12 : i));
169
+ const minutesArray = Array.from(Array(60).keys()).filter((i) => i % minuteStep === 0);
170
+ const secondsArray = Array.from(Array(60).keys()).filter((i) => i % secondStep === 0);
171
+ hoursArray.forEach((hour) => {
172
+ if (precision === 'hours') {
173
+ data.push(new Date(time.getFullYear(), time.getMonth(), time.getDate(), hour, time.getMinutes(), time.getSeconds()));
174
+ }
175
+ else {
176
+ minutesArray.forEach((minute) => {
177
+ if (precision === 'minutes') {
178
+ data.push(new Date(time.getFullYear(), time.getMonth(), time.getDate(), hour, minute, time.getSeconds()));
179
+ }
180
+ else {
181
+ secondsArray.forEach((second) => {
182
+ data.push(new Date(time.getFullYear(), time.getMonth(), time.getDate(), hour, minute, second));
183
+ });
184
+ }
185
+ });
186
+ }
187
+ });
188
+ return data;
189
+ }, [hourStep, minuteStep, secondStep, selectedTime, use12Hours, precision]);
109
190
  const hours = React.useMemo(() => {
110
191
  const time = selectedTime !== null && selectedTime !== void 0 ? selectedTime : new Date();
111
192
  return generateDataList(use12Hours ? 12 : 24, (i) => new Date(time.getFullYear(), time.getMonth(), time.getDate(), use12Hours && i === 0 ? 12 : i, time.getMinutes(), time.getSeconds()), hourStep);
@@ -119,13 +200,14 @@ export const TimePicker = (props) => {
119
200
  return generateDataList(60, (i) => new Date(time.getFullYear(), time.getMonth(), time.getDate(), time.getHours(), time.getMinutes(), i), secondStep);
120
201
  }, [secondStep, selectedTime]);
121
202
  return (React.createElement("div", { className: cx('iui-time-picker', className), ...rest },
122
- React.createElement(TimePickerColumn, { data: hours, isSameFocused: (val) => isSameHour(val, focusedTime, use12Hours ? meridiem : undefined), isSameSelected: (val) => isSameHour(val, selectedTime, use12Hours ? meridiem : undefined), onFocusChange: onHourFocus, onSelectChange: onHourClick, setFocus: setFocusHour, valueRenderer: hourRenderer }),
123
- precision != 'hours' && (React.createElement(TimePickerColumn, { data: minutes, isSameFocused: (val) => isSameMinute(val, focusedTime), isSameSelected: (val) => isSameMinute(val, selectedTime), onFocusChange: (date) => setFocusedTime(date), onSelectChange: (date) => updateCurrentTime(date), valueRenderer: minuteRenderer })),
124
- precision == 'seconds' && (React.createElement(TimePickerColumn, { data: seconds, isSameFocused: (val) => isSameSecond(val, focusedTime), isSameSelected: (val) => isSameSecond(val, selectedTime), onFocusChange: (date) => setFocusedTime(date), onSelectChange: (date) => updateCurrentTime(date), valueRenderer: secondRenderer })),
203
+ useCombinedRenderer ? (React.createElement(TimePickerColumn, { data: time, isSameFocused: (val) => isSameTime(val, focusedTime, precision, use12Hours ? meridiem : undefined), isSameSelected: (val) => isSameTime(val, selectedTime, precision, use12Hours ? meridiem : undefined), onFocusChange: onTimeFocus, onSelectChange: onTimeClick, setFocus: setFocusHour, precision: precision, valueRenderer: combinedRenderer })) : (React.createElement(React.Fragment, null,
204
+ React.createElement(TimePickerColumn, { data: hours, isSameFocused: (val) => isSameHour(val, focusedTime, use12Hours ? meridiem : undefined), isSameSelected: (val) => isSameHour(val, selectedTime, use12Hours ? meridiem : undefined), onFocusChange: onHourFocus, onSelectChange: onHourClick, setFocus: setFocusHour, valueRenderer: hourRenderer }),
205
+ precision !== 'hours' && (React.createElement(TimePickerColumn, { data: minutes, isSameFocused: (val) => isSameMinute(val, focusedTime), isSameSelected: (val) => isSameMinute(val, selectedTime), onFocusChange: (date) => setFocusedTime(date), onSelectChange: (date) => updateCurrentTime(date), valueRenderer: minuteRenderer })),
206
+ precision === 'seconds' && (React.createElement(TimePickerColumn, { data: seconds, isSameFocused: (val) => isSameSecond(val, focusedTime), isSameSelected: (val) => isSameSecond(val, selectedTime), onFocusChange: (date) => setFocusedTime(date), onSelectChange: (date) => updateCurrentTime(date), valueRenderer: secondRenderer })))),
125
207
  use12Hours && (React.createElement(TimePickerColumn, { data: ['AM', 'PM'], isSameFocused: (val) => isSameMeridiem(val, focusedTime), isSameSelected: (val) => isSameMeridiem(val, selectedTime), onFocusChange: (date) => onMeridiemFocus(date), onSelectChange: (value) => onMeridiemClick(value), valueRenderer: meridiemRenderer, className: 'iui-period' }))));
126
208
  };
127
209
  const TimePickerColumn = (props) => {
128
- const { data, onFocusChange, onSelectChange, isSameFocused, isSameSelected, setFocus = false, valueRenderer, className = 'iui-time', } = props;
210
+ const { data, onFocusChange, onSelectChange, isSameFocused, isSameSelected, setFocus = false, valueRenderer, precision = 'minutes', className = 'iui-time', } = props;
129
211
  const needFocus = React.useRef(setFocus);
130
212
  // Used to focus row only when changed (keyboard navigation)
131
213
  // e.g. without this on every rerender it would be focused
@@ -175,7 +257,7 @@ const TimePickerColumn = (props) => {
175
257
  needFocus.current && isSameFocus && (ref === null || ref === void 0 ? void 0 : ref.focus());
176
258
  }, onClick: () => {
177
259
  onSelectChange(value);
178
- } }, valueRenderer(value)));
260
+ } }, valueRenderer(value, precision)));
179
261
  }))));
180
262
  };
181
263
  export default TimePicker;
@@ -60,6 +60,8 @@ export { Menu, MenuItem, MenuDivider, MenuExtraContent, MenuItemSkeleton, } from
60
60
  export type { MenuProps, MenuItemProps, MenuDividerProps, MenuExtraContentProps, MenuItemSkeletonProps, } from './Menu';
61
61
  export { Modal, ModalButtonBar, ModalContent } from './Modal';
62
62
  export type { ModalProps, ModalButtonBarProps, ModalContentProps, } from './Modal';
63
+ export { NotificationMarker } from './NotificationMarker';
64
+ export type { NotificationMarkerProps } from './NotificationMarker';
63
65
  export { ProgressLinear, ProgressRadial } from './ProgressIndicators';
64
66
  export type { ProgressLinearProps, ProgressRadialProps, } from './ProgressIndicators';
65
67
  export { Radio } from './Radio';
package/esm/core/index.js CHANGED
@@ -33,6 +33,7 @@ export { LabeledSelect } from './LabeledSelect';
33
33
  export { LabeledTextarea } from './LabeledTextarea';
34
34
  export { Menu, MenuItem, MenuDivider, MenuExtraContent, MenuItemSkeleton, } from './Menu';
35
35
  export { Modal, ModalButtonBar, ModalContent } from './Modal';
36
+ export { NotificationMarker } from './NotificationMarker';
36
37
  export { ProgressLinear, ProgressRadial } from './ProgressIndicators';
37
38
  export { Radio } from './Radio';
38
39
  export { RadioTile, RadioTileGroup } from './RadioTiles';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itwin/itwinui-react",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "author": "Bentley Systems",
5
5
  "license": "MIT",
6
6
  "main": "cjs/index.js",