@ssa-ui-kit/core 1.0.5 → 1.0.8

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 (27) hide show
  1. package/dist/components/Typeahead/Typeahead.context.d.ts +4 -2
  2. package/dist/components/Typeahead/Typeahead.d.ts +1 -1
  3. package/dist/components/Typeahead/styles.d.ts +10 -9
  4. package/dist/components/Typeahead/types.d.ts +4 -2
  5. package/dist/components/Typeahead/useTypeahead.d.ts +2 -1
  6. package/dist/index.js +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/components/CollapsibleNavBar/CollapsibleNavBarBase.ts +3 -3
  10. package/src/components/NavBar/NavBar.stories.tsx +7 -2
  11. package/src/components/NavBar/NavBarWrapper.ts +1 -1
  12. package/src/components/NotificationMenu/NotificationMenu.spec.tsx +1 -1
  13. package/src/components/NotificationMenu/stories/StoryComponent.tsx +1 -1
  14. package/src/components/Radio/RadioBase.tsx +1 -1
  15. package/src/components/RadioGroup/RadioGroup.stories.tsx +1 -1
  16. package/src/components/Typeahead/Typeahead.context.ts +2 -0
  17. package/src/components/Typeahead/Typeahead.spec.tsx +27 -10
  18. package/src/components/Typeahead/Typeahead.stories.tsx +38 -4
  19. package/src/components/Typeahead/Typeahead.tsx +9 -4
  20. package/src/components/Typeahead/components/MultipleTrigger.tsx +6 -5
  21. package/src/components/Typeahead/components/SingleTrigger.tsx +8 -5
  22. package/src/components/Typeahead/components/TypeaheadItem.ts +1 -1
  23. package/src/components/Typeahead/styles.ts +4 -2
  24. package/src/components/Typeahead/types.ts +4 -2
  25. package/src/components/Typeahead/useTypeahead.tsx +36 -13
  26. package/src/components/UserProfile/UserProfile.stories.tsx +4 -2
  27. package/tsbuildcache +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssa-ui-kit/core",
3
- "version": "1.0.5",
3
+ "version": "1.0.8",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "private": false,
@@ -75,7 +75,7 @@ const CollapsibleNavBarBase = styled(NavBarBase)`
75
75
 
76
76
  & ~ div:nth-of-type(2) {
77
77
  display: block;
78
- border-radius: 12px 12px 0 0;
78
+ border-radius: 0;
79
79
  height: calc(100vh - 60px);
80
80
 
81
81
  ${({ theme }) => theme.mediaQueries.xlg} {
@@ -101,8 +101,8 @@ const CollapsibleNavBarBase = styled(NavBarBase)`
101
101
 
