@utahdts/utah-design-system 1.15.5 → 1.16.1

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 (33) hide show
  1. package/css/1-settings/_spacing.scss +16 -0
  2. package/css/6-components/_components-index.scss +1 -0
  3. package/css/6-components/base-components/forms/_combo-box-input.scss +5 -0
  4. package/css/6-components/base-components/forms/_time-input.scss +28 -0
  5. package/css/6-components/base-components/navigation/_menu-item.scss +35 -0
  6. package/css/6-components/base-components/navigation/_side-panel-navigation.scss +6 -1
  7. package/css/6-components/base-components/navigation/_vertical-menu.scss +45 -1
  8. package/dist/style.css +297 -9
  9. package/dist/utah-design-system.es.js +3846 -3441
  10. package/dist/utah-design-system.umd.js +3845 -3440
  11. package/index.js +2 -0
  12. package/package.json +12 -12
  13. package/react/components/forms/ComboBox/ComboBox.jsx +9 -5
  14. package/react/components/forms/ComboBox/ComboBoxOption.jsx +10 -0
  15. package/react/components/forms/ComboBox/context/ComboBoxContextProvider.jsx +10 -0
  16. package/react/components/forms/ComboBox/internal/CombBoxListBox.jsx +6 -4
  17. package/react/components/forms/ComboBox/internal/ComboBoxTextInput.jsx +11 -1
  18. package/react/components/forms/FormContext/useFormContextInputValue.js +4 -1
  19. package/react/components/forms/MultiSelect/MultiSelect.jsx +6 -0
  20. package/react/components/forms/MultiSelect/MultiSelectComboBox.jsx +6 -0
  21. package/react/components/forms/PlainText.jsx +7 -2
  22. package/react/components/forms/Select.jsx +3 -3
  23. package/react/components/forms/TextArea.jsx +3 -3
  24. package/react/components/forms/TextInput.jsx +3 -3
  25. package/react/components/forms/TimeInput.jsx +166 -0
  26. package/react/components/navigation/HorizontalMenu.jsx +2 -2
  27. package/react/components/navigation/VerticalMenu.jsx +45 -8
  28. package/react/components/navigation/items/MenuItemFlyout.jsx +119 -0
  29. package/react/components/navigation/{MenuItem.jsx → items/MenuItemInline.jsx} +24 -8
  30. package/react/components/navigation/items/MenuItemPlain.jsx +63 -0
  31. package/react/enums/menuTypes.js +7 -0
  32. package/react/hooks/useDebounceFunc.js +1 -1
  33. package/react/hooks/useGlobalKeyEvent.js +1 -1
package/index.js CHANGED
@@ -45,6 +45,7 @@ export { SelectOption } from './react/components/forms/SelectOption';
45
45
  export { Switch } from './react/components/forms/Switch';
46
46
  export { TextArea } from './react/components/forms/TextArea';
47
47
  export { TextInput } from './react/components/forms/TextInput';
48
+ export { TimeInput } from './react/components/forms/TimeInput';
48
49
  export { Icons } from './react/components/icons/Icons';
49
50
  export { ExternalLink } from './react/components/navigation/ExternalLink';
50
51
  export { HorizontalMenu } from './react/components/navigation/HorizontalMenu';
@@ -103,6 +104,7 @@ export { BANNER_PLACEMENT } from './react/enums/bannerPlacement';
103
104
  export { BUTTON_APPEARANCE, BUTTON_TYPES, ICON_BUTTON_APPEARANCE } from './react/enums/buttonEnums';
104
105
  export { componentColors } from './react/enums/componentColors';
105
106
  export { formElementSizesEnum } from './react/enums/formElementSizesEnum';
107
+ export { menuTypes } from './react/enums/menuTypes';
106
108
  export { popupPlacement } from './react/enums/popupPlacement';
107
109
  export { tableSortingRuleFieldType } from './react/enums/tableSortingRuleFieldType';
