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

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axinom/mosaic-ui",
3
- "version": "0.55.0-rc.5",
3
+ "version": "0.55.0-rc.6",
4
4
  "description": "UI components for building Axinom Mosaic applications",
5
5
  "author": "Axinom",
6
6
  "license": "PROPRIETARY",
@@ -32,9 +32,10 @@
32
32
  "build-storybook": "storybook build"
33
33
  },
34
34
  "dependencies": {
35
- "@axinom/mosaic-core": "^0.4.28-rc.5",
35
+ "@axinom/mosaic-core": "^0.4.28-rc.6",
36
36
  "@faker-js/faker": "^7.4.0",
37
37
  "@geoffcox/react-splitter": "^2.1.2",
38
+ "@mui/base": "5.0.0-beta.40",
38
39
  "@popperjs/core": "^2.11.8",
39
40
  "clsx": "^1.1.0",
40
41
  "lodash": "^4.17.21",
@@ -106,5 +107,5 @@
106
107
  "publishConfig": {
107
108
  "access": "public"
108
109
  },
109
- "gitHead": "e689e0f8c19bbca2a3aeddaab18f3ebf3d7b0e1b"
110
+ "gitHead": "1f58b4dfda36f87d416466b789e9372c0216a1c4"
110
111
  }
@@ -44,12 +44,17 @@ describe('createSelectRenderer', () => {
44
44
  });
45
45
 
46
46
  it(`sets 'select' value to current value`, () => {
47
- const mockValue = 'test-value';
48
- const wrapper = mount(<RendererWrapper currentValue={mockValue} />);
47
+ const mockOptions = [{ value: 'test-value', label: 'Test Value' }];
48
+ const wrapper = mount(
49
+ <RendererWrapper
50
+ currentValue={mockOptions[0].value}
51
+ options={mockOptions}
52
+ />,
53
+ );
49
54
 
50
- const select = wrapper.find('select');
55
+ const input = wrapper.find('[role="combobox"]');
51
56
 
52
- expect(select.prop('value')).toBe(mockValue);
57
+ expect(input.prop('value')).toBe(mockOptions[0].label);
53
58
  });
54
59
 