102
102
  ${({ theme }) => theme.mediaQueries.lg} {
103
103
  &.opened {
104
- min-width: unset;
105
- width: 240px;
104
+ min-width: 240px;
105
+ width: 291px;
106
106
 
107
107
  & > div:nth-of-type(2) {
108
108
  width: 240px;
@@ -1,7 +1,7 @@
1
1
  import { Fragment } from 'react';
2
2
  import { Routes, Route, MemoryRouter } from 'react-router-dom';
3
3
  import { Meta } from '@storybook/react';
4
- import { Title, Description, Source } from '@storybook/addon-docs';
4
+ import { Title, Description, Subtitle, Primary } from '@storybook/addon-docs';
5
5
 
6
6
  import { NavBar } from './NavBar';
7
7
  import { DecoratorFunction } from '@storybook/types';
@@ -42,12 +42,17 @@ export default {
42
42
  viewport: {
43
43
  defaultViewport: 'mobile2',
44
44
  },
45
+ source: {
46
+ type: 'code',
47
+ },
45
48
  docs: {
49
+ inlineStories: false,
46
50
  page: () => (
47
51
  <Fragment>
48
52
  <Title />
53
+ <Subtitle />
49
54
  <Description />
50
- <Source code={`<NavBar />`} />
55
+ <Primary />
51
56
  </Fragment>
52
57
  ),
53
58
  },
@@ -15,7 +15,7 @@ const NavBarWrapper = styled.div`
15
15
  ${({ theme }) => theme.colors.greyDarker} 100%
16
16
  );
17
17
 
18
- transform: translateY(-200vh);
18
+ transform: translateY(-300vh);
19
19
  transition: height 475ms ease, transform 450ms ease, border-radius 450ms ease;
20
20
 
21
21
  ${({ theme }) => theme.mediaQueries.md} {
@@ -58,7 +58,7 @@ describe('NotificationMenu', () => {
58
58
  await user.click(getByTestId('trigger-button'));
59
59
 
60
60
  getByRole('link', {
61
- name: new RegExp('View all notification', 'i'),
61
+ name: new RegExp('View all notifications', 'i'),
62
62
  });
63
63
  });
64
64
  });
@@ -71,7 +71,7 @@ export const StoryComponent = () => {
71
71
  }
72
72
  rightButton={
73
73
  <Link to={'/'} css={{ gridColumn: 2 }}>
74
- <Button variant="info" text="View all notification" />
74
+ <Button variant="info" text="View all notifications" />
75
75
  </Link>
76
76
  }
77
77
  isLoading={isLoading}>
@@ -6,6 +6,7 @@ export const RadioBase = styled(Label)`
6
6
  flex-grow: 0;
7
7
  align-items: center;
8
8
  cursor: pointer;
9
+ gap: 5px;
9
10
 
10
11
  &:has(input:disabled) {
11
12
  cursor: default;
@@ -29,7 +30,6 @@ export const RadioBase = styled(Label)`
29
30
  }
30
31
 
31
32
  span {
32
- margin-left: 10px;
33
33
  font-size: 14px;
34
34
  font-weight: 100;
35
35
  }
@@ -47,7 +47,7 @@ export const HorizontalRadioGroupStories: StoryObj<typeof RadioGroup> = (
47
47
  ) => (
48
48
  <Fragment>
49
49
  <Typography variant="h4">Horizontal Radio Group</Typography>
50
- <RadioGroup {...args} css={{ marginTop: '10px' }}>
50
+ <RadioGroup {...args} css={{ marginTop: '10px', gap: 10, display: 'flex' }}>
51
51
  <Radio id="radio1" value="apple" text="Apple" />
52
52
  <Radio id="radio2" value="orange" text="Orange" />
53
53
  <Radio id="radio3" value="banana" text="Banana" isDisabled={true} />
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ import { FieldValues, UseFormReturn } from 'react-hook-form';
2
3
  import { UseTypeaheadResult } from './useTypeahead';
3
4
 
4
5
  export const TypeaheadContext = React.createContext<UseTypeaheadResult>({
@@ -23,6 +24,7 @@ export const TypeaheadContext = React.createContext<UseTypeaheadResult>({
23
24
  isDisabled: false,
24
25
  options: [],
25
26
  placeholder: '',
27
+ useFormResult: {} as UseFormReturn<FieldValues>,
26
28
  setValue: () => {
27
29
  /* no-op */
28
30
  },
@@ -60,7 +60,9 @@ describe('Typeahead', () => {
60
60
  it('Renders without a selected item', async () => {
61
61
  const { user, mockOnChange, getByRole, queryByRole, getByTestId } = setup();
62
62
 
63
- expect(mockOnChange).toBeCalledWith('typeahead-dropdown', undefined);
63
+ expect(mockOnChange).toBeCalledWith('typeahead-dropdown', undefined, {
64
+ shouldDirty: false,
65
+ });
64
66
 
65
67
  const mainElement = getByTestId('typeahead');
66
68
 
@@ -104,7 +106,9 @@ describe('Typeahead', () => {
104
106
  label: 'Label',
105
107
  });
106
108
 
107
- expect(mockOnChange).toBeCalledWith('typeahead-dropdown', selectedIDs);
109
+ expect(mockOnChange).toBeCalledWith('typeahead-dropdown', selectedIDs, {
110
+ shouldDirty: false,
111
+ });
108
112
 
109
113
  let mainElement = getByTestId('typeahead');
110
114
 
@@ -164,7 +168,9 @@ describe('Typeahead', () => {
164
168
  />,
165
169
  );
166
170
 
167
- expect(mockOnChange).toBeCalledWith('typeahead-dropdown', []);
171
+ expect(mockOnChange).toBeCalledWith('typeahead-dropdown', [], {
172
+ shouldDirty: false,
173
+ });
168
174
 
169
175
  let mainElement = getByTestId('typeahead');
170
176
 
@@ -203,7 +209,9 @@ describe('Typeahead', () => {
203
209
  label: 'Label',
204
210
  });
205
211
 
206
- expect(mockOnChange).toBeCalledWith('typeahead-dropdown', selectedIDs);
212
+ expect(mockOnChange).toBeCalledWith('typeahead-dropdown', selectedIDs, {
213
+ shouldDirty: false,
214
+ });
207
215
 
208
216
  const mainElement = getByTestId('typeahead');
209
217
 
@@ -243,7 +251,9 @@ describe('Typeahead', () => {
243
251
  label: 'Label',
244
252
  });
245
253
 
246
- expect(mockOnChange).lastCalledWith('typeahead-dropdown', selectedIDs[0]);
254
+ expect(mockOnChange).lastCalledWith('typeahead-dropdown', selectedIDs[0], {
255
+ shouldDirty: true,
256
+ });
247
257
 
248
258
  let inputEl = screen.queryByTestId('typeahead-input');
249
259
  expect(inputEl).toHaveValue('First');
@@ -345,7 +355,9 @@ describe('Typeahead', () => {
345
355
  label: 'Label',
346
356
  });
347
357
 
348
- expect(mockOnChange).toBeCalledWith('typeahead-dropdown', selectedIDs);
358
+ expect(mockOnChange).toBeCalledWith('typeahead-dropdown', selectedIDs, {
359
+ shouldDirty: false,
360
+ });
349
361
 
350
362
  let mainElement = getByTestId('typeahead');
351
363
 
@@ -448,7 +460,9 @@ describe('Typeahead', () => {
448
460
  label: 'Label',
449
461
  });
450
462
 
451
- expect(mockOnChange).toBeCalledWith('typeahead-dropdown', selectedIDs);
463
+ expect(mockOnChange).toBeCalledWith('typeahead-dropdown', selectedIDs, {
464
+ shouldDirty: false,
465
+ });
452
466
 
453
467
  let mainElement = getByTestId('typeahead');
454
468
  let toggleElement = within(mainElement).getByRole('combobox');
@@ -492,15 +506,18 @@ describe('Typeahead', () => {
492
506
 
493
507
  it('Error should be displayed', () => {
494
508
  const selectedIDs = selectedItems.map((item) => item.id);
495
- const { getByTestId } = setup({
509
+
510
+ const additionalProps = {
496
511
  initialSelectedItems: selectedIDs,
497
512
  isMultiple: true,
498
513
  label: 'Label',
499
- errors: {
514
+ error: {
515
+ type: 'required',
500
516
  message: 'Error message',
501
517
  },
502
- });
518
+ };
503
519
 
520
+ const { getByTestId } = setup(additionalProps);
504
521
  expect(getByTestId('helper-text')).toBeInTheDocument();
505
522
  });
506
523
  });
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
2
  import {
3
3
  FieldError,
4
4
  FieldValues,
@@ -86,8 +86,38 @@ Basic.args = { isDisabled: false };
86
86
 
87
87
  export const Multiple: StoryObj = (args: TypeaheadProps) => {
88
88
  const useFormResult = useForm<FieldValues>();
89
- const { handleSubmit, register, setValue } = useFormResult;
89
+ const {
90
+ handleSubmit,
91
+ register,
92
+ setValue,
93
+ setError,
94
+ clearErrors,
95
+ watch,
96
+ formState: { errors, isDirty },
97
+ } = useFormResult;
98
+ const fieldName = 'typeahead-dropdown';
99
+ const error = errors[fieldName]
100
+ ? {
101
+ type: errors[fieldName].type,
102
+ message: errors[fieldName].message,
103
+ }
104
+ : undefined;
105
+
90
106
  const onSubmit: SubmitHandler<FieldValues> = (data) => console.log(data);
107
+ const fieldWatch = watch(fieldName);
108
+ useEffect(() => {
109
+ if (isDirty) {
110
+ if (Array.isArray(fieldWatch) && !fieldWatch.length) {
111
+ setError(fieldName, {
112
+ message: 'Required field',
113
+ type: 'required',
114
+ });
115
+ } else {
116
+ clearErrors(fieldName);
117
+ }
118
+ }
119
+ }, [fieldWatch, isDirty]);
120
+
91
121
  return (
92
122
  <form onSubmit={handleSubmit(onSubmit)}>
93
123
  <Typeahead
@@ -98,7 +128,11 @@ export const Multiple: StoryObj = (args: TypeaheadProps) => {
98
128
  helperText="Helper Text"
99
129
  register={register}
100
130
  setValue={setValue}
101
- name={'typeahead-dropdown'}
131
+ validationSchema={{
132
+ required: 'Required',
133
+ }}
134
+ name={fieldName}
135
+ error={error as FieldError}
102
136
  renderOption={({ label, input }) => highlightInputMatch(label, input)}>
103
137
  {items.map(({ label, value, id }) => (
104
138
  <TypeaheadOption key={id} value={id} label={label || value}>
@@ -189,7 +223,7 @@ export const WithError: StoryObj = (args: TypeaheadProps) => {
189
223
  validationSchema={{
190
224
  required: 'Required',
191
225
  }}
192
- errors={mockError}
226
+ error={mockError}
193
227
  renderOption={({ label, input }) => highlightInputMatch(label, input)}>
194
228
  {items.map(({ label, value, id }) => (
195
229
  <TypeaheadOption key={id} value={id} label={label || value}>
@@ -32,7 +32,7 @@ export const Typeahead = ({
32
32
  className,
33
33
  startIcon,
34
34
  endIcon,
35
- errors,
35
+ error,
36
36
  success,
37
37
  helperText,
38
38
  validationSchema,
@@ -40,6 +40,8 @@ export const Typeahead = ({
40
40
  startIconClassName,
41
41
  endIconClassName,
42
42
  optionsClassName,
43
+ wrapperClassName,
44
+ width = 300,
43
45
  setValue,
44
46
  register,
45
47
  onChange,
@@ -58,7 +60,7 @@ export const Typeahead = ({
58
60
  endIcon,
59
61
  startIconClassName,
60
62
  endIconClassName,
61
- errors,
63
+ error,
62
64
  success,
63
65
  validationSchema,
64
66
  placeholder,
@@ -74,7 +76,9 @@ export const Typeahead = ({
74
76
  css={{
75
77
  flexDirection: 'column',
76
78
  alignItems: 'flex-start',
79
+ width,
77
80
  }}
81
+ className={wrapperClassName}
78
82
  data-testid="typeahead">
79
83
  {label && (
80
84
  <Label
@@ -94,6 +98,7 @@ export const Typeahead = ({
94
98
  css={{
95
99
  width: hookResult.triggerRef.current?.clientWidth,
96
100
  boxShadow: `-4px 4px 14px 0px ${theme.colors.greyDarker14}`,
101
+ zIndex: 100,
97
102
  }}
98
103
  isFocusManagerDisabled>
99
104
  <PopoverDescription css={{ width: '100%' }}>
@@ -105,13 +110,13 @@ export const Typeahead = ({
105
110
  </PopoverDescription>
106
111
  </PopoverContent>
107
112
  </Popover>
108
- {(errors?.message || helperText) && (
113
+ {(hookResult.status === 'error' || helperText) && (
109
114
  <FormHelperText
110
115
  role="status"
111
116
  status={hookResult.status}
112
117
  disabled={isDisabled}
113
118
  data-testid="helper-text">
114
- {errors ? errors?.message : helperText}
119
+ {error ? error?.message : helperText}
115
120
  </FormHelperText>
116
121
  )}
117
122
  </Wrapper>
@@ -69,7 +69,7 @@ export const MultipleTrigger = () => {
69
69
  onChange: context.handleInputChange,
70
70
  value: context.inputValue,
71
71
  autoComplete: 'off',
72
- className: ['typeahead-input', S.TypeaheadInput].join(' '),
72
+ className: ['typeahead-input', S.TypeaheadInput(theme)].join(' '),
73
73
  }}
74
74
  wrapperClassName={S.TypeaheadInputWrapper}
75
75
  ref={context.inputRef}
@@ -78,20 +78,20 @@ export const MultipleTrigger = () => {
78
78
  <input
79
79
  type="text"
80
80
  data-testid="typeahead-input"
81
- aria-hidden
81
+ aria-hidden={context.isOpen}
82
82
  readOnly
83
83
  value={context.firstSuggestion}
84
+ tabIndex={-1}
84
85
  disabled={context.isDisabled}
85
86
  className={[
86
87
  'typeahead-input',
87
- S.TypeaheadInput,
88
+ S.TypeaheadInput(theme),
88
89
  S.TypeaheadInputPlaceholder,
89
90
  ].join(' ')}
90
91
  {...typeaheadInputAdditionalProps}
91
92
  />
92
93
  <input
93
94
  type="hidden"
94
- aria-hidden
95
95
  readOnly
96
96
  value={context.selectedItems as string[]}
97
97
  {...context.register?.(context.name, context.validationSchema)}
@@ -103,7 +103,8 @@ export const MultipleTrigger = () => {
103
103
  data-testid="remove-all-button"
104
104
  endIcon={<Icon name="cross" size={8} tooltip="Remove all" />}
105
105
  css={{
106
- padding: '0 14px 0 10px',
106
+ padding: '0 10px',
107
+ marginRight: 4,
107
108
  position: 'absolute',
108
109
  right: 0,
109
110
  zIndex: 10,
@@ -1,4 +1,5 @@
1
1
  import { InputHTMLAttributes } from 'react';
2
+ import { useTheme } from '@emotion/react';
2
3
  import Input from '@components/Input';
3
4
  import Button from '@components/Button';
4
5
  import Icon from '@components/Icon';
@@ -7,6 +8,7 @@ import * as S from '../styles';
7
8
 
8
9
  export const SingleTrigger = () => {
9
10
  const context = useTypeaheadContext();
11
+ const theme = useTheme();
10
12
  const typeaheadInputAdditionalProps: InputHTMLAttributes<HTMLInputElement> =
11
13
  {};
12
14
  if (!context.selectedItems.length && !!context.placeholder) {
@@ -26,7 +28,7 @@ export const SingleTrigger = () => {
26
28
  onChange: context.handleInputChange,
27
29
  value: context.inputValue,
28
30
  autoComplete: 'off',
29
- className: ['typeahead-input', S.TypeaheadInput].join(' '),
31
+ className: ['typeahead-input', S.TypeaheadInput(theme)].join(' '),
30
32
  }}
31
33
  wrapperClassName={S.TypeaheadInputWrapper}
32
34
  ref={context.inputRef}
@@ -35,19 +37,19 @@ export const SingleTrigger = () => {
35
37
  <input
36
38
  type="text"
37
39
  data-testid="typeahead-input"
38
- aria-hidden
40
+ aria-hidden={context.isOpen}
39
41
  readOnly
40
42
  value={context.firstSuggestion}
43
+ tabIndex={-1}
41
44
  className={[
42
45
  'typeahead-input',
43
- S.TypeaheadInput,
46
+ S.TypeaheadInput(theme),
44
47
  S.TypeaheadInputPlaceholder,
45
48
  ].join(' ')}
46
49
  {...typeaheadInputAdditionalProps}
47
50
  />
48
51
  <input
49
52
  type="hidden"
50
- aria-hidden
51
53
  readOnly
52
54
  value={(context.selectedItems[0] || '') as string | undefined}
53
55
  {...context.register?.(context.name, context.validationSchema)}
@@ -58,7 +60,8 @@ export const SingleTrigger = () => {
58
60
  data-testid="remove-all-button"
59
61
  endIcon={<Icon name="cross" size={8} tooltip="Remove" />}
60
62
  css={{
61
- padding: '0 14px 0 10px',
63
+ padding: '0 10px',
64
+ marginRight: 4,
62
65
  position: 'absolute',
63
66
  right: -28,
64
67
  zIndex: 10,
@@ -1,5 +1,5 @@
1
- import Icon from '@components/Icon';
2
1
  import styled from '@emotion/styled';
2
+ import Icon from '@components/Icon';
3
3
 
4
4
  export const TypeaheadItemImage = styled.img`
5
5
  width: 24px;
@@ -1,3 +1,4 @@
1
+ import { Theme } from '@emotion/react';
1
2
  import { css } from '@emotion/css';
2
3
  import styled from '@emotion/styled';
3
4
  import Wrapper from '@components/Wrapper';
@@ -35,8 +36,9 @@ export const TypeaheadOption = styled.li<TypeaheadItemProps>`
35
36
  }
36
37
  `;
37
38
 
38
- export const TypeaheadInput = css`
39
+ export const TypeaheadInput = (theme: Theme) => css`
39
40
  &.typeahead-input {
41
+ color: ${theme.colors.greyDarker};
40
42
  border: none;
41
43
  border-radius: 0;
42
44
  height: 32px;
@@ -148,7 +150,7 @@ export const TypeaheadTrigger = styled(PopoverTrigger)<{
148
150
  background: #fff;
149
151
  gap: 8px;
150
152
  padding: 5px 28px 5px 8px;
151
- width: 300px;
153
+ width: 100%;
152
154
  flex-wrap: wrap;
153
155
  border-color: ${({ isOpen, theme, status }) =>
154
156
  isOpen &&
@@ -17,6 +17,8 @@ export interface TypeaheadProps {
17
17
  children?: React.ReactNode;
18
18
  className?: string;
19
19
  optionsClassName?: string;
20
+ wrapperClassName?: string;
21
+ width?: string | number;
20
22
  isOpen?: boolean;
21
23
  startIcon?: React.ReactNode;
22
24
  endIcon?: React.ReactNode;
@@ -25,7 +27,7 @@ export interface TypeaheadProps {
25
27
  name?: string;
26
28
  label?: string;
27
29
  helperText?: string;
28
- errors?: FieldError;
30
+ error?: FieldError;
29
31
  success?: boolean;
30
32
  validationSchema?: Record<string, unknown>;
31
33
  placeholder?: string | null;
@@ -57,7 +59,7 @@ export type UseTypeaheadProps = Pick<
57
59
  | 'register'
58
60
  | 'setValue'
59
61
  | 'validationSchema'
60
- | 'errors'
62
+ | 'error'
61
63
  | 'success'
62
64
  | 'placeholder'
63
65
  >;
@@ -6,6 +6,7 @@ import React, {
6
6
  useRef,
7
7
  useState,
8
8
  } from 'react';
9
+ import { useForm } from 'react-hook-form';
9
10
  import { propOr } from '@ssa-ui-kit/utils';
10
11
  import { TypeaheadOptionProps, UseTypeaheadProps } from './types';
11
12
 
@@ -22,7 +23,7 @@ export const useTypeahead = ({
22
23
  startIconClassName,
23
24
  endIconClassName,
24
25
  validationSchema,
25
- errors,
26
+ error,
26
27
  success,
27
28
  placeholder,
28
29
  register,
@@ -38,15 +39,17 @@ export const useTypeahead = ({
38
39
  const [optionsWithKey, setOptionsWithKey] = useState<
39
40
  Record<number | string, Record<string, string | number>>
40
41
  >({});
42
+ const [isFirstRender, setFirstRender] = useState<boolean>(true);
41
43
  const [items, setItems] = useState<Array<React.ReactElement> | undefined>();
42
44
  const [inputValue, setInputValue] = useState<string>('');
43
45
  const [status, setStatus] = useState<'basic' | 'success' | 'error'>('basic');
46
+ const [firstSuggestion, setFirstSuggestion] = useState('');
44
47
 
45
48
  const inputRef = useRef<HTMLInputElement>(null);
46
49
  const typeaheadId = useId();
47
50
  const triggerRef: React.MutableRefObject<HTMLDivElement | null> =
48
51
  useRef<HTMLDivElement>(null);
49
- const [firstSuggestion, setFirstSuggestion] = useState('');
52
+ const useFormResult = useForm();
50
53
 
51
54
  useEffect(() => {
52
55
  if (!register) {
@@ -56,11 +59,15 @@ export const useTypeahead = ({
56
59
 
57
60
  useEffect(() => {
58
61
  if (isMultiple) {
59
- setValue?.(name, selected);
62
+ setValue?.(name, selected, {
63
+ shouldDirty: !isFirstRender,
64
+ });
60
65
  setInputValue('');
61
66
  setFirstSuggestion('');
62
67
  } else {
63
- setValue?.(name, selected.length ? selected[0] : undefined);
68
+ setValue?.(name, selected.length ? selected[0] : undefined, {
69
+ shouldDirty: !isFirstRender,
70
+ });
64
71
  }
65
72
  }, [selected]);
66
73
 
@@ -71,9 +78,22 @@ export const useTypeahead = ({
71
78
  }, [isDisabled]);
72
79
 
73
80
  useEffect(() => {
74
- const status = success ? 'success' : errors ? 'error' : 'basic';
81
+ const status = success
82
+ ? 'success'
83
+ : useFormResult.formState.errors[name]
84
+ ? 'error'
85
+ : 'basic';
75
86
  setStatus(status);
76
- }, [errors, success]);
87
+ }, [useFormResult.formState.errors[name], success]);
88
+
89
+ useEffect(() => {
90
+ if (error) {
91
+ useFormResult.setError(name, error);
92
+ } else {
93
+ setStatus('basic');
94
+ useFormResult.resetField(name);
95
+ }
96
+ }, [error]);
77
97
 
78
98
  useEffect(() => {
79
99
  const keyedOptions: Record<
@@ -94,6 +114,7 @@ export const useTypeahead = ({
94
114
  });
95
115
  setOptionsWithKey(keyedOptions);
96
116
  setItems(childItems);
117
+ setFirstRender(false);
97
118
  }, [initialSelectedItems, children]);
98
119
 
99
120
  useEffect(() => {
@@ -201,6 +222,9 @@ export const useTypeahead = ({
201
222
  setIsOpen(false);
202
223
  setFirstSuggestion('');
203
224
  inputRef.current?.focus();
225
+ setStatus('basic');
226
+ useFormResult.clearErrors(name);
227
+ useFormResult.trigger(name);
204
228
  onChange && onChange(changingValue, isNewSelected);
205
229
  };
206
230
 
@@ -214,6 +238,7 @@ export const useTypeahead = ({
214
238
  setInputValue('');
215
239
  setIsOpen(false);
216
240
  setFirstSuggestion('');
241
+ useFormResult.trigger(name);
217
242
  inputRef.current?.focus();
218
243
  };
219
244
 
@@ -231,13 +256,12 @@ export const useTypeahead = ({
231
256
  const handleInputKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (
232
257
  event,
233
258
  ) => {
234
- if (['Tab', 'Space'].includes(event.code) && !firstSuggestion) {
259
+ if (['Space'].includes(event.code) && !firstSuggestion) {
235
260
  setIsOpen(true);
236
261
  inputRef.current?.focus();
237
262
  event.stopPropagation();
238
263
  event.preventDefault();
239
- }
240
- if (['Tab', 'Enter'].includes(event.code) && firstSuggestion) {
264
+ } else if (['Tab', 'Enter'].includes(event.code) && firstSuggestion) {
241
265
  const foundItem = Object.values(optionsWithKey).find(
242
266
  (item) =>
243
267
  `${item.label}`.toLowerCase() === firstSuggestion.toLowerCase(),
@@ -248,8 +272,7 @@ export const useTypeahead = ({
248
272
  }
249
273
  event.preventDefault();
250
274
  return false;
251
- }
252
- if (
275
+ } else if (
253
276
  isMultiple &&
254
277
  event.code === 'Backspace' &&
255
278
  selected.length > 0 &&
@@ -258,8 +281,7 @@ export const useTypeahead = ({
258
281
  handleChange(selected[selected.length - 1]);
259
282
  event.preventDefault();
260
283
  return false;
261
- }
262
- if (!isOpen) {
284
+ } else if (!isOpen) {
263
285
  setIsOpen(true);
264
286
  }
265
287
  };
@@ -305,6 +327,7 @@ export const useTypeahead = ({
305
327
  status,
306
328
  placeholder,
307
329
  options: items,
330
+ useFormResult,
308
331
  register,
309
332
  setValue,
310
333
  handleChange,
@@ -1,6 +1,7 @@
1
+ import { useTheme } from '@emotion/react';
1
2
  import type { Meta, StoryObj } from '@storybook/react';
2
- import Avatar from '@components/Avatar';
3
3
  import { UserProfile } from './UserProfile';
4
+ import Icon from '@components/Icon';
4
5
 
5
6
  export default {
6
7
  title: 'Widgets/UserProfile',
@@ -8,11 +9,12 @@ export default {
8
9
  } as Meta<typeof UserProfile>;
9
10
 
10
11
  export const Default: StoryObj<typeof UserProfile> = () => {
12
+ const theme = useTheme();
11
13
  return (
12
14
  <UserProfile
13
15
  name="Josh Li"
14
16
  email="Josh@gmail.com"
15
- trigger={<Avatar size={42} image="https://via.placeholder.com/42x42" />}
17
+ trigger={<Icon size={42} name="user" color={theme.colors.grey} />}
16
18
  onClick={() => alert('Clicked!')}
17
19
  />
18
20
  );