Skip to content

Commit dacf38d

Browse files
authored
add resizable support to the PageLayout component
1 parent 9eaa37e commit dacf38d

File tree

3 files changed

+227
-4
lines changed

3 files changed

+227
-4
lines changed

src/PageLayout/PageLayout.stories.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,68 @@ export const CustomStickyHeader: Story = args => (
662662
</Box>
663663
)
664664

665+
export const ResizablePane: Story = args => (
666+
<PageLayout containerWidth="full" padding={args.padding} rowGap={args.rowGap} columnGap={'none'} sx={args.sx}>
667+
{args['Render header?'] ? (
668+
<PageLayout.Header
669+
padding={args['Header.padding']}
670+
divider={{
671+
narrow: args['Header.divider.narrow'],
672+
regular: args['Header.divider.regular'],
673+
wide: args['Header.divider.wide']
674+
}}
675+
hidden={{
676+
narrow: args['Header.hidden.narrow'],
677+
regular: args['Header.hidden.regular'],
678+
wide: args['Header.hidden.wide']
679+
}}
680+
>
681+
<Placeholder height={args['Header placeholder height']} label="Header" />
682+
</PageLayout.Header>
683+
) : null}
684+
<PageLayout.Content
685+
width={args['Content.width']}
686+
padding={args['Content.padding']}
687+
hidden={{
688+
narrow: args['Content.hidden.narrow'],
689+
regular: args['Content.hidden.regular'],
690+
wide: args['Content.hidden.wide']
691+
}}
692+
>
693+
<Placeholder height={args['Content placeholder height']} label="Content" />
694+
</PageLayout.Content>
695+
{args['Render pane?'] ? (
696+
<PageLayout.Pane
697+
width={args['Content.width']}
698+
padding={args['Content.padding']}
699+
position="start"
700+
divider="line"
701+
canResizePane={true}
702+
paneWidthStorageKey="primer-react.pane-width"
703+
>
704+
<Placeholder height={args['Pane placeholder height']} label="Pane" />
705+
</PageLayout.Pane>
706+
) : null}
707+
{args['Render footer?'] ? (
708+
<PageLayout.Footer
709+
padding={args['Footer.padding']}
710+
divider={{
711+
narrow: args['Footer.divider.narrow'],
712+
regular: args['Footer.divider.regular'],
713+
wide: args['Footer.divider.wide']
714+
}}
715+
hidden={{
716+
narrow: args['Footer.hidden.narrow'],
717+
regular: args['Footer.hidden.regular'],
718+
wide: args['Footer.hidden.wide']
719+
}}
720+
>
721+
<Placeholder height={args['Footer placeholder height']} label="Footer" />
722+
</PageLayout.Footer>
723+
) : null}
724+
</PageLayout>
725+
)
726+
665727
CustomStickyHeader.argTypes = {
666728
sticky: {
667729
type: 'boolean',

src/PageLayout/PageLayout.tsx

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import React from 'react'
22
import {useStickyPaneHeight} from './useStickyPaneHeight'
3+
import {useHorizontalResize} from './useHorizontalResize'
34
import Box from '../Box'
45
import {isResponsiveValue, ResponsiveValue, useResponsiveValue} from '../hooks/useResponsiveValue'
6+
import {useRefObjectAsForwardedRef} from '../hooks/useRefObjectAsForwardedRef'
57
import {BetterSystemStyleObject, merge, SxProp} from '../sx'
8+
import {Theme} from '../ThemeProvider'
69

710
const REGION_ORDER = {
811
header: 0,
@@ -105,6 +108,10 @@ Root.displayName = 'PageLayout'
105108

106109
type DividerProps = {
107110
variant?: 'none' | 'line' | 'filled' | ResponsiveValue<'none' | 'line' | 'filled'>
111+
canResize?: boolean
112+
isResizing?: boolean
113+
onClick?: (e: React.MouseEvent) => void
114+
onMouseDown?: (e: React.MouseEvent) => void
108115
} & SxProp
109116

110117
const horizontalDividerVariants = {
@@ -177,18 +184,65 @@ const verticalDividerVariants = {
177184
}
178185
}
179186

180-
const VerticalDivider: React.FC<React.PropsWithChildren<DividerProps>> = ({variant = 'none', sx = {}}) => {
187+
const VerticalDivider: React.FC<React.PropsWithChildren<DividerProps>> = ({
188+
variant = 'none',
189+
canResize,
190+
isResizing,
191+
onClick,
192+
onMouseDown,
193+
sx = {}
194+
}) => {
181195
const responsiveVariant = useResponsiveValue(variant, 'none')
182196
return (
183197
<Box
184198
sx={merge<BetterSystemStyleObject>(
185199
{
186200
height: '100%',
201+
position: 'relative',
187202
...verticalDividerVariants[responsiveVariant]
188203
},
189204
sx
190205
)}
191-
/>
206+
>
207+
{canResize && (
208+
<Box
209+
onMouseDown={onMouseDown}
210+
onClick={onClick}
211+
sx={{
212+
width: '16px',
213+
height: '100%',
214+
display: 'flex',
215+
justifyContent: 'center',
216+
position: 'absolute',
217+
transform: 'translateX(50%)',
218+
right: 0,
219+
opacity: isResizing ? 1 : 0,
220+
cursor: 'col-resize',
221+
'&:hover': {
222+
animation: isResizing ? 'none' : 'resizer-appear 80ms 300ms both',
223+
224+
'@keyframes resizer-appear': {
225+
from: {
226+
opacity: 0
227+
},
228+
229+
to: {
230+
opacity: 1
231+
}
232+
}
233+
}
234+
}}
235+
>
236+
<Box
237+
sx={{
238+
backgroundColor: 'accent.fg',
239+
width: '1px',
240+
height: '100%'
241+
}}
242+
/>
243+
</Box>
244+
)}
245+
</Box>
192246
)
193247
}
194248

@@ -370,6 +424,8 @@ export type PageLayoutPaneProps = {
370424
*/
371425
positionWhenNarrow?: 'inherit' | keyof typeof panePositions
372426
width?: keyof typeof paneWidths
427+
canResizePane?: boolean
428+
paneWidthStorageKey?: string
373429
padding?: keyof typeof SPACING_MAP
374430
divider?: 'none' | 'line' | ResponsiveValue<'none' | 'line', 'none' | 'line' | 'filled'>
375431
/**
@@ -410,6 +466,8 @@ const Pane = React.forwardRef<HTMLDivElement, React.PropsWithChildren<PageLayout
410466
positionWhenNarrow = 'inherit',
411467
width = 'medium',
412468
padding = 'none',
469+
canResizePane = false,
470+
paneWidthStorageKey = 'paneWidth',
413471
divider: responsiveDivider = 'none',
414472
dividerWhenNarrow = 'inherit',
415473
sticky = false,
@@ -448,6 +506,15 @@ const Pane = React.forwardRef<HTMLDivElement, React.PropsWithChildren<PageLayout
448506
}
449507
}, [sticky, enableStickyPane, disableStickyPane, offsetHeader])
450508

509+
const paneRef = React.useRef<HTMLDivElement>(null)
510+
useRefObjectAsForwardedRef(forwardRef, paneRef)
511+
512+
const {onMouseDown, onClick, isResizing, paneWidth} = useHorizontalResize(
513+
canResizePane,
514+
paneRef,
515+
paneWidthStorageKey
516+
)
517+
451518
return (
452519
<Box
453520
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -493,10 +560,27 @@ const Pane = React.forwardRef<HTMLDivElement, React.PropsWithChildren<PageLayout
493560
/>
494561
<VerticalDivider
495562
variant={{narrow: 'none', regular: dividerVariant}}
563+
canResize={canResizePane}
564+
isResizing={isResizing}
565+
onClick={onClick}
566+
onMouseDown={onMouseDown}
496567
sx={{[position === 'end' ? 'marginRight' : 'marginLeft']: SPACING_MAP[columnGap]}}
497568
/>
498-
499-
<Box ref={forwardRef} sx={{width: paneWidths[width], padding: SPACING_MAP[padding], overflow: 'auto'}}>
569+
<Box
570+
ref={paneRef}
571+
sx={(theme: Theme) => ({
572+
padding: SPACING_MAP[padding],
573+
overflow: 'auto',
574+
...(paneWidth
575+
? {
576+
width: `clamp(256px, ${paneWidth}px, 100vw - 511px)`,
577+
[`@media screen and (min-width: ${theme.breakpoints[3]})`]: {
578+
width: `clamp(256px, ${paneWidth}px, 100vw - 959px)`
579+
}
580+
}
581+
: {width: paneWidths[width]})
582+
})}
583+
>
500584
{children}
501585
</Box>
502586
</Box>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React, {useEffect, useCallback, useState, RefObject} from 'react'
2+
3+
export function useHorizontalResize(enabled: boolean, paneRef: RefObject<HTMLDivElement>, storageKey?: string) {
4+
const [isDragging, setIsDragging] = useState(false)
5+
const [isResizing, setIsResizing] = useState(false)
6+
const [paneWidth, setPaneWidth] = useState<undefined | number>()
7+
8+
useEffect(() => {
9+
let hasValue = false
10+
if (storageKey) {
11+
const storedWidth = window.localStorage.getItem(storageKey)
12+
if (storedWidth) {
13+
const number = parseInt(storedWidth, 10)
14+
if (!isNaN(number) && number > 0) {
15+
hasValue = true
16+
setPaneWidth(number)
17+
}
18+
}
19+
}
20+
21+
if (!hasValue && paneRef.current) {
22+
setPaneWidth(paneRef.current.clientWidth)
23+
}
24+
}, [storageKey, setPaneWidth, paneRef])
25+
26+
const onMouseDown = useCallback(
27+
(e: React.MouseEvent) => {
28+
if (!enabled || e.button !== 0) return
29+
setIsDragging(true)
30+
},
31+
[enabled]
32+
)
33+
34+
const onClick = useCallback(
35+
(e: React.MouseEvent) => {
36+
if (!enabled || e.detail !== 2) return
37+
setPaneWidth(undefined)
38+
setIsDragging(false)
39+
},
40+
[enabled]
41+
)
42+
43+
const onMouseMove = useCallback(
44+
(e: MouseEvent) => {
45+
if (isDragging) {
46+
setIsResizing(true)
47+
const newSize = e.clientX
48+
setPaneWidth(newSize)
49+
e.preventDefault()
50+
}
51+
},
52+
[isDragging]
53+
)
54+
55+
const onMouseUp = useCallback(() => {
56+
const newPosition = paneRef.current ? paneRef.current.clientWidth : 0
57+
if (storageKey && newPosition) {
58+
window.localStorage.setItem(storageKey, newPosition.toString())
59+
}
60+
setIsDragging(false)
61+
setIsResizing(false)
62+
}, [paneRef, storageKey])
63+
64+
useEffect(() => {
65+
if (enabled) {
66+
document.addEventListener('mousemove', onMouseMove)
67+
document.addEventListener('mouseup', onMouseUp)
68+
}
69+
70+
return () => {
71+
document.removeEventListener('mousemove', onMouseMove)
72+
document.removeEventListener('mouseup', onMouseUp)
73+
}
74+
})
75+
76+
return {onMouseDown, onClick, isResizing, paneWidth}
77+
}

0 commit comments

Comments
 (0)