@dhis2-ui/select 10.17.0 → 10.19.0

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 (32) hide show
  1. package/build/cjs/simple-single-select/__stories__/WithMenuMaxWidth.js +34 -0
  2. package/build/cjs/simple-single-select/__stories__/WithMenuMinWidth.js +37 -0
  3. package/build/cjs/simple-single-select/menu/menu.js +17 -8
  4. package/build/cjs/simple-single-select/simple-single-select.js +19 -9
  5. package/build/cjs/simple-single-select/simple-single-select.prod.stories.js +14 -0
  6. package/build/cjs/simple-single-select/simple-single-select.test.js +62 -0
  7. package/build/cjs/simple-single-select-field/simple-single-select-field.js +4 -0
  8. package/build/cjs/simple-single-select-field/simple-single-select-field.prod.stories.js +53 -2
  9. package/build/cjs/simple-single-select-field/simple-single-select-field.test.js +4 -0
  10. package/build/cjs/single-select-option/single-select-option.js +4 -4
  11. package/build/es/simple-single-select/__stories__/WithMenuMaxWidth.js +26 -0
  12. package/build/es/simple-single-select/__stories__/WithMenuMinWidth.js +29 -0
  13. package/build/es/simple-single-select/menu/menu.js +17 -8
  14. package/build/es/simple-single-select/simple-single-select.js +19 -9
  15. package/build/es/simple-single-select/simple-single-select.prod.stories.js +2 -0
  16. package/build/es/simple-single-select/simple-single-select.test.js +62 -0
  17. package/build/es/simple-single-select-field/simple-single-select-field.js +4 -0
  18. package/build/es/simple-single-select-field/simple-single-select-field.prod.stories.js +49 -0
  19. package/build/es/simple-single-select-field/simple-single-select-field.test.js +4 -0
  20. package/build/es/single-select-option/single-select-option.js +4 -4
  21. package/package.json +15 -15
  22. package/src/simple-single-select/__stories__/WithMenuMaxWidth.js +24 -0
  23. package/src/simple-single-select/__stories__/WithMenuMinWidth.js +25 -0
  24. package/src/simple-single-select/menu/menu.js +26 -8
  25. package/src/simple-single-select/simple-single-select.js +19 -6
  26. package/src/simple-single-select/simple-single-select.prod.stories.js +2 -0
  27. package/src/simple-single-select/simple-single-select.test.js +68 -0
  28. package/src/simple-single-select-field/simple-single-select-field.js +6 -0
  29. package/src/simple-single-select-field/simple-single-select-field.prod.stories.js +49 -0
  30. package/src/simple-single-select-field/simple-single-select-field.test.js +4 -0
  31. package/src/single-select-option/single-select-option.js +5 -3
  32. package/types/index.d.ts +45 -1
@@ -1,3 +1,4 @@
1
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
1
2
  import '@testing-library/jest-dom';
2
3
  import { render, fireEvent, screen } from '@testing-library/react';
3
4
  import React, { useState } from 'react';
@@ -142,6 +143,67 @@ describe('<SimpleSingleSelect />', () => {
142
143
  const menu = listbox.parentNode.parentNode.parentNode;
143
144
  expect(menu.style.maxHeight).toBe('100px');
144
145
  });
