@dhis2-ui/select 10.16.1 → 10.16.3-alpha.1
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/build/cjs/select/select.js +5 -2
- package/build/cjs/simple-single-select/use-handle-key-press/use-handle-key-press-on-filter-input.js +2 -2
- package/build/es/select/select.js +5 -2
- package/build/es/simple-single-select/use-handle-key-press/use-handle-key-press-on-filter-input.js +2 -2
- package/package.json +17 -16
- package/src/index.js +9 -0
- package/src/locales/ar/translations.json +7 -0
- package/src/locales/ar_IQ/translations.json +7 -0
- package/src/locales/ckb/translations.json +7 -0
- package/src/locales/cs/translations.json +7 -0
- package/src/locales/da/translations.json +7 -0
- package/src/locales/en/translations.json +11 -0
- package/src/locales/en_US/translations.json +9 -0
- package/src/locales/es/translations.json +7 -0
- package/src/locales/es_419/translations.json +7 -0
- package/src/locales/fr/translations.json +7 -0
- package/src/locales/hi_IN/translations.json +7 -0
- package/src/locales/id/translations.json +7 -0
- package/src/locales/index.js +80 -0
- package/src/locales/km/translations.json +7 -0
- package/src/locales/lo/translations.json +7 -0
- package/src/locales/my/translations.json +7 -0
- package/src/locales/nb/translations.json +7 -0
- package/src/locales/nl/translations.json +7 -0
- package/src/locales/prs/translations.json +7 -0
- package/src/locales/ps/translations.json +7 -0
- package/src/locales/pt/translations.json +7 -0
- package/src/locales/pt_BR/translations.json +7 -0
- package/src/locales/ru/translations.json +7 -0
- package/src/locales/sv/translations.json +7 -0
- package/src/locales/tet/translations.json +7 -0
- package/src/locales/tg/translations.json +7 -0
- package/src/locales/uk/translations.json +7 -0
- package/src/locales/ur/translations.json +7 -0
- package/src/locales/uz_Latn/translations.json +7 -0
- package/src/locales/uz_UZ_Cyrl/translations.json +7 -0
- package/src/locales/uz_UZ_Latn/translations.json +7 -0
- package/src/locales/vi/translations.json +7 -0
- package/src/locales/zh/translations.json +9 -0
- package/src/locales/zh_CN/translations.json +7 -0
- package/src/multi-select/__tests__/multi-select.test.js +23 -0
- package/src/multi-select/__tests__/selection-list.test.js +102 -0
- package/src/multi-select/features/accepts_blur_cb/index.js +14 -0
- package/src/multi-select/features/accepts_blur_cb.feature +8 -0
- package/src/multi-select/features/accepts_focus_cb/index.js +14 -0
- package/src/multi-select/features/accepts_focus_cb.feature +6 -0
- package/src/multi-select/features/accepts_initial_focus/index.js +5 -0
- package/src/multi-select/features/accepts_initial_focus.feature +5 -0
- package/src/multi-select/features/accepts_loading/index.js +22 -0
- package/src/multi-select/features/accepts_loading.feature +14 -0
- package/src/multi-select/features/accepts_max_height/index.js +31 -0
- package/src/multi-select/features/accepts_max_height.feature +11 -0
- package/src/multi-select/features/accepts_placeholder/index.js +25 -0
- package/src/multi-select/features/accepts_placeholder.feature +10 -0
- package/src/multi-select/features/accepts_prefix/index.js +19 -0
- package/src/multi-select/features/accepts_prefix.feature +10 -0
- package/src/multi-select/features/allows_invalid_options/index.js +27 -0
- package/src/multi-select/features/allows_invalid_options.feature +12 -0
- package/src/multi-select/features/allows_selecting/index.js +81 -0
- package/src/multi-select/features/allows_selecting.feature +44 -0
- package/src/multi-select/features/can_be_cleared/index.js +22 -0
- package/src/multi-select/features/can_be_cleared.feature +6 -0
- package/src/multi-select/features/can_be_disabled/index.js +36 -0
- package/src/multi-select/features/can_be_disabled.feature +32 -0
- package/src/multi-select/features/can_be_empty/index.js +28 -0
- package/src/multi-select/features/can_be_empty.feature +16 -0
- package/src/multi-select/features/can_be_filtered/index.js +23 -0
- package/src/multi-select/features/can_be_filtered.feature +14 -0
- package/src/multi-select/features/can_be_opened_and_closed/index.js +32 -0
- package/src/multi-select/features/can_be_opened_and_closed.feature +41 -0
- package/src/multi-select/features/common/index.js +49 -0
- package/src/multi-select/features/duplicate_option_values/index.js +21 -0
- package/src/multi-select/features/duplicate_option_values.feature +7 -0
- package/src/multi-select/features/menu_width_matches_input/index.js +53 -0
- package/src/multi-select/features/menu_width_matches_input.feature +11 -0
- package/src/multi-select/features/position/index.js +160 -0
- package/src/multi-select/features/position.feature +46 -0
- package/src/multi-select/features/shows_selections/index.js +18 -0
- package/src/multi-select/features/shows_selections.feature +9 -0
- package/src/multi-select/filterable-menu.js +43 -0
- package/src/multi-select/index.js +1 -0
- package/src/multi-select/input.js +129 -0
- package/src/multi-select/menu.js +91 -0
- package/src/multi-select/multi-select.e2e.stories.js +407 -0
- package/src/multi-select/multi-select.js +148 -0
- package/src/multi-select/multi-select.prod.stories.js +408 -0
- package/src/multi-select/selection-list.js +79 -0
- package/src/multi-select-field/__tests__/multi-select-field.test.js +29 -0
- package/src/multi-select-field/features/accepts_help_text/index.js +9 -0
- package/src/multi-select-field/features/accepts_help_text.feature +5 -0
- package/src/multi-select-field/features/accepts_label/index.js +9 -0
- package/src/multi-select-field/features/accepts_label.feature +5 -0
- package/src/multi-select-field/features/accepts_validation_text/index.js +9 -0
- package/src/multi-select-field/features/accepts_validation_text.feature +5 -0
- package/src/multi-select-field/features/can_be_required/index.js +11 -0
- package/src/multi-select-field/features/can_be_required.feature +5 -0
- package/src/multi-select-field/features/has_default_clear_text/index.js +16 -0
- package/src/multi-select-field/features/has_default_clear_text.feature +6 -0
- package/src/multi-select-field/features/has_default_empty_text/index.js +13 -0
- package/src/multi-select-field/features/has_default_empty_text.feature +6 -0
- package/src/multi-select-field/features/has_default_filter_nomatch_text/index.js +17 -0
- package/src/multi-select-field/features/has_default_filter_nomatch_text.feature +7 -0
- package/src/multi-select-field/features/has_default_filter_placeholder/index.js +15 -0
- package/src/multi-select-field/features/has_default_filter_placeholder.feature +6 -0
- package/src/multi-select-field/features/has_default_loading_text/index.js +13 -0
- package/src/multi-select-field/features/has_default_loading_text.feature +6 -0
- package/src/multi-select-field/index.js +1 -0
- package/src/multi-select-field/multi-select-field.e2e.stories.js +45 -0
- package/src/multi-select-field/multi-select-field.js +178 -0
- package/src/multi-select-field/multi-select-field.prod.stories.js +137 -0
- package/src/multi-select-option/index.js +1 -0
- package/src/multi-select-option/multi-select-option.js +70 -0
- package/src/select/__tests__/select.test.js +31 -0
- package/src/select/debounce/debounce.js +30 -0
- package/src/select/debounce/debounce.spec.js +35 -0
- package/src/select/empty.js +28 -0
- package/src/select/filter-input.js +42 -0
- package/src/select/filterable-menu.js +106 -0
- package/src/select/index.js +14 -0
- package/src/select/input-clear-button.js +66 -0
- package/src/select/input-placeholder.js +30 -0
- package/src/select/input-prefix.js +32 -0
- package/src/select/input-wrapper.js +114 -0
- package/src/select/loading.js +33 -0
- package/src/select/menu-wrapper.js +52 -0
- package/src/select/no-match.js +28 -0
- package/src/select/option-helpers.js +34 -0
- package/src/select/select.js +256 -0
- package/src/simple-single-select/__stories__/DefaultPosition.js +37 -0
- package/src/simple-single-select/__stories__/Dense.js +17 -0
- package/src/simple-single-select/__stories__/Empty.js +15 -0
- package/src/simple-single-select/__stories__/EmptyWithEmptyNode.js +27 -0
- package/src/simple-single-select/__stories__/EmptyWithEmptyText.js +16 -0
- package/src/simple-single-select/__stories__/FlippedPosition.js +36 -0
- package/src/simple-single-select/__stories__/InfiniteLoading.js +65 -0
- package/src/simple-single-select/__stories__/Loading.js +15 -0
- package/src/simple-single-select/__stories__/ShiftedIntoView.js +35 -0
- package/src/simple-single-select/__stories__/WithClearButton.js +26 -0
- package/src/simple-single-select/__stories__/WithCustomLowMaxHeight.js +17 -0
- package/src/simple-single-select/__stories__/WithCustomOptions.js +78 -0
- package/src/simple-single-select/__stories__/WithDisabledOption.js +25 -0
- package/src/simple-single-select/__stories__/WithFilterField.js +30 -0
- package/src/simple-single-select/__stories__/WithInitialFocus.js +29 -0
- package/src/simple-single-select/__stories__/WithManyOptions.js +19 -0
- package/src/simple-single-select/__stories__/WithMultipleSelectsAndScrollableParent.js +65 -0
- package/src/simple-single-select/__stories__/WithNoMatchText.js +25 -0
- package/src/simple-single-select/__stories__/WithOnBlur.js +22 -0
- package/src/simple-single-select/__stories__/WithOnFocus.js +22 -0
- package/src/simple-single-select/__stories__/WithOptionsAndDisabled.js +17 -0
- package/src/simple-single-select/__stories__/WithOptionsAndLoading.js +23 -0
- package/src/simple-single-select/__stories__/WithOptionsAndLoadingText.js +24 -0
- package/src/simple-single-select/__stories__/WithPlaceholder.js +18 -0
- package/src/simple-single-select/__stories__/WithPlaceholderAndSelection.js +20 -0
- package/src/simple-single-select/__stories__/WithPrefix.js +17 -0
- package/src/simple-single-select/__stories__/WithPrefixAndSelection.js +20 -0
- package/src/simple-single-select/__stories__/WithRTL.js +29 -0
- package/src/simple-single-select/__stories__/WithScrollingParent.js +51 -0
- package/src/simple-single-select/__stories__/WithSelection.js +19 -0
- package/src/simple-single-select/__stories__/WithSelectionAndDisabled.js +20 -0
- package/src/simple-single-select/__stories__/WithServerSideFilteringDemo.js +245 -0
- package/src/simple-single-select/__stories__/WithSimpleFilteringDemo.js +50 -0
- package/src/simple-single-select/__stories__/WithoutOptionsAndLoading.js +16 -0
- package/src/simple-single-select/__stories__/WithoutSelection.js +17 -0
- package/src/simple-single-select/__stories__/options.js +96 -0
- package/src/simple-single-select/__stories__/simple-single-select-server-side-filtering/CustomDataProvider.js +70 -0
- package/src/simple-single-select/__stories__/simple-single-select-server-side-filtering/dataElements.js +4271 -0
- package/src/simple-single-select/__stories__/simple-single-select-server-side-filtering/index.js +1 -0
- package/src/simple-single-select/features/keyboard-interactions.test.e2e.js +581 -0
- package/src/simple-single-select/features/menu-positioning.test.e2e.js +187 -0
- package/src/simple-single-select/index.js +2 -0
- package/src/simple-single-select/is-option-hidden.js +21 -0
- package/src/simple-single-select/menu/empty.js +27 -0
- package/src/simple-single-select/menu/filter.js +61 -0
- package/src/simple-single-select/menu/index.js +1 -0
- package/src/simple-single-select/menu/loading.js +38 -0
- package/src/simple-single-select/menu/menu.js +171 -0
- package/src/simple-single-select/menu/no-match.js +28 -0
- package/src/simple-single-select/menu/option.js +161 -0
- package/src/simple-single-select/menu/options-list.js +107 -0
- package/src/simple-single-select/selected-value/clear-button.js +71 -0
- package/src/simple-single-select/selected-value/container.js +146 -0
- package/src/simple-single-select/selected-value/index.js +1 -0
- package/src/simple-single-select/selected-value/placeholder.js +24 -0
- package/src/simple-single-select/selected-value/prefix.js +26 -0
- package/src/simple-single-select/selected-value/selected-value.js +174 -0
- package/src/simple-single-select/shared-prop-types.js +8 -0
- package/src/simple-single-select/simple-single-select.e2e.stories.js +216 -0
- package/src/simple-single-select/simple-single-select.js +389 -0
- package/src/simple-single-select/simple-single-select.prod.stories.js +41 -0
- package/src/simple-single-select/simple-single-select.test.js +1136 -0
- package/src/simple-single-select/use-handle-key-press/index.js +2 -0
- package/src/simple-single-select/use-handle-key-press/use-focus-option.js +24 -0
- package/src/simple-single-select/use-handle-key-press/use-handle-key-press-on-combobox.js +259 -0
- package/src/simple-single-select/use-handle-key-press/use-handle-key-press-on-filter-input.js +147 -0
- package/src/simple-single-select/use-handle-key-press/use-handle-typing.js +79 -0
- package/src/simple-single-select/use-handle-key-press/use-highlight-first-option-on-previous-page.js +62 -0
- package/src/simple-single-select/use-handle-key-press/use-highlight-first-visible-option.js +38 -0
- package/src/simple-single-select/use-handle-key-press/use-highlight-last-option-on-next-page.js +60 -0
- package/src/simple-single-select/use-handle-key-press/use-highlight-last-visible-option.js +52 -0
- package/src/simple-single-select/use-handle-key-press/use-page-down.js +50 -0
- package/src/simple-single-select/use-handle-key-press/use-page-up.js +50 -0
- package/src/simple-single-select/use-handle-key-press/utils.js +23 -0
- package/src/simple-single-select-field/index.js +1 -0
- package/src/simple-single-select-field/simple-single-select-field.js +198 -0
- package/src/simple-single-select-field/simple-single-select-field.prod.stories.js +188 -0
- package/src/simple-single-select-field/simple-single-select-field.test.js +144 -0
- package/src/single-select/__tests__/selection.test.js +102 -0
- package/src/single-select/__tests__/single-select.test.js +23 -0
- package/src/single-select/features/accepts_blur_cb/index.js +14 -0
- package/src/single-select/features/accepts_blur_cb.feature +8 -0
- package/src/single-select/features/accepts_focus_cb/index.js +14 -0
- package/src/single-select/features/accepts_focus_cb.feature +6 -0
- package/src/single-select/features/accepts_initial_focus/index.js +5 -0
- package/src/single-select/features/accepts_initial_focus.feature +5 -0
- package/src/single-select/features/accepts_loading/index.js +22 -0
- package/src/single-select/features/accepts_loading.feature +14 -0
- package/src/single-select/features/accepts_max_height/index.js +31 -0
- package/src/single-select/features/accepts_max_height.feature +11 -0
- package/src/single-select/features/accepts_placeholder/index.js +25 -0
- package/src/single-select/features/accepts_placeholder.feature +10 -0
- package/src/single-select/features/accepts_prefix/index.js +19 -0
- package/src/single-select/features/accepts_prefix.feature +10 -0
- package/src/single-select/features/allows_invalid_options/index.js +28 -0
- package/src/single-select/features/allows_invalid_options.feature +12 -0
- package/src/single-select/features/allows_selecting/index.js +38 -0
- package/src/single-select/features/allows_selecting.feature +21 -0
- package/src/single-select/features/can_be_cleared/index.js +22 -0
- package/src/single-select/features/can_be_cleared.feature +6 -0
- package/src/single-select/features/can_be_disabled/index.js +39 -0
- package/src/single-select/features/can_be_disabled.feature +32 -0
- package/src/single-select/features/can_be_empty/index.js +28 -0
- package/src/single-select/features/can_be_empty.feature +16 -0
- package/src/single-select/features/can_be_filtered/index.js +23 -0
- package/src/single-select/features/can_be_filtered.feature +14 -0
- package/src/single-select/features/can_be_opened_and_closed/index.js +32 -0
- package/src/single-select/features/can_be_opened_and_closed.feature +41 -0
- package/src/single-select/features/common/index.js +44 -0
- package/src/single-select/features/duplicate_option_values/index.js +26 -0
- package/src/single-select/features/duplicate_option_values.feature +7 -0
- package/src/single-select/features/menu_width_matches_input/index.js +53 -0
- package/src/single-select/features/menu_width_matches_input.feature +11 -0
- package/src/single-select/features/position/index.js +111 -0
- package/src/single-select/features/position.feature +26 -0
- package/src/single-select/features/shows_selections/index.js +9 -0
- package/src/single-select/features/shows_selections.feature +5 -0
- package/src/single-select/filterable-menu.js +41 -0
- package/src/single-select/index.js +1 -0
- package/src/single-select/input.js +101 -0
- package/src/single-select/menu.js +75 -0
- package/src/single-select/selection.js +60 -0
- package/src/single-select/single-select.e2e.stories.js +376 -0
- package/src/single-select/single-select.js +146 -0
- package/src/single-select/single-select.prod.stories.js +358 -0
- package/src/single-select-field/__tests__/single-select-field.test.js +29 -0
- package/src/single-select-field/features/accepts_help_text/index.js +9 -0
- package/src/single-select-field/features/accepts_help_text.feature +5 -0
- package/src/single-select-field/features/accepts_label/index.js +9 -0
- package/src/single-select-field/features/accepts_label.feature +5 -0
- package/src/single-select-field/features/accepts_validation_text/index.js +9 -0
- package/src/single-select-field/features/accepts_validation_text.feature +5 -0
- package/src/single-select-field/features/can_be_required/index.js +11 -0
- package/src/single-select-field/features/can_be_required.feature +5 -0
- package/src/single-select-field/features/has_default_clear_text/index.js +16 -0
- package/src/single-select-field/features/has_default_clear_text.feature +6 -0
- package/src/single-select-field/features/has_default_empty_text/index.js +13 -0
- package/src/single-select-field/features/has_default_empty_text.feature +6 -0
- package/src/single-select-field/features/has_default_filter_nomatch_text/index.js +17 -0
- package/src/single-select-field/features/has_default_filter_nomatch_text.feature +7 -0
- package/src/single-select-field/features/has_default_filter_placeholder/index.js +15 -0
- package/src/single-select-field/features/has_default_filter_placeholder.feature +6 -0
- package/src/single-select-field/features/has_default_loading_text/index.js +13 -0
- package/src/single-select-field/features/has_default_loading_text.feature +6 -0
- package/src/single-select-field/index.js +1 -0
- package/src/single-select-field/single-select-field.e2e.stories.js +45 -0
- package/src/single-select-field/single-select-field.js +178 -0
- package/src/single-select-field/single-select-field.prod.stories.js +146 -0
- package/src/single-select-option/index.js +1 -0
- package/src/single-select-option/single-select-option.js +84 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
describe('SimpleSingleSelect: Menu positioning', () => {
|
|
2
|
+
it('should open in the default position', () => {
|
|
3
|
+
// Given there is enough space below the anchor to fit the SingleSelect menu
|
|
4
|
+
cy.visitStory('Simple Single Select', 'Default position')
|
|
5
|
+
|
|
6
|
+
// When the SingleSelect is clicked
|
|
7
|
+
cy.findByRole('combobox').click()
|
|
8
|
+
|
|
9
|
+
// Then the top of the menu is aligned with the bottom of the input
|
|
10
|
+
cy.all(
|
|
11
|
+
() => cy.findByRole('combobox'),
|
|
12
|
+
() => cy.findByRole('listbox')
|
|
13
|
+
).should(([selectedValue, menu]) => {
|
|
14
|
+
expect(selectedValue.length).to.equal(1)
|
|
15
|
+
expect(menu.length).to.equal(1)
|
|
16
|
+
|
|
17
|
+
const $selectedValue = selectedValue[0]
|
|
18
|
+
const $menu = menu[0]
|
|
19
|
+
|
|
20
|
+
const selectedValueRect = $selectedValue.getBoundingClientRect()
|
|
21
|
+
const menuRect = $menu.getBoundingClientRect()
|
|
22
|
+
|
|
23
|
+
// The listbox is inside a container with a border,
|
|
24
|
+
// hence the increased delta
|
|
25
|
+
expect(menuRect.top).to.be.closeTo(selectedValueRect.bottom, 2)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// And the left of the SingleSelect is aligned with the left of the anchor
|
|
29
|
+
cy.all(
|
|
30
|
+
() => cy.findByRole('combobox'),
|
|
31
|
+
() => cy.findByRole('listbox')
|
|
32
|
+
).should(([selectedValue, menu]) => {
|
|
33
|
+
expect(selectedValue.length).to.equal(1)
|
|
34
|
+
expect(menu.length).to.equal(1)
|
|
35
|
+
|
|
36
|
+
const $selectedValue = selectedValue[0]
|
|
37
|
+
const $menu = menu[0]
|
|
38
|
+
|
|
39
|
+
const selectedValueRect = $selectedValue.getBoundingClientRect()
|
|
40
|
+
const menuRect = $menu.getBoundingClientRect()
|
|
41
|
+
|
|
42
|
+
// The listbox is inside a container with a border,
|
|
43
|
+
// hence the increased delta
|
|
44
|
+
expect(selectedValueRect.left).to.be.closeTo(menuRect.left, 2)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should flipp the position when insufficient space below', () => {
|
|
49
|
+
// Given there is not enough space below the anchor to fit the SingleSelect menu
|
|
50
|
+
cy.visitStory('Simple Single Select', 'Flipped position')
|
|
51
|
+
|
|
52
|
+
// When the SingleSelect is clicked
|
|
53
|
+
cy.findByRole('combobox').click()
|
|
54
|
+
|
|
55
|
+
// Then the bottom of the menu is aligned with the top of the input
|
|
56
|
+
cy.all(
|
|
57
|
+
() => cy.findByRole('combobox'),
|
|
58
|
+
// We need to get the parent as the menu itself
|
|
59
|
+
// extends withing the div container
|
|
60
|
+
() => cy.findByRole('listbox').invoke('parent')
|
|
61
|
+
).should(([selectedValue, menu]) => {
|
|
62
|
+
expect(selectedValue.length).to.equal(1)
|
|
63
|
+
expect(menu.length).to.equal(1)
|
|
64
|
+
|
|
65
|
+
const $selectedValue = selectedValue[0]
|
|
66
|
+
const $menu = menu[0]
|
|
67
|
+
|
|
68
|
+
const selectedValueRect = $selectedValue.getBoundingClientRect()
|
|
69
|
+
const menuRect = $menu.getBoundingClientRect()
|
|
70
|
+
|
|
71
|
+
expect(selectedValueRect.top).to.be.closeTo(menuRect.bottom, 2)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// And the left of the SingleSelect is aligned with the left of the anchor
|
|
75
|
+
cy.all(
|
|
76
|
+
() => cy.findByRole('combobox'),
|
|
77
|
+
() => cy.findByRole('listbox')
|
|
78
|
+
).should(([selectedValue, menu]) => {
|
|
79
|
+
expect(selectedValue.length).to.equal(1)
|
|
80
|
+
expect(menu.length).to.equal(1)
|
|
81
|
+
|
|
82
|
+
const $selectedValue = selectedValue[0]
|
|
83
|
+
const $menu = menu[0]
|
|
84
|
+
|
|
85
|
+
const selectedValueRect = $selectedValue.getBoundingClientRect()
|
|
86
|
+
const menuRect = $menu.getBoundingClientRect()
|
|
87
|
+
|
|
88
|
+
// The listbox is inside a container with a border,
|
|
89
|
+
// hence the increased delta
|
|
90
|
+
expect(selectedValueRect.left).to.be.closeTo(menuRect.left, 2)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should shift the menu into view when insufficient space below and above', () => {
|
|
95
|
+
// Given there is not enough space above or below the anchor to fit the SingleSelect menu
|
|
96
|
+
cy.visitStory('Simple Single Select', 'Shifted into view')
|
|
97
|
+
|
|
98
|
+
// When the SingleSelect is clicked
|
|
99
|
+
cy.findByRole('combobox').click()
|
|
100
|
+
|
|
101
|
+
// Then it is rendered on top of the SingleSelect
|
|
102
|
+
cy.all(
|
|
103
|
+
() => cy.findByRole('combobox'),
|
|
104
|
+
() => cy.findByRole('listbox')
|
|
105
|
+
).should(([selectedValue, menu]) => {
|
|
106
|
+
expect(selectedValue.length).to.equal(1)
|
|
107
|
+
expect(menu.length).to.equal(1)
|
|
108
|
+
|
|
109
|
+
const $selectedValue = selectedValue[0]
|
|
110
|
+
const $menu = menu[0]
|
|
111
|
+
|
|
112
|
+
const selectedValueRect = $selectedValue.getBoundingClientRect()
|
|
113
|
+
const menuRect = $menu.getBoundingClientRect()
|
|
114
|
+
|
|
115
|
+
expect(selectedValueRect.top).to.be.greaterThan(menuRect.top)
|
|
116
|
+
expect(menuRect.bottom).to.be.greaterThan(selectedValueRect.bottom)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// And the left of the SingleSelect is aligned with the left of the anchor
|
|
120
|
+
cy.all(
|
|
121
|
+
() => cy.findByRole('combobox'),
|
|
122
|
+
() => cy.findByRole('listbox')
|
|
123
|
+
).should(([selectedValue, menu]) => {
|
|
124
|
+
expect(selectedValue.length).to.equal(1)
|
|
125
|
+
expect(menu.length).to.equal(1)
|
|
126
|
+
|
|
127
|
+
const $selectedValue = selectedValue[0]
|
|
128
|
+
const $menu = menu[0]
|
|
129
|
+
|
|
130
|
+
const selectedValueRect = $selectedValue.getBoundingClientRect()
|
|
131
|
+
const menuRect = $menu.getBoundingClientRect()
|
|
132
|
+
|
|
133
|
+
expect(selectedValueRect.top).to.be.greaterThan(menuRect.top)
|
|
134
|
+
expect(menuRect.bottom).to.be.greaterThan(selectedValueRect.bottom)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('should keep the menu in position while the window is scrolled', () => {
|
|
139
|
+
// Given there is enough space below the anchor to fit the SingleSelect menu
|
|
140
|
+
cy.visitStory('Simple Single Select', 'Default position')
|
|
141
|
+
|
|
142
|
+
// When the SingleSelect is clicked
|
|
143
|
+
cy.findByRole('combobox').click()
|
|
144
|
+
|
|
145
|
+
// And the window is scrolled down
|
|
146
|
+
cy.get('body').then(($body) => $body.height('5000px')) // Ensure the body can scroll first
|
|
147
|
+
cy.scrollTo(0, 800)
|
|
148
|
+
|
|
149
|
+
// Then the top of the menu is aligned with the bottom of the input
|
|
150
|
+
cy.all(
|
|
151
|
+
() => cy.findByRole('combobox'),
|
|
152
|
+
() => cy.findByRole('listbox')
|
|
153
|
+
).should(([selectedValue, menu]) => {
|
|
154
|
+
expect(selectedValue.length).to.equal(1)
|
|
155
|
+
expect(menu.length).to.equal(1)
|
|
156
|
+
|
|
157
|
+
const $selectedValue = selectedValue[0]
|
|
158
|
+
const $menu = menu[0]
|
|
159
|
+
|
|
160
|
+
const selectedValueRect = $selectedValue.getBoundingClientRect()
|
|
161
|
+
const menuRect = $menu.getBoundingClientRect()
|
|
162
|
+
|
|
163
|
+
// The listbox is inside a container with a border,
|
|
164
|
+
// hence the increased delta
|
|
165
|
+
expect(menuRect.top).to.be.closeTo(selectedValueRect.bottom, 2)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// And the left of the SingleSelect is aligned with the left of the anchor
|
|
169
|
+
cy.all(
|
|
170
|
+
() => cy.findByRole('combobox'),
|
|
171
|
+
() => cy.findByRole('listbox')
|
|
172
|
+
).should(([selectedValue, menu]) => {
|
|
173
|
+
expect(selectedValue.length).to.equal(1)
|
|
174
|
+
expect(menu.length).to.equal(1)
|
|
175
|
+
|
|
176
|
+
const $selectedValue = selectedValue[0]
|
|
177
|
+
const $menu = menu[0]
|
|
178
|
+
|
|
179
|
+
const selectedValueRect = $selectedValue.getBoundingClientRect()
|
|
180
|
+
const menuRect = $menu.getBoundingClientRect()
|
|
181
|
+
|
|
182
|
+
// The listbox is inside a container with a border,
|
|
183
|
+
// hence the increased delta
|
|
184
|
+
expect(selectedValueRect.left).to.be.closeTo(menuRect.left, 2)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// If this many pixels are invisible, we still consider the option visible
|
|
2
|
+
// The reason we're doing this is because of JS' number issues,
|
|
3
|
+
// e.g:
|
|
4
|
+
// optionOffsetBottom is 345.6000061035156
|
|
5
|
+
// containerOffsetBottom is 345.5999984741211
|
|
6
|
+
// -> Without tolerance, option is invisible
|
|
7
|
+
const TOLERANCE = 2
|
|
8
|
+
|
|
9
|
+
export function isOptionHidden(option, scrollContainer) {
|
|
10
|
+
const optionOffsetTop = option.getBoundingClientRect().top
|
|
11
|
+
const optionHeight = option.offsetHeight
|
|
12
|
+
const optionOffsetBottom = optionOffsetTop + optionHeight
|
|
13
|
+
const containerOffsetTop = scrollContainer.getBoundingClientRect().top
|
|
14
|
+
const containerHeight = scrollContainer.offsetHeight
|
|
15
|
+
const containerOffsetBottom = containerOffsetTop + containerHeight
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
optionOffsetBottom > containerOffsetBottom + TOLERANCE ||
|
|
19
|
+
optionOffsetTop < containerOffsetTop - TOLERANCE
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { colors, spacers, theme } from '@dhis2/ui-constants'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
const Empty = ({ children }) => (
|
|
6
|
+
<div>
|
|
7
|
+
{children}
|
|
8
|
+
|
|
9
|
+
<style jsx>{`
|
|
10
|
+
div {
|
|
11
|
+
color: ${colors.grey700};
|
|
12
|
+
font-family: ${theme.fonts};
|
|
13
|
+
font-size: 13px;
|
|
14
|
+
line-height: 16px;
|
|
15
|
+
padding-block: ${spacers.dp8};
|
|
16
|
+
padding-inline: ${spacers.dp24};
|
|
17
|
+
text-align: center;
|
|
18
|
+
}
|
|
19
|
+
`}</style>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
Empty.propTypes = {
|
|
24
|
+
children: PropTypes.string.isRequired,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { Empty }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import PropTypes from 'prop-types'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import i18n from '../../locales/index.js'
|
|
4
|
+
|
|
5
|
+
export function Filter({
|
|
6
|
+
ariaControls,
|
|
7
|
+
dataTest,
|
|
8
|
+
inputRef,
|
|
9
|
+
label,
|
|
10
|
+
placeholder,
|
|
11
|
+
value,
|
|
12
|
+
onChange,
|
|
13
|
+
onKeyDown,
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
<input
|
|
18
|
+
ref={inputRef}
|
|
19
|
+
className="filter-input"
|
|
20
|
+
type="text"
|
|
21
|
+
name="filter"
|
|
22
|
+
value={value}
|
|
23
|
+
placeholder={placeholder}
|
|
24
|
+
aria-label={label || i18n.t('Search options')}
|
|
25
|
+
aria-controls={ariaControls}
|
|
26
|
+
aria-haspopup="listbox"
|
|
27
|
+
data-test={dataTest}
|
|
28
|
+
onChange={(e) => onChange(e.target.value)}
|
|
29
|
+
onClick={(e) => e.stopPropagation()}
|
|
30
|
+
onKeyDown={(e) => {
|
|
31
|
+
e.stopPropagation()
|
|
32
|
+
onKeyDown?.(e)
|
|
33
|
+
}}
|
|
34
|
+
/>
|
|
35
|
+
<style jsx>{`
|
|
36
|
+
.filter-input {
|
|
37
|
+
flex: 1;
|
|
38
|
+
border: none;
|
|
39
|
+
outline: none;
|
|
40
|
+
background: transparent;
|
|
41
|
+
font-size: 14px;
|
|
42
|
+
line-height: 16px;
|
|
43
|
+
color: inherit;
|
|
44
|
+
padding: 0;
|
|
45
|
+
min-width: 0;
|
|
46
|
+
}
|
|
47
|
+
`}</style>
|
|
48
|
+
</>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Filter.propTypes = {
|
|
53
|
+
ariaControls: PropTypes.string.isRequired,
|
|
54
|
+
inputRef: PropTypes.object.isRequired,
|
|
55
|
+
value: PropTypes.string.isRequired,
|
|
56
|
+
onChange: PropTypes.func.isRequired,
|
|
57
|
+
dataTest: PropTypes.string,
|
|
58
|
+
label: PropTypes.string,
|
|
59
|
+
placeholder: PropTypes.string,
|
|
60
|
+
onKeyDown: PropTypes.func,
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Menu } from './menu.js'
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { colors, spacers, theme } from '@dhis2/ui-constants'
|
|
2
|
+
import { CircularLoader } from '@dhis2-ui/loader'
|
|
3
|
+
import PropTypes from 'prop-types'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
|
|
6
|
+
export function Loading({ message }) {
|
|
7
|
+
return (
|
|
8
|
+
<div className="container">
|
|
9
|
+
<div>
|
|
10
|
+
<CircularLoader small />
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
{message}
|
|
14
|
+
|
|
15
|
+
<style jsx>{`
|
|
16
|
+
.container {
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
width: 100%;
|
|
19
|
+
height: 100%;
|
|
20
|
+
display: flex;
|
|
21
|
+
gap: ${spacers.dp16};
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
color: ${colors.grey700};
|
|
25
|
+
font-family: ${theme.fonts};
|
|
26
|
+
font-size: 13px;
|
|
27
|
+
padding-block: ${spacers.dp8};
|
|
28
|
+
padding-inline: ${spacers.dp24};
|
|
29
|
+
background: rgba(255, 255, 255, 0.8);
|
|
30
|
+
}
|
|
31
|
+
`}</style>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Loading.propTypes = {
|
|
37
|
+
message: PropTypes.string,
|
|
38
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { colors, elevations } from '@dhis2/ui-constants'
|
|
2
|
+
import { Layer } from '@dhis2-ui/layer'
|
|
3
|
+
import { Popper } from '@dhis2-ui/popper'
|
|
4
|
+
import PropTypes from 'prop-types'
|
|
5
|
+
import React, { useEffect, useState } from 'react'
|
|
6
|
+
import { optionProp } from '../shared-prop-types.js'
|
|
7
|
+
import { Empty } from './empty.js'
|
|
8
|
+
import { Loading } from './loading.js'
|
|
9
|
+
import { NoMatch } from './no-match.js'
|
|
10
|
+
import { OptionsList } from './options-list.js'
|
|
11
|
+
|
|
12
|
+
export function Menu({
|
|
13
|
+
comboBoxId,
|
|
14
|
+
focussedOptionIndex,
|
|
15
|
+
name,
|
|
16
|
+
options,
|
|
17
|
+
onChange,
|
|
18
|
+
optionComponent,
|
|
19
|
+
dataTest,
|
|
20
|
+
disabled,
|
|
21
|
+
empty,
|
|
22
|
+
filterValue,
|
|
23
|
+
hidden,
|
|
24
|
+
labelledBy,
|
|
25
|
+
listBoxRef,
|
|
26
|
+
loading,
|
|
27
|
+
loadingText,
|
|
28
|
+
maxHeight,
|
|
29
|
+
noMatchText,
|
|
30
|
+
optionUpdateStrategy,
|
|
31
|
+
selectRef,
|
|
32
|
+
selectedValue,
|
|
33
|
+
onBlur,
|
|
34
|
+
onClose,
|
|
35
|
+
onEndReached,
|
|
36
|
+
}) {
|
|
37
|
+
const [menuWidth, setWidth] = useState('auto')
|
|
38
|
+
const dataTestPrefix = `${dataTest}-menu`
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
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)
|
|
47
|
+
}
|
|
48
|
+
}, [selectRef])
|
|
49
|
+
|
|
50
|
+
if (hidden) {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const hasNoFilterMatch =
|
|
55
|
+
!options.length &&
|
|
56
|
+
filterValue &&
|
|
57
|
+
// We don't want to show the noMatchText when
|
|
58
|
+
// we're in the process of loading options
|
|
59
|
+
!loading
|
|
60
|
+
|
|
61
|
+
const isEmpty = !options.length && !filterValue
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Layer onBackdropClick={onClose} transparent>
|
|
65
|
+
<Popper
|
|
66
|
+
reference={selectRef}
|
|
67
|
+
placement="bottom-start"
|
|
68
|
+
observeReferenceResize
|
|
69
|
+
>
|
|
70
|
+
<div className="menu" style={{ width: menuWidth, maxHeight }}>
|
|
71
|
+
{isEmpty && <Empty>{empty}</Empty>}
|
|
72
|
+
|
|
73
|
+
{hasNoFilterMatch && <NoMatch>{noMatchText}</NoMatch>}
|
|
74
|
+
|
|
75
|
+
<div className="listbox-container">
|
|
76
|
+
<div className="listbox-wrapper">
|
|
77
|
+
<OptionsList
|
|
78
|
+
ref={listBoxRef}
|
|
79
|
+
comboBoxId={comboBoxId}
|
|
80
|
+
optionComponent={optionComponent}
|
|
81
|
+
dataTest={`${dataTestPrefix}-list`}
|
|
82
|
+
disabled={disabled}
|
|
83
|
+
focussedOptionIndex={focussedOptionIndex}
|
|
84
|
+
labelledBy={labelledBy}
|
|
85
|
+
loading={loading}
|
|
86
|
+
id={`${name}-listbox`}
|
|
87
|
+
optionUpdateStrategy={optionUpdateStrategy}
|
|
88
|
+
options={options}
|
|
89
|
+
selectedValue={selectedValue}
|
|
90
|
+
onBlur={onBlur}
|
|
91
|
+
onChange={onChange}
|
|
92
|
+
onEndReached={onEndReached}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{loading && (
|
|
97
|
+
<div className="menu-loading-container">
|
|
98
|
+
<Loading message={loadingText} />
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<style jsx>{`
|
|
104
|
+
.menu {
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
height: auto;
|
|
108
|
+
background: ${colors.white};
|
|
109
|
+
border: 1px solid ${colors.grey200};
|
|
110
|
+
border-radius: 3px;
|
|
111
|
+
box-shadow: ${elevations.e300};
|
|
112
|
+
|
|
113
|
+
/* We want the provided height to be exact, otherwise
|
|
114
|
+
the consumer would have to know about the border's width */
|
|
115
|
+
box-sizing: content-box;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.listbox-container {
|
|
119
|
+
position: relative;
|
|
120
|
+
flex-grow: 1;
|
|
121
|
+
display: flex;
|
|
122
|
+
flex-direction: column;
|
|
123
|
+
overflow: hidden;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.listbox-wrapper {
|
|
127
|
+
overflow: auto;
|
|
128
|
+
flex-grow: 1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.menu-loading-container {
|
|
132
|
+
position: absolute;
|
|
133
|
+
left: 0;
|
|
134
|
+
top: 0;
|
|
135
|
+
width: 100%;
|
|
136
|
+
height: 100%;
|
|
137
|
+
}
|
|
138
|
+
`}</style>
|
|
139
|
+
</div>
|
|
140
|
+
</Popper>
|
|
141
|
+
</Layer>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
Menu.propTypes = {
|
|
146
|
+
comboBoxId: PropTypes.string.isRequired,
|
|
147
|
+
focussedOptionIndex: PropTypes.number.isRequired,
|
|
148
|
+
listBoxRef: PropTypes.shape({
|
|
149
|
+
current: PropTypes.instanceOf(HTMLElement),
|
|
150
|
+
}).isRequired,
|
|
151
|
+
name: PropTypes.string.isRequired,
|
|
152
|
+
options: PropTypes.arrayOf(optionProp).isRequired,
|
|
153
|
+
onChange: PropTypes.func.isRequired,
|
|
154
|
+
dataTest: PropTypes.string,
|
|
155
|
+
disabled: PropTypes.bool,
|
|
156
|
+
empty: PropTypes.node,
|
|
157
|
+
filterValue: PropTypes.string,
|
|
158
|
+
hidden: PropTypes.bool,
|
|
159
|
+
labelledBy: PropTypes.string,
|
|
160
|
+
loading: PropTypes.bool,
|
|
161
|
+
loadingText: PropTypes.string,
|
|
162
|
+
maxHeight: PropTypes.string,
|
|
163
|
+
noMatchText: PropTypes.string,
|
|
164
|
+
optionComponent: PropTypes.elementType,
|
|
165
|
+
optionUpdateStrategy: PropTypes.oneOf(['off', 'polite', 'assertive']),
|
|
166
|
+
selectRef: PropTypes.instanceOf(HTMLElement),
|
|
167
|
+
selectedValue: PropTypes.string,
|
|
168
|
+
onBlur: PropTypes.func,
|
|
169
|
+
onClose: PropTypes.func,
|
|
170
|
+
onEndReached: PropTypes.func,
|
|
171
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { colors, spacers, theme } from '@dhis2/ui-constants'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
const NoMatch = ({ children, className }) => (
|
|
6
|
+
<div className={className}>
|
|
7
|
+
{children}
|
|
8
|
+
|
|
9
|
+
<style jsx>{`
|
|
10
|
+
div {
|
|
11
|
+
color: ${colors.grey700};
|
|
12
|
+
font-family: ${theme.fonts};
|
|
13
|
+
font-size: 13px;
|
|
14
|
+
line-height: 16px;
|
|
15
|
+
padding-block: ${spacers.dp8};
|
|
16
|
+
padding-inline: ${spacers.dp24};
|
|
17
|
+
text-align: center;
|
|
18
|
+
}
|
|
19
|
+
`}</style>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
NoMatch.propTypes = {
|
|
24
|
+
children: PropTypes.string.isRequired,
|
|
25
|
+
className: PropTypes.string,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { NoMatch }
|