@itwin/itwinui-react 2.9.1 → 2.10.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/cjs/core/Buttons/IconButton/IconButton.d.ts +11 -4
  3. package/cjs/core/Buttons/IconButton/IconButton.js +13 -5
  4. package/cjs/core/Carousel/Carousel.d.ts +2 -0
  5. package/cjs/core/Carousel/CarouselNavigation.d.ts +2 -0
  6. package/cjs/core/DatePicker/DatePicker.js +2 -13
  7. package/cjs/core/FileUpload/FileUploadCard.js +15 -4
  8. package/cjs/core/LabeledInput/LabeledInput.js +4 -3
  9. package/cjs/core/LabeledSelect/LabeledSelect.js +7 -3
  10. package/cjs/core/LabeledTextarea/LabeledTextarea.js +4 -3
  11. package/cjs/core/Select/Select.d.ts +4 -0
  12. package/cjs/core/Select/Select.js +27 -29
  13. package/cjs/core/Table/filters/DateRangeFilter/DatePickerInput.d.ts +8 -0
  14. package/cjs/core/Table/filters/DateRangeFilter/DatePickerInput.js +10 -2
  15. package/cjs/core/Table/filters/DateRangeFilter/DateRangeFilter.js +2 -2
  16. package/cjs/core/utils/components/InputContainer.d.ts +2 -0
  17. package/cjs/core/utils/components/InputContainer.js +4 -3
  18. package/cjs/core/utils/functions/date.d.ts +4 -0
  19. package/cjs/core/utils/functions/date.js +21 -0
  20. package/cjs/core/utils/functions/index.d.ts +1 -0
  21. package/cjs/core/utils/functions/index.js +1 -0
  22. package/cjs/core/utils/hooks/useId.d.ts +3 -3
  23. package/cjs/core/utils/hooks/useId.js +8 -5
  24. package/esm/core/Buttons/IconButton/IconButton.d.ts +11 -4
  25. package/esm/core/Buttons/IconButton/IconButton.js +14 -6
  26. package/esm/core/Carousel/Carousel.d.ts +2 -0
  27. package/esm/core/Carousel/CarouselNavigation.d.ts +2 -0
  28. package/esm/core/DatePicker/DatePicker.js +1 -12
  29. package/esm/core/FileUpload/FileUploadCard.js +15 -4
  30. package/esm/core/LabeledInput/LabeledInput.js +5 -4
  31. package/esm/core/LabeledSelect/LabeledSelect.js +8 -4
  32. package/esm/core/LabeledTextarea/LabeledTextarea.js +5 -4
  33. package/esm/core/Select/Select.d.ts +4 -0
  34. package/esm/core/Select/Select.js +28 -30
  35. package/esm/core/Table/filters/DateRangeFilter/DatePickerInput.d.ts +8 -0
  36. package/esm/core/Table/filters/DateRangeFilter/DatePickerInput.js +11 -3
  37. package/esm/core/Table/filters/DateRangeFilter/DateRangeFilter.js +2 -2
  38. package/esm/core/utils/components/InputContainer.d.ts +2 -0
  39. package/esm/core/utils/components/InputContainer.js +4 -3
  40. package/esm/core/utils/functions/date.d.ts +4 -0
  41. package/esm/core/utils/functions/date.js +17 -0
  42. package/esm/core/utils/functions/index.d.ts +1 -0
  43. package/esm/core/utils/functions/index.js +1 -0
  44. package/esm/core/utils/hooks/useId.d.ts +3 -3
  45. package/esm/core/utils/hooks/useId.js +7 -5
  46. package/package.json +7 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1231](https://github.com/iTwin/iTwinUI/pull/1231): Added `label` prop to `IconButton`. The label will be displayed visually as a tooltip when the button is hovered/focused and will be exposed as the button's accessible name.
8
+ - [#1211](https://github.com/iTwin/iTwinUI/pull/1211): The date range picker used in date filters now prevents users from picking a start date later than the end date or picking an end date earlier than the start date.
9
+ - [#1238](https://github.com/iTwin/iTwinUI/pull/1238): `Select` will now render a `<div role=combobox>` instead of a generic `<div>` for the trigger element. This element now also supports passing DOM props/attributes using `triggerProps`.
10
+
11
+ `LabeledSelect` will correctly associate the label with select's trigger using `aria-labelledby`.
12
+
13
+ - [#1235](https://github.com/iTwin/iTwinUI/pull/1235): Changed the internal DOM structure of `LabeledInput` and `LabeledTextarea` to prefer explicit association over implicit. The `<label>` is now associated with the input using `htmlFor`/`id` and the container is a generic div.
14
+
15
+ This change improves accessibility, with no API changes and no effect on visuals.
16
+
17
+ ### Patch Changes
18
+
19
+ - [#1232](https://github.com/iTwin/iTwinUI/pull/1232): `IconButton` will now set `aria-pressed` attribute when `isActive` prop is used.
20
+ - [#1236](https://github.com/iTwin/iTwinUI/pull/1236): Fixed an issue in FileUploadCard where setState was called during render.
21
+ - [#1238](https://github.com/iTwin/iTwinUI/pull/1238): Fixed an issue in Select where `popoverProps.visible` was not being respected.
22
+ - Updated dependencies:
23
+ - @itwin/itwinui-css@1.10.2
24
+
3
25
  ## 2.9.1
4
26
 
5
27
  ### Patch Changes
@@ -1,19 +1,26 @@
1
- import { ButtonProps } from '../Button';
2
- import { PolymorphicForwardRefComponent } from '../../utils';
1
+ import React from 'react';
2
+ import type { ButtonProps } from '../Button';
3
+ import type { PolymorphicForwardRefComponent } from '../../utils';
3
4
  import '@itwin/itwinui-css/css/button.css';
5
+ import '@itwin/itwinui-css/css/tooltip.css';
4
6
  export declare type IconButtonProps = {
5
7
  /**
6
8
  * Button gets active style.
7
9
  * @default false
8
10
  */
9
11
  isActive?: boolean;
12
+ /**
13
+ * Name of the button, shown in a tooltip and exposed to assistive technologies.
14
+ */
15
+ label?: React.ReactNode;
10
16
  } & Omit<ButtonProps, 'startIcon' | 'endIcon'>;
11
17
  declare type IconButtonComponent = PolymorphicForwardRefComponent<'button', IconButtonProps>;
12
18
  /**
13
19
  * Icon button
14
20
  * @example
15
- * <IconButton><SvgAdd /></IconButton>
16
- * <IconButton styleType='borderless'><SvgAdd /></IconButton>
21
+ * <IconButton label='Add'><SvgAdd /></IconButton>
22
+ * @example
23
+ * <IconButton label='Add' styleType='borderless'><SvgAdd /></IconButton>
17
24
  */
18
25
  export declare const IconButton: IconButtonComponent;
19
26
  export default IconButton;
@@ -12,16 +12,24 @@ const classnames_1 = __importDefault(require("classnames"));
12
12
  const react_1 = __importDefault(require("react"));
13
13
  const utils_1 = require("../../utils");
14
14
  require("@itwin/itwinui-css/css/button.css");
15
+ require("@itwin/itwinui-css/css/tooltip.css");
15
16
  /**
16
17
  * Icon button
17
18
  * @example
18
- * <IconButton><SvgAdd /></IconButton>
19
- * <IconButton styleType='borderless'><SvgAdd /></IconButton>
19
+ * <IconButton label='Add'><SvgAdd /></IconButton>
20
+ * @example
21
+ * <IconButton label='Add' styleType='borderless'><SvgAdd /></IconButton>
20
22
  */
21
23
  exports.IconButton = react_1.default.forwardRef((props, ref) => {
22
- const { isActive, children, styleType = 'default', size, type = 'button', className, as: Element = 'button', ...rest } = props;
24
+ const { isActive, children, styleType = 'default', size, type = 'button', className, as: Element = 'button', label, ...rest } = props;
23
25
  (0, utils_1.useTheme)();
24
- return (react_1.default.createElement(Element, { ref: ref, className: (0, classnames_1.default)('iui-button', className), "data-iui-variant": styleType !== 'default' ? styleType : undefined, "data-iui-size": size, "data-iui-active": isActive, type: type, ...rest },
25
- react_1.default.createElement("span", { className: 'iui-button-icon', "aria-hidden": true }, children)));
26
+ return (react_1.default.createElement(IconButtonTooltip, { label: label },
27
+ react_1.default.createElement(Element, { ref: ref, className: (0, classnames_1.default)('iui-button', className), "data-iui-variant": styleType !== 'default' ? styleType : undefined, "data-iui-size": size, "data-iui-active": isActive, "aria-pressed": isActive, type: type, ...rest },
28
+ react_1.default.createElement("span", { className: 'iui-button-icon', "aria-hidden": true }, children),
29
+ label ? react_1.default.createElement(utils_1.VisuallyHidden, null, label) : null)));
26
30
  });
31
+ const IconButtonTooltip = (props) => {
32
+ const { label, children } = props;
33
+ return label ? (react_1.default.createElement(utils_1.Popover, { interactive: false, offset: [0, 4], aria: { content: null }, content: react_1.default.createElement("div", { "aria-hidden": 'true', className: 'iui-tooltip' }, label) }, children)) : (children);
34
+ };
27
35
  exports.default = exports.IconButton;
@@ -49,9 +49,11 @@ export declare const Carousel: React.ForwardRefExoticComponent<{
49
49
  Navigation: React.ForwardRefExoticComponent<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>, "key" | keyof React.HTMLAttributes<HTMLElement>> & React.RefAttributes<HTMLElement>> & {
50
50
  PreviousButton: React.ForwardRefExoticComponent<{
51
51
  isActive?: boolean | undefined;
52
+ label?: React.ReactNode;
52
53
  } & Omit<import("..").ButtonProps<"button">, "startIcon" | "endIcon"> & React.RefAttributes<HTMLButtonElement>>;
53
54
  NextButton: React.ForwardRefExoticComponent<{
54
55
  isActive?: boolean | undefined;
56
+ label?: React.ReactNode;
55
57
  } & Omit<import("..").ButtonProps<"button">, "startIcon" | "endIcon"> & React.RefAttributes<HTMLButtonElement>>;
56
58
  };
57
59
  DotsList: React.ForwardRefExoticComponent<{
@@ -8,8 +8,10 @@ import React from 'react';
8
8
  export declare const CarouselNavigation: React.ForwardRefExoticComponent<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>, "key" | keyof React.HTMLAttributes<HTMLElement>> & React.RefAttributes<HTMLElement>> & {
9
9
  PreviousButton: React.ForwardRefExoticComponent<{
10
10
  isActive?: boolean | undefined;
11
+ label?: React.ReactNode;
11
12
  } & Omit<import("../Buttons").ButtonProps<"button">, "startIcon" | "endIcon"> & React.RefAttributes<HTMLButtonElement>>;
12
13
  NextButton: React.ForwardRefExoticComponent<{
13
14
  isActive?: boolean | undefined;
15
+ label?: React.ReactNode;
14
16
  } & Omit<import("../Buttons").ButtonProps<"button">, "startIcon" | "endIcon"> & React.RefAttributes<HTMLButtonElement>>;
15
17
  };
@@ -33,17 +33,6 @@ const isInDateRange = (date, startDate, endDate) => {
33
33
  maxDate && maxDate.setHours(0, 0, 0, 0);
34
34
  return testDate > minDate && testDate < maxDate;
35
35
  };
36
- // compares to see if one date is earlier than another
37
- const isBefore = (beforeDate, afterDate) => {
38
- if (!beforeDate || !afterDate) {
39
- return false;
40
- }
41
- const firstDate = new Date(beforeDate);
42
- const secondDate = new Date(afterDate);
43
- firstDate && firstDate.setHours(0, 0, 0, 0);
44
- secondDate && secondDate.setHours(0, 0, 0, 0);
45
- return firstDate < secondDate;
46
- };
47
36
  // Type guard for multiple did not work
48
37
  const isSingleOnChange = (onChange, enableRangeSelect) => {
49
38
  return !enableRangeSelect;
@@ -238,7 +227,7 @@ const DatePicker = (props) => {
238
227
  setSelectedStartDay(newStartDate);
239
228
  setFocusedDay(newStartDate);
240
229
  // if the start date is after the end date or the end date is undefined, reset the end date
241
- if (!isBefore(newStartDate, selectedEndDay)) {
230
+ if (!(0, utils_1.isBefore)(newStartDate, selectedEndDay)) {
242
231
  setSelectedEndDay(newStartDate);
243
232
  onChange === null || onChange === void 0 ? void 0 : onChange(newStartDate, newStartDate);
244
233
  }
@@ -256,7 +245,7 @@ const DatePicker = (props) => {
256
245
  const newEndDate = new Date(day.getFullYear(), day.getMonth(), day.getDate(), currentEndDate.getHours(), currentEndDate.getMinutes(), currentEndDate.getSeconds());
257
246
  setFocusedDay(newEndDate);
258
247
  // if the end date is before the start date, move back the start date and still have user select end date
259
- if (!isBefore(newEndDate, selectedStartDay)) {
248
+ if (!(0, utils_1.isBefore)(newEndDate, selectedStartDay)) {
260
249
  setSelectedEndDay(newEndDate);
261
250
  selectedStartDay && (onChange === null || onChange === void 0 ? void 0 : onChange(selectedStartDay, newEndDate));
262
251
  setIsSelectingStartDate(true);
@@ -29,16 +29,19 @@ const FileUploadCardIcon = react_1.default.forwardRef((props, ref) => {
29
29
  const { children, className, ...rest } = props;
30
30
  return (react_1.default.createElement("span", { className: (0, classnames_1.default)('iui-file-card-icon', className), ref: ref, ...rest }, children !== null && children !== void 0 ? children : react_1.default.createElement(utils_1.SvgDocument, null)));
31
31
  });
32
+ FileUploadCardIcon.displayName = 'FileUploadCard.Icon';
32
33
  const FileUploadCardInfo = react_1.default.forwardRef((props, ref) => {
33
34
  const { children, className, ...rest } = props;
34
35
  return (react_1.default.createElement("span", { className: (0, classnames_1.default)('iui-file-card-text', className), ref: ref, ...rest }, children));
35
36
  });
37
+ FileUploadCardInfo.displayName = 'FileUploadCard.Info';
36
38
  const FileUploadCardTitle = react_1.default.forwardRef((props, ref) => {
37
39
  const { children, className, ...rest } = props;
38
40
  const { files } = (0, utils_1.useSafeContext)(exports.FileUploadCardContext);
39
41
  const title = files.length > 1 ? files.length + ' files selected' : files[0].name;
40
42
  return (react_1.default.createElement("span", { className: (0, classnames_1.default)('iui-file-card-title', className), ref: ref, ...rest }, children !== null && children !== void 0 ? children : title));
41
43
  });
44
+ FileUploadCardTitle.displayName = 'FileUploadCard.Title';
42
45
  const FileUploadCardDescription = react_1.default.forwardRef((props, ref) => {
43
46
  const { children, className, ...rest } = props;
44
47
  const { files } = (0, utils_1.useSafeContext)(exports.FileUploadCardContext);
@@ -50,15 +53,18 @@ const FileUploadCardDescription = react_1.default.forwardRef((props, ref) => {
50
53
  }
51
54
  return (react_1.default.createElement("span", { className: (0, classnames_1.default)('iui-file-card-description', className), ref: ref, ...rest }, children !== null && children !== void 0 ? children : description));
52
55
  });
56
+ FileUploadCardDescription.displayName = 'FileUploadCard.Description';
53
57
  const FileUploadCardAction = react_1.default.forwardRef((props, ref) => {
54
58
  const { children, className, ...rest } = props;
55
59
  return (react_1.default.createElement("div", { className: (0, classnames_1.default)('iui-file-card-action', className), ref: ref, ...rest }, children));
56
60
  });
61
+ FileUploadCardAction.displayName = 'FileUploadCard.Action';
57
62
  const FileUploadCardInputLabel = react_1.default.forwardRef((props, ref) => {
58
63
  const { children, className, ...rest } = props;
59
64
  const { inputId } = (0, utils_1.useSafeContext)(exports.FileUploadCardContext);
60
65
  return (react_1.default.createElement("label", { className: (0, classnames_1.default)('iui-anchor', className), ref: ref, htmlFor: inputId, ...rest }, children));
61
66
  });
67
+ FileUploadCardInputLabel.displayName = 'FileUploadCard.InputLabel';
62
68
  const FileUploadCardInput = react_1.default.forwardRef((props, ref) => {
63
69
  const { children, className, onChange, id, ...rest } = props;
64
70
  const { files, onFilesChange, setInternalFiles, inputId, setInputId } = (0, utils_1.useSafeContext)(exports.FileUploadCardContext);
@@ -73,9 +79,11 @@ const FileUploadCardInput = react_1.default.forwardRef((props, ref) => {
73
79
  node.files = dataTransfer.files;
74
80
  }, [files]);
75
81
  const refs = (0, utils_1.useMergedRefs)(ref, setNativeFilesRef);
76
- if (id && id !== inputId) {
77
- setInputId(id);
78
- }
82
+ react_1.default.useEffect(() => {
83
+ if (id && id !== inputId) {
84
+ setInputId(id);
85
+ }
86
+ }, [id, inputId, setInputId]);
79
87
  return (react_1.default.createElement(react_1.default.Fragment, null,
80
88
  react_1.default.createElement("input", { className: (0, classnames_1.default)('iui-visually-hidden', className), type: 'file', onChange: (e) => {
81
89
  onChange === null || onChange === void 0 ? void 0 : onChange(e);
@@ -87,6 +95,7 @@ const FileUploadCardInput = react_1.default.forwardRef((props, ref) => {
87
95
  }, ref: refs, id: id !== null && id !== void 0 ? id : inputId, ...rest }),
88
96
  children));
89
97
  });
98
+ FileUploadCardInput.displayName = 'FileUploadCard.Input';
90
99
  /**
91
100
  * Default card to be used with the `FileUpload` wrapper component for single-file uploading.
92
101
  * @example
@@ -115,7 +124,8 @@ exports.FileUploadCard = Object.assign(react_1.default.forwardRef((props, ref) =
115
124
  var _a;
116
125
  const { className, children, files: filesProp, onFilesChange, emptyCard = react_1.default.createElement(FileEmptyCard_1.FileEmptyCard, null), input, ...rest } = props;
117
126
  const [internalFiles, setInternalFiles] = react_1.default.useState();
118
- const [inputId, setInputId] = react_1.default.useState((0, utils_2.useId)());
127
+ const uid = (0, utils_2.useId)();
128
+ const [inputId, setInputId] = react_1.default.useState(uid);
119
129
  const files = (_a = filesProp !== null && filesProp !== void 0 ? filesProp : internalFiles) !== null && _a !== void 0 ? _a : [];
120
130
  return (react_1.default.createElement(exports.FileUploadCardContext.Provider, { value: {
121
131
  files,
@@ -140,5 +150,6 @@ exports.FileUploadCard = Object.assign(react_1.default.forwardRef((props, ref) =
140
150
  InputLabel: FileUploadCardInputLabel,
141
151
  Input: FileUploadCardInput,
142
152
  });
153
+ exports.FileUploadCard.displayName = 'FileUploadCard';
143
154
  exports.default = exports.FileUploadCard;
144
155
  exports.FileUploadCardContext = react_1.default.createContext(undefined);
@@ -21,10 +21,11 @@ require("@itwin/itwinui-css/css/input.css");
21
21
  * <LabeledInput status='negative' label='Negative' setFocus />
22
22
  */
23
23
  exports.LabeledInput = react_1.default.forwardRef((props, ref) => {
24
- const { className, disabled = false, label, message, status, svgIcon, style, inputClassName, inputStyle, displayStyle = 'default', iconDisplayStyle = displayStyle === 'default' ? 'block' : 'inline', required = false, ...rest } = props;
24
+ const uid = (0, utils_1.useId)();
25
+ const { className, disabled = false, label, message, status, svgIcon, style, inputClassName, inputStyle, displayStyle = 'default', iconDisplayStyle = displayStyle === 'default' ? 'block' : 'inline', required = false, id = uid, ...rest } = props;
25
26
  (0, utils_1.useTheme)();
26
27
  const icon = svgIcon !== null && svgIcon !== void 0 ? svgIcon : (status && utils_1.StatusIconMap[status]());
27
- return (react_1.default.createElement(utils_1.InputContainer, { as: 'label', label: label, disabled: disabled, required: required, status: status, message: message, icon: icon, isLabelInline: displayStyle === 'inline', isIconInline: iconDisplayStyle === 'inline', className: className, style: style },
28
- react_1.default.createElement(Input_1.Input, { disabled: disabled, className: inputClassName, style: inputStyle, required: required, ref: ref, ...rest })));
28
+ return (react_1.default.createElement(utils_1.InputContainer, { label: label, disabled: disabled, required: required, status: status, message: message, icon: icon, isLabelInline: displayStyle === 'inline', isIconInline: iconDisplayStyle === 'inline', className: className, style: style, inputId: id },
29
+ react_1.default.createElement(Input_1.Input, { disabled: disabled, className: inputClassName, style: inputStyle, required: required, ref: ref, id: id, ...rest })));
29
30
  });
30
31
  exports.default = exports.LabeledInput;
@@ -46,8 +46,9 @@ require("@itwin/itwinui-css/css/input.css");
46
46
  * />
47
47
  */
48
48
  const LabeledSelect = (props) => {
49
- const { className, disabled = false, label, message, status, svgIcon, displayStyle = 'default', style, selectClassName, selectStyle, required = false, ...rest } = props;
49
+ const { className, disabled = false, label, message, status, svgIcon, displayStyle = 'default', style, selectClassName, selectStyle, required = false, triggerProps, ...rest } = props;
50
50
  (0, utils_1.useTheme)();
51
+ const labelId = `${(0, utils_1.useId)()}-label`;
51
52
  const icon = () => {
52
53
  if (svgIcon) {
53
54
  return react_1.default.cloneElement(svgIcon, { 'aria-hidden': true });
@@ -57,8 +58,11 @@ const LabeledSelect = (props) => {
57
58
  }
58
59
  return undefined;
59
60
  };
60
- return (react_1.default.createElement(utils_1.InputContainer, { label: label, disabled: disabled, required: required, status: status, message: message, icon: displayStyle === 'default' ? icon() : undefined, isLabelInline: displayStyle === 'inline', className: className, style: style },
61
- react_1.default.createElement(Select_1.Select, { disabled: disabled, className: selectClassName, style: selectStyle, ...rest })));
61
+ return (react_1.default.createElement(utils_1.InputContainer, { label: label, disabled: disabled, required: required, status: status, message: message, icon: displayStyle === 'default' ? icon() : undefined, isLabelInline: displayStyle === 'inline', className: className, style: style, labelId: labelId },
62
+ react_1.default.createElement(Select_1.Select, { disabled: disabled, className: selectClassName, style: selectStyle, ...rest, triggerProps: {
63
+ 'aria-labelledby': labelId,
64
+ ...triggerProps,
65
+ } })));
62
66
  };
63
67
  exports.LabeledSelect = LabeledSelect;
64
68
  exports.default = exports.LabeledSelect;
@@ -34,10 +34,11 @@ require("@itwin/itwinui-css/css/input.css");
34
34
  * />
35
35
  */
36
36
  exports.LabeledTextarea = react_1.default.forwardRef((props, ref) => {
37
- const { className, style, disabled = false, label, message, status, textareaClassName, textareaStyle, displayStyle = 'default', iconDisplayStyle = displayStyle === 'default' ? 'block' : 'inline', svgIcon, required = false, ...textareaProps } = props;
37
+ const uid = (0, utils_1.useId)();
38
+ const { className, style, disabled = false, label, message, status, textareaClassName, textareaStyle, displayStyle = 'default', iconDisplayStyle = displayStyle === 'default' ? 'block' : 'inline', svgIcon, required = false, id = uid, ...textareaProps } = props;
38
39
  (0, utils_1.useTheme)();
39
40
  const icon = svgIcon !== null && svgIcon !== void 0 ? svgIcon : (status && utils_1.StatusIconMap[status]());
40
- return (react_1.default.createElement(utils_1.InputContainer, { as: 'label', label: label, disabled: disabled, required: required, status: status, message: message, icon: icon, isLabelInline: displayStyle === 'inline', isIconInline: iconDisplayStyle === 'inline', className: className, style: style },
41
- react_1.default.createElement(Textarea_1.Textarea, { disabled: disabled, className: textareaClassName, style: textareaStyle, required: required, ...textareaProps, ref: ref })));
41
+ return (react_1.default.createElement(utils_1.InputContainer, { label: label, disabled: disabled, required: required, status: status, message: message, icon: icon, isLabelInline: displayStyle === 'inline', isIconInline: iconDisplayStyle === 'inline', className: className, style: style, inputId: id },
42
+ react_1.default.createElement(Textarea_1.Textarea, { disabled: disabled, className: textareaClassName, style: textareaStyle, required: required, id: id, ...textareaProps, ref: ref })));
42
43
  });
43
44
  exports.default = exports.LabeledTextarea;
@@ -111,6 +111,10 @@ export declare type SelectProps<T> = {
111
111
  * @see [tippy.js props](https://atomiks.github.io/tippyjs/v6/all-props/)
112
112
  */
113
113
  popoverProps?: Omit<PopoverProps, 'onShow' | 'onHide' | 'disabled'>;
114
+ /**
115
+ * Props to pass to the select button (trigger) element.
116
+ */
117
+ triggerProps?: React.ComponentPropsWithoutRef<'div'>;
114
118
  } & SelectMultipleTypeProps<T> & Pick<PopoverProps, 'onShow' | 'onHide'> & Omit<React.ComponentPropsWithoutRef<'div'>, 'size' | 'disabled' | 'placeholder' | 'onChange'>;
115
119
  /**
116
120
  * Select component to select value from options.
@@ -10,8 +10,7 @@ exports.Select = void 0;
10
10
  *--------------------------------------------------------------------------------------------*/
11
11
  const react_1 = __importDefault(require("react"));
12
12
  const classnames_1 = __importDefault(require("classnames"));
13
- const DropdownMenu_1 = require("../DropdownMenu");
14
- const MenuItem_1 = require("../Menu/MenuItem");
13
+ const Menu_1 = require("../Menu");
15
14
  const utils_1 = require("../utils");
16
15
  require("@itwin/itwinui-css/css/select.css");
17
16
  const SelectTag_1 = __importDefault(require("./SelectTag"));
@@ -74,14 +73,12 @@ const isSingleOnChange = (onChange, multiple) => {
74
73
  */
75
74
  const Select = (props) => {
76
75
  var _a;
77
- const { options, value, onChange, placeholder, disabled = false, size, setFocus = false, itemRenderer, selectedItemRenderer, className, style, menuClassName, menuStyle, onShow, onHide, popoverProps, multiple = false, ...rest } = props;
76
+ const uid = (0, utils_1.useId)();
77
+ const { options, value, onChange, placeholder, disabled = false, size, setFocus = false, itemRenderer, selectedItemRenderer, className, style, menuClassName, menuStyle, onShow, onHide, popoverProps, multiple = false, triggerProps, ...rest } = props;
78
78
  (0, utils_1.useTheme)();
79
- const [isOpen, setIsOpen] = react_1.default.useState((_a = popoverProps === null || popoverProps === void 0 ? void 0 : popoverProps.visible) !== null && _a !== void 0 ? _a : false);
80
- react_1.default.useEffect(() => {
81
- setIsOpen((open) => { var _a; return (_a = popoverProps === null || popoverProps === void 0 ? void 0 : popoverProps.visible) !== null && _a !== void 0 ? _a : open; });
82
- }, [popoverProps]);
79
+ const [isOpenState, setIsOpen] = react_1.default.useState(false);
80
+ const isOpen = (_a = popoverProps === null || popoverProps === void 0 ? void 0 : popoverProps.visible) !== null && _a !== void 0 ? _a : isOpenState;
83
81
  const [minWidth, setMinWidth] = react_1.default.useState(0);
84
- const toggle = () => setIsOpen((open) => !open);
85
82
  const selectRef = react_1.default.useRef(null);
86
83
  const toggleButtonRef = react_1.default.useRef(null);
87
84
  const onShowHandler = react_1.default.useCallback((instance) => {
@@ -89,7 +86,9 @@ const Select = (props) => {
89
86
  onShow === null || onShow === void 0 ? void 0 : onShow(instance);
90
87
  }, [onShow]);
91
88
  const onHideHandler = react_1.default.useCallback((instance) => {
89
+ var _a;
92
90
  setIsOpen(false);
91
+ (_a = selectRef.current) === null || _a === void 0 ? void 0 : _a.focus({ preventScroll: true }); // move focus back to select button
93
92
  onHide === null || onHide === void 0 ? void 0 : onHide(instance);
94
93
  }, [onHide]);
95
94
  react_1.default.useEffect(() => {
@@ -102,30 +101,29 @@ const Select = (props) => {
102
101
  setMinWidth(selectRef.current.offsetWidth);
103
102
  }
104
103
  }, [isOpen]);
105
- const onKeyDown = (event, toggle) => {
104
+ const onKeyDown = (event) => {
106
105
  if (event.altKey) {
107
106
  return;
108
107
  }
109
108
  switch (event.key) {
110
109
  case 'Enter':
111
110
  case ' ':
112
- case 'Spacebar':
113
- if (event.target === selectRef.current) {
114
- toggle();
115
- event.preventDefault();
116
- }
111
+ case 'Spacebar': {
112
+ setIsOpen((o) => !o);
113
+ event.preventDefault();
117
114
  break;
115
+ }
118
116
  default:
119
117
  break;
120
118
  }
121
119
  };
122
- const menuItems = react_1.default.useCallback((close) => {
120
+ const menuItems = react_1.default.useMemo(() => {
123
121
  return options.map((option, index) => {
124
122
  var _a;
125
123
  const isSelected = isMultipleEnabled(value, multiple)
126
124
  ? (_a = value === null || value === void 0 ? void 0 : value.includes(option.value)) !== null && _a !== void 0 ? _a : false
127
125
  : value === option.value;
128
- const menuItem = itemRenderer ? (itemRenderer(option, { close, isSelected })) : (react_1.default.createElement(MenuItem_1.MenuItem, null, option.label));
126
+ const menuItem = itemRenderer ? (itemRenderer(option, { close: () => setIsOpen(false), isSelected })) : (react_1.default.createElement(Menu_1.MenuItem, null, option.label));
129
127
  const { label, ...restOption } = option;
130
128
  return react_1.default.cloneElement(menuItem, {
131
129
  key: `${label}-${index}`,
@@ -136,7 +134,7 @@ const Select = (props) => {
136
134
  }
137
135
  if (isSingleOnChange(onChange, multiple)) {
138
136
  onChange === null || onChange === void 0 ? void 0 : onChange(option.value);
139
- close();
137
+ setIsOpen(false);
140
138
  }
141
139
  else {
142
140
  onChange === null || onChange === void 0 ? void 0 : onChange(option.value, isSelected ? 'removed' : 'added');
@@ -144,7 +142,7 @@ const Select = (props) => {
144
142
  },
145
143
  ref: (el) => {
146
144
  if (isSelected && !multiple) {
147
- el === null || el === void 0 ? void 0 : el.scrollIntoView();
145
+ el === null || el === void 0 ? void 0 : el.scrollIntoView({ block: 'nearest' });
148
146
  }
149
147
  },
150
148
  role: 'option',
@@ -164,29 +162,29 @@ const Select = (props) => {
164
162
  const tagRenderer = react_1.default.useCallback((item) => {
165
163
  return react_1.default.createElement(SelectTag_1.default, { key: item.label, label: item.label });
166
164
  }, []);
167
- return (react_1.default.createElement("div", { className: (0, classnames_1.default)('iui-input-with-icon', className), "aria-expanded": isOpen, "aria-haspopup": 'listbox', style: style, ...rest },
168
- react_1.default.createElement(DropdownMenu_1.DropdownMenu, { menuItems: menuItems, placement: 'bottom-start', className: (0, classnames_1.default)('iui-scroll', menuClassName), style: {
169
- minWidth,
170
- maxWidth: `min(${minWidth * 2}px, 90vw)`,
171
- ...menuStyle,
172
- }, role: 'listbox', onShow: onShowHandler, onHide: onHideHandler, disabled: disabled, ...popoverProps, visible: isOpen, onClickOutside: (_, { target }) => {
165
+ return (react_1.default.createElement("div", { className: (0, classnames_1.default)('iui-input-with-icon', className), style: style, ...rest },
166
+ react_1.default.createElement(utils_1.Popover, { content: react_1.default.createElement(Menu_1.Menu, { role: 'listbox', className: (0, classnames_1.default)('iui-scroll', menuClassName), style: {
167
+ minWidth,
168
+ maxWidth: `min(${minWidth * 2}px, 90vw)`,
169
+ ...menuStyle,
170
+ }, id: `${uid}-menu`, key: `${uid}-menu` }, menuItems), placement: 'bottom-start', aria: { content: null }, onShow: onShowHandler, onHide: onHideHandler, ...popoverProps, visible: isOpen, onClickOutside: (_, { target }) => {
173
171
  var _a;
174
172
  if (!((_a = toggleButtonRef.current) === null || _a === void 0 ? void 0 : _a.contains(target))) {
175
173
  setIsOpen(false);
176
174
  }
177
175
  } },
178
- react_1.default.createElement("div", { ref: selectRef, className: (0, classnames_1.default)('iui-select-button', {
176
+ react_1.default.createElement("div", { tabIndex: 0, role: 'combobox', ref: selectRef, "data-iui-size": size, onClick: () => !disabled && setIsOpen((o) => !o), onKeyDown: (e) => !disabled && onKeyDown(e), "aria-disabled": disabled, "aria-autocomplete": 'none', "aria-expanded": isOpen, "aria-haspopup": 'listbox', "aria-controls": `${uid}-menu`, ...triggerProps, className: (0, classnames_1.default)('iui-select-button', {
179
177
  'iui-placeholder': (!selectedItems || selectedItems.length === 0) && !!placeholder,
180
178
  'iui-disabled': disabled,
181
- }), "data-iui-size": size, onClick: () => !disabled && toggle(), onKeyDown: (e) => !disabled && onKeyDown(e, toggle), tabIndex: !disabled ? 0 : undefined },
179
+ }, triggerProps === null || triggerProps === void 0 ? void 0 : triggerProps.className) },
182
180
  (!selectedItems || selectedItems.length === 0) && (react_1.default.createElement("span", { className: 'iui-content' }, placeholder)),
183
181
  isMultipleEnabled(selectedItems, multiple) ? (react_1.default.createElement(MultipleSelectButton, { selectedItems: selectedItems, selectedItemsRenderer: selectedItemRenderer, tagRenderer: tagRenderer })) : (react_1.default.createElement(SingleSelectButton, { selectedItem: selectedItems, selectedItemRenderer: selectedItemRenderer })))),
184
- react_1.default.createElement("span", { ref: toggleButtonRef, className: (0, classnames_1.default)('iui-end-icon', {
182
+ react_1.default.createElement("span", { "aria-hidden": true, ref: toggleButtonRef, className: (0, classnames_1.default)('iui-end-icon', {
185
183
  'iui-actionable': !disabled,
186
184
  'iui-disabled': disabled,
187
185
  'iui-open': isOpen,
188
- }), onClick: () => !disabled && toggle() },
189
- react_1.default.createElement(utils_1.SvgCaretDownSmall, { "aria-hidden": true }))));
186
+ }), onClick: () => !disabled && setIsOpen((o) => !o) },
187
+ react_1.default.createElement(utils_1.SvgCaretDownSmall, null))));
190
188
  };
191
189
  exports.Select = Select;
192
190
  const SingleSelectButton = ({ selectedItem, selectedItemRenderer, }) => {
@@ -5,6 +5,14 @@ export declare type DatePickerInputProps = {
5
5
  onChange: (date?: Date) => void;
6
6
  parseInput: (text: string) => Date;
7
7
  formatDate: (date: Date) => string;
8
+ /**
9
+ * Decides if this component is used for the 'from' or 'to' date
10
+ */
11
+ isFromOrTo?: 'from' | 'to';
12
+ /**
13
+ * The 'to' date for the 'from' DatePickerInput or the 'from' date for the 'to' DatePickerInput
14
+ */
15
+ selectedDate?: Date;
8
16
  } & Omit<LabeledInputProps, 'value' | 'onChange' | 'svgIcon' | 'displayStyle'>;
9
17
  declare const DatePickerInput: (props: DatePickerInputProps) => JSX.Element;
10
18
  export default DatePickerInput;
@@ -13,7 +13,15 @@ const LabeledInput_1 = require("../../../LabeledInput");
13
13
  const DatePicker_1 = require("../../../DatePicker");
14
14
  const Buttons_1 = require("../../../Buttons");
15
15
  const DatePickerInput = (props) => {
16
- const { onChange, date, parseInput, formatDate, ...rest } = props;
16
+ const { onChange, date, parseInput, formatDate, isFromOrTo, selectedDate, ...rest } = props;
17
+ const isDateDisabled = (date) => {
18
+ if (isFromOrTo === 'to') {
19
+ return (0, utils_1.isBefore)(date, selectedDate);
20
+ }
21
+ else {
22
+ return (0, utils_1.isBefore)(selectedDate, date);
23
+ }
24
+ };
17
25
  const buttonRef = react_1.default.useRef(null);
18
26
  const [inputValue, setInputValue] = react_1.default.useState('');
19
27
  react_1.default.useEffect(() => {
@@ -40,7 +48,7 @@ const DatePickerInput = (props) => {
40
48
  onChange(parsedDate);
41
49
  }
42
50
  }, [onChange, parseInput]);
43
- return (react_1.default.createElement(utils_1.Popover, { content: react_1.default.createElement(DatePicker_1.DatePicker, { date: date, onChange: onDateSelected, setFocus: true }), placement: 'bottom', visible: isVisible, onClickOutside: (_, e) => {
51
+ return (react_1.default.createElement(utils_1.Popover, { content: react_1.default.createElement(DatePicker_1.DatePicker, { date: date, onChange: onDateSelected, setFocus: true, isDateDisabled: isDateDisabled }), placement: 'bottom', visible: isVisible, onClickOutside: (_, e) => {
44
52
  var _a;
45
53
  if (!((_a = buttonRef.current) === null || _a === void 0 ? void 0 : _a.contains(e.target))) {
46
54
  close();
@@ -65,8 +65,8 @@ const DateRangeFilter = (props) => {
65
65
  }
66
66
  };
67
67
  return (react_1.default.createElement(BaseFilter_1.BaseFilter, null,
68
- react_1.default.createElement(DatePickerInput_1.default, { label: translatedStrings.from, date: from, onChange: onFromChange, formatDate: formatDate, parseInput: parseInput, onKeyDown: onKeyDown, placeholder: placeholder, setFocus: true }),
69
- react_1.default.createElement(DatePickerInput_1.default, { label: translatedStrings.to, date: to, onChange: onToChange, formatDate: formatDate, parseInput: parseInput, onKeyDown: onKeyDown, placeholder: placeholder }),
68
+ react_1.default.createElement(DatePickerInput_1.default, { label: translatedStrings.from, date: from, onChange: onFromChange, formatDate: formatDate, parseInput: parseInput, onKeyDown: onKeyDown, placeholder: placeholder, selectedDate: to, isFromOrTo: 'from', setFocus: true }),
69
+ react_1.default.createElement(DatePickerInput_1.default, { label: translatedStrings.to, date: to, onChange: onToChange, formatDate: formatDate, parseInput: parseInput, onKeyDown: onKeyDown, placeholder: placeholder, selectedDate: from, isFromOrTo: 'to' }),
70
70
  react_1.default.createElement(FilterButtonBar_1.FilterButtonBar, { setFilter: () => setFilter([from, to]), clearFilter: clearFilter, translatedLabels: translatedLabels })));
71
71
  };
72
72
  exports.DateRangeFilter = DateRangeFilter;
@@ -11,6 +11,8 @@ export declare type InputContainerProps<T extends React.ElementType = 'div'> = {
11
11
  isLabelInline?: boolean;
12
12
  isIconInline?: boolean;
13
13
  statusMessage?: React.ReactNode;
14
+ inputId?: string;
15
+ labelId?: string;
14
16
  } & React.ComponentPropsWithoutRef<T>;
15
17
  /**
16
18
  * Input container to wrap inputs with label, and add optional message and icon.
@@ -17,7 +17,8 @@ require("@itwin/itwinui-css/css/utils.css");
17
17
  */
18
18
  const InputContainer = (props) => {
19
19
  var _a;
20
- const { as: Element = 'div', label, disabled, required, status, message, icon, isLabelInline, isIconInline, children, className, style, statusMessage, ...rest } = props;
20
+ const { as: Element = 'div', label, disabled, required, status, message, icon, isLabelInline, isIconInline, children, className, style, statusMessage, inputId, labelId, ...rest } = props;
21
+ const LabelElement = inputId && Element !== 'label' ? 'label' : 'div';
21
22
  return (react_1.default.createElement(Element, { className: (0, classnames_1.default)('iui-input-container', {
22
23
  'iui-disabled': disabled,
23
24
  [`iui-${status}`]: !!status,
@@ -25,9 +26,9 @@ const InputContainer = (props) => {
25
26
  'iui-inline-icon': isIconInline,
26
27
  'iui-with-message': (!!message || !!icon || !!statusMessage) && !isLabelInline,
27
28
  }, className), style: style, ...rest },
28
- label && (react_1.default.createElement("div", { className: (0, classnames_1.default)('iui-label', {
29
+ label && (react_1.default.createElement(LabelElement, { className: (0, classnames_1.default)('iui-label', {
29
30
  'iui-required': required,
30
- }) }, label)),
31
+ }), htmlFor: inputId, id: labelId }, label)),
31
32
  children,
32
33
  statusMessage ? (statusMessage) : (react_1.default.createElement(react_1.default.Fragment, null,
33
34
  icon &&
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Return true if the first date is earlier than the second date
3
+ */
4
+ export declare const isBefore: (beforeDate: Date | undefined, afterDate: Date | undefined) => boolean;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
4
+ * See LICENSE.md in the project root for license terms and full copyright notice.
5
+ *--------------------------------------------------------------------------------------------*/
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.isBefore = void 0;
8
+ /**
9
+ * Return true if the first date is earlier than the second date
10
+ */
11
+ const isBefore = (beforeDate, afterDate) => {
12
+ if (!beforeDate || !afterDate) {
13
+ return false;
14
+ }
15
+ const firstDate = new Date(beforeDate);
16
+ const secondDate = new Date(afterDate);
17
+ firstDate && firstDate.setHours(0, 0, 0, 0);
18
+ secondDate && secondDate.setHours(0, 0, 0, 0);
19
+ return firstDate < secondDate;
20
+ };
21
+ exports.isBefore = isBefore;
@@ -1,3 +1,4 @@
1
+ export * from './date';
1
2
  export * from './dom';
2
3
  export * from './colors';
3
4
  export * from './numbers';
@@ -18,6 +18,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
18
18
  * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
19
19
  * See LICENSE.md in the project root for license terms and full copyright notice.
20
20
  *--------------------------------------------------------------------------------------------*/
21
+ __exportStar(require("./date"), exports);
21
22
  __exportStar(require("./dom"), exports);
22
23
  __exportStar(require("./colors"), exports);
23
24
  __exportStar(require("./numbers"), exports);
@@ -1,5 +1,5 @@
1
- import React from 'react';
2
1
  /**
3
- * Return custom useId function as a fallback for React.useId
2
+ * Wrapper around React's `useId` hook, which prefixes the id with `iui-` and uses
3
+ * a random value as fallback for older React versions which don't include `useId`.
4
4
  */
5
- export declare const useId: typeof React.useId;
5
+ export declare const useId: () => string;
@@ -12,9 +12,12 @@ exports.useId = void 0;
12
12
  const react_1 = __importDefault(require("react"));
13
13
  const numbers_1 = require("../functions/numbers");
14
14
  /**
15
- * Return custom useId function as a fallback for React.useId
15
+ * Wrapper around React's `useId` hook, which prefixes the id with `iui-` and uses
16
+ * a random value as fallback for older React versions which don't include `useId`.
16
17
  */
17
- exports.useId = (_a = react_1.default.useId) !== null && _a !== void 0 ? _a : (() => {
18
- const [id] = react_1.default.useState(() => `iui-${(0, numbers_1.getRandomValue)(10)}`);
19
- return id;
20
- });
18
+ const useId = () => {
19
+ const uniqueValue = useUniqueValue();
20
+ return react_1.default.useMemo(() => `iui-${uniqueValue}`, [uniqueValue]);
21
+ };
22
+ exports.useId = useId;
23
+ const useUniqueValue = (_a = react_1.default.useId) !== null && _a !== void 0 ? _a : (() => (0, numbers_1.getRandomValue)(10));