146
+ describe('dropdown menu width', () => {
147
+ let offsetWidth;
148
+ beforeEach(() => {
149
+ // The menu's width is derived from the select's measured width,
150
+ // which is always 0 in jsdom
151
+ offsetWidth = jest.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(120);
152
+ });
153
+ afterEach(() => {
154
+ offsetWidth.mockRestore();
155
+ });
156
+ const renderAndOpen = props => {
157
+ render(/*#__PURE__*/React.createElement(SimpleSingleSelect, _extends({
158
+ name: "simple",
159
+ selected: {
160
+ value: 'foo',
161
+ label: 'Foo'
162
+ },
163
+ onChange: () => null,
164
+ options: [{
165
+ value: 'foo',
166
+ label: 'Foo'
167
+ }]
168
+ }, props)));
169
+ fireEvent.click(screen.getByRole('combobox'));
170
+ const listbox = screen.getByRole('listbox');
171
+ return listbox.parentNode.parentNode.parentNode;
172
+ };
173
+ it('should match the width of the select by default', () => {
174
+ const menu = renderAndOpen();
175
+ expect(menu.style.width).toBe('120px');
176
+ });
177
+ it('should not be narrower than menuMinWidth or the select', () => {
178
+ const menu = renderAndOpen({
179
+ menuMinWidth: '240px'
180
+ });
181
+ expect(menu.style.minWidth).toBe('max(120px, 240px)');
182
+ });
183
+ it('should not be wider than menuMaxWidth', () => {
184
+ const menu = renderAndOpen({
185
+ menuMaxWidth: '200px'
186
+ });
187
+ expect(menu.style.maxWidth).toBe('200px');
188
+ expect(menu.style.minWidth).toBe('120px');
189
+ });
190
+ it('should re-measure the select when the menu is reopened', () => {
191
+ renderAndOpen({
192
+ menuMinWidth: '240px'
193
+ });
194
+ const comboBox = screen.getByRole('combobox');
195
+
196
+ // close the menu
197
+ fireEvent.click(comboBox);
198
+
199
+ // widen the select while the menu is closed, then reopen it
200
+ offsetWidth.mockReturnValue(300);
201
+ fireEvent.click(comboBox);
202
+ const listbox = screen.getByRole('listbox');
203
+ const menu = listbox.parentNode.parentNode.parentNode;
204
+ expect(menu.style.minWidth).toBe('max(300px, 240px)');
205
+ });
206
+ });
145
207
  it('should accept a placeholder', () => {
146
208
  render(/*#__PURE__*/React.createElement(SimpleSingleSelect, {
147
209
  name: "simple",
@@ -101,6 +101,10 @@ SimpleSingleSelectField.propTypes = {
101
101
  menuLoadingText: PropTypes.string,
102
102
  /** Allows to modify the max height of the menu **/
103
103
  menuMaxHeight: PropTypes.string,
104
+ /** See [dropdown menu width](https://developers.dhis2.org/docs/ui/components/select#dropdown-menu-width) **/
105
+ menuMaxWidth: PropTypes.string,
106
+ /** See [dropdown menu width](https://developers.dhis2.org/docs/ui/components/select#dropdown-menu-width) **/
107
+ menuMinWidth: PropTypes.string,
104
108
  /** String that will be displayed when the select is being filtered but the options array is empty **/
105
109
  noMatchText: requiredIf(props => props.filterable, PropTypes.string),
106
110
  /** Allows to override what's rendered inside the `button[role="option"]`.
@@ -42,6 +42,19 @@ const options = [{
42
42
  value: '10',
43
43
  label: 'ten'
44
44
  }];
45
+ const longOptions = [{
46
+ value: '1',
47
+ label: 'option one'
48
+ }, {
49
+ value: '2',
50
+ label: 'option two'
51
+ }, {
52
+ value: '3',
53
+ label: 'option three'
54
+ }, {
55
+ value: '4',
56
+ label: 'A longer option that exceeds the minimum'
57
+ }];
45
58
  export default {
46
59
  title: 'SimpleSingleSelectField',
47
60
  component: SimpleSingleSelectField,
@@ -169,4 +182,40 @@ export const InputWidth = () => {
169
182
  onChange: nextValue => setValue(nextValue),
170
183
  options: options
171
184
  }));
185
+ };
186
+ export const WithMenuMinWidth = () => {
187
+ var _longOptions$find;
188
+ const [value, setValue] = useState('');
189
+ const valueLabel = value ? (_longOptions$find = longOptions.find(option => option.value === value)) === null || _longOptions$find === void 0 ? void 0 : _longOptions$find.label : '';
190
+ return /*#__PURE__*/React.createElement("div", {
191
+ style: {
192
+ width: 120
193
+ }
194
+ }, /*#__PURE__*/React.createElement(SimpleSingleSelectField, {
195
+ name: "simple",
196
+ label: "This is the label",
197
+ value: value,
198
+ valueLabel: valueLabel,
199
+ onChange: nextValue => setValue(nextValue),
200
+ options: longOptions,
201
+ menuMinWidth: "240px"
202
+ }));
203
+ };
204
+ export const WithMenuMaxWidth = () => {
205
+ var _longOptions$find2;
206
+ const [value, setValue] = useState('');
207
+ const valueLabel = value ? (_longOptions$find2 = longOptions.find(option => option.value === value)) === null || _longOptions$find2 === void 0 ? void 0 : _longOptions$find2.label : '';
208
+ return /*#__PURE__*/React.createElement("div", {
209
+ style: {
210
+ width: 120
211
+ }
212
+ }, /*#__PURE__*/React.createElement(SimpleSingleSelectField, {
213
+ name: "simple",
214
+ label: "This is the label",
215
+ value: value,
216
+ valueLabel: valueLabel,
217
+ onChange: nextValue => setValue(nextValue),
218
+ options: longOptions,
219
+ menuMaxWidth: "200px"
220
+ }));
172
221
  };
@@ -49,6 +49,8 @@ describe('<SimpleSingleSelectField />', () => {
49
49
  loading: false,
50
50
  menuLoadingText: "",
51
51
  menuMaxHeight: "",
52
+ menuMaxWidth: "400px",
53
+ menuMinWidth: "240px",
52
54
  noMatchText: "",
53
55
  optionUpdateStrategy: "off",
54
56
  placeholder: "",
@@ -86,6 +88,8 @@ describe('<SimpleSingleSelectField />', () => {
86
88
  expect(SimpleSingleSelect.mock.calls[0][0].loading).toBe(false);
87
89
  expect(SimpleSingleSelect.mock.calls[0][0].menuLoadingText).toBe('');
88
90
  expect(SimpleSingleSelect.mock.calls[0][0].menuMaxHeight).toBe('');
91
+ expect(SimpleSingleSelect.mock.calls[0][0].menuMaxWidth).toBe('400px');
92
+ expect(SimpleSingleSelect.mock.calls[0][0].menuMinWidth).toBe('240px');
89
93
  expect(SimpleSingleSelect.mock.calls[0][0].noMatchText).toBe('');
90
94
  expect(SimpleSingleSelect.mock.calls[0][0].optionUpdateStrategy).toBe('off');
91
95
  expect(SimpleSingleSelect.mock.calls[0][0].placeholder).toBe('');
@@ -16,17 +16,17 @@ const SingleSelectOption = ({
16
16
  "data-test": dataTest,
17
17
  "data-value": value,
18
18
  "data-label": label,
19
- className: _JSXStyle.dynamic([["1307386342", [colors.grey900, spacers.dp8, spacers.dp12, colors.grey200, colors.teal050, colors.grey500, active ? `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMuNjMyMDkgNy44MzIxNEMzLjI3MjA5IDcuNDcyMTQgMi43MjAwOSA3LjQ3MjE0IDIuMzYwMDkgNy44MzIxNEMyLjAwMDA5IDguMTkyMTQgMi4wMDAwOSA4Ljc0NDE0IDIuMzYwMDkgOS4xMDQxNEw1LjYyNDA5IDEyLjM2ODFDNS45ODQwOSAxMi43MjgxIDYuNTM2MDkgMTIuNzI4MSA2Ljg5NjA5IDEyLjM2ODFMMTQuNjQ4MSA0LjY0MDE0QzE1LjAwODEgNC4yODAxNCAxNS4wMDgxIDMuNzI4MTQgMTQuNjQ4MSAzLjM2ODE0QzE0LjI4ODEgMy4wMDgxNCAxMy43MzYxIDMuMDA4MTQgMTMuMzUyMSAzLjM2ODE0TDYuMjcyMDkgMTAuNDcyMUwzLjYzMjA5IDcuODMyMTRaIiBmaWxsPSIjMDA2OTVDIi8+Cjwvc3ZnPgo=')` : 'none']]]) + " " + (cx(className, {
19
+ className: _JSXStyle.dynamic([["556453451", [colors.grey900, spacers.dp8, spacers.dp12, colors.grey200, colors.teal050, colors.grey500, active ? `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMuNjMyMDkgNy44MzIxNEMzLjI3MjA5IDcuNDcyMTQgMi43MjAwOSA3LjQ3MjE0IDIuMzYwMDkgNy44MzIxNEMyLjAwMDA5IDguMTkyMTQgMi4wMDAwOSA4Ljc0NDE0IDIuMzYwMDkgOS4xMDQxNEw1LjYyNDA5IDEyLjM2ODFDNS45ODQwOSAxMi43MjgxIDYuNTM2MDkgMTIuNzI4MSA2Ljg5NjA5IDEyLjM2ODFMMTQuNjQ4MSA0LjY0MDE0QzE1LjAwODEgNC4yODAxNCAxNS4wMDgxIDMuNzI4MTQgMTQuNjQ4MSAzLjM2ODE0QzE0LjI4ODEgMy4wMDgxNCAxMy43MzYxIDMuMDA4MTQgMTMuMzUyMSAzLjM2ODE0TDYuMjcyMDkgMTAuNDcyMUwzLjYzMjA5IDcuODMyMTRaIiBmaWxsPSIjMDA2OTVDIi8+Cjwvc3ZnPgo=')` : 'none']]]) + " " + (cx(className, {
20
20
  disabled,
21
21
  active
22
22
  }) || "")
23
23
  }, /*#__PURE__*/React.createElement("span", {
24
24
  "aria-hidden": "true",
25
- className: _JSXStyle.dynamic([["1307386342", [colors.grey900, spacers.dp8, spacers.dp12, colors.grey200, colors.teal050, colors.grey500, active ? `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMuNjMyMDkgNy44MzIxNEMzLjI3MjA5IDcuNDcyMTQgMi43MjAwOSA3LjQ3MjE0IDIuMzYwMDkgNy44MzIxNEMyLjAwMDA5IDguMTkyMTQgMi4wMDAwOSA4Ljc0NDE0IDIuMzYwMDkgOS4xMDQxNEw1LjYyNDA5IDEyLjM2ODFDNS45ODQwOSAxMi43MjgxIDYuNTM2MDkgMTIuNzI4MSA2Ljg5NjA5IDEyLjM2ODFMMTQuNjQ4MSA0LjY0MDE0QzE1LjAwODEgNC4yODAxNCAxNS4wMDgxIDMuNzI4MTQgMTQuNjQ4MSAzLjM2ODE0QzE0LjI4ODEgMy4wMDgxNCAxMy43MzYxIDMuMDA4MTQgMTMuMzUyMSAzLjM2ODE0TDYuMjcyMDkgMTAuNDcyMUwzLjYzMjA5IDcuODMyMTRaIiBmaWxsPSIjMDA2OTVDIi8+Cjwvc3ZnPgo=')` : 'none']]]) + " " + "checkmark"
25
+ className: _JSXStyle.dynamic([["556453451", [colors.grey900, spacers.dp8, spacers.dp12, colors.grey200, colors.teal050, colors.grey500, active ? `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMuNjMyMDkgNy44MzIxNEMzLjI3MjA5IDcuNDcyMTQgMi43MjAwOSA3LjQ3MjE0IDIuMzYwMDkgNy44MzIxNEMyLjAwMDA5IDguMTkyMTQgMi4wMDAwOSA4Ljc0NDE0IDIuMzYwMDkgOS4xMDQxNEw1LjYyNDA5IDEyLjM2ODFDNS45ODQwOSAxMi43MjgxIDYuNTM2MDkgMTIuNzI4MSA2Ljg5NjA5IDEyLjM2ODFMMTQuNjQ4MSA0LjY0MDE0QzE1LjAwODEgNC4yODAxNCAxNS4wMDgxIDMuNzI4MTQgMTQuNjQ4MSAzLjM2ODE0QzE0LjI4ODEgMy4wMDgxNCAxMy43MzYxIDMuMDA4MTQgMTMuMzUyMSAzLjM2ODE0TDYuMjcyMDkgMTAuNDcyMUwzLjYzMjA5IDcuODMyMTRaIiBmaWxsPSIjMDA2OTVDIi8+Cjwvc3ZnPgo=')` : 'none']]]) + " " + "checkmark"
26
26
  }), /*#__PURE__*/React.createElement("span", {
27
- className: _JSXStyle.dynamic([["1307386342", [colors.grey900, spacers.dp8, spacers.dp12, colors.grey200, colors.teal050, colors.grey500, active ? `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMuNjMyMDkgNy44MzIxNEMzLjI3MjA5IDcuNDcyMTQgMi43MjAwOSA3LjQ3MjE0IDIuMzYwMDkgNy44MzIxNEMyLjAwMDA5IDguMTkyMTQgMi4wMDAwOSA4Ljc0NDE0IDIuMzYwMDkgOS4xMDQxNEw1LjYyNDA5IDEyLjM2ODFDNS45ODQwOSAxMi43MjgxIDYuNTM2MDkgMTIuNzI4MSA2Ljg5NjA5IDEyLjM2ODFMMTQuNjQ4MSA0LjY0MDE0QzE1LjAwODEgNC4yODAxNCAxNS4wMDgxIDMuNzI4MTQgMTQuNjQ4MSAzLjM2ODE0QzE0LjI4ODEgMy4wMDgxNCAxMy43MzYxIDMuMDA4MTQgMTMuMzUyMSAzLjM2ODE0TDYuMjcyMDkgMTAuNDcyMUwzLjYzMjA5IDcuODMyMTRaIiBmaWxsPSIjMDA2OTVDIi8+Cjwvc3ZnPgo=')` : 'none']]]) + " " + "content"
27
+ className: _JSXStyle.dynamic([["556453451", [colors.grey900, spacers.dp8, spacers.dp12, colors.grey200, colors.teal050, colors.grey500, active ? `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMuNjMyMDkgNy44MzIxNEMzLjI3MjA5IDcuNDcyMTQgMi43MjAwOSA3LjQ3MjE0IDIuMzYwMDkgNy44MzIxNEMyLjAwMDA5IDguMTkyMTQgMi4wMDAwOSA4Ljc0NDE0IDIuMzYwMDkgOS4xMDQxNEw1LjYyNDA5IDEyLjM2ODFDNS45ODQwOSAxMi43MjgxIDYuNTM2MDkgMTIuNzI4MSA2Ljg5NjA5IDEyLjM2ODFMMTQuNjQ4MSA0LjY0MDE0QzE1LjAwODEgNC4yODAxNCAxNS4wMDgxIDMuNzI4MTQgMTQuNjQ4MSAzLjM2ODE0QzE0LjI4ODEgMy4wMDgxNCAxMy43MzYxIDMuMDA4MTQgMTMuMzUyMSAzLjM2ODE0TDYuMjcyMDkgMTAuNDcyMUwzLjYzMjA5IDcuODMyMTRaIiBmaWxsPSIjMDA2OTVDIi8+Cjwvc3ZnPgo=')` : 'none']]]) + " " + "content"
28
28
  }, label), /*#__PURE__*/React.createElement(_JSXStyle, {
29
- id: "1307386342",
29
+ id: "556453451",
30
30
  dynamic: [colors.grey900, spacers.dp8, spacers.dp12, colors.grey200, colors.teal050, colors.grey500, active ? `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMuNjMyMDkgNy44MzIxNEMzLjI3MjA5IDcuNDcyMTQgMi43MjAwOSA3LjQ3MjE0IDIuMzYwMDkgNy44MzIxNEMyLjAwMDA5IDguMTkyMTQgMi4wMDAwOSA4Ljc0NDE0IDIuMzYwMDkgOS4xMDQxNEw1LjYyNDA5IDEyLjM2ODFDNS45ODQwOSAxMi43MjgxIDYuNTM2MDkgMTIuNzI4MSA2Ljg5NjA5IDEyLjM2ODFMMTQuNjQ4MSA0LjY0MDE0QzE1LjAwODEgNC4yODAxNCAxNS4wMDgxIDMuNzI4MTQgMTQuNjQ4MSAzLjM2ODE0QzE0LjI4ODEgMy4wMDgxNCAxMy43MzYxIDMuMDA4MTQgMTMuMzUyMSAzLjM2ODE0TDYuMjcyMDkgMTAuNDcyMUwzLjYzMjA5IDcuODMyMTRaIiBmaWxsPSIjMDA2OTVDIi8+Cjwvc3ZnPgo=')` : 'none']
31
31
  }, [`div.__jsx-style-dynamic-selector{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;cursor:pointer;font-size:14px;-webkit-text-decoration:none;text-decoration:none;color:${colors.grey900};padding-block:${spacers.dp8};padding-inline:6px ${spacers.dp12};}`, `div.__jsx-style-dynamic-selector:hover{background-color:${colors.grey200};}`, `div.__jsx-style-dynamic-selector:active,div.active.__jsx-style-dynamic-selector{background-color:${colors.teal050};cursor:auto;}`, `div.disabled.__jsx-style-dynamic-selector{color:${colors.grey500};cursor:not-allowed;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;}`, "div.disabled.__jsx-style-dynamic-selector:hover{background-color:initial;}", `div.__jsx-style-dynamic-selector .checkmark.__jsx-style-dynamic-selector{width:16px;height:16px;margin-inline-end:6px;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;background-size:contain;background-repeat:no-repeat;background-image:${active ? `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMuNjMyMDkgNy44MzIxNEMzLjI3MjA5IDcuNDcyMTQgMi43MjAwOSA3LjQ3MjE0IDIuMzYwMDkgNy44MzIxNEMyLjAwMDA5IDguMTkyMTQgMi4wMDAwOSA4Ljc0NDE0IDIuMzYwMDkgOS4xMDQxNEw1LjYyNDA5IDEyLjM2ODFDNS45ODQwOSAxMi43MjgxIDYuNTM2MDkgMTIuNzI4MSA2Ljg5NjA5IDEyLjM2ODFMMTQuNjQ4MSA0LjY0MDE0QzE1LjAwODEgNC4yODAxNCAxNS4wMDgxIDMuNzI4MTQgMTQuNjQ4MSAzLjM2ODE0QzE0LjI4ODEgMy4wMDgxNCAxMy43MzYxIDMuMDA4MTQgMTMuMzUyMSAzLjM2ODE0TDYuMjcyMDkgMTAuNDcyMUwzLjYzMjA5IDcuODMyMTRaIiBmaWxsPSIjMDA2OTVDIi8+Cjwvc3ZnPgo=')` : 'none'};}`]));
32
32
  SingleSelectOption.propTypes = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhis2-ui/select",
3
- "version": "10.17.0",
3
+ "version": "10.19.0",
4
4
  "description": "UI Select",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,21 +33,21 @@
33
33
  "styled-jsx": "^4"
34
34
  },
35
35
  "dependencies": {
36
- "@dhis2-ui/box": "10.17.0",
37
- "@dhis2-ui/button": "10.17.0",
38
- "@dhis2-ui/card": "10.17.0",
39
- "@dhis2-ui/checkbox": "10.17.0",
40
- "@dhis2-ui/chip": "10.17.0",
41
- "@dhis2-ui/field": "10.17.0",
42
- "@dhis2-ui/input": "10.17.0",
43
- "@dhis2-ui/layer": "10.17.0",
44
- "@dhis2-ui/loader": "10.17.0",
45
- "@dhis2-ui/popper": "10.17.0",
46
- "@dhis2-ui/status-icon": "10.17.0",
47
- "@dhis2-ui/tooltip": "10.17.0",
36
+ "@dhis2-ui/box": "10.19.0",
37
+ "@dhis2-ui/button": "10.19.0",
38
+ "@dhis2-ui/card": "10.19.0",
39
+ "@dhis2-ui/checkbox": "10.19.0",
40
+ "@dhis2-ui/chip": "10.19.0",
41
+ "@dhis2-ui/field": "10.19.0",
42
+ "@dhis2-ui/input": "10.19.0",
43
+ "@dhis2-ui/layer": "10.19.0",
44
+ "@dhis2-ui/loader": "10.19.0",
45
+ "@dhis2-ui/popper": "10.19.0",
46
+ "@dhis2-ui/status-icon": "10.19.0",
47
+ "@dhis2-ui/tooltip": "10.19.0",
48
48
  "@dhis2/prop-types": "^3.1.2",
49
- "@dhis2/ui-constants": "10.17.0",
50
- "@dhis2/ui-icons": "10.17.0",
49
+ "@dhis2/ui-constants": "10.19.0",
50
+ "@dhis2/ui-icons": "10.19.0",
51
51
  "classnames": "^2.3.1",
52
52
  "prop-types": "^15.7.2"
53
53
  },
@@ -0,0 +1,24 @@
1
+ import React, { useState } from 'react'
2
+ import { SimpleSingleSelect } from '../simple-single-select.js'
3
+
4
+ const options = [
5
+ { value: '1', label: 'option one' },
6
+ { value: '2', label: 'option two' },
7
+ { value: '3', label: 'A much longer option label that gets clamped' },
8
+ ]
9
+
10
+ export const WithMenuMaxWidth = () => {
11
+ const [selected, setSelected] = useState(null)
12
+
13
+ return (
14
+ <div style={{ width: 120 }}>
15
+ <SimpleSingleSelect
16
+ name="simple"
17
+ selected={selected}
18
+ onChange={setSelected}
19
+ options={options}
20
+ menuMaxWidth="200px"
21
+ />
22
+ </div>
23
+ )
24
+ }
@@ -0,0 +1,25 @@
1
+ import React, { useState } from 'react'
2
+ import { SimpleSingleSelect } from '../simple-single-select.js'
3
+
4
+ const options = [
5
+ { value: '1', label: 'option one' },
6
+ { value: '2', label: 'option two' },
7
+ { value: '3', label: 'option three' },
8
+ { value: '4', label: 'A longer option that exceeds the minimum' },
9
+ ]
10
+
11
+ export const WithMenuMinWidth = () => {
12
+ const [selected, setSelected] = useState(null)
13
+
14
+ return (
15
+ <div style={{ width: 120 }}>
16
+ <SimpleSingleSelect
17
+ name="simple"
18
+ selected={selected}
19
+ onChange={setSelected}
20
+ options={options}
21
+ menuMinWidth="240px"
22
+ />
23
+ </div>
24
+ )
25
+ }
@@ -26,6 +26,8 @@ export function Menu({
26
26
  loading,
27
27
  loadingText,
28
28
  maxHeight,
29
+ maxWidth,
30
+ minWidth,
29
31
  noMatchText,
30
32
  optionUpdateStrategy,
31
33
  selectRef,
@@ -34,18 +36,16 @@ export function Menu({
34
36
  onClose,
35
37
  onEndReached,
36
38
  }) {
37
- const [menuWidth, setWidth] = useState('auto')
39
+ const [selectWidth, setSelectWidth] = useState()
38
40
  const dataTestPrefix = `${dataTest}-menu`
39
41
 
42
+ // Re-measuring whenever `hidden` changes keeps the width current when the
43
+ // select's container has been resized since the menu was last opened
40
44
  useEffect(() => {
41
45
  if (selectRef) {
42
- const callback = () => setWidth(`${selectRef.offsetWidth}px`)
43
- callback() // We want to know the width as soon as the
44
-
45
- selectRef.addEventListener('resize', callback)
46
- return () => selectRef.removeEventListener('resize', callback)
46
+ setSelectWidth(`${selectRef.offsetWidth}px`)
47
47
  }
48
- }, [selectRef])
48
+ }, [selectRef, hidden])
49
49
 
50
50
  if (hidden) {
51
51
  return null
@@ -60,6 +60,14 @@ export function Menu({
60
60
 
61
61
  const isEmpty = !options.length && !filterValue
62
62
 
63
+ const flexible = Boolean(minWidth || maxWidth)
64
+ // We never want the menu narrower than the select, so a maxWidth below
65
+ // the select's width intentionally has no effect
66
+ const flexibleMinWidth =
67
+ minWidth && selectWidth
68
+ ? `max(${selectWidth}, ${minWidth})`
69
+ : minWidth || selectWidth
70
+
63
71
  return (
64
72
  <Layer onBackdropClick={onClose} transparent>
65
73
  <Popper
@@ -67,7 +75,15 @@ export function Menu({
67
75
  placement="bottom-start"
68
76
  observeReferenceResize
69
77
  >
70
- <div className="menu" style={{ width: menuWidth, maxHeight }}>
78
+ <div
79
+ className="menu"
80
+ style={{
81
+ width: flexible ? 'fit-content' : selectWidth,
82
+ minWidth: flexible ? flexibleMinWidth : undefined,
83
+ maxWidth,
84
+ maxHeight,
85
+ }}
86
+ >
71
87
  {isEmpty && <Empty>{empty}</Empty>}
72
88
 
73
89
  {hasNoFilterMatch && <NoMatch>{noMatchText}</NoMatch>}
@@ -160,6 +176,8 @@ Menu.propTypes = {
160
176
  loading: PropTypes.bool,
161
177
  loadingText: PropTypes.string,
162
178
  maxHeight: PropTypes.string,
179
+ maxWidth: PropTypes.string,
180
+ minWidth: PropTypes.string,
163
181
  noMatchText: PropTypes.string,
164
182
  optionComponent: PropTypes.elementType,
165
183
  optionUpdateStrategy: PropTypes.oneOf(['off', 'polite', 'assertive']),
@@ -12,6 +12,9 @@ import {
12
12
  useHandleKeyPressOnFilterInput,
13
13
  } from './use-handle-key-press/index.js'
14
14
 
15
+ const defaultSelected = { label: '', value: '' }
16
+ const noop = () => undefined
17
+
15
18
  function useFocussedOptionIndex({ filterable, filterValue, options }) {
16
19
  const [defaultFocussedOptionIndex, setDefaultFocussedOptionIndex] =
17
20
  useState(0)
@@ -75,19 +78,21 @@ export function SimpleSingleSelect({
75
78
  loading = false,
76
79
  menuLoadingText: _menuLoadingText = '',
77
80
  menuMaxHeight = '288px',
81
+ menuMaxWidth,
82
+ menuMinWidth,
78
83
  noMatchText: _noMatchText = '',
79
84
  optionUpdateStrategy = 'polite',
80
85
  placeholder = '',
81
86
  prefix = '',
82
- selected = { label: '', value: '' },
87
+ selected = defaultSelected,
83
88
  tabIndex = '0',
84
89
  valid = false,
85
90
  warning = false,
86
- onBlur = () => undefined,
87
- onClear = () => undefined,
88
- onEndReached = () => undefined,
89
- onFilterChange = () => undefined,
90
- onFocus = () => undefined,
91
+ onBlur = noop,
92
+ onClear = noop,
93
+ onEndReached = noop,
94
+ onFilterChange = noop,
95
+ onFocus = noop,
91
96
  }) {
92
97
  // We can't translate with default props as the translation function would
93
98
  // be called before we get any translations from the consuming app/lib
@@ -261,6 +266,8 @@ export function SimpleSingleSelect({
261
266
  loading={loading}
262
267
  loadingText={menuLoadingText}
263
268
  maxHeight={menuMaxHeight}
269
+ maxWidth={menuMaxWidth}
270
+ minWidth={menuMinWidth}
264
271
  noMatchText={noMatchText}
265
272
  optionUpdateStrategy={optionUpdateStrategy}
266
273
  options={options}
@@ -342,6 +349,12 @@ SimpleSingleSelect.propTypes = {
342
349
  /** Allows to modify the max height of the menu **/
343
350
  menuMaxHeight: PropTypes.string,
344
351
 
352
+ /** See [dropdown menu width](https://developers.dhis2.org/docs/ui/components/select#dropdown-menu-width) **/
353
+ menuMaxWidth: PropTypes.string,
354
+
355
+ /** See [dropdown menu width](https://developers.dhis2.org/docs/ui/components/select#dropdown-menu-width) **/
356
+ menuMinWidth: PropTypes.string,
357
+
345
358
  /** String that will be displayed when the select is being filtered but the options array is empty **/
346
359
  noMatchText: requiredIf((props) => props.filterable, PropTypes.string),
347
360
 
@@ -22,6 +22,8 @@ export { WithOptionsAndLoadingText } from './__stories__/WithOptionsAndLoadingTe
22
22
  export { WithoutOptionsAndLoading } from './__stories__/WithoutOptionsAndLoading.js'
23
23
  export { WithManyOptions } from './__stories__/WithManyOptions.js'
24
24
  export { WithCustomLowMaxHeight } from './__stories__/WithCustomLowMaxHeight.js'
25
+ export { WithMenuMinWidth } from './__stories__/WithMenuMinWidth.js'
26
+ export { WithMenuMaxWidth } from './__stories__/WithMenuMaxWidth.js'
25
27
  export { WithOptionsAndDisabled } from './__stories__/WithOptionsAndDisabled.js'
26
28
  export { WithSelectionAndDisabled } from './__stories__/WithSelectionAndDisabled.js'
27
29
  export { WithPrefix } from './__stories__/WithPrefix.js'
@@ -150,6 +150,74 @@ describe('<SimpleSingleSelect />', () => {
150
150
  expect(menu.style.maxHeight).toBe('100px')
151
151
  })
152
152
 
153
+ describe('dropdown menu width', () => {
154
+ let offsetWidth
155
+
156
+ beforeEach(() => {
157
+ // The menu's width is derived from the select's measured width,
158
+ // which is always 0 in jsdom
159
+ offsetWidth = jest
160
+ .spyOn(HTMLElement.prototype, 'offsetWidth', 'get')
161
+ .mockReturnValue(120)
162
+ })
163
+
164
+ afterEach(() => {
165
+ offsetWidth.mockRestore()
166
+ })
167
+
168
+ const renderAndOpen = (props) => {
169
+ render(
170
+ <SimpleSingleSelect
171
+ name="simple"
172
+ selected={{ value: 'foo', label: 'Foo' }}
173
+ onChange={() => null}
174
+ options={[{ value: 'foo', label: 'Foo' }]}
175
+ {...props}
176
+ />
177
+ )
178
+
179
+ fireEvent.click(screen.getByRole('combobox'))
180
+
181
+ const listbox = screen.getByRole('listbox')
182
+ return listbox.parentNode.parentNode.parentNode
183
+ }
184
+
185
+ it('should match the width of the select by default', () => {
186
+ const menu = renderAndOpen()
187
+
188
+ expect(menu.style.width).toBe('120px')
189
+ })
190
+
191
+ it('should not be narrower than menuMinWidth or the select', () => {
192
+ const menu = renderAndOpen({ menuMinWidth: '240px' })
193
+
194
+ expect(menu.style.minWidth).toBe('max(120px, 240px)')
195
+ })
196
+
197
+ it('should not be wider than menuMaxWidth', () => {
198
+ const menu = renderAndOpen({ menuMaxWidth: '200px' })
199
+
200
+ expect(menu.style.maxWidth).toBe('200px')
201
+ expect(menu.style.minWidth).toBe('120px')
202
+ })
203
+
204
+ it('should re-measure the select when the menu is reopened', () => {
205
+ renderAndOpen({ menuMinWidth: '240px' })
206
+ const comboBox = screen.getByRole('combobox')
207
+
208
+ // close the menu
209
+ fireEvent.click(comboBox)
210
+
211
+ // widen the select while the menu is closed, then reopen it
212
+ offsetWidth.mockReturnValue(300)
213
+ fireEvent.click(comboBox)
214
+
215
+ const listbox = screen.getByRole('listbox')
216
+ const menu = listbox.parentNode.parentNode.parentNode
217
+ expect(menu.style.minWidth).toBe('max(300px, 240px)')
218
+ })
219
+ })
220
+
153
221
  it('should accept a placeholder', () => {
154
222
  render(
155
223
  <SimpleSingleSelect
@@ -137,6 +137,12 @@ SimpleSingleSelectField.propTypes = {
137
137
  /** Allows to modify the max height of the menu **/
138
138
  menuMaxHeight: PropTypes.string,
139
139
 
140
+ /** See [dropdown menu width](https://developers.dhis2.org/docs/ui/components/select#dropdown-menu-width) **/
141
+ menuMaxWidth: PropTypes.string,
142
+
143
+ /** See [dropdown menu width](https://developers.dhis2.org/docs/ui/components/select#dropdown-menu-width) **/
144
+ menuMinWidth: PropTypes.string,
145
+
140
146
  /** String that will be displayed when the select is being filtered but the options array is empty **/
141
147
  noMatchText: requiredIf((props) => props.filterable, PropTypes.string),
142
148
 
@@ -26,6 +26,13 @@ const options = [
26
26
  { value: '10', label: 'ten' },
27
27
  ]
28
28
 
29
+ const longOptions = [
30
+ { value: '1', label: 'option one' },
31
+ { value: '2', label: 'option two' },
32
+ { value: '3', label: 'option three' },
33
+ { value: '4', label: 'A longer option that exceeds the minimum' },
34
+ ]
35
+
29
36
  export default {
30
37
  title: 'SimpleSingleSelectField',
31
38
  component: SimpleSingleSelectField,
@@ -186,3 +193,45 @@ export const InputWidth = () => {
186
193
  </div>
187
194
  )
188
195
  }
196
+
197
+ export const WithMenuMinWidth = () => {
198
+ const [value, setValue] = useState('')
199
+ const valueLabel = value
200
+ ? longOptions.find((option) => option.value === value)?.label
201
+ : ''
202
+
203
+ return (
204
+ <div style={{ width: 120 }}>
205
+ <SimpleSingleSelectField
206
+ name="simple"
207
+ label="This is the label"
208
+ value={value}
209
+ valueLabel={valueLabel}
210
+ onChange={(nextValue) => setValue(nextValue)}
211
+ options={longOptions}
212
+ menuMinWidth="240px"
213
+ />
214
+ </div>
215
+ )
216
+ }
217
+
218
+ export const WithMenuMaxWidth = () => {
219
+ const [value, setValue] = useState('')
220
+ const valueLabel = value
221
+ ? longOptions.find((option) => option.value === value)?.label
222
+ : ''
223
+
224
+ return (
225
+ <div style={{ width: 120 }}>
226
+ <SimpleSingleSelectField
227
+ name="simple"
228
+ label="This is the label"
229
+ value={value}
230
+ valueLabel={valueLabel}
231
+ onChange={(nextValue) => setValue(nextValue)}
232
+ options={longOptions}
233
+ menuMaxWidth="200px"
234
+ />
235
+ </div>
236
+ )
237
+ }
@@ -52,6 +52,8 @@ describe('<SimpleSingleSelectField />', () => {
52
52
  loading={false}
53
53
  menuLoadingText=""
54
54
  menuMaxHeight=""
55
+ menuMaxWidth="400px"
56
+ menuMinWidth="240px"
55
57
  noMatchText=""
56
58
  optionUpdateStrategy="off"
57
59
  placeholder=""
@@ -91,6 +93,8 @@ describe('<SimpleSingleSelectField />', () => {
91
93
  expect(SimpleSingleSelect.mock.calls[0][0].loading).toBe(false)
92
94
  expect(SimpleSingleSelect.mock.calls[0][0].menuLoadingText).toBe('')
93
95
  expect(SimpleSingleSelect.mock.calls[0][0].menuMaxHeight).toBe('')
96
+ expect(SimpleSingleSelect.mock.calls[0][0].menuMaxWidth).toBe('400px')
97
+ expect(SimpleSingleSelect.mock.calls[0][0].menuMinWidth).toBe('240px')
94
98
  expect(SimpleSingleSelect.mock.calls[0][0].noMatchText).toBe('')
95
99
  expect(SimpleSingleSelect.mock.calls[0][0].optionUpdateStrategy).toBe(
96
100
  'off'
@@ -63,9 +63,11 @@ const SingleSelectOption = ({
63
63
  flex-shrink: 0;
64
64
  background-size: contain;
65
65
  background-repeat: no-repeat;
66
- background-image: ${active
67
- ? `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMuNjMyMDkgNy44MzIxNEMzLjI3MjA5IDcuNDcyMTQgMi43MjAwOSA3LjQ3MjE0IDIuMzYwMDkgNy44MzIxNEMyLjAwMDA5IDguMTkyMTQgMi4wMDAwOSA4Ljc0NDE0IDIuMzYwMDkgOS4xMDQxNEw1LjYyNDA5IDEyLjM2ODFDNS45ODQwOSAxMi43MjgxIDYuNTM2MDkgMTIuNzI4MSA2Ljg5NjA5IDEyLjM2ODFMMTQuNjQ4MSA0LjY0MDE0QzE1LjAwODEgNC4yODAxNCAxNS4wMDgxIDMuNzI4MTQgMTQuNjQ4MSAzLjM2ODE0QzE0LjI4ODEgMy4wMDgxNCAxMy43MzYxIDMuMDA4MTQgMTMuMzUyMSAzLjM2ODE0TDYuMjcyMDkgMTAuNDcyMUwzLjYzMjA5IDcuODMyMTRaIiBmaWxsPSIjMDA2OTVDIi8+Cjwvc3ZnPgo=')`
68
- : 'none'};
66
+ background-image: ${
67
+ active
68
+ ? `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMuNjMyMDkgNy44MzIxNEMzLjI3MjA5IDcuNDcyMTQgMi43MjAwOSA3LjQ3MjE0IDIuMzYwMDkgNy44MzIxNEMyLjAwMDA5IDguMTkyMTQgMi4wMDAwOSA4Ljc0NDE0IDIuMzYwMDkgOS4xMDQxNEw1LjYyNDA5IDEyLjM2ODFDNS45ODQwOSAxMi43MjgxIDYuNTM2MDkgMTIuNzI4MSA2Ljg5NjA5IDEyLjM2ODFMMTQuNjQ4MSA0LjY0MDE0QzE1LjAwODEgNC4yODAxNCAxNS4wMDgxIDMuNzI4MTQgMTQuNjQ4MSAzLjM2ODE0QzE0LjI4ODEgMy4wMDgxNCAxMy43MzYxIDMuMDA4MTQgMTMuMzUyMSAzLjM2ODE0TDYuMjcyMDkgMTAuNDcyMUwzLjYzMjA5IDcuODMyMTRaIiBmaWxsPSIjMDA2OTVDIi8+Cjwvc3ZnPgo=')`
69
+ : 'none'
70
+ };
69
71
  }
70
72
  `}</style>
71
73
  </div>