Skip to content
33 changes: 27 additions & 6 deletions src/Overlay.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import styled from 'styled-components'
import React, {ReactElement, useRef} from 'react'
import React, {ReactElement, useEffect, useRef} from 'react'
import {get, COMMON, POSITION, SystemPositionProps, SystemCommonProps} from './constants'
import {ComponentProps} from './utils/types'
import {useOverlay, TouchOrMouseEvent} from './hooks'
Expand All @@ -10,6 +10,7 @@ import {useCombinedRefs} from './hooks/useCombinedRefs'
type StyledOverlayProps = {
width?: keyof typeof widthMap
height?: keyof typeof heightMap
maxHeight?: keyof Omit<typeof heightMap, 'auto' | 'initial'>
visibility?: 'visible' | 'hidden'
}

Expand All @@ -19,7 +20,8 @@ const heightMap = {
medium: '320px',
large: '432px',
xlarge: '600px',
auto: 'auto'
auto: 'auto',
initial: 'auto' // Passing 'initial' initially applies 'auto'
}

const widthMap = {
Expand All @@ -39,6 +41,7 @@ const StyledOverlay = styled.div<StyledOverlayProps & SystemCommonProps & System
min-width: 192px;
max-width: 640px;
height: ${props => heightMap[props.height || 'auto']};
max-height: ${props => props.maxHeight && heightMap[props.maxHeight]};
width: ${props => widthMap[props.width || 'auto']};
border-radius: 12px;
overflow: hidden;
Expand Down Expand Up @@ -78,31 +81,49 @@ export type OverlayProps = {
* @param onClickOutside Required. Function to call when clicking outside of the `Overlay`. Typically this function sets the `Overlay` visibility state to `false`.
* @param onEscape Required. Function to call when user presses `Escape`. Typically this function sets the `Overlay` visibility state to `false`.
* @param width 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`.
* @param height 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`.
* @param height 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`, or pass `initial` to set the height based on the initial content of the `Overlay` (i.e. ignoring content changes). `xsmall` corresponds to `192px`, `small` corresponds to `256px`, `medium` corresponds to `320px`, `large` corresponds to `432px`, `xlarge` corresponds to `600px`.
* @param maxHeight Sets the maximum height of the `Overlay`, pick from our set list of heights. `xsmall` corresponds to `192px`, `small` corresponds to `256px`, `medium` corresponds to `320px`, `large` corresponds to `432px`, `xlarge` corresponds to `600px`.
* @param visibility Sets the visibility of the `Overlay`
*/
const Overlay = React.forwardRef<HTMLDivElement, OverlayProps>(
(
{onClickOutside, role = 'dialog', initialFocusRef, returnFocusRef, ignoreClickRefs, onEscape, visibility, ...rest},
{
onClickOutside,
role = 'dialog',
initialFocusRef,
returnFocusRef,
ignoreClickRefs,
onEscape,
visibility,
height,
...rest
},
forwardedRef
): ReactElement => {
const overlayRef = useRef<HTMLDivElement>(null)
const combinedRef = useCombinedRefs(overlayRef, forwardedRef)

const overlayProps = useOverlay({
useOverlay({
overlayRef,
returnFocusRef,
onEscape,
ignoreClickRefs,
onClickOutside,
initialFocusRef
})

useEffect(() => {
if (height === 'initial' && combinedRef.current?.clientHeight) {
combinedRef.current.style.height = `${combinedRef.current.clientHeight}px`
}
}, [height, combinedRef])

return (
<Portal>
<StyledOverlay
{...overlayProps}
aria-modal="true"
role={role}
height={height}
{...rest}
ref={combinedRef}
visibility={visibility}
Expand Down
63 changes: 63 additions & 0 deletions src/stories/SelectPanel.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,66 @@ export function SingleSelectStory(): JSX.Element {
)
}
SingleSelectStory.storyName = 'Single Select'

export function SelectPanelHeightInitialWithOverflowingItemsStory(): JSX.Element {
const [selected, setSelected] = React.useState<ItemInput | undefined>(items[0])
const [filter, setFilter] = React.useState('')
const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase()))
const [open, setOpen] = useState(false)

return (
<>
<h1>Single Select Panel</h1>
<div>Please select a label that describe your issue:</div>
<SelectPanel
renderAnchor={({children, 'aria-labelledby': ariaLabelledBy, ...anchorProps}) => (
<DropdownButton aria-labelledby={` ${ariaLabelledBy}`} {...anchorProps}>
{children ?? 'Select Labels'}
</DropdownButton>
)}
placeholderText="Filter Labels"
open={open}
onOpenChange={setOpen}
items={filteredItems}
selected={selected}
onSelectedChange={setSelected}
onFilterChange={setFilter}
showItemDividers={true}
overlayProps={{width: 'small', height: 'initial', maxHeight: 'xsmall'}}
/>
</>
)
}
SelectPanelHeightInitialWithOverflowingItemsStory.storyName = 'SelectPanel, Height: Initial, Overflowing Items'

export function SelectPanelHeightInitialWithUnderflowingItemsStory(): JSX.Element {
const underflowingItems = [items[0], items[1]]
const [selected, setSelected] = React.useState<ItemInput | undefined>(underflowingItems[0])
const [filter, setFilter] = React.useState('')
const filteredItems = underflowingItems.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase()))
const [open, setOpen] = useState(false)

return (
<>
<h1>Single Select Panel</h1>
<div>Please select a label that describe your issue:</div>
<SelectPanel
renderAnchor={({children, 'aria-labelledby': ariaLabelledBy, ...anchorProps}) => (
<DropdownButton aria-labelledby={` ${ariaLabelledBy}`} {...anchorProps}>
{children ?? 'Select Labels'}
</DropdownButton>
)}
placeholderText="Filter Labels"
open={open}
onOpenChange={setOpen}
items={filteredItems}
selected={selected}
onSelectedChange={setSelected}
onFilterChange={setFilter}
showItemDividers={true}
overlayProps={{width: 'small', height: 'initial', maxHeight: 'xsmall'}}
/>
</>
)
}
SelectPanelHeightInitialWithUnderflowingItemsStory.storyName = 'SelectPanel, Height: Initial, Underflowing Items'