diff --git a/.changeset/smart-badgers-yawn.md b/.changeset/smart-badgers-yawn.md
new file mode 100644
index 00000000000..20f343e39bb
--- /dev/null
+++ b/.changeset/smart-badgers-yawn.md
@@ -0,0 +1,5 @@
+---
+'@shopify/polaris': patch
+---
+
+Fixed the position of `SelectAllActions` when inside of an offset scrollable container
diff --git a/polaris-react/src/components/IndexTable/IndexTable.stories.tsx b/polaris-react/src/components/IndexTable/IndexTable.stories.tsx
index 7c49341a7c3..8b6b9aa8b2c 100644
--- a/polaris-react/src/components/IndexTable/IndexTable.stories.tsx
+++ b/polaris-react/src/components/IndexTable/IndexTable.stories.tsx
@@ -6,6 +6,10 @@ import type {
IndexTableRowProps,
} from '@shopify/polaris';
import {
+ Frame,
+ Button,
+ Modal,
+ Scrollable,
useBreakpoints,
InlineStack,
LegacyCard,
@@ -6776,3 +6780,123 @@ export function WithLongDataSetSelectable() {
);
}
+
+export function WithinAModal() {
+ const [active, setActive] = useState(true);
+ const [checked, setChecked] = useState(false);
+
+ const toggleActive = useCallback(() => setActive((active) => !active), []);
+
+ const handleCheckbox = useCallback((value) => setChecked(value), []);
+
+ const activator = ;
+
+ const orders = Array.from(Array(100).keys()).map((i) => ({
+ id: `${i}`,
+ order: i,
+ date: 'Jul 20 at 4:34pm',
+ customer: 'Jaydon Stanton',
+ total: `$969.44${i}`,
+ paymentStatus: Paid,
+ fulfillmentStatus: Unfulfilled,
+ }));
+
+ const resourceName = {
+ singular: 'order',
+ plural: 'orders',
+ };
+
+ const {selectedResources, allResourcesSelected, handleSelectionChange} =
+ useIndexResourceState(orders);
+
+ const rowMarkup = orders.map(
+ (
+ {id, order, date, customer, total, paymentStatus, fulfillmentStatus},
+ index,
+ ) => (
+
+
+
+ {order}
+
+
+ {date}
+ {customer}
+ {total}
+ {paymentStatus}
+ {fulfillmentStatus}
+
+ ),
+ );
+
+ const bulkActions = [
+ {
+ content: 'Add tags',
+ onAction: () => console.log('Todo: implement bulk add tags'),
+ },
+ {
+ content: 'Remove tags',
+ onAction: () => console.log('Todo: implement bulk remove tags'),
+ },
+ {
+ content: 'Delete customers',
+ onAction: () => console.log('Todo: implement bulk delete'),
+ },
+ ];
+
+ const table = (
+
+ {rowMarkup}
+
+ );
+
+ return (
+
+
+
+
+ {table}
+
+
+
+
+ );
+}
diff --git a/polaris-react/src/components/IndexTable/IndexTable.tsx b/polaris-react/src/components/IndexTable/IndexTable.tsx
index 7a318508c21..4b36c9e17fc 100644
--- a/polaris-react/src/components/IndexTable/IndexTable.tsx
+++ b/polaris-react/src/components/IndexTable/IndexTable.tsx
@@ -225,6 +225,7 @@ function IndexTableBase({
selectAllActionsAbsoluteOffset,
selectAllActionsMaxWidth,
selectAllActionsOffsetLeft,
+ selectAllActionsOffsetBottom,
computeTableDimensions,
isScrolledPastTop,
selectAllActionsPastTopOffset,
@@ -657,6 +658,9 @@ function IndexTableBase({
', () => {
selectAllActionsAbsoluteOffset: 0,
selectAllActionsMaxWidth: 0,
selectAllActionsOffsetLeft: 0,
+ selectAllActionsOffsetBottom: 0,
computeTableDimensions: jest.fn(),
isScrolledPastTop: false,
scrollbarPastTopOffset: 0,
@@ -790,6 +791,7 @@ describe('
', () => {
selectAllActionsAbsoluteOffset: 0,
selectAllActionsMaxWidth: 0,
selectAllActionsOffsetLeft: 0,
+ selectAllActionsOffsetBottom: 0,
computeTableDimensions,
isScrolledPastTop: false,
scrollbarPastTopOffset: 0,
diff --git a/polaris-react/src/components/ResourceList/ResourceList.stories.tsx b/polaris-react/src/components/ResourceList/ResourceList.stories.tsx
index fd48cb2a1bc..cb9d2e8a188 100644
--- a/polaris-react/src/components/ResourceList/ResourceList.stories.tsx
+++ b/polaris-react/src/components/ResourceList/ResourceList.stories.tsx
@@ -15,6 +15,9 @@ import {
BlockStack,
Box,
InlineStack,
+ Frame,
+ Modal,
+ Scrollable,
} from '@shopify/polaris';
export default {
@@ -1310,7 +1313,7 @@ export function WithPagination() {
}
export function WithBulkActionsAndPagination() {
- const [selectedItems, setSelectedItems] = useState([]);
+ const [selectedItems, setSelectedItems] = useState([]);
const resourceName = {
singular: 'customer',
@@ -1386,3 +1389,110 @@ export function WithBulkActionsAndPagination() {
);
}
+
+export function WithinAModal() {
+ const [active, setActive] = useState(true);
+ const [checked, setChecked] = useState(false);
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ const toggleActive = () => setActive((active) => !active);
+
+ const activator = ;
+
+ const resourceName = {
+ singular: 'customer',
+ plural: 'customers',
+ };
+
+ const items = Array.from({length: 50}, (_, num) => {
+ return {
+ id: `${num}`,
+ url: '#',
+ name: `Mae Jemison ${num}`,
+ location: 'Decatur, USA',
+ orders: 20,
+ amountSpent: '$24,00',
+ };
+ });
+
+ const promotedBulkActions = [
+ {
+ content: 'Edit customers',
+ onAction: () => console.log('Todo: implement bulk edit'),
+ },
+ ];
+
+ const bulkActions = [
+ {
+ content: 'Add tags',
+ onAction: () => console.log('Todo: implement bulk add tags'),
+ },
+ {
+ content: 'Remove tags',
+ onAction: () => console.log('Todo: implement bulk remove tags'),
+ },
+ {
+ content: 'Delete customers',
+ onAction: () => console.log('Todo: implement bulk delete'),
+ },
+ ];
+
+ const listMarkup = (
+ {
+ const {id, url, name, location} = item;
+ const media = ;
+
+ return (
+
+
+
+ {name}
+
+
+ {location}
+
+ );
+ }}
+ />
+ );
+
+ return (
+
+
+
+
+ {listMarkup}
+
+
+
+
+ );
+}
diff --git a/polaris-react/src/components/ResourceList/ResourceList.tsx b/polaris-react/src/components/ResourceList/ResourceList.tsx
index e32b1c9f8bd..c9019a97f3b 100644
--- a/polaris-react/src/components/ResourceList/ResourceList.tsx
+++ b/polaris-react/src/components/ResourceList/ResourceList.tsx
@@ -179,6 +179,7 @@ export function ResourceList({
selectAllActionsAbsoluteOffset,
selectAllActionsMaxWidth,
selectAllActionsOffsetLeft,
+ selectAllActionsOffsetBottom,
computeTableDimensions,
isScrolledPastTop,
selectAllActionsPastTopOffset,
@@ -582,6 +583,9 @@ export function ResourceList({
? undefined
: selectAllActionsAbsoluteOffset,
width: selectAllActionsMaxWidth,
+ bottom: isSelectAllActionsSticky
+ ? selectAllActionsOffsetBottom
+ : undefined,
left: isSelectAllActionsSticky ? selectAllActionsOffsetLeft : undefined,
}}
>
diff --git a/polaris-react/src/components/ResourceList/tests/ResourceList.test.tsx b/polaris-react/src/components/ResourceList/tests/ResourceList.test.tsx
index ad4148b3d84..14ace66394e 100644
--- a/polaris-react/src/components/ResourceList/tests/ResourceList.test.tsx
+++ b/polaris-react/src/components/ResourceList/tests/ResourceList.test.tsx
@@ -79,6 +79,7 @@ describe('', () => {
selectAllActionsAbsoluteOffset: 0,
selectAllActionsMaxWidth: 0,
selectAllActionsOffsetLeft: 0,
+ selectAllActionsOffsetBottom: 0,
computeTableDimensions: jest.fn(),
scrollbarPastTopOffset: 0,
selectAllActionsPastTopOffset: 0,
@@ -1339,6 +1340,7 @@ describe('', () => {
selectAllActionsAbsoluteOffset: 0,
selectAllActionsMaxWidth: 0,
selectAllActionsOffsetLeft: 0,
+ selectAllActionsOffsetBottom: 0,
computeTableDimensions,
scrollbarPastTopOffset: 0,
selectAllActionsPastTopOffset: 0,
diff --git a/polaris-react/src/components/SelectAllActions/hooks/tests/use-is-select-all-actions-sticky.test.tsx b/polaris-react/src/components/SelectAllActions/hooks/tests/use-is-select-all-actions-sticky.test.tsx
index f37ba9085b5..9705a5fbd1f 100644
--- a/polaris-react/src/components/SelectAllActions/hooks/tests/use-is-select-all-actions-sticky.test.tsx
+++ b/polaris-react/src/components/SelectAllActions/hooks/tests/use-is-select-all-actions-sticky.test.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import {intersectionObserver} from '@shopify/jest-dom-mocks';
import {mountWithApp} from 'tests/utilities';
+import {Scrollable} from '../../../Scrollable';
import {useIsSelectAllActionsSticky} from '../use-is-select-all-actions-sticky';
import type {UseIsSelectAllActionsStickyProps} from '../use-is-select-all-actions-sticky';
@@ -17,6 +18,7 @@ function Component({
selectAllActionsAbsoluteOffset,
selectAllActionsMaxWidth,
selectAllActionsOffsetLeft,
+ selectAllActionsOffsetBottom,
} = useIsSelectAllActionsSticky({selectMode, hasPagination, tableType});
return (
@@ -25,6 +27,7 @@ function Component({
{selectAllActionsAbsoluteOffset}
{selectAllActionsMaxWidth}
{selectAllActionsOffsetLeft}
+ {selectAllActionsOffsetBottom}
@@ -33,22 +36,28 @@ function Component({
describe('useIsSelectAllActionsSticky', () => {
let getBoundingClientRectSpy: jest.SpyInstance;
+ let getComputedStyleSpy: jest.SpyInstance;
beforeEach(() => {
+ getComputedStyleSpy = jest.spyOn(window, 'getComputedStyle');
+
getBoundingClientRectSpy = jest.spyOn(
Element.prototype,
'getBoundingClientRect',
);
+
setGetBoundingClientRect({
width: 600,
height: 400,
left: 20,
+ y: 18,
});
intersectionObserver.mock();
});
afterEach(() => {
+ getComputedStyleSpy.mockRestore();
getBoundingClientRectSpy.mockRestore();
intersectionObserver.restore();
});
@@ -83,10 +92,38 @@ describe('useIsSelectAllActionsSticky', () => {
const result = component.findAll('span')[2]?.text();
expect(result).toBe('20');
});
+
+ it('returns the bottom value correctly when not in a scroll container', () => {
+ setGetComputedStyle({
+ overflow: 'visible',
+ overflowX: 'visible',
+ overflowY: 'visible',
+ });
+
+ const component = mountWithApp();
+ const result = component.findAll('span')[3]?.text();
+ expect(result).toBe('0');
+ });
+
+ it('returns the bottom value correctly when in a scroll container', () => {
+ setGetComputedStyle({
+ overflow: 'auto',
+ overflowX: 'auto',
+ overflowY: 'auto',
+ });
+
+ const component = mountWithApp(
+
+
+ ,
+ );
+ const result = component.findAll('span')[3]?.text();
+ expect(result).toBe('26');
+ });
});
describe('when isIntersecting', () => {
- it('sets the useIsSelectAllActionsSticky value to false', () => {
+ it('sets the isSelectAllActionsSticky value to false', () => {
const component = mountWithApp();
const intersector = component.find('i');
@@ -104,7 +141,7 @@ describe('useIsSelectAllActionsSticky', () => {
});
describe('when not isIntersecting', () => {
- it('sets the useIsSelectAllActionsSticky value to true', () => {
+ it('sets the isSelectAllActionsSticky value to true', () => {
const component = mountWithApp();
const intersector = component.find('i');
@@ -121,14 +158,35 @@ describe('useIsSelectAllActionsSticky', () => {
});
});
+ function setGetComputedStyle({
+ overflow,
+ overflowX,
+ overflowY,
+ }: {
+ overflow: string;
+ overflowX: string;
+ overflowY: string;
+ }) {
+ getComputedStyleSpy.mockImplementation(() => {
+ return {
+ overflow,
+ overflowX,
+ overflowY,
+ toJSON() {},
+ };
+ });
+ }
+
function setGetBoundingClientRect({
width,
height,
left,
+ y,
}: {
width: number;
height: number;
left: number;
+ y: number;
}) {
getBoundingClientRectSpy.mockImplementation(() => {
return {
@@ -139,7 +197,7 @@ describe('useIsSelectAllActionsSticky', () => {
bottom: 0,
right: 0,
x: 0,
- y: 0,
+ y,
toJSON() {},
};
});
diff --git a/polaris-react/src/components/SelectAllActions/hooks/use-is-select-all-actions-sticky.ts b/polaris-react/src/components/SelectAllActions/hooks/use-is-select-all-actions-sticky.ts
index 79ac82e48a0..9b2cae81b0c 100644
--- a/polaris-react/src/components/SelectAllActions/hooks/use-is-select-all-actions-sticky.ts
+++ b/polaris-react/src/components/SelectAllActions/hooks/use-is-select-all-actions-sticky.ts
@@ -6,7 +6,8 @@ const DEBOUNCE_PERIOD = 250;
const SELECT_ALL_ACTIONS_HEIGHT = 41;
const PAGINATION_WIDTH_OFFSET = 64;
-const SCROLL_BAR_HEIGHT = 13;
+const SCROLL_BAR_CONTAINER_HEIGHT = 13;
+const SCROLL_BAR_HEIGHT = 8;
const INDEX_TABLE_INITIAL_OFFSET = 32;
const RESOURCE_LIST_INITIAL_OFFSET = 48;
@@ -18,11 +19,32 @@ export interface UseIsSelectAllActionsStickyProps {
tableType: TableType;
}
+export interface SelectAllActionsStickyCalculations {
+ selectAllActionsIntersectionRef: React.RefObject;
+ tableMeasurerRef: React.RefObject;
+ isSelectAllActionsSticky: boolean;
+ selectAllActionsAbsoluteOffset: number;
+ selectAllActionsMaxWidth: number;
+ selectAllActionsOffsetLeft: number;
+ selectAllActionsOffsetBottom: number;
+ computeTableDimensions():
+ | {
+ maxWidth: number;
+ offsetHeight: number;
+ offsetLeft: number;
+ offsetBottom: number;
+ }
+ | undefined;
+ isScrolledPastTop: boolean;
+ selectAllActionsPastTopOffset: number;
+ scrollbarPastTopOffset: number;
+}
+
export function useIsSelectAllActionsSticky({
selectMode,
hasPagination,
tableType,
-}: UseIsSelectAllActionsStickyProps) {
+}: UseIsSelectAllActionsStickyProps): SelectAllActionsStickyCalculations {
const hasIOSupport =
typeof window !== 'undefined' && Boolean(window.IntersectionObserver);
const [isSelectAllActionsSticky, setIsSticky] = useState(false);
@@ -32,6 +54,9 @@ export function useIsSelectAllActionsSticky({
const [selectAllActionsMaxWidth, setSelectAllActionsMaxWidth] = useState(0);
const [selectAllActionsOffsetLeft, setSelectAllActionsOffsetLeft] =
useState(0);
+ const [selectAllActionsOffsetBottom, setSelectAllActionsOffsetBottom] =
+ useState(0);
+
const selectAllActionsIntersectionRef = useRef(null);
const tableMeasurerRef = useRef(null);
@@ -39,7 +64,7 @@ export function useIsSelectAllActionsSticky({
const initialPostOffset =
tableType === 'index-table'
- ? INDEX_TABLE_INITIAL_OFFSET + SCROLL_BAR_HEIGHT
+ ? INDEX_TABLE_INITIAL_OFFSET + SCROLL_BAR_CONTAINER_HEIGHT
: RESOURCE_LIST_INITIAL_OFFSET;
const postScrollOffset = initialPostOffset + SELECT_ALL_ACTIONS_HEIGHT;
@@ -86,6 +111,26 @@ export function useIsSelectAllActionsSticky({
: null,
);
+ const getClosestScrollContainer = (node: HTMLElement) => {
+ let container: HTMLElement | null = node;
+
+ while (container && container !== document.body) {
+ const style = window.getComputedStyle(container);
+ const isScrollContainer =
+ style.overflow === 'auto' ||
+ style.overflowX === 'auto' ||
+ style.overflowY === 'auto' ||
+ style.overflow === 'scroll' ||
+ style.overflowX === 'scroll' ||
+ style.overflowY === 'scroll';
+
+ if (isScrollContainer) return container;
+ container = container.parentElement;
+ }
+
+ return null;
+ };
+
const computeTableDimensions = useCallback(() => {
const node = tableMeasurerRef.current;
if (!node) {
@@ -93,17 +138,25 @@ export function useIsSelectAllActionsSticky({
maxWidth: 0,
offsetHeight: 0,
offsetLeft: 0,
+ offsetBottom: 0,
};
}
+
+ const scrollContainer =
+ getClosestScrollContainer(node)?.getBoundingClientRect();
const box = node.getBoundingClientRect();
const paddingHeight = selectMode ? SELECT_ALL_ACTIONS_HEIGHT : 0;
const offsetHeight = box.height - paddingHeight;
const maxWidth = box.width - widthOffset;
const offsetLeft = box.left;
+ const offsetBottomScrollable = scrollContainer
+ ? Math.round(scrollContainer.y + SCROLL_BAR_HEIGHT)
+ : 0;
setSelectAllActionsAbsoluteOffset(offsetHeight);
setSelectAllActionsMaxWidth(maxWidth);
setSelectAllActionsOffsetLeft(offsetLeft);
+ setSelectAllActionsOffsetBottom(offsetBottomScrollable);
}, [selectMode, widthOffset]);
const computeDimensionsPastScroll = useCallback(() => {
@@ -162,9 +215,10 @@ export function useIsSelectAllActionsSticky({
selectAllActionsAbsoluteOffset,
selectAllActionsMaxWidth,
selectAllActionsOffsetLeft,
+ selectAllActionsOffsetBottom,
computeTableDimensions,
isScrolledPastTop,
selectAllActionsPastTopOffset: initialPostOffset,
- scrollbarPastTopOffset: initialPostOffset - SCROLL_BAR_HEIGHT,
+ scrollbarPastTopOffset: initialPostOffset - SCROLL_BAR_CONTAINER_HEIGHT,
};
}