@primer/react
Version:
An implementation of GitHub's Primer Design System using React
361 lines (360 loc) • 13.6 kB
JavaScript
import { canUseDOM } from "../utils/environment.js";
import useIsomorphicLayoutEffect from "../utils/useIsomorphicLayoutEffect.js";
import PageLayout_module_css_default from "./PageLayout.module.css.js";
import { c } from "react-compiler-runtime";
import React, { startTransition } from "react";
//#region src/PageLayout/usePaneWidth.ts
/**
* Width value for the pane - defines constraints and defaults only.
* - `PaneWidth`: Preset size ('small' | 'medium' | 'large')
* - `CustomWidthOptions`: Explicit min/default/max constraints
*/
/**
* Default value for --pane-max-width-diff CSS variable.
* Imported from CSS to ensure JS fallback matches the CSS default.
*/
const DEFAULT_MAX_WIDTH_DIFF = Number(PageLayout_module_css_default.paneMaxWidthDiffDefault);
/**
* Default value for --sidebar-max-width-diff CSS variable.
* Unlike --pane-max-width-diff, this is constant across all viewport sizes.
*/
const DEFAULT_SIDEBAR_MAX_WIDTH_DIFF = Number(PageLayout_module_css_default.sidebarMaxWidthDiffDefault);
const WIDE_MAX_WIDTH_DIFF = Number(PageLayout_module_css_default.paneMaxWidthDiffWide);
const DEFAULT_PANE_MAX_WIDTH_DIFF_BREAKPOINT = Number(PageLayout_module_css_default.paneMaxWidthDiffBreakpoint);
/** Default widths for preset size options */
const defaultPaneWidth = {
small: 256,
medium: 296,
large: 320
};
const isCustomWidthOptions = (width) => {
return typeof width === "object" && "min" in width && "default" in width && "max" in width;
};
const isPaneWidth = (width) => {
return width === "small" || width === "medium" || width === "large";
};
const getDefaultPaneWidth = (w) => {
if (isPaneWidth(w)) return defaultPaneWidth[w];
else if (isCustomWidthOptions(w)) return parseInt(w.default, 10);
return 0;
};
/**
* Derives the --pane-max-width-diff value from viewport width alone.
* Avoids the expensive getComputedStyle call that forces a synchronous layout recalc.
* The CSS only defines two breakpoint-dependent values, so a simple width check is equivalent.
*/
function getMaxWidthDiffFromViewport() {
if (!canUseDOM) return DEFAULT_MAX_WIDTH_DIFF;
return window.innerWidth >= DEFAULT_PANE_MAX_WIDTH_DIFF_BREAKPOINT ? WIDE_MAX_WIDTH_DIFF : DEFAULT_MAX_WIDTH_DIFF;
}
const updateAriaValues = (handle, values) => {
if (!handle) return;
if (values.min !== void 0) handle.setAttribute("aria-valuemin", String(values.min));
if (values.max !== void 0) handle.setAttribute("aria-valuemax", String(values.max));
if (values.current !== void 0) {
handle.setAttribute("aria-valuenow", String(values.current));
handle.setAttribute("aria-valuetext", `Pane width ${values.current} pixels`);
}
};
const localStoragePersister = {
save: (key, width) => {
try {
localStorage.setItem(key, width.toString());
} catch {}
},
get: (key) => {
try {
const storedWidth = localStorage.getItem(key);
if (storedWidth !== null) {
const parsed = Number(storedWidth);
if (!isNaN(parsed) && parsed > 0) return Math.round(parsed);
}
} catch {}
return null;
}
};
/**
* Manages pane width state with storage persistence and viewport constraints.
* Handles initialization from storage, clamping on viewport resize, and provides
* functions to save and reset width.
*
* Storage behavior:
* - When `resizable` is `true` and `onResizeEnd` is not provided: Uses localStorage
* - When `onResizeEnd` is provided: Calls the callback instead of localStorage
* - When `resizable` is `false` or `undefined`: Not resizable, no persistence
*/
function usePaneWidth(t0) {
const $ = c(42);
const { width, minWidth, resizable, widthStorageKey, paneRef, handleRef, contentWrapperRef, constrainToViewport: t1, onResizeEnd, currentWidth: controlledWidth } = t0;
const constrainToViewport = t1 === void 0 ? false : t1;
const isCustomWidth = isCustomWidthOptions(width);
const minPaneWidth = isCustomWidth ? parseInt(width.min, 10) : minWidth;
const customMaxWidth = isCustomWidth ? parseInt(width.max, 10) : null;
const widthStorageKeyRef = React.useRef(widthStorageKey);
const onResizeEndRef = React.useRef(onResizeEnd);
let t2;
if ($[0] !== onResizeEnd || $[1] !== widthStorageKey) {
t2 = () => {
widthStorageKeyRef.current = widthStorageKey;
onResizeEndRef.current = onResizeEnd;
};
$[0] = onResizeEnd;
$[1] = widthStorageKey;
$[2] = t2;
} else t2 = $[2];
useIsomorphicLayoutEffect(t2);
const maxWidthDiffRef = React.useRef(constrainToViewport ? DEFAULT_SIDEBAR_MAX_WIDTH_DIFF : DEFAULT_MAX_WIDTH_DIFF);
let t3;
if ($[3] !== constrainToViewport || $[4] !== customMaxWidth || $[5] !== minPaneWidth) {
t3 = () => {
const viewportWidth = window.innerWidth;
const viewportMax = viewportWidth > 0 ? Math.max(minPaneWidth, viewportWidth - maxWidthDiffRef.current) : minPaneWidth;
if (customMaxWidth !== null) return constrainToViewport ? Math.min(customMaxWidth, viewportMax) : customMaxWidth;
return viewportMax;
};
$[3] = constrainToViewport;
$[4] = customMaxWidth;
$[5] = minPaneWidth;
$[6] = t3;
} else t3 = $[6];
const getMaxPaneWidth = t3;
let t4;
if ($[7] !== width) {
t4 = getDefaultPaneWidth(width);
$[7] = width;
$[8] = t4;
} else t4 = $[8];
const defaultWidth = t4;
let t5;
if ($[9] !== controlledWidth || $[10] !== defaultWidth || $[11] !== onResizeEnd || $[12] !== resizable || $[13] !== widthStorageKey) {
t5 = () => {
if (typeof controlledWidth === "number") return controlledWidth;
if (onResizeEnd === void 0 && resizable === true && widthStorageKey !== void 0) {
const storedWidth = localStoragePersister.get(widthStorageKey);
if (storedWidth !== null) return storedWidth;
}
return defaultWidth;
};
$[9] = controlledWidth;
$[10] = defaultWidth;
$[11] = onResizeEnd;
$[12] = resizable;
$[13] = widthStorageKey;
$[14] = t5;
} else t5 = $[14];
const [currentWidthState, setCurrentWidthState] = React.useState(t5);
const [prevDefaultWidth, setPrevDefaultWidth] = React.useState(defaultWidth);
const [prevControlledWidth, setPrevControlledWidth] = React.useState(controlledWidth);
const controlledWidthChanged = controlledWidth !== prevControlledWidth;
const defaultWidthChanged = defaultWidth !== prevDefaultWidth;
if (controlledWidthChanged) {
setPrevControlledWidth(controlledWidth);
if (typeof controlledWidth === "number") setCurrentWidthState(controlledWidth);
else if (prevControlledWidth !== void 0) setCurrentWidthState(defaultWidth);
}
if (defaultWidthChanged) {
setPrevDefaultWidth(defaultWidth);
if (controlledWidth === void 0 && !controlledWidthChanged) setCurrentWidthState(defaultWidth);
}
const currentWidth = controlledWidth !== null && controlledWidth !== void 0 ? controlledWidth : currentWidthState;
const currentWidthRef = React.useRef(currentWidth);
let t6;
if ($[15] !== customMaxWidth) {
t6 = () => customMaxWidth !== null && customMaxWidth !== void 0 ? customMaxWidth : 600;
$[15] = customMaxWidth;
$[16] = t6;
} else t6 = $[16];
const [maxPaneWidth, setMaxPaneWidth] = React.useState(t6);
const maxPaneWidthRef = React.useRef(maxPaneWidth);
let t7;
let t8;
if ($[17] !== currentWidth) {
t7 = () => {
currentWidthRef.current = currentWidth;
};
t8 = [currentWidth];
$[17] = currentWidth;
$[18] = t7;
$[19] = t8;
} else {
t7 = $[18];
t8 = $[19];
}
useIsomorphicLayoutEffect(t7, t8);
let t9;
if ($[20] !== width) {
t9 = () => getDefaultPaneWidth(width);
$[20] = width;
$[21] = t9;
} else t9 = $[21];
const getDefaultWidth = t9;
let t10;
if ($[22] !== resizable) {
t10 = (value) => {
const rounded = Math.round(value);
currentWidthRef.current = rounded;
startTransition(() => {
setCurrentWidthState(rounded);
});
if (onResizeEndRef.current) {
try {
onResizeEndRef.current(rounded);
} catch {}
return;
}
if (resizable && widthStorageKeyRef.current) localStoragePersister.save(widthStorageKeyRef.current, rounded);
};
$[22] = resizable;
$[23] = t10;
} else t10 = $[23];
const saveWidth = t10;
const getMaxPaneWidthRef = React.useRef(getMaxPaneWidth);
let t11;
if ($[24] !== getMaxPaneWidth) {
t11 = () => {
getMaxPaneWidthRef.current = getMaxPaneWidth;
};
$[24] = getMaxPaneWidth;
$[25] = t11;
} else t11 = $[25];
useIsomorphicLayoutEffect(t11);
let t12;
let t13;
if ($[26] !== constrainToViewport || $[27] !== contentWrapperRef || $[28] !== customMaxWidth || $[29] !== handleRef || $[30] !== minPaneWidth || $[31] !== paneRef || $[32] !== resizable) {
t12 = () => {
var _paneRef$current3;
if (!resizable) return;
let lastViewportWidth = window.innerWidth;
const syncAll = () => {
var _paneRef$current;
const currentViewportWidth = window.innerWidth;
const crossedBreakpoint = lastViewportWidth < DEFAULT_PANE_MAX_WIDTH_DIFF_BREAKPOINT && currentViewportWidth >= DEFAULT_PANE_MAX_WIDTH_DIFF_BREAKPOINT || lastViewportWidth >= DEFAULT_PANE_MAX_WIDTH_DIFF_BREAKPOINT && currentViewportWidth < DEFAULT_PANE_MAX_WIDTH_DIFF_BREAKPOINT;
lastViewportWidth = currentViewportWidth;
if (crossedBreakpoint) maxWidthDiffRef.current = getMaxWidthDiffFromViewport();
const actualMax = getMaxPaneWidthRef.current();
(_paneRef$current = paneRef.current) === null || _paneRef$current === void 0 || _paneRef$current.style.setProperty("--pane-max-width", `${actualMax}px`);
const wasClamped = currentWidthRef.current > actualMax;
if (wasClamped) {
var _paneRef$current2;
currentWidthRef.current = actualMax;
(_paneRef$current2 = paneRef.current) === null || _paneRef$current2 === void 0 || _paneRef$current2.style.setProperty("--pane-width", `${actualMax}px`);
}
updateAriaValues(handleRef.current, {
max: actualMax,
current: currentWidthRef.current
});
if (actualMax !== maxPaneWidthRef.current || wasClamped) {
maxPaneWidthRef.current = actualMax;
startTransition(() => {
setMaxPaneWidth(actualMax);
if (wasClamped) setCurrentWidthState(actualMax);
});
}
};
maxWidthDiffRef.current = getMaxWidthDiffFromViewport();
const initialMax = getMaxPaneWidthRef.current();
maxPaneWidthRef.current = initialMax;
(_paneRef$current3 = paneRef.current) === null || _paneRef$current3 === void 0 || _paneRef$current3.style.setProperty("--pane-max-width", `${initialMax}px`);
updateAriaValues(handleRef.current, {
min: minPaneWidth,
max: initialMax,
current: currentWidthRef.current
});
startTransition(() => {
setMaxPaneWidth(initialMax);
});
if (customMaxWidth !== null && !constrainToViewport) return;
let lastUpdateTime = 0;
let pendingUpdate = false;
let rafId = null;
let debounceId = null;
let isResizing = false;
const startResizeOptimizations = () => {
var _paneRef$current4, _contentWrapperRef$cu;
if (isResizing) return;
isResizing = true;
(_paneRef$current4 = paneRef.current) === null || _paneRef$current4 === void 0 || _paneRef$current4.setAttribute("data-dragging", "true");
(_contentWrapperRef$cu = contentWrapperRef.current) === null || _contentWrapperRef$cu === void 0 || _contentWrapperRef$cu.setAttribute("data-dragging", "true");
};
const endResizeOptimizations = () => {
var _paneRef$current5, _contentWrapperRef$cu2;
if (!isResizing) return;
isResizing = false;
(_paneRef$current5 = paneRef.current) === null || _paneRef$current5 === void 0 || _paneRef$current5.removeAttribute("data-dragging");
(_contentWrapperRef$cu2 = contentWrapperRef.current) === null || _contentWrapperRef$cu2 === void 0 || _contentWrapperRef$cu2.removeAttribute("data-dragging");
};
const handleResize = () => {
startResizeOptimizations();
const now = Date.now();
if (now - lastUpdateTime >= 16) {
lastUpdateTime = now;
syncAll();
} else if (!pendingUpdate) {
pendingUpdate = true;
rafId = requestAnimationFrame(() => {
pendingUpdate = false;
rafId = null;
lastUpdateTime = Date.now();
syncAll();
});
}
if (debounceId !== null) clearTimeout(debounceId);
debounceId = setTimeout(() => {
debounceId = null;
endResizeOptimizations();
}, 150);
};
window.addEventListener("resize", handleResize);
return () => {
if (rafId !== null) cancelAnimationFrame(rafId);
if (debounceId !== null) clearTimeout(debounceId);
endResizeOptimizations();
window.removeEventListener("resize", handleResize);
};
};
t13 = [
resizable,
customMaxWidth,
constrainToViewport,
minPaneWidth,
paneRef,
handleRef,
contentWrapperRef
];
$[26] = constrainToViewport;
$[27] = contentWrapperRef;
$[28] = customMaxWidth;
$[29] = handleRef;
$[30] = minPaneWidth;
$[31] = paneRef;
$[32] = resizable;
$[33] = t12;
$[34] = t13;
} else {
t12 = $[33];
t13 = $[34];
}
useIsomorphicLayoutEffect(t12, t13);
let t14;
if ($[35] !== currentWidth || $[36] !== getDefaultWidth || $[37] !== getMaxPaneWidth || $[38] !== maxPaneWidth || $[39] !== minPaneWidth || $[40] !== saveWidth) {
t14 = {
currentWidth,
currentWidthRef,
minPaneWidth,
maxPaneWidth,
getMaxPaneWidth,
saveWidth,
getDefaultWidth
};
$[35] = currentWidth;
$[36] = getDefaultWidth;
$[37] = getMaxPaneWidth;
$[38] = maxPaneWidth;
$[39] = minPaneWidth;
$[40] = saveWidth;
$[41] = t14;
} else t14 = $[41];
return t14;
}
//#endregion
export { DEFAULT_MAX_WIDTH_DIFF, DEFAULT_SIDEBAR_MAX_WIDTH_DIFF, defaultPaneWidth, getDefaultPaneWidth, getMaxWidthDiffFromViewport, isCustomWidthOptions, isPaneWidth, updateAriaValues, usePaneWidth };