Skip to content

Commit 0eef368

Browse files
committed
feat(ui-popover): add shouldScrollContent prop
1 parent 64edddb commit 0eef368

5 files changed

Lines changed: 102 additions & 4 deletions

File tree

packages/ui-popover/src/Popover/v2/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,37 @@ const Example = () => {
425425
render(<Example />)
426426
```
427427

428+
#### Scrolling content (`shouldScrollContent`)
429+
430+
When the popover content can grow taller than the available viewport space, set `shouldScrollContent` to wrap the content in a scroll container that fits to the room between the trigger element and the viewport edge. To try the example below, zoom in until the available space becomes small enough that a scrollbar is displayed in the Popover.
431+
432+
```js
433+
---
434+
type: example
435+
---
436+
const fpo = lorem.paragraphs(8)
437+
class Example extends React.Component {
438+
state = { isOpen: false }
439+
render () {
440+
return (
441+
<Popover
442+
on="click"
443+
placement="bottom start"
444+
isShowingContent={this.state.isOpen}
445+
onShowContent={() => this.setState({ isOpen: true })}
446+
onHideContent={() => this.setState({ isOpen: false })}
447+
shouldScrollContent
448+
screenReaderLabel="A long popover"
449+
renderTrigger={<Button>Open scrollable popover</Button>}
450+
>
451+
<View as="div" padding="small" maxWidth="22rem">{fpo}</View>
452+
</Popover>
453+
)
454+
}
455+
}
456+
render(<Example />)
457+
```
458+
428459
### Guidelines
429460

430461
```js

packages/ui-popover/src/Popover/v2/__tests__/Popover.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,4 +306,43 @@ describe('<Popover />', () => {
306306

307307
expect(popover).toHaveStyle('display: block')
308308
})
309+
310+
describe('shouldScrollContent', () => {
311+
it('does not wrap content when the prop is unset', async () => {
312+
render(
313+
<Popover
314+
isShowingContent
315+
on="click"
316+
renderTrigger={<button>Trigger</button>}
317+
>
318+
<h2 data-testid="content">Popover content</h2>
319+
</Popover>
320+
)
321+
const content = await screen.findByTestId('content')
322+
expect(
323+
content.closest('[class*="popover__scrollContainer"]')
324+
).toBeNull()
325+
})
326+
327+
it('wraps content in an overflow:auto + auto-fit max-height container when set', async () => {
328+
render(
329+
<Popover
330+
isShowingContent
331+
on="click"
332+
renderTrigger={<button>Trigger</button>}
333+
shouldScrollContent
334+
>
335+
<h2 data-testid="content">Popover content</h2>
336+
</Popover>
337+
)
338+
const content = await screen.findByTestId('content')
339+
const wrapper = content.closest(
340+
'[class*="popover__scrollContainer"]'
341+
) as HTMLElement
342+
expect(wrapper).not.toBeNull()
343+
const computed = getComputedStyle(wrapper)
344+
expect(computed.overflowY).toBe('auto')
345+
expect(computed.maxHeight).toContain('--ui-position-available-height')
346+
})
347+
})
309348
})

packages/ui-popover/src/Popover/v2/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,12 @@ class Popover extends Component<PopoverProps, PopoverState> {
517517
renderContent() {
518518
let content = callRenderProp(this.props.children)
519519

520+
if (this.props.shouldScrollContent) {
521+
content = (
522+
<div css={this.props.styles?.scrollContainer}>{content}</div>
523+
)
524+
}
525+
520526
if (this.shown && !this.isTooltip) {
521527
// if popover is NOT being used as a tooltip, create a Dialog
522528
// to manage the content FocusRegion, when showing

packages/ui-popover/src/Popover/v2/props.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@
2424
import React from 'react'
2525
import { BorderWidth } from '@instructure/emotion'
2626

27-
import type { Shadow, Stacking, WithStyleProps } from '@instructure/emotion'
27+
import type {
28+
Shadow,
29+
Stacking,
30+
StyleObject,
31+
WithStyleProps
32+
} from '@instructure/emotion'
2833

2934
import type {
3035
PlacementPropValues,
@@ -284,6 +289,12 @@ type PopoverOwnProps = {
284289
* otherwise its calculated automatically based on whether the content is shown.
285290
*/
286291
shouldSetAriaExpanded?: boolean
292+
/**
293+
* When `true`, wraps the popover content in a container that caps its
294+
* height to the available viewport space and scrolls when content
295+
* overflows.
296+
*/
297+
shouldScrollContent?: boolean
287298
}
288299

289300
type PopoverProps = PopoverOwnProps &
@@ -302,7 +313,11 @@ type PropKeys = keyof PopoverOwnProps
302313

303314
type AllowedPropKeys = Readonly<Array<PropKeys>>
304315

305-
type PopoverStyle = { borderRadius: string; borderColor: string }
316+
type PopoverStyle = {
317+
borderRadius: string
318+
borderColor: string
319+
scrollContainer: StyleObject
320+
}
306321
const allowedProps: AllowedPropKeys = [
307322
'isShowingContent',
308323
'defaultIsShowingContent',
@@ -347,7 +362,8 @@ const allowedProps: AllowedPropKeys = [
347362
'children',
348363
'elementRef',
349364
'borderWidth',
350-
'shouldSetAriaExpanded'
365+
'shouldSetAriaExpanded',
366+
'shouldScrollContent'
351367
]
352368

353369
export type { PopoverOwnProps, PopoverProps, PopoverState, PopoverStyle }

packages/ui-popover/src/Popover/v2/styles.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,13 @@ const generateStyle = (
3838
): PopoverStyle => {
3939
return {
4040
borderColor: componentTheme.borderColor,
41-
borderRadius: componentTheme.borderRadius
41+
borderRadius: componentTheme.borderRadius,
42+
scrollContainer: {
43+
label: 'popover__scrollContainer',
44+
maxHeight: 'var(--ui-position-available-height, 100vh)',
45+
overflowY: 'auto',
46+
overflowX: 'hidden'
47+
}
4248
}
4349
}
4450

0 commit comments

Comments
 (0)