Skip to content

Commit

Permalink
STCOM-1335 add StripesOverlayContext to Modal, MCL... implement in …
Browse files Browse the repository at this point in the history
…Popper (#2351)

* add StripesOverlayContext to Modal, MCL... implement in Popper

* move useOverlayContainer to hooks dir

* modify getHookExecutionResults, add tests for useOverlayContainer

* don't replace getHookExecutionResults

* include tests for null initial results and for refresh() on useOverlayContainer

* remove only

* just a little more coverage...

* STCOM-1335

* fix falsy initial result test

* log changes

* use isEqual for default HookExecResult comparator

* close MCL wrapping div for shortcut keys

* rollback, re-wrap in overlayContext, have non-findDOMNode cake and eat it.
  • Loading branch information
JohnC-80 authored Sep 30, 2024
1 parent 5792577 commit 19765dd
Show file tree
Hide file tree
Showing 10 changed files with 316 additions and 83 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
* Apply `inert` attribute to header and siblings of `div#OverlayContainer` when modals are open. Refs STCOM-1334.
* Expand focus trapping of modal to the `div#OverlayContainer` so that overlay components can function within `<Modal>` using the `usePortal` prop. Refs STCOM-1334.
* Render string for `FilterGroups` clear button. Refs STCOM-1337.
* Add OverlayContext for Overlay-style components rendered within Modals and MCL's. Refs STCOM-1335.
* Refactored away from `findDOMNode` in codebase for React 19 preparation. Refs STCOM-1343.
* AdvancedSearch - added a wrapping div to ref for a HotKeys ref. Refs STCOM-1343.
* `<MultiColumnList>` components `<CellMeasurer>` and `<RowMeasurer>` updated to use refs vs `findDOMNode`. Refs STCOM-1343.
Expand Down
140 changes: 140 additions & 0 deletions hooks/tests/useOverlayContainer-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import {
describe,
beforeEach,
it,
} from 'mocha';
import { converge } from '@folio/stripes-testing';
import { getHookExecutionHarness } from '../../tests/helpers/getHookExecutionResult';
import useOverlayContainer from '../useOverlayContainer';

import { OVERLAY_CONTAINER_ID } from '../../util/consts';

const Harness = ({ children }) => (
<div id="rootChild">
<div id="OverlayContainer" />
{children}
</div>
);

const HarnessWithout = ({ children }) => (
<div id="rootChild">
{children}
</div>
);

// We need to compare the `element` field of the result to discern difference on the re-render.
const areDomNodesEqual = (current, candidate) => {
return current?.element === candidate?.element;
}

describe('useOverlayContainer', () => {
const overlayElement = () => document.getElementById(OVERLAY_CONTAINER_ID);
let res;
beforeEach(() => {
res = null;
});

describe('withoutOverlay fallback to a child of root.', () => {
beforeEach(async () => {
await getHookExecutionHarness(
useOverlayContainer,
[overlayElement()],
HarnessWithout,
(result) => { res = result.element },
areDomNodesEqual
);
});

it('should fallback to child of root element',
() => converge(() => {
if (res.parentNode?.id !== 'root') throw new Error(`expected child of root: ${res.id}`);
}));
});

describe('successfully resolving overlay container if it exists...', () => {
beforeEach(async () => {
await getHookExecutionHarness(
useOverlayContainer,
[overlayElement()],
Harness,
(result) => { res = result.element },
areDomNodesEqual
);
});

it('div#OverlayContainer in place, it should return it',
() => converge(() => {
if (res.id !== OVERLAY_CONTAINER_ID) throw new Error(`expected element to fallback to body: ${res.id}`);
}));
});

describe('successfully initial null result...', () => {
beforeEach(async () => {
await getHookExecutionHarness(
useOverlayContainer,
[null],
Harness,
(result) => { res = result.element },
areDomNodesEqual
);
});

it('div#OverlayContainer in place, it should return it',
() => converge(() => {
if (res.id !== OVERLAY_CONTAINER_ID) throw new Error(`expected element to fallback to body: ${res.id}`);
}));
});

describe('successfully initial result...', () => {
beforeEach(async () => {
await getHookExecutionHarness(
useOverlayContainer,
['true'],
Harness,
(result) => { res = result.element },
areDomNodesEqual
);
});

it('div#OverlayContainer in place, it should return it',
() => converge(() => {
if (res !== 'true') throw new Error('should just return truthy value');
}));
});

describe('falsy initial result...', () => {
beforeEach(async () => {
res = await getHookExecutionHarness(
useOverlayContainer,
[false],
Harness,
(result) => { res = result.element },
areDomNodesEqual
);
res.refresh();
});

it('div#OverlayContainer in place, it should return it',
() => converge(() => {
if (res.element !== false) throw new Error('should just return false value');
}));
});

describe('refresh', () => {
beforeEach(async () => {
res = await getHookExecutionHarness(
useOverlayContainer,
[null],
Harness,
(result) => { res = result },
areDomNodesEqual
);
res.refresh();
});

it('div#OverlayContainer in place, it should return it',
() => converge(() => {
if (res.element.id !== OVERLAY_CONTAINER_ID) throw new Error(`expected element to fallback to body: ${res.id}`);
}));
});
});
1 change: 1 addition & 0 deletions hooks/useOverlayContainer/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './useOverlayContainer';
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const resolveElement = (ref) => {
return el;
}
return ref;
}
};

