From dacf38d97ed108bd89b08bfe285f004007764c3e Mon Sep 17 00:00:00 2001 From: Jeremy Alles Date: Tue, 20 Sep 2022 09:49:28 +0000 Subject: [PATCH 01/21] add resizable support to the PageLayout component --- src/PageLayout/PageLayout.stories.tsx | 62 +++++++++++++++++ src/PageLayout/PageLayout.tsx | 92 ++++++++++++++++++++++++-- src/PageLayout/useHorizontalResize.tsx | 77 +++++++++++++++++++++ 3 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 src/PageLayout/useHorizontalResize.tsx diff --git a/src/PageLayout/PageLayout.stories.tsx b/src/PageLayout/PageLayout.stories.tsx index 85e49f39924..ea772f20bb9 100644 --- a/src/PageLayout/PageLayout.stories.tsx +++ b/src/PageLayout/PageLayout.stories.tsx @@ -662,6 +662,68 @@ export const CustomStickyHeader: Story = args => ( ) +export const ResizablePane: Story = args => ( + + {args['Render header?'] ? ( + + ) : null} + + {args['Render pane?'] ? ( + + + + ) : null} + {args['Render footer?'] ? ( + + ) : null} + +) + CustomStickyHeader.argTypes = { sticky: { type: 'boolean', diff --git a/src/PageLayout/PageLayout.tsx b/src/PageLayout/PageLayout.tsx index 7f0ca096576..edff0f34ad0 100644 --- a/src/PageLayout/PageLayout.tsx +++ b/src/PageLayout/PageLayout.tsx @@ -1,8 +1,11 @@ import React from 'react' import {useStickyPaneHeight} from './useStickyPaneHeight' +import {useHorizontalResize} from './useHorizontalResize' import Box from '../Box' import {isResponsiveValue, ResponsiveValue, useResponsiveValue} from '../hooks/useResponsiveValue' +import {useRefObjectAsForwardedRef} from '../hooks/useRefObjectAsForwardedRef' import {BetterSystemStyleObject, merge, SxProp} from '../sx' +import {Theme} from '../ThemeProvider' const REGION_ORDER = { header: 0, @@ -105,6 +108,10 @@ Root.displayName = 'PageLayout' type DividerProps = { variant?: 'none' | 'line' | 'filled' | ResponsiveValue<'none' | 'line' | 'filled'> + canResize?: boolean + isResizing?: boolean + onClick?: (e: React.MouseEvent) => void + onMouseDown?: (e: React.MouseEvent) => void } & SxProp const horizontalDividerVariants = { @@ -177,18 +184,65 @@ const verticalDividerVariants = { } } -const VerticalDivider: React.FC> = ({variant = 'none', sx = {}}) => { +const VerticalDivider: React.FC> = ({ + variant = 'none', + canResize, + isResizing, + onClick, + onMouseDown, + sx = {} +}) => { const responsiveVariant = useResponsiveValue(variant, 'none') return ( ( { height: '100%', + position: 'relative', ...verticalDividerVariants[responsiveVariant] }, sx )} - /> + > + {canResize && ( + + + + )} + ) } @@ -370,6 +424,8 @@ export type PageLayoutPaneProps = { */ positionWhenNarrow?: 'inherit' | keyof typeof panePositions width?: keyof typeof paneWidths + canResizePane?: boolean + paneWidthStorageKey?: string padding?: keyof typeof SPACING_MAP divider?: 'none' | 'line' | ResponsiveValue<'none' | 'line', 'none' | 'line' | 'filled'> /** @@ -410,6 +466,8 @@ const Pane = React.forwardRef(null) + useRefObjectAsForwardedRef(forwardRef, paneRef) + + const {onMouseDown, onClick, isResizing, paneWidth} = useHorizontalResize( + canResizePane, + paneRef, + paneWidthStorageKey + ) + return ( - - + ({ + padding: SPACING_MAP[padding], + overflow: 'auto', + ...(paneWidth + ? { + width: `clamp(256px, ${paneWidth}px, 100vw - 511px)`, + [`@media screen and (min-width: ${theme.breakpoints[3]})`]: { + width: `clamp(256px, ${paneWidth}px, 100vw - 959px)` + } + } + : {width: paneWidths[width]}) + })} + > {children} diff --git a/src/PageLayout/useHorizontalResize.tsx b/src/PageLayout/useHorizontalResize.tsx new file mode 100644 index 00000000000..ab5b60db9a4 --- /dev/null +++ b/src/PageLayout/useHorizontalResize.tsx @@ -0,0 +1,77 @@ +import React, {useEffect, useCallback, useState, RefObject} from 'react' + +export function useHorizontalResize(enabled: boolean, paneRef: RefObject, storageKey?: string) { + const [isDragging, setIsDragging] = useState(false) + const [isResizing, setIsResizing] = useState(false) + const [paneWidth, setPaneWidth] = useState() + + useEffect(() => { + let hasValue = false + if (storageKey) { + const storedWidth = window.localStorage.getItem(storageKey) + if (storedWidth) { + const number = parseInt(storedWidth, 10) + if (!isNaN(number) && number > 0) { + hasValue = true + setPaneWidth(number) + } + } + } + + if (!hasValue && paneRef.current) { + setPaneWidth(paneRef.current.clientWidth) + } + }, [storageKey, setPaneWidth, paneRef]) + + const onMouseDown = useCallback( + (e: React.MouseEvent) => { + if (!enabled || e.button !== 0) return + setIsDragging(true) + }, + [enabled] + ) + + const onClick = useCallback( + (e: React.MouseEvent) => { + if (!enabled || e.detail !== 2) return + setPaneWidth(undefined) + setIsDragging(false) + }, + [enabled] + ) + + const onMouseMove = useCallback( + (e: MouseEvent) => { + if (isDragging) { + setIsResizing(true) + const newSize = e.clientX + setPaneWidth(newSize) + e.preventDefault() + } + }, + [isDragging] + ) + + const onMouseUp = useCallback(() => { + const newPosition = paneRef.current ? paneRef.current.clientWidth : 0 + if (storageKey && newPosition) { + window.localStorage.setItem(storageKey, newPosition.toString()) + } + setIsDragging(false) + setIsResizing(false) + }, [paneRef, storageKey]) + + useEffect(() => { + if (enabled) { + document.addEventListener('mousemove', onMouseMove) + document.addEventListener('mouseup', onMouseUp) + } + + return () => { + document.removeEventListener('mousemove', onMouseMove) + document.removeEventListener('mouseup', onMouseUp) + } + }) + + return {onMouseDown, onClick, isResizing, paneWidth} +} From 5e74d8bd5e30db5c9b8a587b5316c5b00b0ee162 Mon Sep 17 00:00:00 2001 From: Jeremy Alles Date: Tue, 20 Sep 2022 09:59:50 +0000 Subject: [PATCH 02/21] Add changeset file --- .changeset/polite-ligers-repair.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/polite-ligers-repair.md diff --git a/.changeset/polite-ligers-repair.md b/.changeset/polite-ligers-repair.md new file mode 100644 index 00000000000..7fe0e78c9b8 --- /dev/null +++ b/.changeset/polite-ligers-repair.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Add resizable support to the PageLayout component. From ffad694fda62589602178c22102e768603c3955c Mon Sep 17 00:00:00 2001 From: Jeremy Alles Date: Tue, 20 Sep 2022 12:26:00 +0000 Subject: [PATCH 03/21] update snapshot --- .../__snapshots__/PageLayout.test.tsx.snap | 12 ++++++++---- .../__snapshots__/SplitPageLayout.test.tsx.snap | 5 +++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/PageLayout/__snapshots__/PageLayout.test.tsx.snap b/src/PageLayout/__snapshots__/PageLayout.test.tsx.snap index afbd486ccc5..6c98a975a2c 100644 --- a/src/PageLayout/__snapshots__/PageLayout.test.tsx.snap +++ b/src/PageLayout/__snapshots__/PageLayout.test.tsx.snap @@ -55,9 +55,9 @@ exports[`PageLayout renders condensed layout 1`] = ` } .c10 { - width: 100%; padding: 0; overflow: auto; + width: 100%; } .c0 { @@ -105,6 +105,7 @@ exports[`PageLayout renders condensed layout 1`] = ` .c9 { height: 100%; + position: relative; display: none; margin-right: 16px; } @@ -321,14 +322,15 @@ exports[`PageLayout renders default layout 1`] = ` .c9 { height: 100%; + position: relative; display: none; margin-right: 16px; } .c10 { - width: 100%; padding: 0; overflow: auto; + width: 100%; } .c11 { @@ -589,14 +591,15 @@ exports[`PageLayout renders pane in different position when narrow 1`] = ` .c9 { height: 100%; + position: relative; display: none; margin-right: 16px; } .c10 { - width: 100%; padding: 0; overflow: auto; + width: 100%; } .c11 { @@ -839,9 +842,9 @@ exports[`PageLayout renders with dividers 1`] = ` } .c9 { - width: 100%; padding: 0; overflow: auto; + width: 100%; } .c10 { @@ -871,6 +874,7 @@ exports[`PageLayout renders with dividers 1`] = ` .c8 { height: 100%; + position: relative; display: none; margin-left: 16px; } diff --git a/src/SplitPageLayout/__snapshots__/SplitPageLayout.test.tsx.snap b/src/SplitPageLayout/__snapshots__/SplitPageLayout.test.tsx.snap index 954c2343267..7a2eede8d4f 100644 --- a/src/SplitPageLayout/__snapshots__/SplitPageLayout.test.tsx.snap +++ b/src/SplitPageLayout/__snapshots__/SplitPageLayout.test.tsx.snap @@ -101,14 +101,15 @@ exports[`SplitPageLayout renders default layout 1`] = ` .c9 { height: 100%; + position: relative; display: none; margin-left: 0; } .c10 { - width: 100%; padding: 16px; overflow: auto; + width: 100%; } .c11 { @@ -179,8 +180,8 @@ exports[`SplitPageLayout renders default layout 1`] = ` @media screen and (min-width:1012px) { .c10 { - width: 296px; padding: 24px; + width: 296px; } } From a5291c4b62d5fa53cbe4a70be3377bfdbdb23c72 Mon Sep 17 00:00:00 2001 From: Jeremy Alles Date: Fri, 23 Sep 2022 11:53:45 +0000 Subject: [PATCH 04/21] take client left position into account when pane is not left aligned --- src/PageLayout/useHorizontalResize.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PageLayout/useHorizontalResize.tsx b/src/PageLayout/useHorizontalResize.tsx index ab5b60db9a4..1e3ddde9a54 100644 --- a/src/PageLayout/useHorizontalResize.tsx +++ b/src/PageLayout/useHorizontalResize.tsx @@ -42,14 +42,15 @@ export function useHorizontalResize(enabled: boolean, paneRef: RefObject { - if (isDragging) { + if (isDragging && paneRef.current) { setIsResizing(true) - const newSize = e.clientX + const clientLeft = paneRef.current.getBoundingClientRect().left + const newSize = e.clientX - clientLeft setPaneWidth(newSize) e.preventDefault() } }, - [isDragging] + [isDragging, paneRef] ) const onMouseUp = useCallback(() => { From 53ca22a27b7695162d907878f2afcad785ed842d Mon Sep 17 00:00:00 2001 From: Jeremy Alles Date: Fri, 23 Sep 2022 12:01:59 +0000 Subject: [PATCH 05/21] fix resize behavior when pane is not at start position --- src/PageLayout/PageLayout.tsx | 1 + src/PageLayout/useHorizontalResize.tsx | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/PageLayout/PageLayout.tsx b/src/PageLayout/PageLayout.tsx index edff0f34ad0..48a538cab94 100644 --- a/src/PageLayout/PageLayout.tsx +++ b/src/PageLayout/PageLayout.tsx @@ -511,6 +511,7 @@ const Pane = React.forwardRef, storageKey?: string) { +export function useHorizontalResize( + enabled: boolean, + panePosition: 'start' | 'end', + paneRef: RefObject, + storageKey?: string +) { const [isDragging, setIsDragging] = useState(false) const [isResizing, setIsResizing] = useState(false) const [paneWidth, setPaneWidth] = useState() @@ -44,13 +49,20 @@ export function useHorizontalResize(enabled: boolean, paneRef: RefObject { if (isDragging && paneRef.current) { setIsResizing(true) - const clientLeft = paneRef.current.getBoundingClientRect().left - const newSize = e.clientX - clientLeft - setPaneWidth(newSize) + if (panePosition === 'start') { + const clientLeft = paneRef.current.getBoundingClientRect().left + const newSize = e.clientX - clientLeft + setPaneWidth(newSize) + } else { + const clientRight = paneRef.current.getBoundingClientRect().right + const newSize = clientRight - e.clientX + console.log(newSize) + setPaneWidth(newSize) + } e.preventDefault() } }, - [isDragging, paneRef] + [isDragging, panePosition, paneRef] ) const onMouseUp = useCallback(() => { From d12ba1913570b03bae4d2363872cd115dd95e889 Mon Sep 17 00:00:00 2001 From: Jeremy Alles Date: Fri, 23 Sep 2022 12:42:33 +0000 Subject: [PATCH 06/21] fix resize when there are column gaps --- src/PageLayout/useHorizontalResize.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/PageLayout/useHorizontalResize.tsx b/src/PageLayout/useHorizontalResize.tsx index c7d27281d63..77ba1793339 100644 --- a/src/PageLayout/useHorizontalResize.tsx +++ b/src/PageLayout/useHorizontalResize.tsx @@ -49,14 +49,18 @@ export function useHorizontalResize( (e: MouseEvent) => { if (isDragging && paneRef.current) { setIsResizing(true) + + const rect = paneRef.current.getBoundingClientRect() + const clientLeft = rect.left + const clientRight = rect.right + const paneCurrentWidth = rect.width + const marginRight = clientRight - paneCurrentWidth - clientLeft + if (panePosition === 'start') { - const clientLeft = paneRef.current.getBoundingClientRect().left - const newSize = e.clientX - clientLeft + const newSize = e.clientX - clientLeft - marginRight setPaneWidth(newSize) } else { - const clientRight = paneRef.current.getBoundingClientRect().right const newSize = clientRight - e.clientX - console.log(newSize) setPaneWidth(newSize) } e.preventDefault() From 8ef210fb1d5a1ddfe2028622840d4c5b13580c6e Mon Sep 17 00:00:00 2001 From: Jeremy Alles Date: Fri, 23 Sep 2022 12:42:46 +0000 Subject: [PATCH 07/21] use story debug values --- src/PageLayout/PageLayout.stories.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/PageLayout/PageLayout.stories.tsx b/src/PageLayout/PageLayout.stories.tsx index ea772f20bb9..3764db7b348 100644 --- a/src/PageLayout/PageLayout.stories.tsx +++ b/src/PageLayout/PageLayout.stories.tsx @@ -663,7 +663,7 @@ export const CustomStickyHeader: Story = args => ( ) export const ResizablePane: Story = args => ( - + {args['Render header?'] ? ( ( Date: Fri, 23 Sep 2022 12:53:34 +0000 Subject: [PATCH 08/21] decouple the resize handle from the vertical divider --- src/PageLayout/PageLayout.stories.tsx | 6 +- src/PageLayout/PageLayout.tsx | 140 ++++++++++++++----------- src/PageLayout/useHorizontalResize.tsx | 21 ++-- 3 files changed, 95 insertions(+), 72 deletions(-) diff --git a/src/PageLayout/PageLayout.stories.tsx b/src/PageLayout/PageLayout.stories.tsx index 3764db7b348..2ca0aa5a254 100644 --- a/src/PageLayout/PageLayout.stories.tsx +++ b/src/PageLayout/PageLayout.stories.tsx @@ -701,7 +701,11 @@ export const ResizablePane: Story = args => ( regular: args['Pane.position.regular'], wide: args['Pane.position.wide'] }} - divider="line" + divider={{ + narrow: args['Pane.divider.narrow'], + regular: args['Pane.divider.regular'], + wide: args['Pane.divider.wide'] + }} canResizePane={true} paneWidthStorageKey="primer-react.pane-width" > diff --git a/src/PageLayout/PageLayout.tsx b/src/PageLayout/PageLayout.tsx index 48a538cab94..37f14f1acb6 100644 --- a/src/PageLayout/PageLayout.tsx +++ b/src/PageLayout/PageLayout.tsx @@ -104,14 +104,68 @@ const Root: React.FC> = ({ Root.displayName = 'PageLayout' // ---------------------------------------------------------------------------- -// Divider (internal) +// ResizeHandle (internal) -type DividerProps = { - variant?: 'none' | 'line' | 'filled' | ResponsiveValue<'none' | 'line' | 'filled'> - canResize?: boolean +type ResizeHandleProps = { isResizing?: boolean onClick?: (e: React.MouseEvent) => void onMouseDown?: (e: React.MouseEvent) => void +} + +const ResizeHandle: React.FC> = ({isResizing, onClick, onMouseDown}) => { + return ( + + + + + + ) +} + +// ---------------------------------------------------------------------------- +// Divider (internal) + +type DividerProps = { + variant?: 'none' | 'line' | 'filled' | ResponsiveValue<'none' | 'line' | 'filled'> } & SxProp const horizontalDividerVariants = { @@ -184,65 +238,20 @@ const verticalDividerVariants = { } } -const VerticalDivider: React.FC> = ({ - variant = 'none', - canResize, - isResizing, - onClick, - onMouseDown, - sx = {} -}) => { +const VerticalDivider: React.FC> = ({variant = 'none', sx = {}}) => { const responsiveVariant = useResponsiveValue(variant, 'none') return ( ( { - height: '100%', - position: 'relative', + position: 'absolute', + top: 0, + bottom: 0, ...verticalDividerVariants[responsiveVariant] }, sx )} - > - {canResize && ( - - - - )} - + /> ) } @@ -506,6 +515,8 @@ const Pane = React.forwardRef(null) + const paneRef = React.useRef(null) useRefObjectAsForwardedRef(forwardRef, paneRef) @@ -513,11 +524,13 @@ const Pane = React.forwardRef merge( @@ -559,14 +572,17 @@ const Pane = React.forwardRef - + + + + {canResizePane && } + ({ diff --git a/src/PageLayout/useHorizontalResize.tsx b/src/PageLayout/useHorizontalResize.tsx index 77ba1793339..61ed1ec2eab 100644 --- a/src/PageLayout/useHorizontalResize.tsx +++ b/src/PageLayout/useHorizontalResize.tsx @@ -4,6 +4,7 @@ export function useHorizontalResize( enabled: boolean, panePosition: 'start' | 'end', paneRef: RefObject, + containerRef: RefObject, storageKey?: string ) { const [isDragging, setIsDragging] = useState(false) @@ -47,26 +48,28 @@ export function useHorizontalResize( const onMouseMove = useCallback( (e: MouseEvent) => { - if (isDragging && paneRef.current) { + if (isDragging && paneRef.current && containerRef.current) { setIsResizing(true) - const rect = paneRef.current.getBoundingClientRect() - const clientLeft = rect.left - const clientRight = rect.right - const paneCurrentWidth = rect.width - const marginRight = clientRight - paneCurrentWidth - clientLeft + const paneRect = paneRef.current.getBoundingClientRect() + const containerRect = containerRef.current.getBoundingClientRect() + + const paneCurrentWidth = paneRect.width + + const marginLeft = paneRect.left + const marginRight = containerRect.width - paneCurrentWidth if (panePosition === 'start') { - const newSize = e.clientX - clientLeft - marginRight + const newSize = e.clientX - marginLeft - marginRight setPaneWidth(newSize) } else { - const newSize = clientRight - e.clientX + const newSize = paneRect.right - e.clientX - marginRight setPaneWidth(newSize) } e.preventDefault() } }, - [isDragging, panePosition, paneRef] + [isDragging, panePosition, paneRef, containerRef] ) const onMouseUp = useCallback(() => { From bbff181187c0feaf2e243d82a3b39447bd6b209a Mon Sep 17 00:00:00 2001 From: Jeremy Alles Date: Mon, 26 Sep 2022 14:21:59 +0000 Subject: [PATCH 09/21] update snap file --- .../__snapshots__/PageLayout.test.tsx.snap | 118 ++++++++++++------ 1 file changed, 79 insertions(+), 39 deletions(-) diff --git a/src/PageLayout/__snapshots__/PageLayout.test.tsx.snap b/src/PageLayout/__snapshots__/PageLayout.test.tsx.snap index 6c98a975a2c..4d9b5917d03 100644 --- a/src/PageLayout/__snapshots__/PageLayout.test.tsx.snap +++ b/src/PageLayout/__snapshots__/PageLayout.test.tsx.snap @@ -55,6 +55,13 @@ exports[`PageLayout renders condensed layout 1`] = ` } .c10 { + position: absolute; + top: 0; + bottom: 0; + display: none; +} + +.c11 { padding: 0; overflow: auto; width: 100%; @@ -104,13 +111,12 @@ exports[`PageLayout renders condensed layout 1`] = ` } .c9 { - height: 100%; position: relative; - display: none; + height: 100%; margin-right: 16px; } -.c11 { +.c12 { -webkit-order: 4; -ms-flex-order: 4; order: 4; @@ -119,13 +125,13 @@ exports[`PageLayout renders condensed layout 1`] = ` } @media screen and (min-width:768px) { - .c10 { + .c11 { width: 256px; } } @media screen and (min-width:1012px) { - .c10 { + .c11 { width: 296px; } } @@ -199,15 +205,19 @@ exports[`PageLayout renders condensed layout 1`] = ` />
+ > +
+
Pane