diff --git a/lib/AutoSuggest/AutoSuggest.js b/lib/AutoSuggest/AutoSuggest.js
index 5d464512a..52ed5e5ab 100644
--- a/lib/AutoSuggest/AutoSuggest.js
+++ b/lib/AutoSuggest/AutoSuggest.js
@@ -1,11 +1,12 @@
import React, { useRef, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
+import { deprecated } from 'prop-types-extra';
import { useCombobox } from 'downshift';
import classNames from 'classnames';
import noop from 'lodash/noop';
import isEqual from 'lodash/isEqual';
import Label from '../Label';
-import Popper from '../Popper';
+import Popper, { OVERLAY_MODIFIERS } from '../Popper';
import TextField from '../TextField';
import formField from '../FormField';
import parseMeta from '../FormField/parseMeta';
@@ -48,6 +49,7 @@ const AutoSuggest = ({
renderOption = defaultRender,
renderValue = defaultRender,
required,
+ usePortal,
validationEnabled = true,
value,
valueKey = 'value',
@@ -56,11 +58,11 @@ const AutoSuggest = ({
const textfield = useRef(null);
const testId = useProvidedIdOrCreate(id, 'autoSuggest-');
const [filterValue, setFilterValue] = useState(value);
+ const getPortalElement = useRef(() => document.getElementById('OverlayContainer')).current;
const filteredItems = useMemo(() => items
.filter(item => !filterValue || includeItem(item, filterValue)),
- [filterValue, items]
- );
+ [filterValue, items]);
const {
getInputProps,
@@ -82,6 +84,12 @@ const AutoSuggest = ({
initialInputValue: filterValue,
});
+ const popperProps = {
+ portal: (isOpen && usePortal) ? getPortalElement() : undefined,
+ modifiers: OVERLAY_MODIFIERS,
+ ...popper
+ };
+
const control = (
{isOpen
- ? filteredItems.map((item, index) => (
-
- {renderOption(item)}
-
- ))
+ ? filteredItems.map((item, index) => (
+
+ {renderOption(item)}
+
+ ))
: null}
);
@@ -149,7 +157,7 @@ const AutoSuggest = ({
return (
@@ -168,10 +176,13 @@ AutoSuggest.propTypes = {
onFocus: PropTypes.func,
onSelect: PropTypes.func,
placeholder: PropTypes.node,
- popper: PropTypes.object,
+ popper: PropTypes.shape({
+ portal: deprecated(PropTypes.element, 'use the boolean prop "usePortal" instead'),
+ }),
renderOption: PropTypes.func,
renderValue: PropTypes.func,
required: PropTypes.bool,
+ usePortal: PropTypes.bool,
validationEnabled: PropTypes.bool,
value: PropTypes.string,
valueKey: PropTypes.string,
diff --git a/lib/AutoSuggest/readme.md b/lib/AutoSuggest/readme.md
index 3cf08cd95..0e937249e 100644
--- a/lib/AutoSuggest/readme.md
+++ b/lib/AutoSuggest/readme.md
@@ -17,6 +17,9 @@ prop | description | default | required
`onChange` | Callback called when the value changes | |
`renderOption` | Callback that renders the item in the dropdown | `item => item.value` |
`renderValue` | Callback that render the item in the input field | `item => item.value` |
+`usePortal` | bool | If `true`, suggestion list will render to the `div[#OverlayContainer]` element in the FOLIO UI. | |
`valueKey` | The key in the item object to use as the value. | `"value"`
`withFinalForm` | toggle form time: true - final-form, false - redux-from (default) | |
`popper` | object | Used to adjust placement of options list overlay via underlying Popper component. [See `
` props](../Popper/readme.md) | |
+
+
diff --git a/lib/AutoSuggest/tests/AutoSuggest-test.js b/lib/AutoSuggest/tests/AutoSuggest-test.js
index 774a14e8e..149d186d6 100644
--- a/lib/AutoSuggest/tests/AutoSuggest-test.js
+++ b/lib/AutoSuggest/tests/AutoSuggest-test.js
@@ -1,7 +1,8 @@
import React from 'react';
import { describe, beforeEach, it } from 'mocha';
-import { AutoSuggest as Interactor, converge, runAxeTest } from '@folio/stripes-testing';
import sinon from 'sinon';
+import { AutoSuggest as Interactor, converge, runAxeTest, HTML } from '@folio/stripes-testing';
+import { RoledHTML } from '../../../tests/helpers/localInteractors';
import { mountWithContext } from '../../../tests/helpers';
import AutoSuggest from '../AutoSuggest';
@@ -109,4 +110,23 @@ describe('AutoSuggest', () => {
});
});
})
+
+ describe('usePortal prop', () => {
+ beforeEach(async () => {
+ await mountWithContext(
+ <>
+
+
+ >
+ );
+ await autosuggest.enterFilter('b');
+ });
+
+ it('renders menu to overlay element', () => HTML({ id: 'OverlayContainer' }).find(RoledHTML({ tagName:'UL' })).exists())
+ });
});
diff --git a/lib/MultiSelection/MultiSelectOptionsList.js b/lib/MultiSelection/MultiSelectOptionsList.js
index 5e86acfec..cc6da097c 100644
--- a/lib/MultiSelection/MultiSelectOptionsList.js
+++ b/lib/MultiSelection/MultiSelectOptionsList.js
@@ -16,8 +16,8 @@ const getListStyle = (atSmallMedia, maxHeight) => {
{ maxHeight: `${maxHeight}px` }
};
-const getPortal = (renderToOverlay) => {
- return renderToOverlay ? document.getElementById('OverlayContainer') : undefined;
+const getPortal = (renderToOverlay, usePortal) => {
+ return (renderToOverlay || usePortal) ? document.getElementById('OverlayContainer') : undefined;
}
const MultiSelectOptionsList = ({
@@ -38,6 +38,7 @@ const MultiSelectOptionsList = ({
renderFilterInput,
renderOptions,
renderToOverlay,
+ usePortal,
warning,
}) => {
const control = (
@@ -81,7 +82,7 @@ const MultiSelectOptionsList = ({
}}
modifiers={modifiers}
isOpen={isOpen}
- portal={getPortal(renderToOverlay)}
+ portal={getPortal(renderToOverlay, usePortal)}
placement="bottom-start"
hideIfClosed
>
@@ -113,6 +114,7 @@ MultiSelectOptionsList.propTypes = {
renderOptions: PropTypes.func,
renderToOverlay: PropTypes.bool,
useLegacy: PropTypes.bool,
+ usePortal: PropTypes.bool,
warning: PropTypes.node,
};
diff --git a/lib/MultiSelection/MultiSelection.js b/lib/MultiSelection/MultiSelection.js
index 34c318aac..f19a65c0d 100644
--- a/lib/MultiSelection/MultiSelection.js
+++ b/lib/MultiSelection/MultiSelection.js
@@ -1,14 +1,15 @@
import React, { useRef, useEffect, useState, useMemo, useCallback } from 'react';
import PropTypes from 'prop-types';
+import { deprecated } from 'prop-types-extra';
import classnames from 'classnames';
import {
isEqual,
noop,
debounce,
} from 'lodash';
-
import { useIntl } from 'react-intl';
import { useMultipleSelection, useCombobox } from 'downshift';
+import { OVERLAY_MODIFIERS } from '../Popper';
import SelectedValuesList from './SelectedValuesList';
import MultiSelectFilterField from './MultiSelectFilterField';
import MultiSelectResponsiveRenderer from './MultiSelectResponsiveRenderer';
@@ -88,10 +89,7 @@ const MultiSelection = ({
label: labelProp,
maxHeight = 168,
marginBottom0,
- modifiers = {
- flip: { boundariesElement: 'viewport', padding: 5 },
- preventOverflow: { boundariesElement: 'viewport', padding: 5 },
- },
+ modifiers = OVERLAY_MODIFIERS,
noBorder,
onAdd = noop,
onBlur = noop,
@@ -102,6 +100,7 @@ const MultiSelection = ({
renderToOverlay = false,
required = false,
showLoading,
+ usePortal,
validationEnabled,
validStylesEnabled = false,
value,
@@ -663,6 +662,7 @@ const MultiSelection = ({
warning={warning}
atSmallMedia={atSmallMedia}
modifiers={modifiers}
+ usePortal={usePortal}
/>
@@ -712,9 +712,10 @@ const propTypes = {
onFocus: PropTypes.func,
onRemove: PropTypes.func,
placeholder: PropTypes.string,
- renderToOverlay: PropTypes.bool,
+ renderToOverlay: deprecated(PropTypes.bool, 'use usePortal prop instead'),
required: PropTypes.bool,
showLoading: PropTypes.bool,
+ usePortal: PropTypes.bool,
validationEnabled: PropTypes.bool,
validStylesEnabled: PropTypes.bool,
value: PropTypes.oneOfType([
diff --git a/lib/MultiSelection/readme.md b/lib/MultiSelection/readme.md
index f058b83e2..0bc484799 100644
--- a/lib/MultiSelection/readme.md
+++ b/lib/MultiSelection/readme.md
@@ -41,8 +41,8 @@ Name | type | description | default | required
`onChange` | func | Change event handler for when internal state changes. `selectedItems` is passed as parameter to function. | |
`onRemove` | func | Event handler specifically called when an item is removed from the selection. The removed item is passed to the handler. | |
`placeholder` | string | Rendered as a placeholder for the control when no value is present. | |
-`renderToOverlay` | bool | For use in situations where the dropdown may be cut off due to a containing dom element's `overflow: hidden/auto` css attribute. | false |
`showLoading` | bool | Should render loading indicator on the field | |
+`usePortal` | bool | If `true`, option list will render to the `div[#OverlayContainer]` element in the FOLIO UI. | |
`value` | array | Array of selected objects. | |
`valueFormatter` | func | Render function that accepts an object with keys for the option. The function is called to display values in the selected values list. If the prop is missing, `formatter` will be used instead. | |
`ariaLabelledBy` | string | Used for applying an accessible label if no `label` prop is provided | |
diff --git a/lib/MultiSelection/tests/MultiSelection-test.js b/lib/MultiSelection/tests/MultiSelection-test.js
index 407c1cfa2..6021799b2 100644
--- a/lib/MultiSelection/tests/MultiSelection-test.js
+++ b/lib/MultiSelection/tests/MultiSelection-test.js
@@ -873,4 +873,20 @@ describe('MultiSelect', () => {
});
});
});
+
+ describe('usePortal prop', () => {
+ beforeEach(async () => {
+ await mountWithContext(
+
+ );
+ await multiselection.open();
+ });
+
+ it('renders options list to overlay element', () => HTML({ id: 'OverlayContainer' }).find(MenuInteractor()).exists());
+ })
});
diff --git a/lib/MultiSelection/tests/MultiSelectionHarness.js b/lib/MultiSelection/tests/MultiSelectionHarness.js
index 58ebbed29..597fcee79 100644
--- a/lib/MultiSelection/tests/MultiSelectionHarness.js
+++ b/lib/MultiSelection/tests/MultiSelectionHarness.js
@@ -14,6 +14,7 @@ const MultiSelectionHarness = ({
return (
<>
+
;
diff --git a/lib/Popper/Popper.js b/lib/Popper/Popper.js
index fcea7b500..1a87c88df 100644
--- a/lib/Popper/Popper.js
+++ b/lib/Popper/Popper.js
@@ -4,6 +4,11 @@ import PropTypes from 'prop-types';
import PopperJS from 'popper.js';
import css from './Popper.css';
+export const OVERLAY_MODIFIERS = {
+ flip: { boundariesElement: 'viewport', padding: 5 },
+ preventOverflow: { boundariesElement: 'viewport', padding: 5 },
+}
+
export const AVAILABLE_PLACEMENTS = [
'bottom',
'top',
diff --git a/lib/Popper/index.js b/lib/Popper/index.js
index a822eaf57..519b84812 100644
--- a/lib/Popper/index.js
+++ b/lib/Popper/index.js
@@ -1 +1 @@
-export { default, AVAILABLE_PLACEMENTS } from './Popper';
+export { default, AVAILABLE_PLACEMENTS, OVERLAY_MODIFIERS } from './Popper';
diff --git a/lib/Selection/Selection.css b/lib/Selection/Selection.css
index 01286c00c..df73dc993 100644
--- a/lib/Selection/Selection.css
+++ b/lib/Selection/Selection.css
@@ -9,6 +9,7 @@
background-color: #fff;
border: 1px solid #ccc;
box-shadow: var(--shadow);
+ pointer-events: all;
}
.selectionListStatus {
diff --git a/lib/Selection/Selection.js b/lib/Selection/Selection.js
index 815287ffd..ffd8cf9a3 100644
--- a/lib/Selection/Selection.js
+++ b/lib/Selection/Selection.js
@@ -1,5 +1,6 @@
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
import PropTypes from 'prop-types';
+import { deprecated } from 'prop-types-extra';
import { useIntl } from 'react-intl';
import { useCombobox } from 'downshift';
import classNames from 'classnames';
@@ -113,6 +114,7 @@ const Selection = ({
readonly,
required,
useValidStyle = false,
+ usePortal,
valid,
value,
warning,
@@ -259,6 +261,7 @@ const Selection = ({
key={`${item.label}-option-${i}`}
{...getItemProps({
index: reducedIndex,
+ onMouseUp: e => e.stopPropagation(),
})}
className={getItemClass(item, reducedIndex, { selectedItem, highlightedIndex, dataOptions })}
>
@@ -291,6 +294,11 @@ const Selection = ({
{...getInputProps({
ref: filterRef,
value: filterValue,
+ // stopPropagation to keep from unwantedly triggering shortcuts and
+ // address downshift issues with portal rendering/functionality.
+ // https://github.com/downshift-js/downshift/issues/287
+ onKeyDown: (e) => e.stopPropagation(),
+ onMouseUp: (e) => e.stopPropagation(),
})}
onClick={() => {}}
onChange={(e) => updateFilterValue(e.target.value)}
@@ -373,6 +381,7 @@ const Selection = ({
popper={popper}
renderFilterInput={renderFilterInput}
renderOptions={renderOptions}
+ usePortal={usePortal}
width={getControlWidth(controlRef.current)}
labelId={labelId}
/>
@@ -403,10 +412,13 @@ Selection.propTypes = {
onFocus: PropTypes.func,
optionAlignment: PropTypes.string,
placeholder: PropTypes.node,
- popper: PropTypes.object,
+ popper: PropTypes.shape({
+ portal: deprecated(PropTypes.element, 'use the boolean usePortal prop of Selection instead')
+ }),
readOnly: PropTypes.bool,
readonly: PropTypes.bool,
required: PropTypes.bool,
+ usePortal: PropTypes.bool,
useValidStyle: PropTypes.bool,
valid: PropTypes.bool,
value: PropTypes.string,
diff --git a/lib/Selection/SelectionOverlay.js b/lib/Selection/SelectionOverlay.js
index 58bbffdfd..fc860c430 100644
--- a/lib/Selection/SelectionOverlay.js
+++ b/lib/Selection/SelectionOverlay.js
@@ -2,7 +2,7 @@ import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import Portal from 'react-overlays/Portal';
import SelectionList from './SelectionList';
-import Popper from '../Popper';
+import Popper, { OVERLAY_MODIFIERS } from '../Popper';
import css from './Selection.css';
@@ -14,13 +14,16 @@ const SelectionOverlay = ({
listMaxHeight,
onChangeFilterValue,
optionAlignment,
+ popper,
renderFilterInput,
renderOptions,
+ usePortal,
width,
...props
}) => {
const containerRef = useRef(null);
const filterRef = useRef(null);
+ const getPortalElement = useRef(() => document.getElementById('OverlayContainer')).current;
useEffect(() => {
// if the overlay is open and focus is outside of it, move focus to the filter.
@@ -56,7 +59,7 @@ const SelectionOverlay = ({
return (
{isOpen &&
-
+
` props](../Popper/readme.md) | | false |
+`usePortal` | bool | If `true`, option list will render to the `div[#OverlayContainer]` element in the FOLIO UI. | |
+
## Labeling
Like other form controls in stripes-components, `
` abides by standard conventions for labeling props if alternatives to `label` (visible label with the control) are required... `aria-label` and `aria-labelledby` are useful for this. See [Accessiblity for developers documentation](https://github.com/folio-org/stripes-components/blob/master/guides/AccessibilityDevPrimer.stories.mdx#labeling) for more details about which to choose.
diff --git a/lib/Selection/tests/Selection-test.js b/lib/Selection/tests/Selection-test.js
index 6f27c1b97..4d989d849 100644
--- a/lib/Selection/tests/Selection-test.js
+++ b/lib/Selection/tests/Selection-test.js
@@ -715,4 +715,19 @@ describe('Selection', () => {
it('displays the value', () => selection.has({ singleValue: 'Option 2' }))
});
});
+
+ describe('usePortal prop', () => {
+ beforeEach(async () => {
+ await mountWithContext(
+
+ );
+ await selection.open();
+ });
+
+ it('renders overlay within portal element', () => HTML({ id: 'OverlayContainer' }).find(SelectListInteractor()).exists());
+ });
});
diff --git a/lib/Selection/tests/SingleSelectionHarness.js b/lib/Selection/tests/SingleSelectionHarness.js
index 513acbf48..1995e3f32 100644
--- a/lib/Selection/tests/SingleSelectionHarness.js
+++ b/lib/Selection/tests/SingleSelectionHarness.js
@@ -4,26 +4,27 @@ import Button from '../../Button/Button';
const SingleSelectionHarness = ({
initValue,
- label,
options: optionsProp,
delayedOptions = [],
onChange = (fn, val) => { fn(val) },
forcedValue,
+ ...props
}) => {
const [fieldVal, setFieldVal] = useState(initValue);
const [options, updateOptions] = useState(optionsProp);
const [increment, updateIncrement] = useState(0);
return (
<>
+
;
{ onChange(setFieldVal, val) }}
+ {...props}
/>
>
);