export default (ref) => {
const [element, setElement] = useState(resolveElement(ref));
Expand All @@ -39,10 +39,10 @@ export default (ref) => {
const el = resolveElement(ref);
if (!el) setElement(el);
}
}
};

return {
element,
refresh
}
};
};
2 changes: 1 addition & 1 deletion lib/Modal/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
import { Transition, TransitionGroup } from 'react-transition-group';
import { uniqueId, noop } from 'lodash';
import { getFirstFocusable } from '../../util/getFocusableElements';
import useOverlayContainer from './useOverlayContainer';
import useOverlayContainer from '../../hooks/useOverlayContainer';
import WrappingElement from './WrappingElement';
import { OVERLAY_CONTAINER_ID } from '../../util/consts';
import IconButton from '../IconButton';
Expand Down
7 changes: 5 additions & 2 deletions lib/Modal/WrappingElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import PropTypes from 'prop-types';
import * as focusTrap from 'focus-trap';
import { listen } from '../../util/listen';
import StripesOverlayContext from '../../util/StripesOverlayContext';
import { OVERLAY_CONTAINER_SELECTOR } from '../../util/consts';
import calloutCSS from '../Callout/Callout.css';
import overlayCSS from '../Popper/Popper.css';
Expand Down Expand Up @@ -154,9 +155,11 @@ const WrappingElement = forwardRef(({
ref={wrappingElementRef}
{...rest}
>
{children}
<StripesOverlayContext.Provider value={{ usePortal: true }}>
{children}
</StripesOverlayContext.Provider>
</WrappingElementComponent>
)
);
});

