Skip to content

Commit d887586

Browse files
radglobericwbailey
andauthored
Make resizable SplitPageLayout.Pane keyboard accessible. (#2593)
* Added tabIndex declaration and onKeyDown event handler to VerticalDivider so resizable panes are keyboard accessible. * Create selfish-clocks-hug.md * Add input for screen reader to allow users to input specific pane width percentage. * Linting. * Use `useSSRSafeId` to generate id for panel width input. * Fix min and max percentage calculations. * Clamp min and max percents, revert to last known good value if unable to parse input. * Pane width form should not be visible. * Fixing some type errors. * Updated jest snapshots. * Handle case where screen was not as wide as the possible diff value. * Linting. * Fix issue where merged css was breaking resizable panes. * Updated snapshots. * Remove tabindex-based keyboard navigation for resizable panes. * Update src/PageLayout/PageLayout.tsx Co-authored-by: Eric Bailey <ericwbailey@users.noreply.github.com> * Disable eslint for label to make exception for screen reader edge cases. * Use useId instead of useSSRSafeId. * Updated snapshots. Co-authored-by: Eric Bailey <ericwbailey@users.noreply.github.com>
1 parent 3a8bb76 commit d887586

4 files changed

Lines changed: 102 additions & 12 deletions

File tree

.changeset/selfish-clocks-hug.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/react": minor
3+
---
4+
5+
Make resizable SplitPageLayout.Pane keyboard accessible.

src/PageLayout/PageLayout.tsx

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import React from 'react'
22
import {createGlobalStyle} from 'styled-components'
33
import Box from '../Box'
4+
import {useId} from '../hooks/useId'
45
import {useRefObjectAsForwardedRef} from '../hooks/useRefObjectAsForwardedRef'
56
import {isResponsiveValue, ResponsiveValue, useResponsiveValue} from '../hooks/useResponsiveValue'
67
import {BetterSystemStyleObject, merge, SxProp} from '../sx'
78
import {Theme} from '../ThemeProvider'
89
import createSlots from '../utils/create-slots'
910
import {canUseDOM} from '../utils/environment'
11+
import VisuallyHidden from '../_VisuallyHidden'
1012
import {useStickyPaneHeight} from './useStickyPaneHeight'
1113

1214
const {Slots, Slot} = createSlots(['Header', 'Footer'])
@@ -506,6 +508,7 @@ export type PageLayoutPaneProps = {
506508
sticky?: boolean
507509
offsetHeader?: string | number
508510
hidden?: boolean | ResponsiveValue<boolean>
511+
id?: string
509512
} & SxProp
510513

511514
const panePositions = {
@@ -536,6 +539,7 @@ const Pane = React.forwardRef<HTMLDivElement, React.PropsWithChildren<PageLayout
536539
offsetHeader = 0,
537540
hidden: responsiveHidden = false,
538541
children,
542+
id,
539543
sx = {},
540544
},
541545
forwardRef,
@@ -597,8 +601,57 @@ const Pane = React.forwardRef<HTMLDivElement, React.PropsWithChildren<PageLayout
597601
const paneRef = React.useRef<HTMLDivElement>(null)
598602
useRefObjectAsForwardedRef(forwardRef, paneRef)
599603

604+
const MIN_PANE_WIDTH = 256 // 256px, related to `--pane-min-width CSS var.
605+
const [minPercent, setMinPercent] = React.useState(0)
606+
const [maxPercent, setMaxPercent] = React.useState(0)
607+
608+
const measuredRef = React.useCallback(() => {
609+
if (paneRef.current !== null) {
610+
const maxPaneWidthDiffPixels = getComputedStyle(paneRef.current as Element).getPropertyValue(
611+
'--pane-max-width-diff',
612+
)
613+
const paneWidth = paneRef.current.getBoundingClientRect().width
614+
const maxPaneWidthDiff = Number(maxPaneWidthDiffPixels.split('px')[0])
615+
const viewportWidth = window.innerWidth
616+
const maxPaneWidth = viewportWidth > maxPaneWidthDiff ? viewportWidth - maxPaneWidthDiff : viewportWidth
617+
618+
const minPercent = Math.round((100 * MIN_PANE_WIDTH) / viewportWidth)
619+
setMinPercent(minPercent)
620+
621+
const maxPercent = Math.round((100 * maxPaneWidth) / viewportWidth)
622+
setMaxPercent(maxPercent)
623+
624+
const widthPercent = Math.round((100 * paneWidth) / viewportWidth)
625+
setWidthPercent(widthPercent.toString())
626+
}
627+
}, [paneRef])
628+
629+
const [widthPercent, setWidthPercent] = React.useState('')
630+
const [prevPercent, setPrevPercent] = React.useState('')
631+
632+
const handleWidthFormSubmit = (event: React.FormEvent<HTMLElement>) => {
633+
event.preventDefault()
634+
let percent = Number(widthPercent)
635+
if (Number.isNaN(percent)) {
636+
percent = Number(prevPercent) || minPercent
637+
} else if (percent > maxPercent) {
638+
percent = maxPercent
639+
} else if (percent < minPercent) {
640+
percent = minPercent
641+
}
642+
643+
setWidthPercent(percent.toString())
644+
// Cache previous valid percent.
645+
setPrevPercent(percent.toString())
646+
647+
updatePaneWidth((percent / 100) * window.innerWidth)
648+
}
649+
650+
const paneId = useId(id)
651+
600652
return (
601653
<Box
654+
ref={measuredRef}
602655
// eslint-disable-next-line @typescript-eslint/no-explicit-any
603656
sx={(theme: any) =>
604657
merge<BetterSystemStyleObject>(
@@ -671,18 +724,45 @@ const Pane = React.forwardRef<HTMLDivElement, React.PropsWithChildren<PageLayout
671724
}}
672725
sx={(theme: Theme) => ({
673726
'--pane-min-width': `256px`,
674-
'--pane-max-width': `calc(100vw - 511px)`,
727+
'--pane-max-width-diff': '511px',
728+
'--pane-max-width': `calc(100vw - var(--pane-max-width-diff))`,
675729
width: resizable
676730
? ['100%', null, 'clamp(var(--pane-min-width), var(--pane-width), var(--pane-max-width))']
677731
: paneWidths[width],
678732
padding: SPACING_MAP[padding],
679733
overflow: [null, null, 'auto'],
680734

681735
[`@media screen and (min-width: ${theme.breakpoints[3]})`]: {
682-
'--pane-max-width': 'calc(100vw - 959px)',
736+
'--pane-max-width-diff': '959px',
683737
},
684738
})}
685739
>
740+
{resizable && (
741+
<VisuallyHidden>
742+
<form onSubmit={handleWidthFormSubmit}>
743+
{/* eslint-disable-next-line jsx-a11y/label-has-for */}
744+
<label htmlFor={`${paneId}-width-input`}>Pane width</label>
745+
<p id={`${paneId}-input-hint`}>
746+
Use a value between {minPercent}% and {maxPercent}%
747+
</p>
748+
<input
749+
id={`${paneId}-width-input`}
750+
aria-describedby={`${paneId}-input-hint`}
751+
name="pane-width"
752+
inputMode="numeric"
753+
pattern="[0-9]*"
754+
value={widthPercent}
755+
autoCorrect="off"
756+
autoComplete="off"
757+
type="text"
758+
onChange={event => {
759+
setWidthPercent(event.target.value)
760+
}}
761+
/>
762+
<button type="submit">Change width</button>
763+
</form>
764+
</VisuallyHidden>
765+
)}
686766
{children}
687767
</Box>
688768
</Box>

src/PageLayout/__snapshots__/PageLayout.test.tsx.snap

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ exports[`PageLayout renders condensed layout 1`] = `
6666
6767
.c11 {
6868
--pane-min-width: 256px;
69-
--pane-max-width: calc(100vw - 511px);
69+
--pane-max-width-diff: 511px;
70+
--pane-max-width: calc(100vw - var(--pane-max-width-diff));
7071
width: 100%;
7172
padding: 0;
7273
}
@@ -145,7 +146,7 @@ exports[`PageLayout renders condensed layout 1`] = `
145146
146147
@media screen and (min-width:1280px) {
147148
.c11 {
148-
--pane-max-width: calc(100vw - 959px);
149+
--pane-max-width-diff: 959px;
149150
}
150151
}
151152
@@ -348,7 +349,8 @@ exports[`PageLayout renders default layout 1`] = `
348349
349350
.c11 {
350351
--pane-min-width: 256px;
351-
--pane-max-width: calc(100vw - 511px);
352+
--pane-max-width-diff: 511px;
353+
--pane-max-width: calc(100vw - var(--pane-max-width-diff));
352354
width: 100%;
353355
padding: 0;
354356
}
@@ -437,7 +439,7 @@ exports[`PageLayout renders default layout 1`] = `
437439
438440
@media screen and (min-width:1280px) {
439441
.c11 {
440-
--pane-max-width: calc(100vw - 959px);
442+
--pane-max-width-diff: 959px;
441443
}
442444
}
443445
@@ -641,7 +643,8 @@ exports[`PageLayout renders pane in different position when narrow 1`] = `
641643
642644
.c11 {
643645
--pane-min-width: 256px;
644-
--pane-max-width: calc(100vw - 511px);
646+
--pane-max-width-diff: 511px;
647+
--pane-max-width: calc(100vw - var(--pane-max-width-diff));
645648
width: 100%;
646649
padding: 0;
647650
}
@@ -730,7 +733,7 @@ exports[`PageLayout renders pane in different position when narrow 1`] = `
730733
731734
@media screen and (min-width:1280px) {
732735
.c11 {
733-
--pane-max-width: calc(100vw - 959px);
736+
--pane-max-width-diff: 959px;
734737
}
735738
}
736739
@@ -910,7 +913,8 @@ exports[`PageLayout renders with dividers 1`] = `
910913
911914
.c10 {
912915
--pane-min-width: 256px;
913-
--pane-max-width: calc(100vw - 511px);
916+
--pane-max-width-diff: 511px;
917+
--pane-max-width: calc(100vw - var(--pane-max-width-diff));
914918
width: 100%;
915919
padding: 0;
916920
}
@@ -999,7 +1003,7 @@ exports[`PageLayout renders with dividers 1`] = `
9991003
10001004
@media screen and (min-width:1280px) {
10011005
.c10 {
1002-
--pane-max-width: calc(100vw - 959px);
1006+
--pane-max-width-diff: 959px;
10031007
}
10041008
}
10051009

src/SplitPageLayout/__snapshots__/SplitPageLayout.test.tsx.snap

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ exports[`SplitPageLayout renders default layout 1`] = `
101101
102102
.c11 {
103103
--pane-min-width: 256px;
104-
--pane-max-width: calc(100vw - 511px);
104+
--pane-max-width-diff: 511px;
105+
--pane-max-width: calc(100vw - var(--pane-max-width-diff));
105106
width: 100%;
106107
padding: 16px;
107108
}
@@ -186,7 +187,7 @@ exports[`SplitPageLayout renders default layout 1`] = `
186187
187188
@media screen and (min-width:1280px) {
188189
.c11 {
189-
--pane-max-width: calc(100vw - 959px);
190+
--pane-max-width-diff: 959px;
190191
}
191192
}
192193

0 commit comments

Comments
 (0)