Skip to content

Commit c0f1ca5

Browse files
authored
fix(AnchedOverlay): hide overlay while positioning (#1333)
1 parent 36fba43 commit c0f1ca5

File tree

7 files changed

+323
-18
lines changed

7 files changed

+323
-18
lines changed

docs/content/Overlay.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,4 @@ System props are deprecated in all components except [Box](/Box). Please use the
8484
| width | `'small' │ 'medium' │ 'large' │ 'xlarge' │ 'xxlarge' │ 'auto'` | `auto` | Sets the width of the `Overlay`, pick from our set list of widths, or pass `auto` to automatically set the width based on the content of the `Overlay`. `small` corresponds to `256px`, `medium` corresponds to `320px`, `large` corresponds to `480px`, `xlarge` corresponds to `640px`, `xxlarge` corresponds to `960px`. |
8585
| height | `'xsmall', 'small', 'medium', 'large', 'xlarge', 'auto'` | `auto` | Sets the height of the `Overlay`, pick from our set list of heights, or pass `auto` to automatically set the height based on the content of the `Overlay`. `xsmall` corresponds to `192px`, `small` corresponds to `256px`, `medium` corresponds to `320px`, `large` corresponds to `432px`, `xlarge` corresponds to `600px`. |
8686
| visibility | `'visible', 'hidden'` | `visible` | Sets the visibility of the `Overlay`. |
87+
| anchorSide | `AnchorSide` | undefined | Optional. If provided, the Overlay will slide into position from the side of the anchor with a brief animation |

src/AnchoredOverlay/AnchoredOverlay.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export const AnchoredOverlay: React.FC<AnchoredOverlayProps> = ({
161161
onEscape={onEscape}
162162
ref={updateOverlayRef}
163163
role="none"
164+
visibility={position ? 'visible' : 'hidden'}
164165
height={height}
165166
width={width}
166167
{...overlayPosition}

src/FilteredActionList/FilteredActionList.tsx

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -88,21 +88,27 @@ export function FilteredActionList({
8888
[activeDescendantRef]
8989
)
9090

91-
useFocusZone({
92-
containerRef: listContainerRef,
93-
focusOutBehavior: 'wrap',
94-
focusableElementFilter: element => {
95-
return !(element instanceof HTMLInputElement)
96-
},
97-
activeDescendantFocus: inputRef,
98-
onActiveDescendantChanged: (current, previous, directlyActivated) => {
99-
activeDescendantRef.current = current
91+
useFocusZone(
92+
{
93+
containerRef: listContainerRef,
94+
focusOutBehavior: 'wrap',
95+
focusableElementFilter: element => {
96+
return !(element instanceof HTMLInputElement)
97+
},
98+
activeDescendantFocus: inputRef,
99+
onActiveDescendantChanged: (current, previous, directlyActivated) => {
100+
activeDescendantRef.current = current
100101

101-
if (current && scrollContainerRef.current && directlyActivated) {
102-
scrollIntoViewingArea(current, scrollContainerRef.current)
102+
if (current && scrollContainerRef.current && directlyActivated) {
103+
scrollIntoViewingArea(current, scrollContainerRef.current)
104+
}
103105
}
104-
}
105-
})
106+
},
107+
[
108+
// List ref isn't set while loading. Need to re-bind focus zone when it changes
109+
loading
110+
]
111+
)
106112

107113
useEffect(() => {
108114
// if items changed, we want to instantly move active descendant into view

src/Overlay.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type StyledOverlayProps = {
1313
width?: keyof typeof widthMap
1414
height?: keyof typeof heightMap
1515
maxHeight?: keyof Omit<typeof heightMap, 'auto' | 'initial'>
16+
visibility?: 'visible' | 'hidden'
1617
anchorSide?: AnchorSide
1718
}
1819

@@ -71,7 +72,7 @@ const StyledOverlay = styled.div<StyledOverlayProps & SystemCommonProps & System
7172
opacity: 1;
7273
}
7374
}
74-
75+
visibility: var(--styled-overlay-visibility);
7576
:focus {
7677
outline: none;
7778
}
@@ -85,8 +86,9 @@ export type OverlayProps = {
8586
returnFocusRef: React.RefObject<HTMLElement>
8687
onClickOutside: (e: TouchOrMouseEvent) => void
8788
onEscape: (e: KeyboardEvent) => void
89+
visibility?: 'visible' | 'hidden'
8890
[additionalKey: string]: unknown
89-
} & Omit<ComponentProps<typeof StyledOverlay>, keyof SystemPositionProps>
91+
} & Omit<ComponentProps<typeof StyledOverlay>, 'visibility' | keyof SystemPositionProps>
9092

9193
/**
9294
* An `Overlay` is a flexible floating surface, used to display transient content such as menus,
@@ -111,6 +113,7 @@ const Overlay = React.forwardRef<HTMLDivElement, OverlayProps>(
111113
returnFocusRef,
112114
ignoreClickRefs,
113115
onEscape,
116+
visibility = 'visible',
114117
height,
115118
anchorSide,
116119
...rest
@@ -140,7 +143,7 @@ const Overlay = React.forwardRef<HTMLDivElement, OverlayProps>(
140143

141144
useLayoutEffect(() => {
142145
const {x, y} = getSlideAnimationStartingVector(anchorSide)
143-
if ((!x && !y) || !overlayRef.current?.animate) {
146+
if ((!x && !y) || !overlayRef.current?.animate || visibility === 'hidden') {
144147
return
145148
}
146149

@@ -152,11 +155,22 @@ const Overlay = React.forwardRef<HTMLDivElement, OverlayProps>(
152155
easing: slideAnimationEasing
153156
}
154157
)
155-
}, [anchorSide, slideAnimationDistance, slideAnimationEasing])
158+
}, [anchorSide, slideAnimationDistance, slideAnimationEasing, visibility])
156159

157160
return (
158161
<Portal>
159-
<StyledOverlay height={height} role={role} {...rest} ref={combinedRef} />
162+
<StyledOverlay
163+
height={height}
164+
role={role}
165+
{...rest}
166+
ref={combinedRef}
167+
style={
168+
{
169+
...rest.style,
170+
'--styled-overlay-visibility': visibility
171+
} as React.CSSProperties
172+
}
173+
/>
160174
</Portal>
161175
)
162176
}

src/__tests__/AnchoredOverlay.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,9 @@ describe('AnchoredOverlay', () => {
141141
expect(mockCloseCallback).toHaveBeenCalledTimes(1)
142142
expect(mockCloseCallback).toHaveBeenCalledWith('escape')
143143
})
144+
145+
it('should render consistently when open', () => {
146+
const anchoredOverlay = HTMLRender(<AnchoredOverlayTestComponent initiallyOpen={true} />)
147+
expect(anchoredOverlay).toMatchSnapshot()
148+
})
144149
})

src/__tests__/__snapshots__/AnchoredOverlay.tsx.snap

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,241 @@ exports[`AnchoredOverlay renders consistently 1`] = `
9292
</button>
9393
</div>
9494
`;
95+
96+
exports[`AnchoredOverlay should render consistently when open 1`] = `
97+
Object {
98+
"asFragment": [Function],
99+
"baseElement": .c0 {
100+
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
101+
line-height: 1.5;
102+
color: #24292e;
103+
}
104+
105+
.c1 {
106+
position: relative;
107+
display: inline-block;
108+
padding: 6px 16px;
109+
font-family: inherit;
110+
font-weight: 600;
111+
line-height: 20px;
112+
white-space: nowrap;
113+
vertical-align: middle;
114+
cursor: pointer;
115+
-webkit-user-select: none;
116+
-moz-user-select: none;
117+
-ms-user-select: none;
118+
user-select: none;
119+
border-radius: 6px;
120+
-webkit-appearance: none;
121+
-moz-appearance: none;
122+
appearance: none;
123+
-webkit-text-decoration: none;
124+
text-decoration: none;
125+
text-align: center;
126+
font-size: 14px;
127+
color: #24292e;
128+
background-color: #fafbfc;
129+
border: 1px solid rgba(27,31,35,0.15);
130+
box-shadow: 0 1px 0 rgba(27,31,35,0.04),inset 0 1px 0 rgba(255,255,255,0.25);
131+
}
132+
133+
.c1:hover {
134+
-webkit-text-decoration: none;
135+
text-decoration: none;
136+
}
137+
138+
.c1:focus {
139+
outline: none;
140+
}
141+
142+
.c1:disabled {
143+
cursor: default;
144+
}
145+
146+
.c1:disabled svg {
147+
opacity: 0.6;
148+
}
149+
150+
.c1:hover {
151+
background-color: #f3f4f6;
152+
border-color: rgba(27,31,35,0.15);
153+
}
154+
155+
.c1:focus {
156+
border-color: rgba(27,31,35,0.15);
157+
box-shadow: 0 0 0 3px rgba(3,102,214,0.3);
158+
}
159+
160+
.c1:active {
161+
background-color: hsla(220,14%,94%,1);
162+
box-shadow: inset 0 0.15em 0.3em rgba(27,31,35,0.15);
163+
}
164+
165+
.c1:disabled {
166+
color: #959da5;
167+
background-color: #fafbfc;
168+
border-color: rgba(27,31,35,0.15);
169+
}
170+
171+
.c2 {
172+
background-color: #ffffff;
173+
box-shadow: 0 1px 3px rgba(27,31,35,0.12),0 8px 24px rgba(68,77,86,0.12);
174+
position: absolute;
175+
min-width: 192px;
176+
max-width: 640px;
177+
height: auto;
178+
width: auto;
179+
border-radius: 12px;
180+
overflow: hidden;
181+
-webkit-animation: overlay-appear 200ms cubic-bezier(0.33,1,0.68,1);
182+
animation: overlay-appear 200ms cubic-bezier(0.33,1,0.68,1);
183+
visibility: var(--styled-overlay-visibility);
184+
top: 4px;
185+
left: 0px;
186+
}
187+
188+
.c2:focus {
189+
outline: none;
190+
}
191+
192+
<body>
193+
<div>
194+
<div
195+
class="c0"
196+
color="text.primary"
197+
data-portal-root="true"
198+
font-family="normal"
199+
>
200+
<button
201+
aria-haspopup="true"
202+
aria-labelledby="__primer_id_10007"
203+
class="c1"
204+
id="__primer_id_10007"
205+
tabindex="0"
206+
>
207+
Anchor Button
208+
</button>
209+
<div
210+
id="__primerPortalRoot__"
211+
>
212+
<div
213+
style="position: relative; z-index: 1;"
214+
>
215+
<div
216+
class="c2"
217+
data-focus-trap="active"
218+
height="auto"
219+
role="none"
220+
style="--styled-overlay-visibility: visible;"
221+
width="auto"
222+
>
223+
<button
224+
class="focus-visible"
225+
data-focus-visible-added=""
226+
tabindex="0"
227+
type="button"
228+
>
229+
Focusable Child
230+
</button>
231+
</div>
232+
</div>
233+
</div>
234+
</div>
235+
</div>
236+
</body>,
237+
"container": <div>
238+
<div
239+
class="BaseStyles__Base-qvuaww-0 dhupSQ"
240+
color="text.primary"
241+
data-portal-root="true"
242+
font-family="normal"
243+
>
244+
<button
245+
aria-haspopup="true"
246+
aria-labelledby="__primer_id_10007"
247+
class="ButtonBase-sc-181ps9o-0 Button-xjtz72-0 jhgDtb"
248+
id="__primer_id_10007"
249+
tabindex="0"
250+
>
251+
Anchor Button
252+
</button>
253+
<div
254+
id="__primerPortalRoot__"
255+
>
256+
<div
257+
style="position: relative; z-index: 1;"
258+
>
259+
<div
260+
class="Overlay__StyledOverlay-jhwkzw-0 leFwDU"
261+
data-focus-trap="active"
262+
height="auto"
263+
role="none"
264+
style="--styled-overlay-visibility: visible;"
265+
width="auto"
266+
>
267+
<button
268+
class="focus-visible"
269+
data-focus-visible-added=""
270+
tabindex="0"
271+
type="button"
272+
>
273+
Focusable Child
274+
</button>
275+
</div>
276+
</div>
277+
</div>
278+
</div>
279+
</div>,
280+
"debug": [Function],
281+
"findAllByAltText": [Function],
282+
"findAllByDisplayValue": [Function],
283+
"findAllByLabelText": [Function],
284+
"findAllByPlaceholderText": [Function],
285+
"findAllByRole": [Function],
286+
"findAllByTestId": [Function],
287+
"findAllByText": [Function],
288+
"findAllByTitle": [Function],
289+
"findByAltText": [Function],
290+
"findByDisplayValue": [Function],
291+
"findByLabelText": [Function],
292+
"findByPlaceholderText": [Function],
293+
"findByRole": [Function],
294+
"findByTestId": [Function],
295+
"findByText": [Function],
296+
"findByTitle": [Function],
297+
"getAllByAltText": [Function],
298+
"getAllByDisplayValue": [Function],
299+
"getAllByLabelText": [Function],
300+
"getAllByPlaceholderText": [Function],
301+
"getAllByRole": [Function],
302+
"getAllByTestId": [Function],
303+
"getAllByText": [Function],
304+
"getAllByTitle": [Function],
305+
"getByAltText": [Function],
306+
"getByDisplayValue": [Function],
307+
"getByLabelText": [Function],
308+
"getByPlaceholderText": [Function],
309+
"getByRole": [Function],
310+
"getByTestId": [Function],
311+
"getByText": [Function],
312+
"getByTitle": [Function],
313+
"queryAllByAltText": [Function],
314+
"queryAllByDisplayValue": [Function],
315+
"queryAllByLabelText": [Function],
316+
"queryAllByPlaceholderText": [Function],
317+
"queryAllByRole": [Function],
318+
"queryAllByTestId": [Function],
319+
"queryAllByText": [Function],
320+
"queryAllByTitle": [Function],
321+
"queryByAltText": [Function],
322+
"queryByDisplayValue": [Function],
323+
"queryByLabelText": [Function],
324+
"queryByPlaceholderText": [Function],
325+
"queryByRole": [Function],
326+
"queryByTestId": [Function],
327+
"queryByText": [Function],
328+
"queryByTitle": [Function],
329+
"rerender": [Function],
330+
"unmount": [Function],
331+
}
332+
`;

0 commit comments

Comments
 (0)