WrappingElement.propTypes = {
Expand Down
151 changes: 77 additions & 74 deletions lib/MultiColumnList/MCLRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import memoizeOne from 'memoize-one';

import Icon from '../Icon';
import EmptyMessage from '../EmptyMessage';
import StripesOverlayContext from '../../util/StripesOverlayContext';
import { HotKeys } from '../HotKeys';
import SRStatus from '../SRStatus';
import css from './MCLRenderer.css';
Expand Down Expand Up @@ -1930,88 +1931,90 @@ class MCLRenderer extends React.Component {
: css.mclRowContainer;

return (
<HotKeys handlers={this.handlers} attach={this.shortcutsRef} noWrapper>
<div className={css.mclContainer} ref={this.shortcutsRef} style={this.getOuterElementStyle()}>
<SRStatus ref={this.status} />
<div
tabIndex="0" // eslint-disable-line jsx-a11y/no-noninteractive-tabindex
id={this.props.id}
ref={this.container}
role="grid"
aria-rowcount={this.getAccessibleRowCount()}
className={classnames({ [css.hasMargin]: hasMargin })}
data-total-count={totalCount}
>
<div ref={this.headerContainer} className={css.mclHeaderContainer}>
<div
className={this.getHeaderStyle()}
style={{ display: 'flex' }}
ref={this.headerRow}
role="row"
aria-rowindex="1"
>
{renderedHeaders}
</div>
</div>
<StripesOverlayContext.Provider value={{ usePortal: true }}>
<HotKeys handlers={this.handlers} attach={this.shortcutsRef} noWrapper>
<div className={css.mclContainer} ref={this.shortcutsRef} style={this.getOuterElementStyle()}>
<SRStatus ref={this.status} />
<div
className={css.mclScrollable}
style={this.getScrollableStyle()}
onScroll={this.handleScroll}
ref={this.scrollContainer}
{...getScrollableTabIndex(this.scrollContainer)}
tabIndex="0" // eslint-disable-line jsx-a11y/no-noninteractive-tabindex
id={this.props.id}
ref={this.container}
role="grid"
aria-rowcount={this.getAccessibleRowCount()}
className={classnames({ [css.hasMargin]: hasMargin })}
data-total-count={totalCount}
>
<div ref={this.headerContainer} className={css.mclHeaderContainer}>
<div
className={this.getHeaderStyle()}
style={{ display: 'flex' }}
ref={this.headerRow}
role="row"
aria-rowindex="1"
>
{renderedHeaders}
</div>
</div>
<div
className={rowContainerClass}
role="rowgroup"
style={this.getRowContainerStyle()}
ref={this.rowContainer}
className={css.mclScrollable}
style={this.getScrollableStyle()}
onScroll={this.handleScroll}
ref={this.scrollContainer}
{...getScrollableTabIndex(this.scrollContainer)}
>
{renderedRows}
{dndProvided.placeholder}
<div
className={rowContainerClass}
role="rowgroup"
style={this.getRowContainerStyle()}
ref={this.rowContainer}
>
{renderedRows}
{dndProvided.placeholder}
</div>
</div>
{
loading &&
<div className={css.mclContentLoadingRow}>
<div className={css.mclContentLoading}>
<Icon icon="spinner-ellipsis" width="35px" />
</div>
</div>
}
</div>
{
loading &&
<div className={css.mclContentLoadingRow}>
<div className={css.mclContentLoading}>
<Icon icon="spinner-ellipsis" width="35px" />
</div>
</div>
}
pagingType === pagingTypes.PREV_NEXT && (
<PrevNextPaginationRow
activeNext={this.getCanGoNext()}
activePrevious={this.getCanGoPrevious()}
dataEndReached={dataEndReached}
handleLoadMore={this.handleLoadMore}
id={id}
key="mcl-prev-next-pagination-row"
keyId={this.keyId}
loading={loadingState}
loadingNext={pagingCanGoNextLoading}
loadingPrevious={pagingCanGoPreviousLoading}
pageAmount={pageAmount}
pagingButtonLabel={pagingButtonLabel}
pagingButtonRef={this.pageButton}
positionCache={this.positionCache}
rowHeightCache={this.rowHeightCache}
rowIndex={lastIndex}
sendMessage={this.sendMessage}
setFocusIndex={this.setFocusIndex}
staticBody={staticBody}
virtualize={virtualize}
dataStartIndex={typeof pagingOffset !== 'undefined' ? pagingOffset + 1 : dataStartIndex + 1}
dataEndIndex={typeof pagingOffset !== 'undefined' ? pagingOffset + dataEndIndex + 1 : dataEndIndex + 1} // eslint-disable-line
hidePageIndices={hidePageIndices}
pagingOffset={pagingOffset}
containerRef={this.paginationContainer}
/>
)
}
</div>
{
pagingType === pagingTypes.PREV_NEXT && (
<PrevNextPaginationRow
activeNext={this.getCanGoNext()}
activePrevious={this.getCanGoPrevious()}
dataEndReached={dataEndReached}
handleLoadMore={this.handleLoadMore}
id={id}
key="mcl-prev-next-pagination-row"
keyId={this.keyId}
loading={loadingState}
loadingNext={pagingCanGoNextLoading}
loadingPrevious={pagingCanGoPreviousLoading}
pageAmount={pageAmount}
pagingButtonLabel={pagingButtonLabel}
pagingButtonRef={this.pageButton}
positionCache={this.positionCache}
rowHeightCache={this.rowHeightCache}
rowIndex={lastIndex}
sendMessage={this.sendMessage}
setFocusIndex={this.setFocusIndex}
staticBody={staticBody}
virtualize={virtualize}
dataStartIndex={typeof pagingOffset !== 'undefined' ? pagingOffset + 1 : dataStartIndex + 1}
dataEndIndex={typeof pagingOffset !== 'undefined' ? pagingOffset + dataEndIndex + 1 : dataEndIndex + 1} // eslint-disable-line
hidePageIndices={hidePageIndices}
pagingOffset={pagingOffset}
containerRef={this.paginationContainer}
/>
)
}
</div>
</HotKeys>
</HotKeys>
</StripesOverlayContext.Provider>
);
}
}
Expand Down
Loading

0 comments on commit 19765dd

Please sign in to comment.