108
110
  export { useGlobalKeyEvent } from './react/hooks/useGlobalKeyEvent';
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@utahdts/utah-design-system",
3
3
  "description": "Utah Design System React Library",
4
4
  "displayName": "Utah Design System React Library",
5
- "version": "1.15.5",
5
+ "version": "1.16.1",
6
6
  "exports": {
7
7
  ".": {
8
8
  "development-local": "./index.js",
@@ -28,7 +28,7 @@
28
28
  ],
29
29
  "peerDependencies": {
30
30
  "react": "18.x",
31
- "react-router-dom": "6.21.2"
31
+ "react-router-dom": "6.21.3"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "vite build",
@@ -65,29 +65,29 @@
65
65
  },
66
66
  "homepage": "https://github.com/utahdts/utah-design-system",
67
67
  "dependencies": {
68
- "@utahdts/utah-design-system-header": "1.15.5",
68
+ "@utahdts/utah-design-system-header": "1.16.1",
69
69
  "date-fns": "3.3.1",
70
70
  "lodash": "4.17.21",
71
71
  "prop-types": "15.8.1",
72
72
  "react": "18.x",
73
73
  "react-popper": "2.3.0",
74
- "react-router-dom": "6.21.2",
74
+ "react-router-dom": "6.21.3",
75
75
  "use-immer": "0.9.0",
76
76
  "uuid": "9.0.1"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@types/lodash": "4.14.202",
80
- "@types/react": "18.2.48",
80
+ "@types/react": "18.2.79",
81
81
  "@types/react-dom": "18.2.18",
82
- "@types/uuid": "9.0.7",
82
+ "@types/uuid": "9.0.8",
83
83
  "@vitejs/plugin-react": "4.2.1",
84
- "@vitest/coverage-istanbul": "1.2.0",
85
- "@vitest/ui": "1.2.0",
86
- "jsdom": "23.2.0",
87
- "sass": "1.69.7",
84
+ "@vitest/coverage-istanbul": "1.2.2",
85
+ "@vitest/ui": "1.2.2",
86
+ "jsdom": "24.0.0",
87
+ "sass": "1.70.0",
88
88
  "typescript": "5.3.3",
89
- "vite": "5.0.11",
90
- "vitest": "1.2.0"
89
+ "vite": "5.0.12",
90
+ "vitest": "1.2.2"
91
91
  },
92
92
  "type": "module"
93
93
  }
@@ -1,4 +1,4 @@
1
- import { useId, useRef } from 'react';
1
+ import { useId, useState } from 'react';
2
2
  import { joinClassNames } from '../../../util/joinClassNames';
3
3
  import { ComboBoxContextProvider } from './context/ComboBoxContextProvider';
4
4
  import { CombBoxListBox } from './internal/CombBoxListBox';
@@ -17,6 +17,7 @@ import { ComboBoxTextInput } from './internal/ComboBoxTextInput';
17
17
  * @param {string} [props.className]
18
18
  * @param {string} [props.defaultValue]
19
19
  * @param {string} [props.errorMessage]
20
+ * @param {(isOptionsExpanded: boolean) => React.ReactNode} [props.iconCallback] Can provide a custom icon to show for the popup icon
20
21
  * @param {string} props.id
21
22
  * @param {MutableRef<HTMLDivElement | null>} [props.innerRef]
22
23
  * @param {boolean} [props.isClearable]