55
60
  it(`emits 'onValueChange' with the new value when a new option has been selected`, () => {
@@ -1,4 +1,4 @@
1
- import React, { FormEvent } from 'react';
1
+ import React, { ChangeEvent } from 'react';
2
2
  import { DynamicListDataEntryRenderer } from '../../../../DynamicDataList/DynamicDataList.model';
3
3
  import { Select } from '../../../../FormElements';
4
4
  import { CreateSelectRendererConfig } from '../renderers.model';
@@ -33,7 +33,7 @@ export const createSelectRenderer = ({
33
33
  onValueChange(defaultValue);
34
34
  }
35
35
 
36
- const onChangeHandler = (e: FormEvent<HTMLSelectElement>): void => {
36
+ const onChangeHandler = (e: ChangeEvent<HTMLInputElement>): void => {
37
37
  onValueChange(transform(e.currentTarget.value)); // emit onChange with transformed value
38
38
  };
39
39
 
@@ -1,79 +1,128 @@
1
1
  @import '../../../styles/common.scss';
2
2
 
3
- @function svg-arrow-glyph($color) {
4
- @return url('data:image/svg+xml;utf8,<svg stroke="' + $color + '" version="1.1" xmlns="http://www.w3.org/2000/svg" width="40px" height="20px" viewBox="0 0 40 40"><path vector-effect="non-scaling-stroke" fill="none" stroke-width="2" d="M38.5,9.5L20,30.5L1.5,9.5" /></svg>');
5
- }
6
-
7
3
  .container {
8
- select {
9
- -moz-appearance: none;
10
- -webkit-appearance: none;
11
- appearance: none;
12
- cursor: pointer;
13
- color: var(--input-color, $input-color);
14
- border: 1px solid var(--input-border-color, $input-border-color);
15
- width: max-content;
16
- outline: none;
17
- font-size: var(--label-font-size, $label-font-size);
18
- max-width: $select-max-width;
4
+ .inputWrapper {
5
+ position: relative;
6
+ max-width: var(--input-max-width, $input-max-width);
19
7
 
20
- // CSS variables will not work inside the background-image url - we still pass it in there for consistency reasons.
21
- background-image: svg-arrow-glyph(
22
- var(--select-arrow-color, encodecolor($select-arrow-color))
23
- );
24
- background-repeat: no-repeat;
25
- background-position-y: center;
26
- background-position-x: 100%;
8
+ .button {
9
+ position: absolute;
10
+ right: 0;
11
+ top: 0;
27
12
 
28
- background-color: var(--select-background-color, $select-background-color);
13
+ svg {
14
+ height: 50%;
15
+ * {
16
+ stroke: var(
17
+ --popper-trigger-button-color,
18
+ $popper-trigger-button-color
19
+ );
20
+ }
21
+ }
29
22
 
30
- height: 50px;
23
+ &:disabled {
24
+ svg * {
25
+ stroke: var(--select-arrow-color, $select-disabled-arrow-color);
26
+ }
27
+ }
28
+ }
31
29
 
32
- padding: 0 40px 0 12px;
30
+ input {
31
+ color: var(--input-color, $input-color);
32
+ border: 1px solid var(--input-border-color, $input-border-color);
33
+ padding: 0 48px 0 12px;
34
+ display: inline-block;
35
+ font-size: var(--label-font-size, $label-font-size);
36
+ outline: none;
37
+ height: 50px;
38
+ width: 100%;
39
+ transition: box-shadow 0.15s ease-in-out 0s;
33
40
 
34
- transition: box-shadow 0.15s ease-in-out 0s;
41
+ &::placeholder {
42
+ color: var(--input-placeholder-color, $input-placeholder-color);
43
+ }
35
44
 
36
- &.hasError {
37
- border: 1px solid
38
- var(--input-invalid-border-color, $input-invalid-border-color);
39
- }
45
+ &.hasError {
46
+ border: 1px solid
47
+ var(--input-invalid-border-color, $input-invalid-border-color);
48
+ }
40
49
 
41
- &:disabled {
42
- border-color: var(
43
- --input-disabled-border-color,
44
- $input-disabled-border-color
45
- );
50
+ &:disabled {
51
+ background-color: var(
52
+ --input-disabled-background-color,
53
+ $input-disabled-background-color
54
+ );
55
+ color: var(--input-disabled-font-color, $input-disabled-font-color);
56
+ border-color: var(
57
+ --input-disabled-border-color,
58
+ $input-disabled-border-color
59
+ );
60
+ }
46
61
 
47
- cursor: default;
62
+ &:hover:enabled,
63
+ &:focus {
64
+ border-color: var(--input-hover-color, $input-hover-color);
65
+ box-shadow: 0 0 0 2px var(--input-hover-color, $input-hover-color);
48
66
 
49
- background-image: svg-arrow-glyph(
50
- var(--select-arrow-color, encodecolor($select-disabled-arrow-color))
51
- );
52
- background-color: var(
53
- --input-disabled-background-color,
54
- $input-disabled-background-color
55
- );
67
+ &.hasError {
68
+ border-color: var(
69
+ --input-invalid-border-color,
70
+ $input-invalid-border-color
71
+ );
72
+ box-shadow: 0 0 0 2px
73
+ var(--input-invalid-hover-color, $input-invalid-hover-color);
74
+ }
75
+
76
+ &:disabled {
77
+ border-color: var(
78
+ --input-disabled-border-color,
79
+ $input-disabled-border-color
80
+ );
81
+ box-shadow: none;
82
+ }
83
+ }
56
84
  }
57
85
  }
86
+ }
58
87
 
59
- select,
60
- option {
61
- width: 325px;
62
- overflow: hidden;
63
- white-space: nowrap;
64
- text-overflow: ellipsis;
65
- }
88
+ .popperContent {
89
+ ul {
90
+ display: grid;
91
+ row-gap: 1px;
92
+ background-color: var(--popper-background-color, $popper-background-color);
93
+ padding: 0px;
94
+ margin-top: 1px;
95
+ margin-bottom: 1px;
96
+ border: 1px solid var(--popper-border-color, $popper-border-color);
97
+ overflow-y: auto;
98
+ max-height: 509px;
99
+
100
+ li {
101
+ display: grid;
102
+ place-items: center left;
103
+ height: 50px;
104
+ font-size: var(--popper-item-font-size, $popper-item-font-size);
105
+ color: var(--popper-text-color, $popper-text-color);
106
+ background-color: white;
107
+ padding: 0 12px;
108
+ list-style: none;
109
+ cursor: default;
66
110
 
67
- select:hover:enabled,
68
- select:focus:enabled {
69
- border: 1px solid var(--input-hover-color, $input-hover-color);
70
- box-shadow: 0 0 0 2px var(--input-hover-color, $input-hover-color);
111
+ &:hover {
112
+ cursor: pointer;
113
+ background-color: var(
114
+ --popper-background-selected-color,
115
+ $popper-background-selected-color
116
+ );
117
+ }
71
118
 
72
- &.hasError {
73
- border: 1px solid
74
- var(--input-invalid-border-color, $input-invalid-border-color);
75
- box-shadow: 0 0 0 2px
76
- var(--input-invalid-hover-color, $input-invalid-hover-color);
119
+ &[aria-selected='true'],
120
+ &[class='Mui-focused Mui-focusVisible'] {
121
+ background-color: var(
122
+ --popper-background-selected-color,
123
+ $popper-background-selected-color
124
+ );
125
+ }
77
126
  }
78
127
  }
79
128
  }
@@ -1,95 +1,104 @@
1
- import { shallow } from 'enzyme';
1
+ import { mount, shallow } from 'enzyme';
2
2
  import React from 'react';
3
3
  import { Select } from './Select';
4
4
 
5
+ const mockLabel = 'mockLabel';
6
+
7
+ const mockOptions = [
8
+ {
9
+ value: '1',
10
+ label: 'One',
11
+ },
12
+ {
13
+ value: '2',
14
+ label: 'Two',
15
+ },
16
+ {
17
+ value: '3',
18
+ label: 'Three',
19
+ },
20
+ ];
21
+
5
22
  describe('Select', () => {
23
+ afterEach(() => {
24
+ document.body.innerHTML = '';
25
+ });
26
+
6
27
  it('renders the component without crashing', () => {
7
- const wrapper = shallow(<Select name={'test-name'} />);
28
+ const wrapper = shallow(<Select name="test-name" options={[]} />);
8
29
 
9
30
  expect(wrapper).toBeTruthy();
10
31
  });
11
32
 
12
33
  it('displays a label', () => {
13
- const mockLabel = 'mockLabel';
14
- const wrapper = shallow(<Select name={'test-name'} label={mockLabel} />);
34
+ const wrapper = shallow(
35
+ <Select name="test-name" label={mockLabel} options={[]} />,
36
+ );
15
37
 
16
- const label = wrapper.dive().find('label');
38
+ const label = wrapper.dive().find('[data-test-id="form-field-label"]');
17
39
 
18
40
  expect(label.text()).toBe(mockLabel);
19
41
  });
20
42
 
21
- it('uses optional props when passed in', () => {
22
- const mockProps = {
23
- autoFocus: true,
24
- className: '',
25
- disabled: true,
26
- id: 'test-id',
27
- name: 'test-name',
28
- onBlur: () => null,
29
- onChange: () => null,
30
- onFocus: () => null,
31
- } as Record<string, unknown>;
32
-
33
- const wrapper = shallow(<Select name="test-name" {...mockProps} />);
43
+ it('the given value must be filled in input', () => {
44
+ const wrapper = mount(
45
+ <Select name={'test-name'} options={mockOptions} value="2" />,
46
+ );
34
47
 
35
- const select = wrapper.find('select');
48
+ const input = wrapper.find('[role="combobox"]');
36
49
 
37
- expect(select.props()).toEqual(expect.objectContaining(mockProps));
50
+ expect(input.prop('value')).toEqual(mockOptions[1].label);
38
51
  });
39
52
 
40
- it('sets select field using the value prop and emits updated values', () => {
41
- const spy = jest.fn();
42
- const mockValue = 'test-value';
43
- const mockValueUpdated = 'updated-test-value';
44
- const wrapper = shallow(
45
- <Select name="test-name" value={mockValue} onChange={spy} />,
53
+ it('the popper must be rendered on input click', () => {
54
+ const wrapper = mount(
55
+ <Select name={'test-name'} options={mockOptions} value="2" />,
46
56
  );
57
+ const input = wrapper.find('[role="combobox"]');
58
+ input.simulate('mousedown');
47
59
 
48
- const select = wrapper.find('select');
49
-
50
- expect(select.prop('value')).toEqual(mockValue);
51
-
52
- select.simulate('change', { target: { value: mockValueUpdated } });
53
-
54
- expect(spy).toHaveBeenCalledTimes(1);
55
- expect(spy).toHaveBeenCalledWith({ target: { value: mockValueUpdated } });
60
+ const popper = document.querySelector('[role="tooltip"]');
61
+ expect(popper).not.toBeNull();
56
62
  });
57
63
 
58
64
  it('raises change, blur, and focus events', () => {
59
65
  const changeSpy = jest.fn();
60
66
  const blurSpy = jest.fn();
61
67
  const focusSpy = jest.fn();
62
- const wrapper = shallow(
68
+ const wrapper = mount(
63
69
  <Select
64
70
  name={'test-name'}
65
71
  onChange={changeSpy}
66
72
  onBlur={blurSpy}
67
73
  onFocus={focusSpy}
74
+ options={mockOptions}
68
75
  />,
69
76
  );
70
77
 
71
- const select = wrapper.find('select');
78
+ const input = wrapper.find('[role="combobox"]');
79
+ input.simulate('mousedown');
80
+ const firstOption = document.querySelector('[role="option"]');
81
+ firstOption?.dispatchEvent(new Event('click', { bubbles: true }));
72
82
 
73
- select.simulate('change');
74
83
  expect(changeSpy).toHaveBeenCalledTimes(1);
75
84
 
76
- select.simulate('blur');
85
+ input.simulate('blur');
77
86
  expect(blurSpy).toHaveBeenCalledTimes(1);
78
87
 
79
- select.simulate('focus');
88
+ input.simulate('focus');
80
89
  expect(focusSpy).toHaveBeenCalledTimes(1);
81
90
  });
82
91
 
83
92
  it('applies error styling and renders error message when an error is passed', () => {
84
93
  const mockErrorMessage = 'test-error-message';
85
94
  const wrapper = shallow(
86
- <Select name={'test-name'} error={mockErrorMessage} />,
95
+ <Select name={'test-name'} error={mockErrorMessage} options={[]} />,
87
96
  );
88
97
 
89
- const errorMsg = wrapper.dive().find('small');
90
- const errorStyling = wrapper.find('select');
98
+ const errorMsg = wrapper.dive().find('[data-test-id="form-field-error"]');
99
+ const input = wrapper.find('[role="combobox"]');
91
100
 
92
101
  expect(errorMsg.text()).toBe(mockErrorMessage);
93
- expect(errorStyling.hasClass('hasError')).toBe(true);
102
+ expect(input.hasClass('hasError')).toBe(true);
94
103
  });
95
104
  });
@@ -6,14 +6,7 @@ import { createGroups } from '../../../helpers/storybook';
6
6
  import { Select } from './Select';
7
7
 
8
8
  const groups = createGroups({
9
- Behavior: [
10
- 'autoFocus',
11
- 'disabled',
12
- 'inlineMode',
13
- 'name',
14
- 'id',
15
- 'addEmptyOption',
16
- ],
9
+ Behavior: ['autoFocus', 'disabled', 'inlineMode', 'name', 'id'],
17
10
  Content: ['label', 'options', 'error', 'tooltipContent', 'value'],
18
11
  Styling: ['className'],
19
12
  Events: ['onChange', 'onBlur', 'onFocus'],
@@ -26,6 +19,7 @@ const meta: Meta<typeof Select> = {
26
19
  ...groups,
27
20
  },
28
21
  };
22
+
29
23
  export default meta;
30
24
 
31
25
  export const Main: StoryObj<typeof Select> = {
@@ -37,14 +31,18 @@ export const Main: StoryObj<typeof Select> = {
37
31
  { value: '2', label: 'Two' },
38
32
  { value: '3', label: 'Three' },
39
33
  { value: '4', label: 'Four' },
34
+ { value: '5', label: 'Five' },
35
+ { value: '6', label: 'Six' },
36
+ { value: '7', label: 'Seven' },
37
+ { value: '8', label: 'Eight' },
38
+ { value: '9', label: 'Nine' },
39
+ { value: '10', label: 'Ten' },
40
40
  ],
41
41
  tooltipContent: faker.lorem.paragraph(2),
42
- addEmptyOption: false,
43
42
  },
44
43
  render: (args) =>
45
44
  React.createElement(() => {
46
45
  const [value, setValue] = useState(args.value);
47
-
48
46
  return (
49
47
  <Select
50
48
  {...args}
@@ -1,73 +1,159 @@
1
+ import { Popper, useAutocomplete, UseAutocompleteProps } from '@mui/base';
1
2
  import clsx from 'clsx';
2
- import React from 'react';
3
- import { BaseFormControl, BaseSelectEvents } from '../Form.models';
3
+ import React, { ChangeEvent } from 'react';
4
+ import { Button, ButtonContext } from '../../Buttons';
5
+ import { IconName } from '../../Icons';
6
+ import { BaseFormControl, BaseInputEvents } from '../Form.models';
4
7
  import { FormElementContainer } from '../FormElementContainer';
5
8
  import classes from './Select.scss';
6
9
 
7
- export interface SelectProps extends BaseFormControl, BaseSelectEvents {
8
- /** Current value the form control has */
9
- value?: string | number | string[] | undefined;
10
- /** Array of options that can be selected from */
11
- options?: { value: string | number; label: string | number }[];
10
+ export interface SelectOption {
11
+ value: string | number | string[];
12
+ label: string | number;
13
+ }
14
+
15
+ type TrimmedUseAutocompleteProps = Omit<
16
+ UseAutocompleteProps<SelectOption, false, true, false>,
17
+ 'value' | 'onChange'
18
+ >;
19
+ export interface SelectProps
20
+ extends BaseFormControl,
21
+ BaseInputEvents,
22
+ TrimmedUseAutocompleteProps {
23
+ /** An array of options */
24
+ options: SelectOption[];
12
25
  /** Whether or not the control should start focused (default: false) */
13
26
  autoFocus?: boolean;
14
- /** Defines whether an empty option should be added as the first option (default: false) */
15
- addEmptyOption?: boolean;
27
+ /** Current value the form control has */
28
+ value?: string | string[] | number;
16
29
  /** Select placeholder */
17
30
  placeholder?: string;
31
+ /** Allows to clear the input field and leave empty */
32
+ addEmptyOption?: boolean;
18
33
  }
19
34
 
20
- export const Select: React.FC<SelectProps> = ({
21
- name,
22
- id,
23
- value = undefined,
24
- options = [],
25
- disabled = false,
26
- error,
27
- autoFocus = false,
28
- addEmptyOption = false,
29
- placeholder,
30
- onChange,
31
- onBlur,
32
- onFocus,
33
- className = '',
34
- ...rest
35
- }) => {
36
- const errorMsg: string | undefined = error;
35
+ export const Select: React.FC<SelectProps> = (props) => {
36
+ const {
37
+ name,
38
+ error,
39
+ disabled,
40
+ id,
41
+ label,
42
+ className,
43
+ tooltipContent,
44
+ inlineMode,
45
+ autoFocus,
46
+ value,
47
+ onBlur,
48
+ onFocus,
49
+ onChange,
50
+ placeholder,
51
+ addEmptyOption,
52
+ ...autoCompleteProps
53
+ } = props;
54
+ const {
55
+ getRootProps,
56
+ getInputProps,
57
+ getListboxProps,
58
+ getOptionProps,
59
+ getPopupIndicatorProps,
60
+ groupedOptions,
61
+ popupOpen,
62
+ setAnchorEl,
63
+ anchorEl,
64
+ } = useAutocomplete({
65
+ id,
66
+ disabled,
67
+ value:
68
+ autoCompleteProps.options.find((option) => option.value === value) ??
69
+ null,
70
+ onChange: ({ target: _, ...event }, value) => {
71
+ onChange?.({
72
+ ...event,
73
+ currentTarget: {
74
+ ...event.currentTarget,
75
+ id,
76
+ name,
77
+ value: value?.value ?? '',
78
+ },
79
+ } as ChangeEvent<HTMLInputElement>);
80
+ },
81
+ disableClearable: !addEmptyOption,
82
+ ...autoCompleteProps,
83
+ });
37
84
 
38
85
  return (
39
86
  <FormElementContainer
40
- {...rest}
87
+ id={id}
88
+ label={label}
89
+ tooltipContent={tooltipContent}
90
+ inlineMode={inlineMode}
41
91
  className={clsx(classes.container, 'select-container', className)}
42
- error={errorMsg}
92
+ error={error}
43
93
  dataTestFieldType="Select"
44
94
  >
45
- <select
46
- className={clsx({ [classes.hasError]: errorMsg !== undefined })}
47
- id={id}
48
- name={name}
49
- value={value}
50
- disabled={disabled}
51
- autoFocus={autoFocus}
52
- onChange={onChange}
53
- onBlur={onBlur}
54
- onFocus={onFocus}
95
+ <div
96
+ ref={setAnchorEl}
97
+ {...getRootProps()}
98
+ className={clsx(classes.inputWrapper)}
55
99
  >
56
- {addEmptyOption &&
57
- (placeholder === null || placeholder === undefined) && (
58
- <option value=""></option>
100
+ <input
101
+ {...getInputProps()}
102
+ name={name}
103
+ className={clsx({ [classes.hasError]: Boolean(error) })}
104
+ autoFocus={autoFocus}
105
+ onBlur={(event) => {
106
+ getInputProps().onBlur?.(event);
107
+ onBlur?.(event);
108
+ }}
109
+ onFocus={(event) => {
110
+ getInputProps().onFocus?.(event);
111
+ onFocus?.(event);
112
+ }}
113
+ placeholder={placeholder}
114
+ />
115
+ <Button
116
+ className={clsx(classes.button)}
117
+ buttonContext={ButtonContext.None}
118
+ icon={popupOpen ? IconName.ChevronUp : IconName.ChevronDown}
119
+ onButtonClicked={getPopupIndicatorProps().onClick}
120
+ onBlur={getPopupIndicatorProps().onBlur}
121
+ disabled={disabled}
122
+ />
123
+ </div>
124
+ <Popper
125
+ open={popupOpen}
126
+ anchorEl={anchorEl}
127
+ style={{
128
+ width: anchorEl ? anchorEl.clientWidth : undefined,
129
+ zIndex: 999,
130
+ }}
131
+ >
132
+ <PopperContent>
133
+ {groupedOptions.length > 0 && (
134
+ <ul {...getListboxProps()}>
135
+ {(groupedOptions as typeof autoCompleteProps.options).map(
136
+ (option, index) => (
137
+ <li
138
+ key={String(option.value)}
139
+ {...getOptionProps({ option, index })}
140
+ >
141
+ {option.label}
142
+ </li>
143
+ ),
144
+ )}
145
+ </ul>
59
146
  )}
60
- {placeholder && (
61
- <option value="" disabled>
62
- {disabled ? '' : placeholder}
63
- </option>
64
- )}
65
- {options.map((option) => (
66
- <option key={option.value} value={option.value}>
67
- {option.label}
68
- </option>
69
- ))}
70
- </select>
147
+ </PopperContent>
148
+ </Popper>
71
149
  </FormElementContainer>
72
150
  );
73
151
  };
152
+
153
+ const PopperContent: React.FC = ({ children }) => {
154
+ return (
155
+ <div className={clsx(classes.popperContent, 'select-popper-content')}>
156
+ {children}
157
+ </div>
158
+ );
159
+ };
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { useFormikError } from '../useFormikError';
3
3
  import { Select, SelectProps } from './Select';
4
+
4
5
  export const SelectField: React.FC<Omit<SelectProps, 'error'>> = (props) => {
5
6
  const error = useFormikError(props.name);
6
7
  return <Select {...props} error={error} />;