@primer/react
Version:
An implementation of GitHub's Primer Design System using React
936 lines (896 loc) • 32 kB
JavaScript
import React, { useRef, memo } from 'react';
import { clsx } from 'clsx';
import { useId } from '../hooks/useId.js';
import { isResponsiveValue } from '../hooks/useResponsiveValue.js';
import { useSlots } from '../hooks/useSlots.js';
import { useOverflow } from '../hooks/useOverflow.js';
import { warning } from '../utils/warning.js';
import { getResponsiveAttributes } from '../internal/utils/getResponsiveAttributes.js';
import classes from './PageLayout.module.css.js';
import { usePaneWidth, isPaneWidth, isCustomWidthOptions, updateAriaValues, ARROW_KEY_STEP } from './usePaneWidth.js';
import { setDraggingStyles, removeDraggingStyles } from './paneUtils.js';
import { jsx, jsxs } from 'react/jsx-runtime';
import { useMergedRefs } from '../hooks/useMergedRefs.js';
const isArrowKey = key => key === 'ArrowLeft' || key === 'ArrowRight' || key === 'ArrowUp' || key === 'ArrowDown';
const isShrinkKey = key => key === 'ArrowLeft' || key === 'ArrowDown';
const PageLayoutContext = /*#__PURE__*/React.createContext({
padding: 'normal',
rowGap: 'normal',
columnGap: 'normal',
paneRef: {
current: null
},
contentWrapperRef: {
current: null
},
sidebarRef: {
current: null
},
sidebarContentWrapperRef: {
current: null
}
});
// ----------------------------------------------------------------------------
// PageLayout
// TODO: refs
const Root = ({
containerWidth = 'xlarge',
padding = 'normal',
rowGap = 'normal',
columnGap = 'normal',
children,
className,
style,
_slotsConfig: slotsConfig
}) => {
const paneRef = useRef(null);
const contentWrapperRef = useRef(null);
const sidebarRef = useRef(null);
const sidebarContentWrapperRef = useRef(null);
const [slots, rest] = useSlots(children, slotsConfig !== null && slotsConfig !== void 0 ? slotsConfig : {
header: Header,
footer: Footer,
sidebar: Sidebar
});
const memoizedContextValue = React.useMemo(() => {
return {
padding,
rowGap,
columnGap,
paneRef,
contentWrapperRef,
sidebarRef,
sidebarContentWrapperRef
};
}, [padding, rowGap, columnGap, paneRef, contentWrapperRef, sidebarRef, sidebarContentWrapperRef]);
return /*#__PURE__*/jsx(PageLayoutContext.Provider, {
value: memoizedContextValue,
children: /*#__PURE__*/jsxs(RootWrapper, {
style: style,
padding: padding,
className: className,
hasSidebar: !!slots.sidebar,
children: [slots.sidebar, /*#__PURE__*/jsxs("div", {
ref: sidebarContentWrapperRef,
className: classes.PageLayoutWrapper,
"data-width": containerWidth,
children: [slots.header, /*#__PURE__*/jsx("div", {
className: clsx(classes.PageLayoutContent),
children: rest
}), slots.footer]
})]
})
});
};
Root.displayName = "Root";
const RootWrapper = /*#__PURE__*/memo(({
style,
padding,
children,
className,
hasSidebar
}) => {
return /*#__PURE__*/jsx("div", {
style: {
'--spacing': `var(--spacing-${padding})`,
...style
},
className: clsx(classes.PageLayoutRoot, className),
"data-has-sidebar": hasSidebar || undefined,
children: children
});
});
Root.displayName = 'PageLayout';
// ----------------------------------------------------------------------------
// Divider (internal)
const HorizontalDivider = /*#__PURE__*/memo(({
variant = 'none',
className,
position,
style
}) => {
const {
padding
} = React.useContext(PageLayoutContext);
return /*#__PURE__*/jsx("div", {
className: clsx(classes.HorizontalDivider, className),
...getResponsiveAttributes('variant', variant),
...getResponsiveAttributes('position', position),
style: {
'--spacing-divider': `var(--spacing-${padding})`,
...style
}
});
});
HorizontalDivider.displayName = 'HorizontalDivider';
const VerticalDivider = /*#__PURE__*/memo(({
variant = 'none',
position,
className,
style,
children
}) => {
return /*#__PURE__*/jsx("div", {
className: clsx(classes.VerticalDivider, className),
...getResponsiveAttributes('variant', variant),
...getResponsiveAttributes('position', position),
style: style,
children: children
});
});
VerticalDivider.displayName = 'VerticalDivider';
const SidebarDivider = /*#__PURE__*/memo(function SidebarDivider({
position,
divider,
resizable,
minPaneWidth,
maxPaneWidth,
currentWidth,
currentWidthRef,
handleRef,
sidebarRef,
dragStartClientXRef,
dragStartWidthRef,
dragMaxWidthRef,
getMaxPaneWidth,
getDefaultWidth,
saveWidth
}) {
const {
columnGap
} = React.useContext(PageLayoutContext);
return /*#__PURE__*/jsx(VerticalDivider, {
variant: resizable ? 'line' : divider,
position: position,
className: classes.SidebarVerticalDivider,
style: {
'--spacing': `var(--spacing-${columnGap})`
},
children: resizable ? /*#__PURE__*/jsx(DragHandle, {
handleRef: handleRef,
"aria-valuemin": minPaneWidth,
"aria-valuemax": maxPaneWidth,
"aria-valuenow": currentWidth,
onDragStart: clientX => {
var _sidebarRef$current$g, _sidebarRef$current;
dragStartClientXRef.current = clientX;
dragStartWidthRef.current = (_sidebarRef$current$g = (_sidebarRef$current = sidebarRef.current) === null || _sidebarRef$current === void 0 ? void 0 : _sidebarRef$current.getBoundingClientRect().width) !== null && _sidebarRef$current$g !== void 0 ? _sidebarRef$current$g : currentWidthRef.current;
dragMaxWidthRef.current = getMaxPaneWidth();
},
onDrag: (value, isKeyboard) => {
const maxWidth = isKeyboard ? getMaxPaneWidth() : dragMaxWidthRef.current;
if (isKeyboard) {
// For position='end': invert the delta so arrow keys feel natural
// ArrowRight should shrink (move divider right), ArrowLeft should expand
const delta = position === 'end' ? -value : value;
const newWidth = Math.max(minPaneWidth, Math.min(maxWidth, currentWidthRef.current + delta));
if (newWidth !== currentWidthRef.current) {
var _sidebarRef$current2;
currentWidthRef.current = newWidth;
(_sidebarRef$current2 = sidebarRef.current) === null || _sidebarRef$current2 === void 0 ? void 0 : _sidebarRef$current2.style.setProperty('--pane-width', `${newWidth}px`);
updateAriaValues(handleRef.current, {
current: newWidth,
max: maxWidth
});
}
} else {
if (sidebarRef.current) {
const deltaX = value - dragStartClientXRef.current;
// For position='end': cursor moving left (negative delta) increases width
// For position='start': cursor moving right (positive delta) increases width
const directedDelta = position === 'end' ? -deltaX : deltaX;
const newWidth = dragStartWidthRef.current + directedDelta;
const clampedWidth = Math.max(minPaneWidth, Math.min(maxWidth, newWidth));
if (Math.round(clampedWidth) !== Math.round(currentWidthRef.current)) {
sidebarRef.current.style.setProperty('--pane-width', `${clampedWidth}px`);
currentWidthRef.current = clampedWidth;
updateAriaValues(handleRef.current, {
current: Math.round(clampedWidth),
max: maxWidth
});
}
}
}
},
onDragEnd: () => {
saveWidth(currentWidthRef.current);
},
onDoubleClick: () => {
const resetWidth = getDefaultWidth();
if (sidebarRef.current) {
sidebarRef.current.style.setProperty('--pane-width', `${resetWidth}px`);
currentWidthRef.current = resetWidth;
updateAriaValues(handleRef.current, {
current: resetWidth
});
}
saveWidth(resetWidth);
}
}) : null
});
});
/**
* DragHandle - handles all pointer and keyboard interactions for resizing
* ARIA values are set in JSX for SSR accessibility,
* then updated via DOM manipulation during drag for performance
*/
const DragHandle = /*#__PURE__*/memo(function DragHandle({
handleRef,
onDragStart,
onDrag,
onDragEnd,
onDoubleClick,
'aria-valuemin': ariaValueMin,
'aria-valuemax': ariaValueMax,
'aria-valuenow': ariaValueNow
}) {
const stableOnDragStart = React.useRef(onDragStart);
const stableOnDrag = React.useRef(onDrag);
const stableOnDragEnd = React.useRef(onDragEnd);
React.useEffect(() => {
stableOnDragStart.current = onDragStart;
stableOnDrag.current = onDrag;
stableOnDragEnd.current = onDragEnd;
});
const {
paneRef,
contentWrapperRef
} = React.useContext(PageLayoutContext);
// Dragging state as a ref - cheaper than reading from DOM style
const isDraggingRef = React.useRef(false);
// Set inline styles for drag optimizations - zero overhead at rest
const startDragging = React.useCallback(() => {
if (isDraggingRef.current) return;
setDraggingStyles({
handle: handleRef.current,
pane: paneRef.current,
contentWrapper: contentWrapperRef.current
});
isDraggingRef.current = true;
}, [handleRef, contentWrapperRef, paneRef]);
const endDragging = React.useCallback(() => {
if (!isDraggingRef.current) return;
removeDraggingStyles({
handle: handleRef.current,
pane: paneRef.current,
contentWrapper: contentWrapperRef.current
});
isDraggingRef.current = false;
}, [handleRef, contentWrapperRef, paneRef]);
/**
* Pointer down starts a drag operation
* Capture the pointer to continue receiving events outside the handle area
*/
const handlePointerDown = React.useCallback(event => {
if (event.button !== 0) return;
event.preventDefault();
const target = event.currentTarget;
// Try to capture pointer - may fail in test environments or if pointer is already released
try {
target.setPointerCapture(event.pointerId);
} catch {
// Ignore - pointer capture is a nice-to-have for dragging outside the element
}
stableOnDragStart.current(event.clientX);
startDragging();
}, [startDragging]);
// Simple rAF throttle - one update per frame, latest position wins
const rafIdRef = React.useRef(null);
const pendingClientXRef = React.useRef(null);
/**
* Pointer move during drag
* Calls onDrag with absolute cursor X position
* Uses rAF to coalesce updates to one per frame
*/
const handlePointerMove = React.useCallback(event => {
if (!isDraggingRef.current) return;
event.preventDefault();
// Store latest position - only the final position before rAF fires matters
pendingClientXRef.current = event.clientX;
// Schedule update if not already scheduled
if (rafIdRef.current === null) {
rafIdRef.current = requestAnimationFrame(() => {
rafIdRef.current = null;
if (pendingClientXRef.current !== null) {
stableOnDrag.current(pendingClientXRef.current, false);
pendingClientXRef.current = null;
}
});
}
}, []);
/**
* Pointer up - cleanup is handled by onLostPointerCapture event
* which fires when pointer capture is released (including on pointerup)
*/
const handlePointerUp = React.useCallback(event => {
if (!isDraggingRef.current) return;
event.preventDefault();
}, []);
/**
* Lost pointer capture ends a drag operation
* Cleans up dragging state and cancels any pending rAF
* Calls onDragEnd callback
*/
const handleLostPointerCapture = React.useCallback(() => {
if (!isDraggingRef.current) return;
// Cancel any pending rAF to prevent stale updates
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current);
rafIdRef.current = null;
pendingClientXRef.current = null;
}
endDragging();
stableOnDragEnd.current();
}, [endDragging]);
/**
* Keyboard handling for accessibility
* Arrow keys adjust the pane size in 3px increments
* Prevents default scrolling behavior
* Sets and clears dragging state via data attribute
* Calls onDrag
*/
const handleKeyDown = React.useCallback(event => {
if (!isArrowKey(event.key)) return;
event.preventDefault();
// https://github.com/github/accessibility/issues/5101#issuecomment-1822870655
const delta = isShrinkKey(event.key) ? -ARROW_KEY_STEP : ARROW_KEY_STEP;
// Only set dragging on first keydown (not repeats)
if (!isDraggingRef.current) {
startDragging();
}
stableOnDrag.current(delta, true);
}, [startDragging]);
const handleKeyUp = React.useCallback(event => {
if (!isArrowKey(event.key)) return;
event.preventDefault();
endDragging();
stableOnDragEnd.current();
}, [endDragging]);
// Cleanup rAF on unmount to prevent stale callbacks
React.useEffect(() => {
return () => {
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current);
rafIdRef.current = null;
}
};
}, []);
return /*#__PURE__*/jsx("div", {
ref: handleRef,
className: classes.DraggableHandle,
role: "slider",
"aria-label": "Draggable pane splitter",
"aria-valuemin": ariaValueMin,
"aria-valuemax": ariaValueMax,
"aria-valuenow": ariaValueNow,
"aria-valuetext": ariaValueNow !== undefined ? `Pane width ${ariaValueNow} pixels` : undefined,
tabIndex: 0,
onPointerDown: handlePointerDown,
onPointerMove: handlePointerMove,
onPointerUp: handlePointerUp,
onLostPointerCapture: handleLostPointerCapture,
onKeyDown: handleKeyDown,
onKeyUp: handleKeyUp,
onDoubleClick: onDoubleClick
});
});
// ----------------------------------------------------------------------------
// PageLayout.Header
const Header = ({
'aria-label': label,
'aria-labelledby': labelledBy,
padding = 'none',
divider = 'none',
dividerWhenNarrow = 'inherit',
hidden = false,
children,
style,
className
}) => {
// Combine divider and dividerWhenNarrow for backwards compatibility
const dividerProp = !isResponsiveValue(divider) && dividerWhenNarrow !== 'inherit' ? {
regular: divider,
narrow: dividerWhenNarrow
} : divider;
const {
rowGap
} = React.useContext(PageLayoutContext);
return /*#__PURE__*/jsxs("header", {
"aria-label": label,
"aria-labelledby": labelledBy,
...getResponsiveAttributes('hidden', hidden),
className: clsx(classes.Header, className),
style: {
'--spacing': `var(--spacing-${rowGap})`,
...style
},
children: [/*#__PURE__*/jsx("div", {
className: classes.HeaderContent,
style: {
'--spacing': `var(--spacing-${padding})`
},
children: children
}), /*#__PURE__*/jsx(HorizontalDivider, {
variant: dividerProp,
className: classes.HeaderHorizontalDivider,
style: {
'--spacing': `var(--spacing-${rowGap})`
}
})]
});
};
Header.displayName = "Header";
Header.displayName = 'PageLayout.Header';
// ----------------------------------------------------------------------------
// PageLayout.Content
// TODO: Account for pane width when centering content
const Content = ({
as = 'main',
'aria-label': label,
'aria-labelledby': labelledBy,
width = 'full',
padding = 'none',
hidden = false,
children,
className,
style
}) => {
const Component = as;
const {
contentWrapperRef
} = React.useContext(PageLayoutContext);
return /*#__PURE__*/jsx(Component, {
ref: contentWrapperRef,
"aria-label": label,
"aria-labelledby": labelledBy,
style: style,
className: clsx(classes.ContentWrapper, className),
...getResponsiveAttributes('is-hidden', hidden),
children: /*#__PURE__*/jsx("div", {
className: classes.Content,
"data-width": width,
style: {
'--spacing': `var(--spacing-${padding})`
},
children: children
})
});
};
Content.displayName = "Content";
Content.displayName = 'PageLayout.Content';
// ----------------------------------------------------------------------------
// PageLayout.Pane
const overflowProps = {
tabIndex: 0,
role: 'region'
};
const Pane = /*#__PURE__*/React.forwardRef(({
'aria-label': label,
'aria-labelledby': labelledBy,
position: responsivePosition = 'end',
positionWhenNarrow = 'inherit',
width = 'medium',
minWidth = 256,
currentWidth: controlledWidth,
onResizeEnd,
padding = 'none',
resizable = false,
widthStorageKey = 'paneWidth',
divider: responsiveDivider = 'none',
dividerWhenNarrow = 'inherit',
sticky = false,
offsetHeader = 0,
hidden: responsiveHidden = false,
children,
id,
className,
style
}, forwardRef) => {
// Combine position and positionWhenNarrow for backwards compatibility
const positionProp = !isResponsiveValue(responsivePosition) && positionWhenNarrow !== 'inherit' ? {
regular: responsivePosition,
narrow: positionWhenNarrow
} : responsivePosition;
// Combine divider and dividerWhenNarrow for backwards compatibility
const dividerProp = !isResponsiveValue(responsiveDivider) && dividerWhenNarrow !== 'inherit' ? {
regular: responsiveDivider,
narrow: dividerWhenNarrow
} : responsiveDivider;
// For components that need responsive values in JavaScript logic, we'll use a fallback value
// The actual responsive behavior will be handled by CSS through data attributes
const position = isResponsiveValue(positionProp) ? 'end' : positionProp;
const dividerVariant = isResponsiveValue(dividerProp) ? 'none' : dividerProp;
const {
rowGap,
columnGap,
paneRef,
contentWrapperRef
} = React.useContext(PageLayoutContext);
// Ref to the drag handle for updating ARIA attributes
const handleRef = React.useRef(null);
// Cache drag start values to calculate relative delta during drag
// This approach is immune to layout shifts (scrollbars appearing/disappearing)
const dragStartClientXRef = React.useRef(0);
const dragStartWidthRef = React.useRef(0);
// Cache max width at drag start - won't change during a drag operation
const dragMaxWidthRef = React.useRef(0);
const {
currentWidth,
currentWidthRef,
minPaneWidth,
maxPaneWidth,
getMaxPaneWidth,
saveWidth,
getDefaultWidth
} = usePaneWidth({
width,
minWidth,
resizable,
widthStorageKey,
paneRef,
handleRef,
contentWrapperRef,
onResizeEnd,
currentWidth: controlledWidth
});
const mergedPaneRef = useMergedRefs(forwardRef, paneRef);
const hasOverflow = useOverflow(paneRef);
const paneId = useId(id);
const labelProp = {};
if (hasOverflow) {
process.env.NODE_ENV !== "production" ? warning(label === undefined && labelledBy === undefined, 'The <PageLayout.Pane> has overflow and `aria-label` or `aria-labelledby` has not been set. ' + 'Please provide `aria-label` or `aria-labelledby` to <PageLayout.Pane> in order to label this ' + 'region.') : void 0;
if (labelledBy) {
labelProp['aria-labelledby'] = labelledBy;
} else if (label) {
labelProp['aria-label'] = label;
}
}
return /*#__PURE__*/jsxs("div", {
className: clsx(classes.PaneWrapper, className),
style: {
'--offset-header': typeof offsetHeader === 'number' ? `${offsetHeader}px` : offsetHeader,
'--spacing-row': `var(--spacing-${rowGap})`,
'--spacing-column': `var(--spacing-${columnGap})`,
...style
},
...getResponsiveAttributes('is-hidden', responsiveHidden),
...getResponsiveAttributes('position', positionProp),
"data-sticky": sticky || undefined,
children: [/*#__PURE__*/jsx(HorizontalDivider, {
variant: isResponsiveValue(dividerProp) ? dividerProp : {
narrow: dividerVariant,
regular: 'none'
},
className: classes.PaneHorizontalDivider,
style: {
'--spacing': `var(--spacing-${rowGap})`
},
position: positionProp
}), /*#__PURE__*/jsx("div", {
ref: mergedPaneRef
// Suppress hydration mismatch for --pane-width when localStorage
// provides a width that differs from the server-rendered default.
// Not needed when onResizeEnd is provided (localStorage isn't read).
,
suppressHydrationWarning: resizable === true && !onResizeEnd,
...(hasOverflow ? overflowProps : {}),
...labelProp,
...(id && {
id: paneId
}),
className: classes.Pane,
"data-resizable": resizable || undefined,
style: {
'--spacing': `var(--spacing-${padding})`,
'--pane-min-width': isCustomWidthOptions(width) ? width.min : `${minWidth}px`,
'--pane-max-width': isCustomWidthOptions(width) ? width.max : `calc(100vw - var(--pane-max-width-diff))`,
'--pane-width-custom': isCustomWidthOptions(width) ? width.default : undefined,
'--pane-width-size': `var(--pane-width-${isPaneWidth(width) ? width : 'custom'})`,
'--pane-width': `${currentWidth}px`
},
children: children
}), /*#__PURE__*/jsx(VerticalDivider, {
variant: isResponsiveValue(dividerProp) ? {
narrow: 'none',
regular: resizable ? 'line' : dividerProp.regular || 'none',
wide: resizable ? 'line' : dividerProp.wide || dividerProp.regular || 'none'
} : {
narrow: 'none',
// If pane is resizable, always show a vertical divider on regular viewports
regular: resizable ? 'line' : dividerVariant
}
// If pane is resizable, the divider should be draggable
,
draggable: resizable,
position: positionProp,
className: classes.PaneVerticalDivider,
style: {
'--spacing': `var(--spacing-${columnGap})`
},
children: resizable ? /*#__PURE__*/jsx(DragHandle, {
handleRef: handleRef,
"aria-valuemin": minPaneWidth,
"aria-valuemax": maxPaneWidth,
"aria-valuenow": currentWidth,
onDragStart: clientX => {
var _paneRef$current$getB, _paneRef$current;
// Cache cursor position and pane width at drag start
// Using relative delta (current - start) is immune to layout shifts
// (e.g., scrollbars appearing/disappearing during drag)
dragStartClientXRef.current = clientX;
dragStartWidthRef.current = (_paneRef$current$getB = (_paneRef$current = paneRef.current) === null || _paneRef$current === void 0 ? void 0 : _paneRef$current.getBoundingClientRect().width) !== null && _paneRef$current$getB !== void 0 ? _paneRef$current$getB : currentWidthRef.current;
// Cache max width - won't change during drag
dragMaxWidthRef.current = getMaxPaneWidth();
},
onDrag: (value, isKeyboard) => {
// Use cached max width for pointer drag, fresh value for keyboard (less frequent)
const maxWidth = isKeyboard ? getMaxPaneWidth() : dragMaxWidthRef.current;
if (isKeyboard) {
// Keyboard: value is a delta (e.g., +3 or -3)
const delta = value;
const newWidth = Math.max(minPaneWidth, Math.min(maxWidth, currentWidthRef.current + delta));
if (newWidth !== currentWidthRef.current) {
var _paneRef$current2;
currentWidthRef.current = newWidth;
(_paneRef$current2 = paneRef.current) === null || _paneRef$current2 === void 0 ? void 0 : _paneRef$current2.style.setProperty('--pane-width', `${newWidth}px`);
updateAriaValues(handleRef.current, {
current: newWidth,
max: maxWidth
});
}
} else {
// Pointer: value is clientX - calculate width using relative delta from drag start
// This approach is immune to layout shifts during drag
if (paneRef.current) {
const deltaX = value - dragStartClientXRef.current;
// For position='end': cursor moving left (negative delta) increases width
// For position='start': cursor moving right (positive delta) increases width
const directedDelta = position === 'end' ? -deltaX : deltaX;
const newWidth = dragStartWidthRef.current + directedDelta;
const clampedWidth = Math.max(minPaneWidth, Math.min(maxWidth, newWidth));
// Only update if width actually changed
if (Math.round(clampedWidth) !== Math.round(currentWidthRef.current)) {
paneRef.current.style.setProperty('--pane-width', `${clampedWidth}px`);
currentWidthRef.current = clampedWidth;
updateAriaValues(handleRef.current, {
current: Math.round(clampedWidth),
max: maxWidth
});
}
}
}
},
onDragEnd: () => {
// Sync React state so parent re-renders use the correct width
saveWidth(currentWidthRef.current);
},
onDoubleClick: () => {
const resetWidth = getDefaultWidth();
if (paneRef.current) {
paneRef.current.style.setProperty('--pane-width', `${resetWidth}px`);
currentWidthRef.current = resetWidth;
updateAriaValues(handleRef.current, {
current: resetWidth
});
}
saveWidth(resetWidth);
}
}) : null
})]
});
});
Pane.displayName = 'PageLayout.Pane';
// ----------------------------------------------------------------------------
// PageLayout.Footer
// ----------------------------------------------------------------------------
// PageLayout.Sidebar
const Sidebar = /*#__PURE__*/React.forwardRef(({
'aria-label': label,
'aria-labelledby': labelledBy,
position = 'start',
width = 'medium',
minWidth = 256,
padding = 'none',
resizable = false,
widthStorageKey,
divider = 'none',
sticky = false,
responsiveVariant = 'default',
hidden: responsiveHidden = false,
children,
id,
className,
style
}, forwardRef) => {
const {
columnGap,
sidebarRef,
sidebarContentWrapperRef
} = React.useContext(PageLayoutContext);
// Ref to the drag handle for updating ARIA attributes
const handleRef = React.useRef(null);
// Cache drag start values to calculate relative delta during drag
const dragStartClientXRef = React.useRef(0);
const dragStartWidthRef = React.useRef(0);
const dragMaxWidthRef = React.useRef(0);
const {
currentWidth,
currentWidthRef,
minPaneWidth,
maxPaneWidth,
getMaxPaneWidth,
saveWidth,
getDefaultWidth
} = usePaneWidth({
width,
minWidth,
resizable,
widthStorageKey,
paneRef: sidebarRef,
handleRef,
contentWrapperRef: sidebarContentWrapperRef,
constrainToViewport: true
});
const mergedSidebarRef = useMergedRefs(forwardRef, sidebarRef);
const hasOverflow = useOverflow(sidebarRef);
const sidebarId = useId(id);
const labelProp = {};
if (hasOverflow) {
process.env.NODE_ENV !== "production" ? warning(label === undefined && labelledBy === undefined, 'The <PageLayout.Sidebar> has overflow and `aria-label` or `aria-labelledby` has not been set. ' + 'Please provide `aria-label` or `aria-labelledby` to <PageLayout.Sidebar> in order to label this ' + 'region.') : void 0;
if (labelledBy) {
labelProp['aria-labelledby'] = labelledBy;
} else if (label) {
labelProp['aria-label'] = label;
}
}
return /*#__PURE__*/jsxs("div", {
className: clsx(classes.SidebarWrapper, className),
style: {
'--spacing-column': `var(--spacing-${columnGap})`,
...style
},
...getResponsiveAttributes('is-hidden', responsiveHidden),
"data-position": position,
"data-sticky": sticky || undefined,
"data-responsive-variant": responsiveVariant !== 'default' ? responsiveVariant : undefined,
children: [position === 'end' && /*#__PURE__*/jsx(SidebarDivider, {
position: position,
divider: divider,
resizable: resizable,
minPaneWidth: minPaneWidth,
maxPaneWidth: maxPaneWidth,
currentWidth: currentWidth,
currentWidthRef: currentWidthRef,
handleRef: handleRef,
sidebarRef: sidebarRef,
dragStartClientXRef: dragStartClientXRef,
dragStartWidthRef: dragStartWidthRef,
dragMaxWidthRef: dragMaxWidthRef,
getMaxPaneWidth: getMaxPaneWidth,
getDefaultWidth: getDefaultWidth,
saveWidth: saveWidth
}), /*#__PURE__*/jsx("div", {
ref: mergedSidebarRef
// Suppress hydration mismatch for --pane-width when localStorage
// provides a width that differs from the server-rendered default.
,
suppressHydrationWarning: resizable === true && !!widthStorageKey,
...(hasOverflow ? overflowProps : {}),
...labelProp,
...(id && {
id: sidebarId
}),
className: classes.Sidebar,
"data-resizable": resizable || undefined,
style: {
'--spacing': `var(--spacing-${padding})`,
'--pane-min-width': isCustomWidthOptions(width) ? width.min : `${minWidth}px`,
'--pane-max-width': isCustomWidthOptions(width) ? width.max : `calc(100vw - var(--sidebar-max-width-diff))`,
'--pane-width-custom': isCustomWidthOptions(width) ? width.default : undefined,
'--pane-width-size': `var(--pane-width-${isPaneWidth(width) ? width : 'custom'})`,
'--pane-width': `${currentWidth}px`
},
children: children
}), position === 'start' && /*#__PURE__*/jsx(SidebarDivider, {
position: position,
divider: divider,
resizable: resizable,
minPaneWidth: minPaneWidth,
maxPaneWidth: maxPaneWidth,
currentWidth: currentWidth,
currentWidthRef: currentWidthRef,
handleRef: handleRef,
sidebarRef: sidebarRef,
dragStartClientXRef: dragStartClientXRef,
dragStartWidthRef: dragStartWidthRef,
dragMaxWidthRef: dragMaxWidthRef,
getMaxPaneWidth: getMaxPaneWidth,
getDefaultWidth: getDefaultWidth,
saveWidth: saveWidth
})]
});
});
Sidebar.displayName = 'PageLayout.Sidebar';
// ----------------------------------------------------------------------------
// PageLayout.Footer
const Footer = ({
'aria-label': label,
'aria-labelledby': labelledBy,
padding = 'none',
divider = 'none',
dividerWhenNarrow = 'inherit',
hidden = false,
children,
className,
style
}) => {
// Combine divider and dividerWhenNarrow for backwards compatibility
const dividerProp = !isResponsiveValue(divider) && dividerWhenNarrow !== 'inherit' ? {
regular: divider,
narrow: dividerWhenNarrow
} : divider;
const {
rowGap
} = React.useContext(PageLayoutContext);
return /*#__PURE__*/jsxs("footer", {
"aria-label": label,
"aria-labelledby": labelledBy,
...getResponsiveAttributes('hidden', hidden),
className: clsx(classes.FooterWrapper, className),
style: {
'--spacing': `var(--spacing-${rowGap})`,
...style
},
children: [/*#__PURE__*/jsx(HorizontalDivider, {
className: classes.FooterHorizontalDivider,
style: {
'--spacing': `var(--spacing-${rowGap})`
},
variant: dividerProp
}), /*#__PURE__*/jsx("div", {
className: classes.FooterContent,
style: {
'--spacing': `var(--spacing-${padding})`
},
children: children
})]
});
};
Footer.displayName = "Footer";
Footer.displayName = 'PageLayout.Footer';
// ----------------------------------------------------------------------------
// Export
const PageLayout = Object.assign(Root, {
__SLOT__: Symbol('PageLayout'),
Header,
Content,
Pane: Pane,
Sidebar: Sidebar,
Footer
});
Header.__SLOT__ = Symbol('PageLayout.Header');
Content.__SLOT__ = Symbol('PageLayout.Content');
Pane.__SLOT__ = Symbol('PageLayout.Pane');
Sidebar.__SLOT__ = Symbol('PageLayout.Sidebar');
Footer.__SLOT__ = Symbol('PageLayout.Footer');
export { PageLayout };