@axinom/mosaic-ui 0.55.0-rc.5 → 0.55.0-rc.7

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.
@@ -10,6 +10,22 @@
10
10
  flex-flow: wrap;
11
11
  gap: 10px;
12
12
 
13
+ .hasError {
14
+ input {
15
+ border: 1px solid
16
+ var(--input-invalid-border-color, $input-invalid-border-color);
17
+
18
+ &:hover {
19
+ border-color: var(
20
+ --input-invalid-border-color,
21
+ $input-invalid-border-color
22
+ );
23
+ box-shadow: 0 0 0 2px
24
+ var(--input-invalid-hover-color, $input-invalid-hover-color);
25
+ }
26
+ }
27
+ }
28
+
13
29
  .selectedItem {
14
30
  width: max-content;
15
31
  display: grid;
@@ -38,82 +54,6 @@
38
54
  display: block;
39
55
  grid-template-columns: none;
40
56
  }
41
-
42
- select {
43
- -moz-appearance: none;
44
- -webkit-appearance: none;
45
- appearance: none;
46
-
47
- color: var(--input-color, $input-color);
48
- border: 1px solid var(--input-border-color, $input-border-color);
49
-
50
- font-size: var(--label-font-size, $label-font-size);
51
-
52
- height: 50px;
53
- max-width: $tags-max-width;
54
-
55
- padding: 0 40px 0 12px;
56
-
57
- // CSS variables will not work inside the background-image url - we still pass it in there for consistency reasons.
58
- background-image: svg-arrow-glyph(
59
- var(--select-arrow-color, encodecolor($select-arrow-color))
60
- );
61
- background-repeat: no-repeat;
62
- background-position-y: center;
63
- background-position-x: 100%;
64
-
65
- background-color: var(
66
- --select-background-color,
67
- $select-background-color
68
- );
69
-
70
- cursor: pointer;
71
- outline: none;
72
-
73
- transition: box-shadow 0.15s ease-in-out 0s;
74
-
75
- &.hasError {
76
- border: 1px solid
77
- var(--input-invalid-border-color, $input-invalid-border-color);
78
- }
79
-
80
- &:disabled {
81
- border-color: var(
82
- --input-disabled-border-color,
83
- $input-disabled-border-color
84
- );
85
-
86
- cursor: default;
87
-
88
- background-image: svg-arrow-glyph(
89
- var(--select-arrow-color, encodecolor($select-disabled-arrow-color))
90
- );
91
- background-color: var(
92
- --input-disabled-background-color,
93
- $input-disabled-background-color
94
- );
95
- }
96
- }
97
-
98
- select,
99
- option {
100
- width: 325px;
101
- overflow: hidden;
102
- white-space: nowrap;
103
- text-overflow: ellipsis;
104
- }
105
-
106
- select:hover:enabled {
107
- border: 1px solid var(--input-hover-color, $input-hover-color);
108
- box-shadow: 0 0 0 2px var(--input-hover-color, $input-hover-color);
109
-
110
- &.hasError {
111
- border: 1px solid
112
- var(--input-invalid-border-color, $input-invalid-border-color);
113
- box-shadow: 0 0 0 2px
114
- var(--input-invalid-hover-color, $input-invalid-hover-color);
115
- }
116
- }
117
57
  }
118
58
  }
119
59
 
@@ -1,9 +1,16 @@
1
- import { mount, shallow } from 'enzyme';
1
+ import { mount, ReactWrapper, shallow } from 'enzyme';
2
2
  import React from 'react';
3
- import { act } from 'react-dom/test-utils';
4
3
  import { Button } from '../../Buttons';
4
+ import { Select } from '../Select/Select';
5
5
  import { Tags } from './Tags';
6
6
 