@@ -47,6 +48,7 @@ export function ComboBox({
47
48
  className,
48
49
  defaultValue,
49
50
  errorMessage,
51
+ iconCallback,
50
52
  id,
51
53
  innerRef: draftInnerRef,
52
54
  isClearable,
@@ -71,8 +73,9 @@ export function ComboBox({
71
73
  wrapperClassName,
72
74
  ...rest
73
75
  }) {
74
- const comboBoxListId = useId();
75
- const contentRef = useRef(/** @type {HTMLInputElement | null} */(null));
76
+ const comboBoxListId = `${id}__${useId()}`;
77
+ // useState (instead of useRef) so changes update ComboBoxListBox
78
+ const [contentRefState, setContentRefState] = useState(/** @type {HTMLInputElement | null} */(null));
76
79
 
77
80
  const child = (
78
81
  <div className={joinClassNames('combo-box-input__inner-wrapper', className)}>
@@ -82,9 +85,10 @@ export function ComboBox({
82
85
  className={textInputClassName}
83
86
  comboBoxListId={comboBoxListId}
84
87
  errorMessage={errorMessage}
88
+ iconCallback={iconCallback}
85
89
  id={id}
86
90
  innerRef={(ref) => {
87
- contentRef.current = ref;
91
+ setContentRefState(ref);
88
92
  }}
89
93
  isClearable={isClearable}
90
94
  isShowingClearableIcon={isShowingClearableIcon}
@@ -102,7 +106,7 @@ export function ComboBox({
102
106
  allowCustomEntry={allowCustomEntry}
103
107
  id={comboBoxListId}
104
108
  ariaLabelledById={id}
105
- popperReferenceElement={popperContentRef ?? contentRef.current ?? null}
109
+ popperReferenceElement={popperContentRef ?? contentRefState ?? null}
106
110
  >
107
111
  {children}
108
112
  </CombBoxListBox>
@@ -155,6 +155,16 @@ export function ComboBoxOption({
155
155
  [optionValueFocused, value]
156
156
  );
157
157
 
158
+ // scroll in to view
159
+ useEffect(
160
+ () => {
161
+ if (isOptionsExpanded && isSelected) {
162
+ optionRef.current?.scrollIntoView({ block: 'nearest' });
163
+ }
164
+ },
165
+ [isOptionsExpanded, isSelected]
166
+ );
167
+
158
168
  return (
159
169
  isVisible
160
170
  ? (
@@ -195,6 +195,16 @@ export function ComboBoxContextProvider({
195
195
  [comboBoxImmer[0].isOptionsExpanded]
196
196
  );
197
197
 
198
+ // update onClear on change
199
+ useEffect(
200
+ () => {
201
+ comboBoxImmer[1]((draftContext) => {
202
+ draftContext.onClear = onClear;
203
+ });
204
+ },
205
+ [onClear]
206
+ );
207
+
198
208
  return (
199
209
  <ComboBoxContext.Provider value={providerValue}>
200
210
  {children}
@@ -53,10 +53,14 @@ export function CombBoxListBox({
53
53
  }
54
54
  );
55
55
 
56
+ const lastMessageRef = useRef(/** @type {string | null} */(null));
56
57
  const addPoliteMessageDebounced = useDebounceFunc(
57
58
  useCallback(
58
59
  (message) => {
59
- addPoliteMessage(message);
60
+ if (lastMessageRef.current !== message) {
61
+ addPoliteMessage(message);
62
+ lastMessageRef.current = message;
63
+ }
60
64
  },
61
65
  [addPoliteMessage]
62
66
  ),
@@ -105,9 +109,7 @@ export function CombBoxListBox({
105
109
  if (allowCustomEntry && filterValue && !options.some((option) => option.labelLowerCase === filterValue.toLocaleLowerCase())) {
106
110
  message.push(`Press Enter to add ${filterValue} to the combo box list.`);
107
111
  }
108
- if (!isOptionsExpanded) {
109
- message.push('Use the down arrow key to begin selecting.');
110
- }
112
+ message.push('Use the down arrow key to begin selecting.');
111
113
  addPoliteMessageDebounced(message.join(' '));
112
114
  }
113
115
  },
@@ -23,6 +23,7 @@ import { moveComboBoxSelectionUp } from '../functions/moveComboBoxSelectionUp';
23
23
  * @param {string} [props.className]
24
24
  * @param {string} props.comboBoxListId
25
25
  * @param {string} [props.errorMessage]
26
+ * @param {(isOptionsExpanded: boolean) => React.ReactNode} [props.iconCallback] Can provide a custom icon to show for the popup icon
26
27
  * @param {string} props.id
27
28
  * @param {MutableRef<HTMLInputElement | null>} [props.innerRef]
28
29
  * @param {boolean} [props.isClearable]
@@ -46,6 +47,7 @@ export function ComboBoxTextInput({
46
47
  className,
47
48
  comboBoxListId,
48
49
  errorMessage,
50
+ iconCallback,
49
51
  id,
50
52
  innerRef: draftInnerRef,
51
53
  isClearable,
@@ -264,7 +266,15 @@ export function ComboBoxTextInput({
264
266
  <IconButton
265
267
  aria-hidden="true"
266
268
  className="combo-box-input__chevron icon-button--borderless icon-button--small1x"
267
- icon={<span className={isOptionsExpanded ? 'utds-icon-before-chevron-up' : 'utds-icon-before-chevron-down'} aria-hidden="true" />}
269
+ icon={
270
+ iconCallback?.(isOptionsExpanded)
271
+ ?? (
272
+ <span
273
+ aria-hidden="true"
274
+ className={isOptionsExpanded ? 'utds-icon-before-chevron-up' : 'utds-icon-before-chevron-down'}
275
+ />
276
+ )
277
+ }
268
278
  isDisabled={isDisabled}
269
279
  onClick={(e) => {
270
280
  e.stopPropagation();
@@ -95,7 +95,10 @@ export function useFormContextInputValue({
95
95
  ?? (contextOnChange && internalOnChange)
96
96
  ?? setInternalState
97
97
  ),
98
- onClear: /** @type {any} */ (onClear ?? (contextOnChange && internalOnClear)),
98
+ onClear: () => {
99
+ (onClear ?? internalOnClear)();
100
+ setInternalState(/** @type {ValueT} */(''));
101
+ },
99
102
 
100
103
  // direct access to form internals to do whatever you want, though be careful to allow
101
104
  // your input's passed in props to trump the form's props
@@ -3,6 +3,7 @@ import MultiSelectContextProvider from './context/MultiSelectContextProvider';
3
3
 
4
4
  /**
5
5
  * @param {object} props
6
+ * @param {boolean} [props.allowCustomEntry] can the user type in their own items to add to the list?
6
7
  * @param {import('react').ReactNode} [props.children]
7
8
  * @param {string} [props.className]
8
9
  * @param {string[]} [props.defaultValues]
@@ -17,12 +18,14 @@ import MultiSelectContextProvider from './context/MultiSelectContextProvider';
17
18
  * @param {string} [props.name]
18
19
  * @param {((newValue: string[]) => void)} [props.onChange]
19
20
  * @param {() => void} [props.onClear]
21
+ * @param {(customValue: string) => void} [props.onCustomEntry] caller is responsible for adding options when they are added
20
22
  * @param {string} [props.placeholder]
21
23
  * @param {string[]} [props.values]
22
24
  * @param {string} [props.wrapperClassName]
23
25
  * @returns {import('react').JSX.Element}
24
26
  */
25
27
  export function MultiSelect({
28
+ allowCustomEntry,
26
29
  children,
27
30
  className,
28
31
  defaultValues,
@@ -37,6 +40,7 @@ export function MultiSelect({
37
40
  name,
38
41
  onChange,
39
42
  onClear,
43
+ onCustomEntry,
40
44
  placeholder,
41
45
  values,
42
46
  wrapperClassName,
@@ -51,6 +55,7 @@ export function MultiSelect({
51
55
  values={values}
52
56
  >
53
57
  <MultiSelectComboBox
58
+ allowCustomEntry={allowCustomEntry}
54
59
  className={className}
55
60
  errorMessage={errorMessage}
56
61
  innerRef={innerRef}
@@ -60,6 +65,7 @@ export function MultiSelect({
60
65
  label={label}
61
66
  labelClassName={labelClassName}
62
67
  name={name}
68
+ onCustomEntry={onCustomEntry}
63
69
  placeholder={placeholder}
64
70
  wrapperClassName={wrapperClassName}
65
71
  // eslint-disable-next-line react/jsx-props-no-spreading
@@ -18,6 +18,7 @@ import { useMultiSelectContext } from './context/useMultiSelectContext';
18
18
 
19
19
  /**
20
20
  * @param {object} props
21
+ * @param {boolean} [props.allowCustomEntry] can the user type in their own items to add to the list?
21
22
  * @param {import('react').ReactNode} [props.children]
22
23
  * @param {string} [props.className]
23
24
  * @param {string} [props.errorMessage]
@@ -28,11 +29,13 @@ import { useMultiSelectContext } from './context/useMultiSelectContext';
28
29
  * @param {string} props.label
29
30
  * @param {string} [props.labelClassName]
30
31
  * @param {string} [props.name]
32
+ * @param {(customValue: string) => void} [props.onCustomEntry] caller is responsible for adding options when they are added
31
33
  * @param {string} [props.placeholder]
32
34
  * @param {string} [props.wrapperClassName]
33
35
  * @returns {import('react').JSX.Element}
34
36
  */
35
37
  export function MultiSelectComboBox({
38
+ allowCustomEntry,
36
39
  children,
37
40
  className,
38
41
  errorMessage,
@@ -43,6 +46,7 @@ export function MultiSelectComboBox({
43
46
  label,
44
47
  labelClassName,
45
48
  name,
49
+ onCustomEntry,
46
50
  placeholder,
47
51
  wrapperClassName,
48
52
  ...rest
@@ -110,6 +114,7 @@ export function MultiSelectComboBox({
110
114
  >
111
115
  <MultiSelectTags isDisabled={isDisabled} />
112
116
  <ComboBox
117
+ allowCustomEntry={allowCustomEntry}
113
118
  className="multi-select__combo-box"
114
119
  id={multiSelectContextValue.multiSelectId}
115
120
  isDisabled={isDisabled}
@@ -122,6 +127,7 @@ export function MultiSelectComboBox({
122
127
  onChange={(newValue) => {
123
128
  multiSelectContextValue.onChange(uniq(selectedValuesRef.current.concat(newValue)));
124
129
  }}
130
+ onCustomEntry={onCustomEntry}
125
131
  onKeyUp={(e, currentFilter) => {
126
132
  let eventIsHandled = false;
127
133
  // check that filter is blank and that there are options selected
@@ -1,4 +1,6 @@
1
+ import { useId } from 'react';
1
2
  import { joinClassNames } from '../../util/joinClassNames';
3
+ import { useFormContextInput } from './FormContext/useFormContextInput';
2
4
 
3
5
  /**
4
6
  * Sometimes you want a label that has static text next to it that looks and fits in to the
@@ -26,8 +28,10 @@ export function PlainText({
26
28
  wrapperClassName,
27
29
  ...rest
28
30
  }) {
31
+ const internalId = useId();
32
+ const { value: currentValue } = useFormContextInput({ id: id || internalId, value });
29
33
  return (
30
- <div className={joinClassNames('plain-text-wrapper', 'plain-text-wrapper--plain-text', wrapperClassName)} ref={innerRef}>
34
+ <div className={joinClassNames('input-wrapper', 'input-wrapper--plain-text', wrapperClassName)} ref={innerRef}>
31
35
  {
32
36
  isLabelSkipped
33
37
  ? null
@@ -39,7 +43,8 @@ export function PlainText({
39
43
  }
40
44
  <div className="plain-text__inner-wrapper">
41
45
  <div className={joinClassNames(className)} id={id} {...rest}>
42
- {value}
46
+ {/* empty div doesn't take up space. the UI was jumping up and down when there wasn't a value. if there is nothing then put something */}
47
+ {currentValue || <>&nbsp;</>}
43
48
  </div>
44
49
  </div>
45
50
  </div>
@@ -65,16 +65,16 @@ export function Select({
65
65
  });
66
66
  const selectInputRef = /** @type {typeof useRef<HTMLSelectElement>} */ (useRef)(null);
67
67
 
68
- const { addAssertiveMessage } = useAriaMessaging();
68
+ const { addPoliteMessage } = useAriaMessaging();
69
69
 
70
70
  const clearInput = useCallback(
71
71
  /** @param {import('react').MouseEvent} e */
72
72
  (e) => {
73
73
  currentOnClear?.(e);
74
- addAssertiveMessage(`${label} input was cleared`);
74
+ addPoliteMessage(`${label} input was cleared`);
75
75
  selectInputRef.current?.focus();
76
76
  },
77
- [addAssertiveMessage, currentOnClear, label]
77
+ [addPoliteMessage, currentOnClear, label]
78
78
  );
79
79
 
80
80
  const showClearIcon = !!((isClearable || onClear) && currentValue);
@@ -65,7 +65,7 @@ export function TextArea({
65
65
 
66
66
  const onChangeSetCursorPosition = useRememberCursorPosition(inputRef, value || '');
67
67
 
68
- const { addAssertiveMessage } = useAriaMessaging();
68
+ const { addPoliteMessage } = useAriaMessaging();
69
69
 
70
70
  const showClearIcon = !!((isClearable || onClear) && currentValue);
71
71
 
@@ -73,10 +73,10 @@ export function TextArea({
73
73
  /** @param {import('react').UIEvent} e */
74
74
  (e) => {
75
75
  currentOnClear?.(e);
76
- addAssertiveMessage(`${label} input was cleared`);
76
+ addPoliteMessage(`${label} input was cleared`);
77
77
  inputRef.current?.focus();
78
78
  },
79
- [addAssertiveMessage, currentOnClear, label]
79
+ [addPoliteMessage, currentOnClear, label]
80
80
  );
81
81
 
82
82
  const checkKeyPressed = useCallback(
@@ -76,7 +76,7 @@ export function TextInput({
76
76
 
77
77
  const onChangeSetCursorPosition = useRememberCursorPosition(inputRef, value || '');
78
78
 
79
- const { addAssertiveMessage } = useAriaMessaging();
79
+ const { addPoliteMessage } = useAriaMessaging();
80
80
 
81
81
  const showClearIcon = isShowingClearableIcon ?? !!((isClearable || onClear) && currentValue);
82
82
 
@@ -88,10 +88,10 @@ export function TextInput({
88
88
  } else if (inputRef.current) {
89
89
  inputRef.current.value = '';
90
90
  }
91
- addAssertiveMessage(`${label} input was cleared`);
91
+ addPoliteMessage(`${label} input was cleared`);
92
92
  inputRef.current?.focus();
93
93
  },
94
- [addAssertiveMessage, currentOnClear, label]
94
+ [addPoliteMessage, currentOnClear, label]
95
95
  );
96
96
 
97
97
  const checkKeyPressed = useCallback(
@@ -0,0 +1,166 @@
1
+ import { add, format, isValid, parse } from 'date-fns';
2
+ import { useMemo } from 'react';
3
+ import { joinClassNames } from '../../util/joinClassNames';
4
+ import { ComboBox } from './ComboBox/ComboBox';
5
+ import { ComboBoxOption } from './ComboBox/ComboBoxOption';
6
+ import { useFormContextInputValue } from './FormContext/useFormContextInputValue';
7
+ import { TextInput } from './TextInput';
8
+
9
+ /**
10
+ * @param {object} props
11
+ * @param {boolean} [props.allowCustomEntry] can the user type in their own time that is not in the popup combobox list
12
+ * @param {string} [props.className]
13
+ * @param {string} [props.defaultValue]
14
+ * @param {string} [props.errorMessage]
15
+ * @param {boolean} [props.hasTimePopup] is there a popup from which the user can select the time?
16
+ * @param {string} props.id when tied to a Form, the `id` is also the 'dot' path to the data in the form's state: ie person.contact.address.line1
17
+ * @param {import('react').Ref<HTMLDivElement>} [props.innerRef]
18
+ * @param {boolean} [props.isClearable] should the clearable "X" icon be shown; is auto set to true if onClear is passed in
19
+ * @param {boolean} [props.isDisabled]
20
+ * @param {boolean} [props.isRequired]
21
+ * @param {string} props.label
22
+ * @param {string} [props.labelClassName]
23
+ * @param {string} [props.name]
24
+ * @param {(newValue: string) => void} [props.onChange] can be omitted to be uncontrolled OR controlled by form
25
+ * @param {() => void} [props.onClear] (not needed if inside a <Form> context)
26
+ * @param {string} [props.placeholder]
27
+ * @param {string} [props.timeFormat] use `date-fns` modifiers for formatting the time options
28
+ * @param {number} [props.timeRangeIncrement] for popup, what increment (in minutes) for the options given to the user
29
+ * @param {string} [props.timeRangeBegin] options in popup can start (inclusive) at a given time; format per `props.timeFormat`
30
+ * @param {string} [props.timeRangeEnd] options in popup can end at the given time (inclusive); format per `props.timeFormat`
31
+ * @param {string} [props.value]
32
+ * @param {string} [props.wrapperClassName]
33
+ * @returns {import('react').JSX.Element}
34
+ */
35
+ export function TimeInput({
36
+ allowCustomEntry,
37
+ className,
38
+ defaultValue,
39
+ errorMessage,
40
+ hasTimePopup = true,
41
+ id,
42
+ innerRef,
43
+ isClearable,
44
+ isDisabled,
45
+ isRequired,
46
+ label,
47
+ labelClassName,
48
+ name,
49
+ onChange,
50
+ onClear,
51
+ placeholder,
52
+ timeFormat = 'h:mm aaa',
53
+ timeRangeBegin,
54
+ timeRangeEnd,
55
+ timeRangeIncrement = 15,
56
+ value,
57
+ wrapperClassName,
58
+ ...rest
59
+ }) {
60
+ const {
61
+ onChange: currentOnChange,
62
+ onClear: currentOnClear,
63
+ value: currentValue,
64
+ } = useFormContextInputValue({
65
+ defaultValue,
66
+ id,
67
+ onChange,
68
+ onClear,
69
+ value,
70
+ });
71
+
72
+ const timeOptions = useMemo(
73
+ () => {
74
+ const defaultStartDate = new Date(new Date().setHours(0, 0, 0, 0));
75
+ const defaultEndDate = new Date(new Date().setHours(23, 59, 0, 0));
76
+
77
+ let optionsBeginDate = (timeRangeBegin && parse(timeRangeBegin, timeFormat, new Date())) || null;
78
+ optionsBeginDate = (optionsBeginDate && isValid(optionsBeginDate)) ? optionsBeginDate : defaultStartDate;
79
+
80
+ let optionsEndDate = (timeRangeEnd && parse(timeRangeEnd, timeFormat, new Date())) || null;
81
+ optionsEndDate = (optionsEndDate && isValid(optionsEndDate)) ? optionsEndDate : defaultEndDate;
82
+
83
+ const timeOptionsRet = [];
84
+ for (
85
+ let loopDate = optionsBeginDate;
86
+ loopDate.getTime() <= optionsEndDate.getTime();
87
+ loopDate = add(loopDate, { minutes: timeRangeIncrement })
88
+ ) {
89
+ timeOptionsRet.push(format(loopDate, timeFormat));
90
+ }
91
+
92
+ return timeOptionsRet;
93
+ },
94
+ [timeRangeBegin, timeRangeEnd, timeRangeIncrement]
95
+ );
96
+
97
+ const clockIcon = useMemo(
98
+ () => (
99
+ <span className={joinClassNames('utds-icon-before-clock', 'time-input__clock-icon', isDisabled && 'time-input__clock-icon--is-disabled', !hasTimePopup && 'time-input__clock-icon--static')} aria-hidden="true" />
100
+ ),
101
+ [isDisabled, hasTimePopup]
102
+ );
103
+
104
+ return (
105
+ <div className={joinClassNames('time-input__wrapper', wrapperClassName)} ref={innerRef}>
106
+ {
107
+ hasTimePopup
108
+ ? (
109
+ <ComboBox
110
+ // COMMON PROPS: make sure these match with TextInput
111
+ className={className}
112
+ errorMessage={errorMessage}
113
+ id={id}
114
+ isClearable={isClearable}
115
+ isDisabled={isDisabled}
116
+ isRequired={isRequired}
117
+ label={label}
118
+ labelClassName={labelClassName}
119
+ name={name || id}
120
+ onClear={isClearable ? currentOnClear : undefined}
121
+ placeholder={placeholder}
122
+ value={currentValue}
123
+ // END COMMON PROPS
124
+ allowCustomEntry={allowCustomEntry}
125
+ iconCallback={() => clockIcon}
126
+ onChange={currentOnChange}
127
+ // eslint-disable-next-line react/jsx-props-no-spreading
128
+ {...rest}
129
+ >
130
+ {
131
+ timeOptions.map((timeOption) => (
132
+ <ComboBoxOption
133
+ key={`time-input__${id}__${timeOption}`}
134
+ label={timeOption}
135
+ value={timeOption}
136
+ />
137
+ ))
138
+ }
139
+ </ComboBox>
140
+ )
141
+ : (
142
+ <TextInput
143
+ // COMMON PROPS: make sure these match with ComboBox
144
+ className={className}
145
+ errorMessage={errorMessage}
146
+ id={id}
147
+ isClearable={isClearable}
148
+ isDisabled={isDisabled}
149
+ isRequired={isRequired}
150
+ label={label}
151
+ labelClassName={labelClassName}
152
+ name={name || id}
153
+ onClear={isClearable ? currentOnClear : undefined}
154
+ placeholder={placeholder}
155
+ value={currentValue}
156
+ // END COMMON PROPS
157
+ onChange={(e) => currentOnChange(e.target.value)}
158
+ rightContent={clockIcon}
159
+ // eslint-disable-next-line react/jsx-props-no-spreading
160
+ {...rest}
161
+ />
162
+ )
163
+ }
164
+ </div>
165
+ );
166
+ }
@@ -1,5 +1,5 @@
1
1
  import { joinClassNames } from '../../util/joinClassNames';
2
- import { MenuItem } from './MenuItem';
2
+ import { MenuItemInline } from './items/MenuItemInline';
3
3
 
4
4
  /** @typedef {import('@utahdts/utah-design-system').WebsiteMainMenu} WebsiteMainMenu */
5
5
  /** @typedef {import('@utahdts/utah-design-system').WebsiteMainMenuItem} WebsiteMainMenuItem */
@@ -27,7 +27,7 @@ export function HorizontalMenu({
27
27
  <TitleTagName id={id} className={titleTagClassName}>Main Menu</TitleTagName>
28
28
  <ul>
29
29
  {menu?.menuItems?.map((menuItem) => (
30
- <MenuItem menuItem={menuItem} key={`horizontal-menu__nav-link__${menuItem.link}-${menuItem.title}}`} currentMenuItem={currentMenuItem} />
30
+ <MenuItemInline menuItem={menuItem} key={`horizontal-menu__nav-link__${menuItem.link}-${menuItem.title}}`} currentMenuItem={currentMenuItem} />
31
31
  ))}
32
32
  </ul>
33
33
  </nav>