1- import React from 'react'
1+ import React , { useRef } from 'react'
22import { createGlobalStyle } from 'styled-components'
33import Box from '../Box'
44import { useId } from '../hooks/useId'
@@ -10,7 +10,6 @@ import {Theme} from '../ThemeProvider'
1010import { canUseDOM } from '../utils/environment'
1111import { useOverflow } from '../internal/hooks/useOverflow'
1212import { warning } from '../utils/warning'
13- import VisuallyHidden from '../_VisuallyHidden'
1413import { useStickyPaneHeight } from './useStickyPaneHeight'
1514
1615const REGION_ORDER = {
@@ -31,6 +30,7 @@ const PageLayoutContext = React.createContext<{
3130 padding : keyof typeof SPACING_MAP
3231 rowGap : keyof typeof SPACING_MAP
3332 columnGap : keyof typeof SPACING_MAP
33+ paneRef ?: React . RefObject < HTMLDivElement >
3434 enableStickyPane ?: ( top : number | string ) => void
3535 disableStickyPane ?: ( ) => void
3636 contentTopRef ?: ( node ?: Element | null | undefined ) => void
@@ -76,6 +76,8 @@ const Root: React.FC<React.PropsWithChildren<PageLayoutProps>> = ({
7676 const { rootRef, enableStickyPane, disableStickyPane, contentTopRef, contentBottomRef, stickyPaneHeight} =
7777 useStickyPaneHeight ( )
7878
79+ const paneRef = useRef < HTMLDivElement > ( null )
80+
7981 const [ slots , rest ] = useSlots ( children , slotsConfig ?? { header : Header , footer : Footer } )
8082
8183 return (
@@ -88,6 +90,7 @@ const Root: React.FC<React.PropsWithChildren<PageLayoutProps>> = ({
8890 disableStickyPane,
8991 contentTopRef,
9092 contentBottomRef,
93+ paneRef,
9194 } }
9295 >
9396 < Box
@@ -197,7 +200,7 @@ const verticalDividerVariants = {
197200type DraggableDividerProps = {
198201 draggable ?: boolean
199202 onDragStart ?: ( ) => void
200- onDrag ?: ( delta : number ) => void
203+ onDrag ?: ( delta : number , isKeyboard : boolean ) => void
201204 onDragEnd ?: ( ) => void
202205 onDoubleClick ?: ( ) => void
203206}
@@ -224,11 +227,34 @@ const VerticalDivider: React.FC<React.PropsWithChildren<DividerProps & Draggable
224227 sx = { } ,
225228} ) => {
226229 const [ isDragging , setIsDragging ] = React . useState ( false )
230+ const [ isKeyboardDrag , setIsKeyboardDrag ] = React . useState ( false )
227231 const responsiveVariant = useResponsiveValue ( variant , 'none' )
228232
229233 const stableOnDrag = React . useRef ( onDrag )
230234 const stableOnDragEnd = React . useRef ( onDragEnd )
231235
236+ const { paneRef} = React . useContext ( PageLayoutContext )
237+
238+ const [ minWidth , setMinWidth ] = React . useState ( 0 )
239+ const [ maxWidth , setMaxWidth ] = React . useState ( 0 )
240+ const [ currentWidth , setCurrentWidth ] = React . useState ( 0 )
241+
242+ React . useEffect ( ( ) => {
243+ if ( paneRef ?. current !== null ) {
244+ const paneStyles = getComputedStyle ( paneRef ?. current as Element )
245+ const maxPaneWidthDiffPixels = paneStyles . getPropertyValue ( '--pane-max-width-diff' )
246+ const minWidthPixels = paneStyles . getPropertyValue ( '--pane-min-width' )
247+ const paneWidth = paneRef ?. current . getBoundingClientRect ( ) . width
248+ const maxPaneWidthDiff = Number ( maxPaneWidthDiffPixels . split ( 'px' ) [ 0 ] )
249+ const minPaneWidth = Number ( minWidthPixels . split ( 'px' ) [ 0 ] )
250+ const viewportWidth = window . innerWidth
251+ const maxPaneWidth = viewportWidth > maxPaneWidthDiff ? viewportWidth - maxPaneWidthDiff : viewportWidth
252+ setMinWidth ( minPaneWidth )
253+ setMaxWidth ( maxPaneWidth )
254+ setCurrentWidth ( paneWidth || 0 )
255+ }
256+ } , [ paneRef , isKeyboardDrag , isDragging ] )
257+
232258 React . useEffect ( ( ) => {
233259 stableOnDrag . current = onDrag
234260 } , [ onDrag ] )
@@ -239,7 +265,7 @@ const VerticalDivider: React.FC<React.PropsWithChildren<DividerProps & Draggable
239265
240266 React . useEffect ( ( ) => {
241267 function handleDrag ( event : MouseEvent ) {
242- stableOnDrag . current ?.( event . movementX )
268+ stableOnDrag . current ?.( event . movementX , false )
243269 event . preventDefault ( )
244270 }
245271
@@ -249,14 +275,38 @@ const VerticalDivider: React.FC<React.PropsWithChildren<DividerProps & Draggable
249275 event . preventDefault ( )
250276 }
251277
278+ function handleKeyDrag ( event : KeyboardEvent ) {
279+ let delta = 0
280+ // Hardcoded a delta for every key press to move the splitter. Should I perhaps expose this as a prop?
281+ if ( event . key === 'ArrowLeft' && currentWidth > minWidth ) {
282+ delta = - 3
283+ } else if ( event . key === 'ArrowRight' && currentWidth < maxWidth ) {
284+ delta = 3
285+ } else {
286+ return
287+ }
288+ setCurrentWidth ( currentWidth + delta )
289+ stableOnDrag . current ?.( delta , true )
290+ event . preventDefault ( )
291+ }
292+
293+ function handleKeyDragEnd ( event : KeyboardEvent ) {
294+ setIsKeyboardDrag ( false )
295+ stableOnDragEnd . current ?.( )
296+ event . preventDefault ( )
297+ }
252298 // TODO: Support touch events
253- if ( isDragging ) {
299+ if ( isDragging || isKeyboardDrag ) {
254300 window . addEventListener ( 'mousemove' , handleDrag )
301+ window . addEventListener ( 'keydown' , handleKeyDrag )
255302 window . addEventListener ( 'mouseup' , handleDragEnd )
303+ window . addEventListener ( 'keyup' , handleKeyDragEnd )
256304 document . body . setAttribute ( 'data-page-layout-dragging' , 'true' )
257305 } else {
258306 window . removeEventListener ( 'mousemove' , handleDrag )
259307 window . removeEventListener ( 'mouseup' , handleDragEnd )
308+ window . removeEventListener ( 'keydown' , handleKeyDrag )
309+ window . removeEventListener ( 'keyup' , handleKeyDragEnd )
260310 document . body . removeAttribute ( 'data-page-layout-dragging' )
261311 }
262312
@@ -265,7 +315,7 @@ const VerticalDivider: React.FC<React.PropsWithChildren<DividerProps & Draggable
265315 window . removeEventListener ( 'mouseup' , handleDragEnd )
266316 document . body . removeAttribute ( 'data-page-layout-dragging' )
267317 }
268- } , [ isDragging ] )
318+ } , [ isDragging , isKeyboardDrag ] )
269319
270320 return (
271321 < Box
@@ -286,17 +336,30 @@ const VerticalDivider: React.FC<React.PropsWithChildren<DividerProps & Draggable
286336 position : 'absolute' ,
287337 inset : '0 -2px' ,
288338 cursor : 'col-resize' ,
289- bg : isDragging ? 'accent.fg' : 'transparent' ,
339+ bg : isDragging || isKeyboardDrag ? 'accent.fg' : 'transparent' ,
290340 transitionDelay : '0.1s' ,
291341 '&:hover' : {
292- bg : isDragging ? 'accent.fg' : 'neutral.muted' ,
342+ bg : isDragging || isKeyboardDrag ? 'accent.fg' : 'neutral.muted' ,
293343 } ,
294344 } }
295345 role = "separator"
346+ aria-label = "Draggable splitter"
347+ aria-valuemin = { minWidth }
348+ aria-valuemax = { maxWidth }
349+ aria-orientation = "vertical"
350+ aria-valuenow = { currentWidth }
351+ aria-valuetext = { `Pane width ${ currentWidth } pixels` }
352+ tabIndex = { 0 }
296353 onMouseDown = { ( ) => {
297354 setIsDragging ( true )
298355 onDragStart ?.( )
299356 } }
357+ onKeyDown = { event => {
358+ if ( event . key === 'ArrowLeft' || event . key === 'ArrowRight' ) {
359+ setIsKeyboardDrag ( true )
360+ onDragStart ?.( )
361+ }
362+ } }
300363 onDoubleClick = { onDoubleClick }
301364 />
302365 < DraggingGlobalStyles />
@@ -593,6 +656,8 @@ const Pane = React.forwardRef<HTMLDivElement, React.PropsWithChildren<PageLayout
593656 const isHidden = useResponsiveValue ( responsiveHidden , false )
594657
595658 const { rowGap, columnGap, enableStickyPane, disableStickyPane} = React . useContext ( PageLayoutContext )
659+ let { paneRef} = React . useContext ( PageLayoutContext )
660+ paneRef = paneRef || useRef < HTMLDivElement > ( null )
596661
597662 React . useEffect ( ( ) => {
598663 if ( sticky ) {
@@ -637,55 +702,10 @@ const Pane = React.forwardRef<HTMLDivElement, React.PropsWithChildren<PageLayout
637702 }
638703 }
639704
640- const paneRef = React . useRef < HTMLDivElement > ( null )
641705 useRefObjectAsForwardedRef ( forwardRef , paneRef )
642706
643- const [ minPercent , setMinPercent ] = React . useState ( 0 )
644- const [ maxPercent , setMaxPercent ] = React . useState ( 0 )
645707 const hasOverflow = useOverflow ( paneRef )
646708
647- const measuredRef = React . useCallback ( ( ) => {
648- if ( paneRef . current !== null ) {
649- const maxPaneWidthDiffPixels = getComputedStyle ( paneRef . current as Element ) . getPropertyValue (
650- '--pane-max-width-diff' ,
651- )
652- const paneWidth = paneRef . current . getBoundingClientRect ( ) . width
653- const maxPaneWidthDiff = Number ( maxPaneWidthDiffPixels . split ( 'px' ) [ 0 ] )
654- const viewportWidth = window . innerWidth
655- const maxPaneWidth = viewportWidth > maxPaneWidthDiff ? viewportWidth - maxPaneWidthDiff : viewportWidth
656-
657- const minPercent = Math . round ( ( 100 * minWidth ) / viewportWidth )
658- setMinPercent ( minPercent )
659-
660- const maxPercent = Math . round ( ( 100 * maxPaneWidth ) / viewportWidth )
661- setMaxPercent ( maxPercent )
662-
663- const widthPercent = Math . round ( ( 100 * paneWidth ) / viewportWidth )
664- setWidthPercent ( widthPercent . toString ( ) )
665- }
666- } , [ paneRef , minWidth ] )
667-
668- const [ widthPercent , setWidthPercent ] = React . useState ( '' )
669- const [ prevPercent , setPrevPercent ] = React . useState ( '' )
670-
671- const handleWidthFormSubmit = ( event : React . FormEvent < HTMLElement > ) => {
672- event . preventDefault ( )
673- let percent = Number ( widthPercent )
674- if ( Number . isNaN ( percent ) ) {
675- percent = Number ( prevPercent ) || minPercent
676- } else if ( percent > maxPercent ) {
677- percent = maxPercent
678- } else if ( percent < minPercent ) {
679- percent = minPercent
680- }
681-
682- setWidthPercent ( percent . toString ( ) )
683- // Cache previous valid percent.
684- setPrevPercent ( percent . toString ( ) )
685-
686- updatePaneWidth ( ( percent / 100 ) * window . innerWidth )
687- }
688-
689709 const paneId = useId ( id )
690710
691711 const labelProp : { 'aria-labelledby' ?: string ; 'aria-label' ?: string } = { }
@@ -706,7 +726,6 @@ const Pane = React.forwardRef<HTMLDivElement, React.PropsWithChildren<PageLayout
706726
707727 return (
708728 < Box
709- ref = { measuredRef }
710729 // eslint-disable-next-line @typescript-eslint/no-explicit-any
711730 sx = { ( theme : any ) =>
712731 merge < BetterSystemStyleObject > (
@@ -756,14 +775,19 @@ const Pane = React.forwardRef<HTMLDivElement, React.PropsWithChildren<PageLayout
756775 // If pane is resizable, the divider should be draggable
757776 draggable = { resizable }
758777 sx = { { [ position === 'end' ? 'marginRight' : 'marginLeft' ] : SPACING_MAP [ columnGap ] } }
759- onDrag = { delta => {
778+ onDrag = { ( delta , isKeyboard = false ) => {
760779 // Get the number of pixels the divider was dragged
761- const deltaWithDirection = position === 'end' ? - delta : delta
780+ let deltaWithDirection
781+ if ( isKeyboard ) {
782+ deltaWithDirection = delta
783+ } else {
784+ deltaWithDirection = position === 'end' ? - delta : delta
785+ }
762786 updatePaneWidth ( paneWidth + deltaWithDirection )
763787 } }
764788 // Ensure `paneWidth` state and actual pane width are in sync when the drag ends
765789 onDragEnd = { ( ) => {
766- const paneRect = paneRef . current ?. getBoundingClientRect ( )
790+ const paneRect = paneRef ? .current ?. getBoundingClientRect ( )
767791 if ( ! paneRect ) return
768792 updatePaneWidth ( paneRect . width )
769793 } }
@@ -797,32 +821,6 @@ const Pane = React.forwardRef<HTMLDivElement, React.PropsWithChildren<PageLayout
797821 { ...labelProp }
798822 { ...( id && { id : paneId } ) }
799823 >
800- { resizable && (
801- // eslint-disable-next-line github/a11y-no-visually-hidden-interactive-element
802- < VisuallyHidden >
803- < form onSubmit = { handleWidthFormSubmit } >
804- < label htmlFor = { `${ paneId } -width-input` } > Pane width</ label >
805- < p id = { `${ paneId } -input-hint` } >
806- Use a value between { minPercent } % and { maxPercent } %
807- </ p >
808- < input
809- id = { `${ paneId } -width-input` }
810- aria-describedby = { `${ paneId } -input-hint` }
811- name = "pane-width"
812- inputMode = "numeric"
813- pattern = "[0-9]*"
814- value = { widthPercent }
815- autoCorrect = "off"
816- autoComplete = "off"
817- type = "text"
818- onChange = { event => {
819- setWidthPercent ( event . target . value )
820- } }
821- />
822- < button type = "submit" > Change width</ button >
823- </ form >
824- </ VisuallyHidden >
825- ) }
826824 { children }
827825 </ Box >
828826 </ Box >
0 commit comments