@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.
- package/css/1-settings/_spacing.scss +16 -0
- package/css/6-components/_components-index.scss +1 -0
- package/css/6-components/base-components/forms/_combo-box-input.scss +5 -0
- package/css/6-components/base-components/forms/_time-input.scss +28 -0
- package/css/6-components/base-components/navigation/_menu-item.scss +35 -0
- package/css/6-components/base-components/navigation/_side-panel-navigation.scss +6 -1
- package/css/6-components/base-components/navigation/_vertical-menu.scss +45 -1
- package/dist/style.css +297 -9
- package/dist/utah-design-system.es.js +3846 -3441
- package/dist/utah-design-system.umd.js +3845 -3440
- package/index.js +2 -0
- package/package.json +12 -12
- package/react/components/forms/ComboBox/ComboBox.jsx +9 -5
- package/react/components/forms/ComboBox/ComboBoxOption.jsx +10 -0
- package/react/components/forms/ComboBox/context/ComboBoxContextProvider.jsx +10 -0
- package/react/components/forms/ComboBox/internal/CombBoxListBox.jsx +6 -4
- package/react/components/forms/ComboBox/internal/ComboBoxTextInput.jsx +11 -1
- package/react/components/forms/FormContext/useFormContextInputValue.js +4 -1
- package/react/components/forms/MultiSelect/MultiSelect.jsx +6 -0
- package/react/components/forms/MultiSelect/MultiSelectComboBox.jsx +6 -0
- package/react/components/forms/PlainText.jsx +7 -2
- package/react/components/forms/Select.jsx +3 -3
- package/react/components/forms/TextArea.jsx +3 -3
- package/react/components/forms/TextInput.jsx +3 -3
- package/react/components/forms/TimeInput.jsx +166 -0
- package/react/components/navigation/HorizontalMenu.jsx +2 -2
- package/react/components/navigation/VerticalMenu.jsx +45 -8
- package/react/components/navigation/items/MenuItemFlyout.jsx +119 -0
- package/react/components/navigation/{MenuItem.jsx → items/MenuItemInline.jsx} +24 -8
- package/react/components/navigation/items/MenuItemPlain.jsx +63 -0
- package/react/enums/menuTypes.js +7 -0
- package/react/hooks/useDebounceFunc.js +1 -1
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
80
|
+
"@types/react": "18.2.79",
|
|
81
81
|
"@types/react-dom": "18.2.18",
|
|
82
|
-
"@types/uuid": "9.0.
|
|
82
|
+
"@types/uuid": "9.0.8",
|
|
83
83
|
"@vitejs/plugin-react": "4.2.1",
|
|
84
|
-
"@vitest/coverage-istanbul": "1.2.
|
|
85
|
-
"@vitest/ui": "1.2.
|
|
86
|
-
"jsdom": "
|
|
87
|
-
"sass": "1.
|
|
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.
|
|
90
|
-
"vitest": "1.2.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
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={
|
|
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:
|
|
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('
|
|
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 || <> </>}
|
|
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 {
|
|
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
|
-
|
|
74
|
+
addPoliteMessage(`${label} input was cleared`);
|
|
75
75
|
selectInputRef.current?.focus();
|
|
76
76
|
},
|
|
77
|
-
[
|
|
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 {
|
|
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
|
-
|
|
76
|
+
addPoliteMessage(`${label} input was cleared`);
|
|
77
77
|
inputRef.current?.focus();
|
|
78
78
|
},
|
|
79
|
-
[
|
|
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 {
|
|
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
|
-
|
|
91
|
+
addPoliteMessage(`${label} input was cleared`);
|
|
92
92
|
inputRef.current?.focus();
|
|
93
93
|
},
|
|
94
|
-
[
|
|
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 {
|
|
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
|
-
<
|
|
30
|
+
<MenuItemInline menuItem={menuItem} key={`horizontal-menu__nav-link__${menuItem.link}-${menuItem.title}}`} currentMenuItem={currentMenuItem} />
|
|
31
31
|
))}
|
|
32
32
|
</ul>
|
|
33
33
|
</nav>
|