Skip to content

Commit afe4564

Browse files
feat(ui): Add useIsStuck + Sticky component (#54215)
These utilities are primarily useful to add additional styling to stuck elements.
1 parent 1d4e4c8 commit afe4564

File tree

3 files changed

+64
-8
lines changed

3 files changed

+64
-8
lines changed

static/app/components/sticky.tsx

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {useRef} from 'react';
2+
import styled from '@emotion/styled';
3+
4+
import {useIsStuck} from 'sentry/utils/useIsStuck';
5+
6+
/**
7+
* A component that will become stuck to the top of the page. Once the user has
8+
* scrolled to it.
9+
*
10+
* The element will recieve a `data-stuck` attribute once it is stuck, useful
11+
* for additional styling when the element becomes stuck.
12+
*/
13+
function TaggedSticky(props: React.ComponentProps<'div'>) {
14+
const elementRef = useRef<HTMLDivElement>(null);
15+
const isStuck = useIsStuck(elementRef.current);
16+
17+
const stuckProps = isStuck ? {'data-stuck': ''} : {};
18+
19+
return <div ref={elementRef} {...stuckProps} {...props} />;
20+
}
21+
22+
const Sticky = styled(TaggedSticky)`
23+
position: sticky;
24+
top: 0;
25+
`;
26+
27+
export {Sticky};

static/app/utils/useIsStuck.tsx

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import 'intersection-observer'; // polyfill
2+
3+
import {useEffect, useState} from 'react';
4+
5+
/**
6+
* Determine if a element with `position: sticky` is currently stuck.
7+
*/
8+
export function useIsStuck(el: HTMLElement | null) {
9+
const [isStuck, setIsStuck] = useState(false);
10+
11+
useEffect(() => {
12+
if (el === null) {
13+
return () => {};
14+
}
15+
16+
const observer = new IntersectionObserver(
17+
([entry]) => setIsStuck(entry.intersectionRatio < 1),
18+
{
19+
rootMargin: '-1px 0px 0px 0px',
20+
threshold: [1],
21+
}
22+
);
23+
24+
observer.observe(el);
25+
26+
return () => observer.disconnect();
27+
}, [el]);
28+
29+
return isStuck;
30+
}

static/app/views/issueList/actions/index.tsx

+7-8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import uniq from 'lodash/uniq';
55
import {bulkDelete, bulkUpdate, mergeGroups} from 'sentry/actionCreators/group';
66
import {Alert} from 'sentry/components/alert';
77
import Checkbox from 'sentry/components/checkbox';
8+
import {Sticky} from 'sentry/components/sticky';
89
import {tct, tn} from 'sentry/locale';
910
import GroupStore from 'sentry/stores/groupStore';
1011
import SelectedGroupStore from 'sentry/stores/selectedGroupStore';
@@ -179,8 +180,8 @@ function IssueListActions({
179180
}
180181

181182
return (
182-
<Sticky>
183-
<StyledFlex>
183+
<StickyActions>
184+
<ActionsBar>
184185
{!disableActions && (
185186
<ActionsCheckbox isReprocessingQuery={displayReprocessingActions}>
186187
<Checkbox
@@ -219,7 +220,7 @@ function IssueListActions({
219220
isReprocessingQuery={displayReprocessingActions}
220221
isSavedSearchesOpen={isSavedSearchesOpen}
221222
/>
222-
</StyledFlex>
223+
</ActionsBar>
223224
{!allResultsVisible && pageSelected && (
224225
<Alert type="warning" system>
225226
<SelectAllNotice data-test-id="issue-list-select-all-notice">
@@ -263,7 +264,7 @@ function IssueListActions({
263264
</SelectAllNotice>
264265
</Alert>
265266
)}
266-
</Sticky>
267+
</StickyActions>
267268
);
268269
}
269270

@@ -323,13 +324,11 @@ function shouldConfirm(
323324
}
324325
}
325326

326-
const Sticky = styled('div')`
327-
position: sticky;
327+
const StickyActions = styled(Sticky)`
328328
z-index: ${p => p.theme.zIndex.issuesList.stickyHeader};
329-
top: -1px;
330329
`;
331330

332-
const StyledFlex = styled('div')`
331+
const ActionsBar = styled('div')`
333332
display: flex;
334333
min-height: 45px;
335334
padding-top: ${space(1)};

0 commit comments

Comments
 (0)