goobs-frontend 0.7.65 → 0.7.67

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.
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import React, { useState, useRef, useEffect } from 'react'
3
+ import React, { useRef, useCallback, useMemo } from 'react'
4
4
  import { Box, InputLabel, OutlinedInput, styled } from '@mui/material'
5
5
  import { session } from 'goobs-cache'
6
6
  import { useDropdown } from './hooks/useDropdown'
@@ -15,32 +15,20 @@ import {
15
15
  } from './hooks/useInputHelperFooter'
16
16
  import { useRequiredFieldsValidator } from './hooks/useRequiredFieldsValidator'
17
17
  import labelStyles from '../../styles/StyledComponent/Label'
18
- import { useHasInputEffect, usePreventAutocompleteEffect } from './useEffects'
19
-
20
- /**
21
- * Props interface for the StyledComponent.
22
- * @interface
23
- */
24
- export interface StyledComponentProps {
25
- /** Name attribute for the input element */
18
+ import { usePreventAutocomplete } from './useCallbacks'
19
+ import { ClientLogger } from 'goobs-testing'
20
+
21
+ export interface StyledComponentProps
22
+ extends React.ComponentPropsWithoutRef<'div'> {
26
23
  name?: string
27
- /** Color of the input outline */
28
24
  outlinecolor?: string
29
- /** Color of the icon */
30
25
  iconcolor?: string
31
- /** Background color of the input */
32
26
  backgroundcolor?: string
33
- /** Whether the input is notched */
34
27
  notched?: boolean
35
- /** Combined font color for the input */
36
28
  combinedfontcolor?: string
37
- /** Font color when the label is not shrunk */
38
29
  unshrunkfontcolor?: string
39
- /** Font color when the label is shrunk */
40
30
  shrunkfontcolor?: string
41
- /** Autocomplete attribute for the input */
42
31
  autoComplete?: string
43
- /** Variant of the component */
44
32
  componentvariant?:
45
33
  | 'multilinetextfield'
46
34
  | 'dropdown'
@@ -58,51 +46,28 @@ export interface StyledComponentProps {
58
46
  | 'time'
59
47
  | 'date'
60
48
  | 'splitbutton'
61
- /** Options for dropdown variant */
62
49
  options?: readonly string[]
63
- /** Default option for dropdown variant */
64
50
  defaultOption?: string
65
- /** Helper footer message */
66
51
  helperfooter?: HelperFooterMessage
67
- /** Placeholder text for the input */
68
52
  placeholder?: string
69
- /** Minimum number of rows for multiline text field */
70
53
  minRows?: number
71
- /** Name of the form the input belongs to */
72
54
  formname?: string
73
- /** Label text for the input */
74
55
  label?: string
75
- /** Location of the shrunk label */
76
56
  shrunklabellocation?: 'onnotch' | 'above'
77
- /** Value of the input */
78
57
  value?: string
79
- /** Status of the value */
80
58
  valuestatus?: boolean
81
- /** Whether the input is focused */
82
59
  focused?: boolean
83
- /** Whether the input is required */
84
60
  required?: boolean
85
- /** Whether the form has been submitted */
86
61
  formSubmitted?: boolean
87
- /** ARIA label for the input */
88
62
  'aria-label'?: string
89
- /** ARIA required attribute */
90
63
  'aria-required'?: boolean
91
- /** ARIA invalid attribute */
92
64
  'aria-invalid'?: boolean
93
- /** ARIA describedby attribute */
94
65
  'aria-describedby'?: string
95
- /** Callback function when an option is selected (for dropdown variant) */
96
66
  onOptionSelect?: (option: string) => void
97
- /** Callback function when the input value changes */
98
67
  onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
99
- /** Priority of the spread message */
100
68
  spreadMessagePriority?: number
101
69
  }
102
70
 
103
- /**
104
- * Styled OutlinedInput component that prevents autofill styling.
105
- */
106
71
  const NoAutofillOutlinedInput = styled(OutlinedInput)(() => ({
107
72
  '& .MuiInputBase-input': {
108
73
  '&:-webkit-autofill': {
@@ -120,12 +85,9 @@ const NoAutofillOutlinedInput = styled(OutlinedInput)(() => ({
120
85
  },
121
86
  }))
122
87
 
123
- /**
124
- * StyledComponent is a versatile input component that can render various types of inputs based on the provided props.
125
- * @param {StyledComponentProps} props - The props for the StyledComponent
126
- * @returns {React.ReactElement} The rendered StyledComponent
127
- */
128
- const StyledComponent: React.FC<StyledComponentProps> = props => {
88
+ const StyledComponent: React.FC<StyledComponentProps> = React.memo(props => {
89
+ ClientLogger.debug('StyledComponent render', { props })
90
+
129
91
  const {
130
92
  label,
131
93
  componentvariant,
@@ -137,7 +99,6 @@ const StyledComponent: React.FC<StyledComponentProps> = props => {
137
99
  shrunkfontcolor,
138
100
  shrunklabellocation,
139
101
  value,
140
- valuestatus,
141
102
  placeholder,
142
103
  formname,
143
104
  formSubmitted = false,
@@ -148,137 +109,226 @@ const StyledComponent: React.FC<StyledComponentProps> = props => {
148
109
  onOptionSelect,
149
110
  onChange,
150
111
  spreadMessagePriority,
112
+ minRows,
113
+ ...restProps
151
114
  } = props
152
115
 
153
- const { validateField } = useInputHelperFooter()
154
- const helperFooterAtom = session.atom<Record<string, HelperFooterMessage>>({})
155
- const [helperFooterValue] = session.useAtom(helperFooterAtom)
156
-
157
- const showErrorAtom = session.atom<boolean>(false)
158
- const [showError, setShowError] = session.useAtom(showErrorAtom)
116
+ const helperFooterAtom = useMemo(
117
+ () => session.atom<Record<string, HelperFooterMessage>>({}),
118
+ []
119
+ )
120
+ const [helperFooters] = session.useAtom(helperFooterAtom)
121
+ const { validateField, useShowErrorEffect } =
122
+ useInputHelperFooter(helperFooterAtom)
159
123
 
160
- const hasInputRef = useRef(false)
124
+ const inputValueAtom = useMemo(() => session.atom(value || ''), [value])
125
+ const [inputValue, setInputValue] = session.useAtom(inputValueAtom)
126
+ const hasInput = useMemo(() => !!inputValue, [inputValue])
127
+ const hasInputRef = useRef(hasInput)
128
+ hasInputRef.current = hasInput
161
129
 
162
- useRequiredFieldsValidator(formname || '', [props], hasInputRef)
130
+ const memoizedRequiredFieldsProps = useMemo(() => [props], [props])
131
+ useRequiredFieldsValidator(
132
+ formname || '',
133
+ memoizedRequiredFieldsProps,
134
+ hasInputRef,
135
+ helperFooterAtom
136
+ )
163
137
 
164
- const [isFocused, setIsFocused] = useState(false)
165
- const [hasInput, setHasInput] = useState(false)
166
- const [passwordVisible, setPasswordVisible] = useState(false)
167
- const inputRefInternal = useRef<HTMLInputElement>(null)
138
+ const isFocusedAtom = useMemo(() => session.atom(false), [])
139
+ const [isFocused, setIsFocused] = session.useAtom(isFocusedAtom)
140
+ const passwordVisibleAtom = useMemo(() => session.atom(false), [])
141
+ const [passwordVisible, setPasswordVisible] =
142
+ session.useAtom(passwordVisibleAtom)
143
+ const inputRefInternal = useRef<HTMLInputElement | null>(null)
168
144
  const inputBoxRef = useRef<HTMLDivElement>(null)
169
145
 
170
- const { renderMenu, selectedOption, handleDropdownClick } = useDropdown(
171
- props,
172
- inputBoxRef,
173
- onOptionSelect
146
+ const memoizedDropdownProps = useMemo(
147
+ () => ({
148
+ componentvariant,
149
+ options: props.options,
150
+ value: inputValue,
151
+ defaultOption: props.defaultOption,
152
+ }),
153
+ [componentvariant, props.options, inputValue, props.defaultOption]
174
154
  )
175
- const { phoneNumber, handlePhoneNumberChange } = usePhoneNumber(
176
- value || '',
177
- componentvariant
155
+
156
+ const {
157
+ renderMenu,
158
+ selectedOption,
159
+ handleDropdownClick,
160
+ updateDropdownState,
161
+ } = useDropdown(memoizedDropdownProps, inputBoxRef, onOptionSelect)
162
+
163
+ const { phoneNumber, handlePhoneNumberChange, checkAndUpdatePhoneNumber } =
164
+ usePhoneNumber(inputValue, componentvariant)
165
+
166
+ const memoizedSplitButtonProps = useMemo(
167
+ () => ({ value: inputValue }),
168
+ [inputValue]
178
169
  )
179
170
  const {
180
171
  value: splitButtonValue,
181
172
  handleIncrement,
182
173
  handleDecrement,
183
- } = useSplitButton(props)
184
-
185
- useHasInputEffect(value, valuestatus, setHasInput)
186
- usePreventAutocompleteEffect(inputRefInternal)
187
-
188
- /**
189
- * Show error after a delay when form is submitted or input has value
190
- */
191
- useEffect(() => {
192
- const timer = setTimeout(() => {
193
- setShowError(formSubmitted || hasInput)
194
- }, 1000)
195
-
196
- return () => clearTimeout(timer)
197
- }, [formSubmitted, hasInput, setShowError])
198
-
199
- useEffect(() => {
200
- hasInputRef.current = !!value
201
- }, [value])
202
-
203
- const currentHelperFooter = name ? helperFooterValue[name] : undefined
204
-
205
- /**
206
- * Handle change event for the input
207
- * @param {React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>} e - The change event
208
- */
209
- const handleChange = (
210
- e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
211
- ) => {
212
- if (componentvariant === 'phonenumber') {
213
- handlePhoneNumberChange(e)
214
- if (onChange) {
215
- onChange(e as React.ChangeEvent<HTMLInputElement>)
174
+ updateValueFromProps,
175
+ } = useSplitButton(memoizedSplitButtonProps)
176
+
177
+ const setInputAttributes = usePreventAutocomplete()
178
+
179
+ const inputRef = useCallback(
180
+ (node: HTMLInputElement | null) => {
181
+ inputRefInternal.current = node
182
+ if (node) {
183
+ setInputAttributes(node)
216
184
  }
217
- } else if (componentvariant === 'splitbutton') {
218
- const numValue = e.target.value.replace(/[^0-9]/g, '')
219
- e.target.value = numValue
220
- if (onChange) {
185
+ },
186
+ [setInputAttributes]
187
+ )
188
+
189
+ const showError = useShowErrorEffect(formSubmitted, hasInput, isFocused)
190
+
191
+ const handleChange = useCallback(
192
+ (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
193
+ ClientLogger.debug('handleChange', {
194
+ componentvariant,
195
+ value: e.target.value,
196
+ name: e.target.name,
197
+ })
198
+
199
+ setInputValue(e.target.value)
200
+
201
+ if (componentvariant === 'phonenumber') {
202
+ handlePhoneNumberChange(e)
203
+ if (onChange) {
204
+ onChange(e as React.ChangeEvent<HTMLInputElement>)
205
+ }
206
+ } else if (componentvariant === 'splitbutton') {
207
+ const numValue = e.target.value.replace(/[^0-9]/g, '')
208
+ e.target.value = numValue
209
+ if (onChange) {
210
+ onChange(e as React.ChangeEvent<HTMLInputElement>)
211
+ }
212
+ } else if (onChange) {
221
213
  onChange(e as React.ChangeEvent<HTMLInputElement>)
222
214
  }
223
- } else if (onChange) {
224
- onChange(e as React.ChangeEvent<HTMLInputElement>)
225
- }
226
215
 
227
- setHasInput(!!e.target.value)
228
- hasInputRef.current = !!e.target.value
216
+ const newHasInput = !!e.target.value
217
+ hasInputRef.current = newHasInput
229
218
 
230
- const formData = new FormData()
231
- formData.append(e.target.name, e.target.value)
232
- if (name && label && formname) {
233
- validateField(name, formData, label, formname, spreadMessagePriority)
234
- }
235
- }
219
+ const formData = new FormData()
220
+ formData.append(e.target.name, e.target.value)
221
+ if (name && label && formname) {
222
+ validateField(name, formData, label, formname, spreadMessagePriority)
223
+ }
236
224
 
237
- /**
238
- * Handle focus event for the input
239
- */
240
- const handleFocus = () => {
225
+ if (componentvariant === 'dropdown') {
226
+ updateDropdownState()
227
+ } else if (componentvariant === 'phonenumber') {
228
+ checkAndUpdatePhoneNumber()
229
+ } else if (componentvariant === 'splitbutton') {
230
+ updateValueFromProps()
231
+ }
232
+ },
233
+ [
234
+ componentvariant,
235
+ handlePhoneNumberChange,
236
+ onChange,
237
+ name,
238
+ label,
239
+ formname,
240
+ validateField,
241
+ spreadMessagePriority,
242
+ updateDropdownState,
243
+ checkAndUpdatePhoneNumber,
244
+ updateValueFromProps,
245
+ setInputValue,
246
+ ]
247
+ )
248
+
249
+ const handleFocus = useCallback(() => {
250
+ ClientLogger.debug('handleFocus')
241
251
  setIsFocused(true)
242
- }
252
+ }, [setIsFocused])
243
253
 
244
- /**
245
- * Handle blur event for the input
246
- */
247
- const handleBlur = () => {
254
+ const handleBlur = useCallback(() => {
255
+ ClientLogger.debug('handleBlur', {
256
+ name,
257
+ label,
258
+ hasInput: hasInputRef.current,
259
+ formname,
260
+ })
248
261
  setIsFocused(false)
249
- if (name && label && !hasInput && formname) {
262
+ if (name && label && !hasInputRef.current && formname) {
250
263
  const formData = new FormData()
251
264
  formData.append(name, '')
252
265
  validateField(name, formData, label, formname, spreadMessagePriority)
253
266
  }
254
- }
255
-
256
- /**
257
- * Toggle password visibility for password input
258
- */
259
- const togglePasswordVisibility = () => {
260
- setPasswordVisible(!passwordVisible)
261
- }
262
-
263
- const isDropdownVariant = componentvariant === 'dropdown'
264
- const isSplitButtonVariant = componentvariant === 'splitbutton'
265
- const isNotchedVariant =
266
- !isDropdownVariant &&
267
- !isSplitButtonVariant &&
268
- shrunklabellocation !== 'above' &&
269
- !!label
270
- const hasPlaceholder = !!placeholder
271
-
272
- const shouldShrinkLabel =
273
- isFocused ||
274
- isDropdownVariant ||
275
- isSplitButtonVariant ||
276
- hasPlaceholder ||
277
- hasInput ||
278
- (componentvariant === 'phonenumber' && phoneNumber !== '')
267
+ }, [
268
+ name,
269
+ label,
270
+ formname,
271
+ validateField,
272
+ spreadMessagePriority,
273
+ setIsFocused,
274
+ ])
275
+
276
+ const togglePasswordVisibility = useCallback(() => {
277
+ setPasswordVisible(prev => {
278
+ ClientLogger.debug('togglePasswordVisibility', { passwordVisible: !prev })
279
+ return !prev
280
+ })
281
+ }, [setPasswordVisible])
282
+
283
+ const isDropdownVariant = useMemo(
284
+ () => componentvariant === 'dropdown',
285
+ [componentvariant]
286
+ )
287
+ const isSplitButtonVariant = useMemo(
288
+ () => componentvariant === 'splitbutton',
289
+ [componentvariant]
290
+ )
291
+ const isNotchedVariant = useMemo(
292
+ () =>
293
+ !isDropdownVariant &&
294
+ !isSplitButtonVariant &&
295
+ shrunklabellocation !== 'above' &&
296
+ !!label,
297
+ [isDropdownVariant, isSplitButtonVariant, shrunklabellocation, label]
298
+ )
299
+ const hasPlaceholder = useMemo(() => !!placeholder, [placeholder])
300
+
301
+ const shouldShrinkLabel = useMemo(
302
+ () => !!inputValue || isFocused || hasPlaceholder,
303
+ [inputValue, isFocused, hasPlaceholder]
304
+ )
305
+
306
+ const shouldNotch = useMemo(
307
+ () => shouldShrinkLabel && isNotchedVariant,
308
+ [shouldShrinkLabel, isNotchedVariant]
309
+ )
310
+
311
+ ClientLogger.debug('Rendering StyledComponent', {
312
+ isDropdownVariant,
313
+ isSplitButtonVariant,
314
+ isNotchedVariant,
315
+ hasPlaceholder,
316
+ shouldShrinkLabel,
317
+ shouldNotch,
318
+ componentvariant,
319
+ name,
320
+ label,
321
+ inputValue,
322
+ phoneNumber,
323
+ selectedOption,
324
+ splitButtonValue,
325
+ showError,
326
+ currentHelperFooter: name ? helperFooters[name]?.statusMessage : undefined,
327
+ })
279
328
 
280
329
  return (
281
330
  <Box
331
+ {...restProps}
282
332
  sx={{
283
333
  boxSizing: 'border-box',
284
334
  display: 'flex',
@@ -316,7 +366,8 @@ const StyledComponent: React.FC<StyledComponentProps> = props => {
316
366
  )}
317
367
  <Box ref={inputBoxRef} sx={{ width: '100%' }}>
318
368
  <NoAutofillOutlinedInput
319
- ref={inputRefInternal}
369
+ inputRef={inputRef}
370
+ minRows={minRows}
320
371
  style={{
321
372
  backgroundColor: backgroundcolor || 'inherit',
322
373
  width: '100%',
@@ -347,7 +398,7 @@ const StyledComponent: React.FC<StyledComponentProps> = props => {
347
398
  'aria-invalid': ariaInvalid,
348
399
  'aria-required': ariaRequired,
349
400
  'aria-describedby':
350
- ariaDescribedBy || currentHelperFooter?.statusMessage
401
+ ariaDescribedBy || (name && helperFooters[name]?.statusMessage)
351
402
  ? `${name}-helper-text`
352
403
  : undefined,
353
404
  }}
@@ -388,39 +439,35 @@ const StyledComponent: React.FC<StyledComponentProps> = props => {
388
439
  ? selectedOption
389
440
  : isSplitButtonVariant
390
441
  ? splitButtonValue
391
- : value
442
+ : inputValue
392
443
  }
393
444
  readOnly={isDropdownVariant}
394
- notched={
395
- (isNotchedVariant && shouldShrinkLabel) ||
396
- ((isDropdownVariant || isSplitButtonVariant) &&
397
- shrunklabellocation !== 'above') ||
398
- hasPlaceholder ||
399
- (componentvariant === 'phonenumber' && phoneNumber !== '')
400
- }
445
+ notched={shouldNotch}
401
446
  />
402
447
  {isDropdownVariant && renderMenu}
403
448
  </Box>
404
449
  </Box>
405
- {showError && currentHelperFooter?.statusMessage && (
450
+ {showError && name && helperFooters[name]?.statusMessage && (
406
451
  <Typography
407
452
  id={`${name}-helper-text`}
408
453
  fontvariant="merrihelperfooter"
409
454
  fontcolor={
410
- currentHelperFooter?.status === 'error'
455
+ helperFooters[name]?.status === 'error'
411
456
  ? red.main
412
- : currentHelperFooter?.status === 'success'
457
+ : helperFooters[name]?.status === 'success'
413
458
  ? green.dark
414
459
  : undefined
415
460
  }
416
461
  marginTop={0.5}
417
462
  marginBottom={0}
418
463
  align="left"
419
- text={currentHelperFooter?.statusMessage}
464
+ text={helperFooters[name]?.statusMessage}
420
465
  />
421
466
  )}
422
467
  </Box>
423
468
  )
424
- }
469
+ })
470
+
471
+ StyledComponent.displayName = 'StyledComponent'
425
472
 
426
473
  export default StyledComponent
@@ -1,25 +1,21 @@
1
- import React, { useEffect } from 'react'
1
+ import { useCallback } from 'react'
2
2
 
3
3
  /**
4
4
  * Custom hook that tracks whether an input field has a value or a specific status.
5
5
  *
6
6
  * @param {string | undefined} value - The current value of the input field.
7
7
  * @param {boolean | undefined} valuestatus - A boolean status associated with the input field.
8
- * @param {React.Dispatch<React.SetStateAction<boolean>>} setHasInput - State setter function to update the hasInput state.
8
+ *
9
+ * @returns {boolean} - Whether the input field has a value or a specific status.
9
10
  *
10
11
  * @example
11
- * const [hasInput, setHasInput] = useState(false);
12
- * useHasInputEffect(inputValue, inputStatus, setHasInput);
12
+ * const hasInput = useHasInput(inputValue, inputStatus);
13
13
  */
14
- export const useHasInputEffect = (
14
+ export const useHasInput = (
15
15
  value: string | undefined,
16
- valuestatus: boolean | undefined,
17
- setHasInput: React.Dispatch<React.SetStateAction<boolean>>
18
- ) => {
19
- useEffect(() => {
20
- const hasInput = !!value || !!valuestatus
21
- setHasInput(hasInput)
22
- }, [value, valuestatus, setHasInput])
16
+ valuestatus: boolean | undefined
17
+ ): boolean => {
18
+ return !!value || !!valuestatus
23
19
  }
24
20
 
25
21
  /**
@@ -28,22 +24,23 @@ export const useHasInputEffect = (
28
24
  *
29
25
  * @param {React.RefObject<HTMLInputElement>} inputRefInternal - React ref object for the input element.
30
26
  *
27
+ * @returns {(input: HTMLInputElement | null) => void} - A callback function to set the attributes on the input element.
28
+ *
31
29
  * @example
32
30
  * const inputRef = useRef<HTMLInputElement>(null);
33
- * usePreventAutocompleteEffect(inputRef);
31
+ * const setInputAttributes = usePreventAutocomplete();
34
32
  * // In your JSX:
35
- * <input ref={inputRef} ... />
33
+ * <input ref={el => { inputRef.current = el; setInputAttributes(el); }} ... />
36
34
  */
37
- export const usePreventAutocompleteEffect = (
38
- inputRefInternal: React.RefObject<HTMLInputElement>
39
- ) => {
40
- useEffect(() => {
41
- const input = inputRefInternal.current
35
+ export const usePreventAutocomplete = (): ((
36
+ input: HTMLInputElement | null
37
+ ) => void) => {
38
+ return useCallback((input: HTMLInputElement | null) => {
42
39
  if (input) {
43
40
  input.setAttribute('autocomplete', 'new-password')
44
41
  input.setAttribute('autocorrect', 'off')
45
42
  input.setAttribute('autocapitalize', 'none')
46
43
  input.setAttribute('spellcheck', 'false')
47
44
  }
48
- }, [inputRefInternal])
45
+ }, [])
49
46
  }