@workday/canvas-kit-react
Version:
The parent module that contains all Workday Canvas Kit React components
137 lines (136 loc) • 7.1 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.usePopupStack = void 0;
const react_1 = __importDefault(require("react"));
const canvas_kit_popup_stack_1 = require("@workday/canvas-kit-popup-stack");
const common_1 = require("@workday/canvas-kit-react/common");
/**
* **Note:** If you're using {@link Popper}, you do not need to use this hook directly.
*
* This hook will add the `stackRef` element to the {@link PopupStack} on mount and remove on unmount. If
* you use `Popper`, the popper `stackRef` is automatically added/removed from the `PopupStack`. The
* `PopupStack` is required for proper z-index values to ensure Popups are rendered correct. It is
* also required for global listeners like click outside or escape key closing a popup. Without the
* `PopupStack`, all popups will close rather than only the topmost one.
*
* If `ref` is provided, it will be the same as `stackRef`. If `ref` is not provided`,
* this hook will create one and return it.
*
* This hook should be used by all stacked UIs unless using the `Popper` component.
*
* ```tsx
* const model = usePopupModel();
* usePopupStack(model.state.stackRef, model.state.targetRef);
*
* // add some popup functionality
* useCloseOnOutsideClick(model);
* useCloseOnEscape(model);
*
* return (
* <>
* <button ref={model.state.targetRef}>Open Popup</button>
* {model.state.visibility !== 'hidden'
* ? ReactDOM.createPortal(<div>Popup Contents</div>, model.state.stackRef.current)
* : null}
* </>
* );
* ```
*
* @param ref This ref will be managed by the PopupStack and should not be managed by React. Do
* not apply this stackRef to a React element. Doing so will result in an error. Instead, use this
* `stackRef` directly with `ReactDOM.createPortal(<YourComponent>, stackRef.current!)`. This is
* definitely strange for React code, but is necessary for the PopupStack to remain framework
* agnostic and flexible to integrate with existing `PopupStack` systems. If not provided, this hook
* will create one and return that `stackRef` instead.
* @param target Usually the trigger of a popup. This will fix `bringToTop` and should be provided
* by all ephemeral-type popups (like Tooltips, Select menus, etc). It will also add in clickOutside
* detection.
*/
const usePopupStack = (ref, target) => {
const { elementRef, localRef } = (0, common_1.useLocalRef)(ref);
// Read brand style from the context provided by CanvasProvider
const style = react_1.default.useContext(common_1.CanvasBrandStyleContext);
const firstLoadRef = react_1.default.useRef(true); // React 19 can call a useState more than once, so we need to track if we've already created a container
// useState function input ensures we only create a container once.
const [popupRef] = react_1.default.useState(() => {
if (firstLoadRef.current) {
const container = canvas_kit_popup_stack_1.PopupStack.createContainer();
elementRef(container);
firstLoadRef.current = false;
return container;
}
return localRef.current;
});
// Forward only CSS custom properties to the popup container when a theme was provided via
// CanvasProvider. We do NOT apply defaultBranding (className) so we don't create a cascade
// barrier. Filter to `--*` keys and string values so consumer layout styles from the provider
// are not copied onto the popup stack. Runs before PopupStack.add to avoid a theme flash.
react_1.default.useLayoutEffect(() => {
const element = localRef.current;
if (!element) {
return undefined;
}
const styleKeys = Object.keys(style).filter(key => key.startsWith('--'));
if (styleKeys.length === 0) {
return undefined;
}
for (const key of styleKeys) {
const value = style[key];
if (typeof value !== 'string') {
continue;
}
element.style.setProperty(key, value);
}
// No cleanup: leave theme on container so reopening doesn't flash
return undefined;
}, [localRef, style]);
// We useLayoutEffect to ensure proper timing of registration of the element to the popup stack.
// Without this, the timing is unpredictable when mixed with other frameworks. Other frameworks
// should also register as soon as the element is available
react_1.default.useLayoutEffect(() => {
if (popupRef !== localRef.current) {
throw Error(`The 'ref' passed to usePopupStack should not be applied to a React element. This will cause a runtime error where the PopupStack and React compete for the element. Instead use ReactDOM.createPortal(<YourComponent />, ref.current)`);
}
const targetEl = target
? 'current' in target
? target.current || undefined
: target
: undefined;
const element = localRef.current;
canvas_kit_popup_stack_1.PopupStack.add({ element: element, owner: targetEl });
return () => {
canvas_kit_popup_stack_1.PopupStack.remove(element);
};
}, [localRef, target, popupRef]);
// The direction will properly follow the theme via React context, but portals lose the `dir`
// hierarchy, so we'll add it back here. When there's no target (e.g. consumer doesn't use
// Popup.Target), find the nearest element with a `dir` attribute: start from the focused element
// (the trigger) or body, then use closest('[dir]'). Prefer reading getAttribute('dir') when
// present to avoid getComputedStyle.
react_1.default.useLayoutEffect(() => {
var _a, _b, _c;
const targetEl = target ? ('current' in target ? target.current : target) : undefined;
let elementToCheck = targetEl !== null && targetEl !== void 0 ? targetEl : undefined;
if (elementToCheck == null && typeof document !== 'undefined') {
const active = document.activeElement;
const container = localRef.current;
const start = active && container && !container.contains(active) ? active : document.body;
elementToCheck = (_a = start.closest('[dir]')) !== null && _a !== void 0 ? _a : document.documentElement;
}
if (elementToCheck) {
const explicitDir = elementToCheck.getAttribute('dir');
const isRTL = explicitDir != null ? explicitDir.toLowerCase() === 'rtl' : (0, common_1.isElementRTL)(elementToCheck);
if (isRTL) {
(_b = localRef.current) === null || _b === void 0 ? void 0 : _b.setAttribute('dir', 'rtl');
}
else {
(_c = localRef.current) === null || _c === void 0 ? void 0 : _c.setAttribute('dir', 'ltr');
}
}
}, [localRef, target]);
return localRef;
};
exports.usePopupStack = usePopupStack;