Skip to content

Commit 944ef65

Browse files
committed
chore: use intersection observer to place bulk actions bar correctly in the DOM
1 parent e51287d commit 944ef65

File tree

14 files changed

+202
-74
lines changed

14 files changed

+202
-74
lines changed

polaris-react/src/components/BulkActions/BulkActions.tsx

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {debounce} from '../../utilities/debounce';
55
import {classNames} from '../../utilities/css';
66
import {useI18n} from '../../utilities/i18n';
77
import {clamp} from '../../utilities/clamp';
8-
import {findFirstKeyboardFocusableNode} from '../../utilities/focus';
98
import type {
109
BadgeAction,
1110
DisableableAction,
@@ -64,7 +63,6 @@ class BulkActionsInner extends PureComponent<CombinedProps, State> {
6463
private containerNode: HTMLElement | null = null;
6564
private buttonsNode: HTMLElement | null = null;
6665
private moreActionsNode: HTMLElement | null = null;
67-
private activatorNode: Element | null = null;
6866
private groupNode = createRef<HTMLDivElement>();
6967
private promotedActionsWidths: number[] = [];
7068
private bulkActionsWidth = 0;
@@ -91,25 +89,6 @@ class BulkActionsInner extends PureComponent<CombinedProps, State> {
9189
{trailing: true},
9290
);
9391

94-
private focusContent() {
95-
if (this.containerNode == null) {
96-
return;
97-
}
98-
99-
requestAnimationFrame(() => {
100-
if (this.containerNode == null) {
101-
return;
102-
}
103-
104-
const focusableChild = findFirstKeyboardFocusableNode(this.containerNode);
105-
if (focusableChild) {
106-
focusableChild.focus({
107-
preventScroll: process.env.NODE_ENV === 'development',
108-
});
109-
}
110-
});
111-
}
112-
11392
private numberOfPromotedActionsToRender(): number {
11493
const {promotedActions} = this.props;
11594
const {containerWidth, measuring} = this.state;
@@ -199,11 +178,6 @@ class BulkActionsInner extends PureComponent<CombinedProps, State> {
199178
this.addedMoreActionsWidthForMeasuring
200179
: 0;
201180

202-
if (document?.activeElement) {
203-
this.activatorNode = document.activeElement;
204-
}
205-
this.focusContent();
206-
207181
if (this.containerNode) {
208182
this.setState({
209183
containerWidth: this.containerNode.getBoundingClientRect().width,
@@ -347,11 +321,7 @@ class BulkActionsInner extends PureComponent<CombinedProps, State> {
347321
</Transition>
348322
);
349323

350-
return (
351-
<div ref={this.setContainerNode} onBlur={this.handleBlur}>
352-
{group}
353-
</div>
354-
);
324+
return <div ref={this.setContainerNode}>{group}</div>;
355325
}
356326

357327
private isNewBadgeInBadgeActions() {
@@ -395,18 +365,6 @@ class BulkActionsInner extends PureComponent<CombinedProps, State> {
395365
this.promotedActionsWidths.push(width);
396366
}
397367
};
398-
399-
private handleBlur = (event: React.FocusEvent<HTMLDivElement>) => {
400-
const currentTarget = event.currentTarget;
401-
402-
// Give browser time to focus the next element
403-
requestAnimationFrame(() => {
404-
// Check if the new focused element is a child of the original container
405-
if (!currentTarget.contains(document.activeElement)) {
406-
console.log('blurrin yo');
407-
}
408-
});
409-
};
410368
}
411369

412370
function instanceOfBulkActionListSectionArray(

polaris-react/src/components/IndexTable/IndexTable.scss

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
}
1515

1616
.IndexTableWithBulkActions {
17-
padding-bottom: var(--p-space-4);
17+
--pc-index-table-bulk-actions-offset: 100px;
18+
padding-bottom: var(--pc-index-table-bulk-actions-offset);
1819
}
1920

2021
.LoadingContainer-enter {
@@ -472,17 +473,22 @@ $loading-panel-height: 53px;
472473

473474
.BulkActionsWrapper {
474475
visibility: visible;
475-
position: sticky;
476+
position: absolute;
476477
z-index: var(--pc-index-table-bulk-actions);
477478
left: 0;
478-
bottom: var(--p-space-4);
479479
width: 100%;
480480
display: flex;
481481
align-items: center;
482482
justify-content: center;
483483
margin-top: var(--p-space-4);
484484
}
485485

486+
.BulkActionsWrapperSticky {
487+
position: fixed;
488+
top: auto;
489+
bottom: var(--p-space-4);
490+
}
491+
486492
.SelectAllActionsWrapper {
487493
visibility: visible;
488494
position: relative;

polaris-react/src/components/IndexTable/IndexTable.stories.tsx

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -565,27 +565,29 @@ export function WithBulkActionsAndSelectionAcrossPages() {
565565
);
566566

567567
return (
568-
<Card>
569-
<IndexTable
570-
resourceName={resourceName}
571-
itemCount={customers.length}
572-
selectedItemsCount={
573-
allResourcesSelected ? 'All' : selectedResources.length
574-
}
575-
onSelectionChange={handleSelectionChange}
576-
hasMoreItems
577-
bulkActions={bulkActions}
578-
promotedBulkActions={promotedBulkActions}
579-
headings={[
580-
{title: 'Name'},
581-
{title: 'Location'},
582-
{title: 'Order count'},
583-
{title: 'Amount spent'},
584-
]}
585-
>
586-
{rowMarkup}
587-
</IndexTable>
588-
</Card>
568+
<div style={{padding: 'var(--p-space-4)'}}>
569+
<Card>
570+
<IndexTable
571+
resourceName={resourceName}
572+
itemCount={customers.length}
573+
selectedItemsCount={
574+
allResourcesSelected ? 'All' : selectedResources.length
575+
}
576+
onSelectionChange={handleSelectionChange}
577+
hasMoreItems
578+
bulkActions={bulkActions}
579+
promotedBulkActions={promotedBulkActions}
580+
headings={[
581+
{title: 'Name'},
582+
{title: 'Location'},
583+
{title: 'Order count'},
584+
{title: 'Amount spent'},
585+
]}
586+
>
587+
{rowMarkup}
588+
</IndexTable>
589+
</Card>
590+
</div>
589591
);
590592
}
591593

polaris-react/src/components/IndexTable/IndexTable.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import type {NonEmptyArray} from '../../types';
3838

3939
import {getTableHeadingsBySelector} from './utilities';
4040
import {ScrollContainer, Cell, Row} from './components';
41+
import {useIsBulkActionsSticky} from './hooks/use-is-bulk-actions-sticky';
4142
import styles from './IndexTable.scss';
4243

4344
interface IndexTableHeadingBase {
@@ -154,6 +155,12 @@ function IndexTableBase({
154155
const scrollContainerElement = useRef<HTMLDivElement>(null);
155156
const scrollingWithBar = useRef(false);
156157
const scrollingContainer = useRef(false);
158+
const {
159+
bulkActionsIntersectionRef,
160+
tableMeasurerRef,
161+
isBulkActionsSticky,
162+
bulkActionsAbsoluteOffset,
163+
} = useIsBulkActionsSticky();
157164

158165
const tableBodyRef = useCallback(
159166
(node) => {
@@ -533,14 +540,23 @@ function IndexTableBase({
533540
const shouldShowBulkActions =
534541
(bulkActionsSelectable && selectedItemsCount) || isSmallScreenSelectable;
535542

536-
const bulkActionClassNames = classNames(styles.BulkActionsWrapper);
543+
const bulkActionClassNames = classNames(
544+
styles.BulkActionsWrapper,
545+
isBulkActionsSticky && styles.BulkActionsWrapperSticky,
546+
);
537547

538548
const shouldShowActions = !condensed || selectedItemsCount;
539549
const promotedActions = shouldShowActions ? promotedBulkActions : [];
540550
const actions = shouldShowActions ? bulkActions : [];
541551

542552
const bulkActionsMarkup = shouldShowBulkActions ? (
543-
<div className={bulkActionClassNames} data-condensed={condensed}>
553+
<div
554+
className={bulkActionClassNames}
555+
data-condensed={condensed}
556+
style={{
557+
top: isBulkActionsSticky ? undefined : bulkActionsAbsoluteOffset,
558+
}}
559+
>
544560
{loadingMarkup}
545561

546562
<BulkActions
@@ -632,6 +648,7 @@ function IndexTableBase({
632648
return stickyContent;
633649
}}
634650
</Sticky>
651+
{bulkActionsMarkup}
635652
</div>
636653
);
637654

@@ -739,11 +756,11 @@ function IndexTableBase({
739756
return (
740757
<>
741758
<div className={styles.IndexTable}>
742-
<div className={bulkActionsWrapperClassNames}>
759+
<div className={bulkActionsWrapperClassNames} ref={tableMeasurerRef}>
743760
{!shouldShowBulkActions && !condensed && loadingMarkup}
744761
{tableContentMarkup}
745-
{bulkActionsMarkup}
746762
</div>
763+
<div ref={bulkActionsIntersectionRef} />
747764
</div>
748765
{scrollBarMarkup}
749766
</>

polaris-react/src/components/IndexTable/components/Cell/tests/Cell.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ import {mountWithApp} from 'tests/utilities';
33

44
import {Cell} from '../Cell';
55

6+
jest.mock('../../../hooks/use-is-bulk-actions-sticky', () => ({
7+
useIsBulkActionsSticky: () => ({
8+
bulkActionsIntersectionRef: null,
9+
tableMeasurerRef: null,
10+
isBulkActionsSticky: false,
11+
bulkActionsAbsoluteOffset: 0,
12+
}),
13+
}));
14+
615
describe('<Cell />', () => {
716
it('renders a table data tag', () => {
817
const cell = mountWithTable(<Cell />);

polaris-react/src/components/IndexTable/components/Checkbox/tests/Checkbox.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ jest.mock('../../../../../utilities/debounce', () => ({
1717
debounce: (callback: () => void) => () => callback(),
1818
}));
1919

20+
jest.mock('../../../hooks/use-is-bulk-actions-sticky', () => ({
21+
useIsBulkActionsSticky: () => ({
22+
bulkActionsIntersectionRef: null,
23+
tableMeasurerRef: null,
24+
isBulkActionsSticky: false,
25+
bulkActionsAbsoluteOffset: 0,
26+
}),
27+
}));
28+
2029
describe('<Checkbox />', () => {
2130
let getBoundingClientRectSpy: jest.SpyInstance;
2231
let setRootPropertySpy: jest.SpyInstance;

polaris-react/src/components/IndexTable/components/Row/tests/Row.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ import {Checkbox} from '../../Checkbox';
99
import {Button} from '../../../../Button';
1010
import {Link} from '../../../../Link';
1111

12+
jest.mock('../../../hooks/use-is-bulk-actions-sticky', () => ({
13+
useIsBulkActionsSticky: () => ({
14+
bulkActionsIntersectionRef: null,
15+
tableMeasurerRef: null,
16+
isBulkActionsSticky: false,
17+
bulkActionsAbsoluteOffset: 0,
18+
}),
19+
}));
20+
1221
const defaultEvent = {
1322
preventDefault: noop,
1423
stopPropagation: noop,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {useIsBulkActionsSticky} from './use-is-bulk-actions-sticky';
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {useEffect, useRef, useState} from 'react';
2+
3+
import {debounce} from '../../../utilities/debounce';
4+
5+
const DEBOUNCE_PERIOD = 250;
6+
7+
export function useIsBulkActionsSticky() {
8+
const [isBulkActionsSticky, setIsSticky] = useState(false);
9+
const [bulkActionsAbsoluteOffset, setBulkActionsAbsoluteOffset] = useState(0);
10+
const bulkActionsIntersectionRef = useRef<HTMLDivElement>(null);
11+
const tableMeasurerRef = useRef<HTMLDivElement>(null);
12+
13+
useEffect(() => {
14+
function computeTableHeight() {
15+
const node = tableMeasurerRef.current;
16+
if (!node) {
17+
return 0;
18+
}
19+
return node.getBoundingClientRect().height;
20+
}
21+
const tableHeight = computeTableHeight();
22+
23+
const debouncedComputeTableHeight = debounce(
24+
computeTableHeight,
25+
DEBOUNCE_PERIOD,
26+
{
27+
trailing: true,
28+
},
29+
);
30+
31+
setBulkActionsAbsoluteOffset(tableHeight);
32+
33+
window.addEventListener('resize', debouncedComputeTableHeight);
34+
35+
return () =>
36+
window.removeEventListener('resize', debouncedComputeTableHeight);
37+
}, [tableMeasurerRef]);
38+
39+
useEffect(() => {
40+
const handleIntersect = (entries: IntersectionObserverEntry[]) => {
41+
entries.forEach((entry: IntersectionObserverEntry) => {
42+
setIsSticky(!entry.isIntersecting);
43+
});
44+
};
45+
46+
const options = {
47+
root: null,
48+
rootMargin: '0px',
49+
threshold: 1,
50+
};
51+
const observer = new IntersectionObserver(handleIntersect, options);
52+
53+
const node = bulkActionsIntersectionRef.current;
54+
55+
if (node) {
56+
observer.observe(node);
57+
}
58+
59+
return () => {
60+
observer.disconnect();
61+
};
62+
}, [bulkActionsIntersectionRef]);
63+
64+
return {
65+
bulkActionsIntersectionRef,
66+
tableMeasurerRef,
67+
isBulkActionsSticky,
68+
bulkActionsAbsoluteOffset,
69+
};
70+
}

polaris-react/src/components/IndexTable/tests/IndexTable.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ jest.mock('../../../utilities/debounce', () => ({
3434
},
3535
}));
3636

37+
jest.mock('../hooks/use-is-bulk-actions-sticky', () => ({
38+
useIsBulkActionsSticky: () => ({
39+
bulkActionsIntersectionRef: null,
40+
tableMeasurerRef: null,
41+
isBulkActionsSticky: false,
42+
bulkActionsAbsoluteOffset: 0,
43+
}),
44+
}));
45+
3746
const mockTableItems = [
3847
{
3948
id: 'item-1',

0 commit comments

Comments
 (0)