7
+ function selectFirstOption(wrapper: ReactWrapper) {
8
+ const input = wrapper.find('[role="combobox"]');
9
+ input.simulate('mousedown');
10
+ const firstOption = document.querySelector('[role="option"]');
11
+ firstOption?.dispatchEvent(new Event('click', { bubbles: true }));
12
+ }
13
+
7
14
  describe('Tags', () => {
8
15
  it('renders the component without crashing', () => {
9
16
  const wrapper = shallow(<Tags name={'test-name'} />);
@@ -41,61 +48,54 @@ describe('Tags', () => {
41
48
  });
42
49
  });
43
50
 
44
- it('raises onChange when selecting a tag along with the new value', async () => {
45
- const spy = jest.fn();
51
+ it('raises onChange when selecting a tag along with the new value', () => {
52
+ const changeSpy = jest.fn();
46
53
  const mockValue: string[] = ['1'];
47
- const mockValueUpdated = '2';
48
54
  const wrapper = mount(
49
- <Tags name={'test-name'} onChange={spy} value={mockValue} />,
55
+ <Tags
56
+ name={'test-name'}
57
+ onChange={changeSpy}
58
+ value={mockValue}
59
+ tagsOptions={['1', '2']}
60
+ />,
50
61
  );
51
62
 
52
- const select = wrapper.find('select');
53
-
54
- await act(async () => {
55
- await select.simulate('change', {
56
- currentTarget: { value: mockValueUpdated },
57
- persist: jest.fn(),
58
- });
59
- wrapper.update();
60
- });
63
+ selectFirstOption(wrapper);
61
64
 
62
- expect(spy).toHaveBeenCalledTimes(1);
63
- // Test is only failing due to a bug with jasmine: https://github.com/jasmine/jasmine/issues/652
64
- // Objects do match, commenting it out for now.
65
- // expect(spy).toHaveBeenCalledWith({
66
- // currentTarget: { value: [...mockValue, mockValueUpdated] },
67
- // persist: jest.fn(),
68
- // });
65
+ expect(changeSpy).toHaveBeenCalledTimes(1);
66
+ expect(changeSpy).toHaveBeenCalledWith(
67
+ expect.objectContaining({
68
+ currentTarget: expect.objectContaining({ value: ['1', '2'] }),
69
+ }),
70
+ );
69
71
  });
70
72
 
71
- it('raises onChange when removing a tag along with the new value', async () => {
72
- const spy = jest.fn();
73
+ it('raises onChange when removing a tag along with the new value', () => {
74
+ const changeSpy = jest.fn();
73
75
  const mockValue: string[] = ['1'];
74
76
  const wrapper = mount(
75
77
  <Tags
76
78
  name={'test-name'}
77
- onChange={spy}
79
+ onChange={changeSpy}
78
80
  value={mockValue}
79
81
  tagsOptions={['1', '2']}
80
82
  />,
81
83
  );
82
84
 
83
- const x = wrapper.find('svg');
85
+ const x = wrapper.find('[data-test-id="tags-delete"]');
84
86
 
85
- await act(async () => {
86
- await x.simulate('click', {
87
- persist: jest.fn(),
88
- });
89
- wrapper.update();
87
+ x.simulate('click', {
88
+ persist: jest.fn(),
90
89
  });
91
90
 
92
- expect(spy).toHaveBeenCalledTimes(1);
93
- // Test is only failing due to a bug with jasmine: https://github.com/jasmine/jasmine/issues/652
94
- // Objects do match, commenting it out for now.
95
- // expect(spy).toHaveBeenCalledWith({
96
- // currentTarget: { value: mockValueUpdated },
97
- // persist: jest.fn(),
98
- // });
91
+ wrapper.update();
92
+
93
+ expect(changeSpy).toHaveBeenCalledTimes(1);
94
+ expect(changeSpy).toHaveBeenCalledWith(
95
+ expect.objectContaining({
96
+ currentTarget: expect.objectContaining({ value: [] }),
97
+ }),
98
+ );
99
99
  });
100
100
 
101
101
  it('shows select element when current selected tags and optional tags are the not same length', () => {
@@ -103,37 +103,38 @@ describe('Tags', () => {
103
103
  <Tags name={'test-name'} value={['1']} tagsOptions={['1', '2']} />,
104
104
  );
105
105
 
106
- const select = wrapper.find('select');
106
+ const select = wrapper.find(Select);
107
107
 
108
- expect(select.prop('hidden')).toBe(false);
108
+ expect(select).toHaveLength(1);
109
109
  });
110
110
 
111
111
  it('hides select element when current selected tags and optional tags are the same length', () => {
112
- const wrapper = shallow(
112
+ const wrapper = mount(
113
113
  <Tags name={'test-name'} value={['1']} tagsOptions={['1', '2']} />,
114
114
  );
115
115
 
116
- let select = wrapper.find('select');
116
+ selectFirstOption(wrapper);
117
117
 
118
- expect(select.prop('hidden')).toBe(false);
118
+ wrapper.update();
119
119
 
120
- select.simulate('change', {
121
- currentTarget: { value: '2' },
122
- persist: jest.fn(),
123
- });
124
- select = wrapper.find('select');
120
+ const select = wrapper.find(Select);
125
121
 
126
- expect(select.prop('hidden')).toBe(true);
122
+ expect(select).toHaveLength(0);
127
123
  });
128
124
 
129
125
  it('raises blur and focus events', () => {
130
126
  const blurSpy = jest.fn();
131
127
  const focusSpy = jest.fn();
132
128
  const wrapper = shallow(
133
- <Tags name={'test-name'} onBlur={blurSpy} onFocus={focusSpy} />,
129
+ <Tags
130
+ name={'test-name'}
131
+ onBlur={blurSpy}
132
+ onFocus={focusSpy}
133
+ tagsOptions={['1']}
134
+ />,
134
135
  );
135
136
 
136
- const select = wrapper.find('select');
137
+ const select = wrapper.find(Select);
137
138
 
138
139
  select.simulate('blur');
139
140
  expect(blurSpy).toHaveBeenCalledTimes(1);
@@ -166,7 +167,7 @@ describe('Tags', () => {
166
167
  });
167
168
 
168
169
  it('uses the displayValue for available tags when tagOptions are objects', () => {
169
- const wrapper = shallow(
170
+ const wrapper = mount(
170
171
  <Tags
171
172
  name={'test-name'}
172
173
  value={['1', '3']}
@@ -181,43 +182,31 @@ describe('Tags', () => {
181
182
  />,
182
183
  );
183
184
 
184
- const tags = wrapper.find('option').map((node) => node.text());
185
+ const input = wrapper.find('[role="combobox"]');
186
+ input.simulate('mousedown');
185
187
 
186
- expect(tags).toHaveLength(3);
188
+ const popper = document.querySelector('[role="tooltip"]');
189
+
190
+ const options = popper?.querySelectorAll('[role="option"]');
191
+
192
+ expect(options).toHaveLength(2);
187
193
 
188
- expect(tags).toEqual(['', 'Test2', 'Test4']);
194
+ expect([options?.[0].innerHTML, options?.[1].innerHTML]).toEqual([
195
+ 'Test2',
196
+ 'Test4',
197
+ ]);
189
198
  });
190
199
 
191
200
  it('applies error styling and renders error message when an error is passed', () => {
192
201
  const mockErrorMessage = 'test-error-message';
193
- const wrapper = shallow(
194
- <Tags name={'test-name'} error={mockErrorMessage} />,
202
+ const wrapper = mount(
203
+ <Tags name={'test-name'} error={mockErrorMessage} tagsOptions={['1']} />,
195
204
  );
196
205
 
197
- const errorMsg = wrapper.dive().find('small');
198
- const errorStyling = wrapper.find('select');
206
+ const errorMsg = wrapper.find('small');
207
+ const select = wrapper.find(Select);
199
208
 
200
209
  expect(errorMsg.text()).toBe(mockErrorMessage);
201
- expect(errorStyling.hasClass('hasError')).toBe(true);
202
- });
203
-
204
- it('defaults drop down label and value to empty strings', () => {
205
- const wrapper = shallow(<Tags name={'test-name'} />);
206
-
207
- const options = wrapper.find('option');
208
-
209
- expect(options.at(0).text()).toBe('');
210
- expect(options.at(0).prop('value')).toBe('');
211
- });
212
-
213
- it('displays a label for the drop down', () => {
214
- const mockDropDownLabel = 'mockLabel';
215
- const wrapper = shallow(
216
- <Tags name={'test-name'} dropDownLabel={mockDropDownLabel} />,
217
- );
218
-
219
- const options = wrapper.find('option');
220
-
221
- expect(options.at(0).text()).toBe(mockDropDownLabel);
210
+ expect(select.hasClass('hasError')).toBe(true);
222
211
  });
223
212
  });
@@ -1,8 +1,9 @@
1
1
  import clsx from 'clsx';
2
2
  import React, {
3
- FormEvent,
3
+ ChangeEvent,
4
4
  PropsWithChildren,
5
5
  useEffect,
6
+ useMemo,
6
7
  useRef,
7
8
  useState,
8
9
  } from 'react';
@@ -10,13 +11,14 @@ import { CSSTransition, TransitionGroup } from 'react-transition-group';
10
11
  import { noop } from '../../../helpers/utils';
11
12
  import { Button, ButtonContext } from '../../Buttons';
12
13
  import { IconName } from '../../Icons';
13
- import { BaseFormControl, BaseSelectEvents } from '../Form.models';
14
+ import { BaseFormControl, BaseInputEvents } from '../Form.models';
14
15
  import { FormElementContainer } from '../FormElementContainer';
16
+ import { Select, SelectOption } from '../Select/Select';
15
17
  import classes from './Tags.scss';
16
18
 
17
19
  export interface TagsProps<T = string>
18
20
  extends BaseFormControl,
19
- BaseSelectEvents {
21
+ BaseInputEvents {
20
22
  /** If set, sets the form control value */
21
23
  value?: string[];
22
24
  /** Array of options that can be selected from */
@@ -52,33 +54,54 @@ export const Tags = <T,>({
52
54
 
53
55
  const [shouldAnimate, setShouldAnimate] = useState<boolean>(false);
54
56
 
55
- const ref = useRef<FormEvent<HTMLSelectElement>>();
57
+ const ref = useRef<ChangeEvent<HTMLInputElement>>();
58
+
59
+ const transformedOptions: SelectOption[] = useMemo(() => {
60
+ return tagsOptions.map((option) => {
61
+ if (valueKey && displayKey) {
62
+ return {
63
+ value: String(option[valueKey]),
64
+ label: String(option[displayKey]),
65
+ };
66
+ }
67
+ return { value: String(option), label: String(option) };
68
+ });
69
+ }, [displayKey, tagsOptions, valueKey]);
70
+
71
+ const visibleOptions = useMemo(() => {
72
+ return transformedOptions.filter(
73
+ (option) => !currentTags.includes(String(option.value)),
74
+ );
75
+ }, [currentTags, transformedOptions]);
56
76
 
57
77
  useEffect(() => {
58
78
  setCurrentTags(value);
59
79
  }, [value]);
60
80
 
61
- const errorMsg: string | undefined = error;
62
-
63
81
  useEffect(() => {
64
82
  // Only emit if there is a current event
65
83
  if (ref.current) {
66
84
  onChange({
67
85
  ...ref.current,
68
-
69
- currentTarget: { value: currentTags as unknown as string },
70
- } as React.FormEvent<HTMLSelectElement>);
86
+ target: {
87
+ ...ref.current.target,
88
+ value: currentTags as unknown as string,
89
+ name,
90
+ id,
91
+ },
92
+ currentTarget: { name, id, value: currentTags as unknown as string },
93
+ } as ChangeEvent<HTMLInputElement>);
71
94
 
72
95
  // Resets event data
73
96
  ref.current = undefined;
74
97
  }
75
- }, [currentTags, onChange]);
98
+ }, [currentTags, id, name, onChange]);
76
99
 
77
100
  /**
78
101
  * Adds a tag to currently selected list
79
102
  * @param e Select FormEvent
80
103
  */
81
- function addTag(e: FormEvent<HTMLSelectElement>): void {
104
+ function addTag(e: ChangeEvent<HTMLInputElement>): void {
82
105
  setShouldAnimate(true);
83
106
 
84
107
  const newTag = e.currentTarget.value;
@@ -96,7 +119,7 @@ export const Tags = <T,>({
96
119
  */
97
120
  function removeTag(idx: number, e: unknown): void {
98
121
  // Set event data
99
- ref.current = e as FormEvent<HTMLSelectElement>;
122
+ ref.current = e as ChangeEvent<HTMLInputElement>;
100
123
 
101
124
  setCurrentTags((prevState) =>
102
125
  prevState.filter((_: string, i: number) => i !== idx),
@@ -119,59 +142,28 @@ export const Tags = <T,>({
119
142
  <FormElementContainer
120
143
  {...rest}
121
144
  className={clsx(classes.container, 'tags-container', className)}
122
- error={errorMsg}
145
+ error={error}
123
146
  dataTestFieldType="Tags"
124
147
  >
125
148
  <div className={clsx(classes.tagsWrapper)}>
126
- <select
127
- className={clsx({ [classes.hasError]: errorMsg !== undefined })}
128
- id={id}
129
- name={name}
130
- disabled={disabled}
131
- autoFocus={autoFocus}
132
- onBlur={onBlur}
133
- onFocus={onFocus}
134
- onChange={(e) => {
135
- e.persist();
136
- e.target?.blur();
137
- addTag(e);
138
- }}
139
- hidden={currentTags.length === tagsOptions.length}
140
- >
141
- {[
142
- <option key={'eae2713d-1a32-4bdb-8f87-c7e1f7e2a3b2'} value={''}>
143
- {dropDownLabel}
144
- </option>,
145
- ].concat(
146
- tagsOptions
147
- .filter((currentTag: T) => {
148
- if (typeof currentTag === 'string') {
149
- return !currentTags.includes(currentTag);
150
- } else if (valueKey) {
151
- return !currentTags.includes(String(currentTag[valueKey]));
152
- }
153
- })
154
- .map((option) => {
155
- if (typeof option === 'string') {
156
- return (
157
- <option key={option} value={option}>
158
- {option}
159
- </option>
160
- );
161
- } else if (valueKey && displayKey) {
162
- return (
163
- <option
164
- key={String(option[valueKey])}
165
- value={String(option[valueKey])}
166
- >
167
- {String(option[displayKey])}
168
- </option>
169
- );
170
- }
171
- return <></>;
172
- }),
173
- )}
174
- </select>
149
+ {currentTags.length < tagsOptions.length ? (
150
+ <Select
151
+ id={id}
152
+ name={`tags-select-${name}`}
153
+ className={clsx({
154
+ [classes.hasError]: error !== undefined,
155
+ })}
156
+ options={visibleOptions}
157
+ onChange={addTag}
158
+ blurOnSelect
159
+ disabled={disabled}
160
+ onBlur={onBlur}
161
+ onFocus={onFocus}
162
+ autoFocus={autoFocus}
163
+ inlineMode={true}
164
+ placeholder={dropDownLabel}
165
+ />
166
+ ) : null}
175
167
  <TransitionGroup component={null}>
176
168
  {currentTags.map((tag, idx) => (
177
169
  <CSSTransition
@@ -1,20 +1,10 @@
1
- import { useField } from 'formik';
2
1
  import React, { PropsWithChildren } from 'react';
3
2
  import { useFormikError } from '../useFormikError';
4
3
  import { Tags, TagsProps } from './Tags';
5
4
  export const TagsField = <T,>(
6
- props: PropsWithChildren<Omit<TagsProps<T>, 'error' | 'onChange'>>,
5
+ props: PropsWithChildren<Omit<TagsProps<T>, 'error'>>,
7
6
  ): JSX.Element => {
8
- const { name } = props;
9
- const error = useFormikError(name);
10
- const [field, , helpers] = useField(name);
7
+ const error = useFormikError(props.name);
11
8
 
12
- return (
13
- <Tags
14
- {...props}
15
- value={field.value}
16
- error={error}
17
- onChange={(e) => helpers.setValue(e.currentTarget.value)}
18
- />
19
- );
9
+ return <Tags {...props} error={error} />;
20
10
  };
@@ -311,3 +311,11 @@ $context-button-hover-border-color: $blue;
311
311
  $context-button-active-color: $blue;
312
312
  $icon-button-stroke-color: $blue;
313
313
  $icon-button-hover-color: $blue;
314
+
315
+ /* Autocomplete */
316
+ $popper-border-color: $blue;
317
+ $popper-trigger-button-color: $blue;
318
+ $popper-background-color: $light-gray-2;
319
+ $popper-item-font-size: 16px;
320
+ $popper-text-color: $blue;
321
+ $popper-background-selected-color: rgba($blue, 0.15);