@wordpress/components
Version:
UI components for WordPress.
141 lines (133 loc) • 4.89 kB
JavaScript
/**
* External dependencies
*/
import * as Ariakit from '@ariakit/react';
import clsx from 'clsx';
/**
* WordPress dependencies
*/
import warning from '@wordpress/warning';
import { forwardRef, useLayoutEffect, useState } from '@wordpress/element';
import { useMergeRefs } from '@wordpress/compose';
/**
* Internal dependencies
*/
import { useTabsContext } from './context';
import { StyledTabList } from './styles';
import { useTrackElementOffsetRect } from '../utils/element-rect';
import { useTrackOverflow } from './use-track-overflow';
import { useAnimatedOffsetRect } from '../utils/hooks/use-animated-offset-rect';
import { jsx as _jsx } from "react/jsx-runtime";
const DEFAULT_SCROLL_MARGIN = 24;
/**
* Scrolls a given parent element so that a given rect is visible.
*
* The scroll is updated initially and whenever the rect changes.
*/
function useScrollRectIntoView(parent, rect, {
margin = DEFAULT_SCROLL_MARGIN
} = {}) {
useLayoutEffect(() => {
if (!parent || !rect) {
return;
}
const {
scrollLeft: parentScroll
} = parent;
const parentWidth = parent.getBoundingClientRect().width;
const {
left: childLeft,
width: childWidth
} = rect;
const parentRightEdge = parentScroll + parentWidth;
const childRightEdge = childLeft + childWidth;
const rightOverflow = childRightEdge + margin - parentRightEdge;
const leftOverflow = parentScroll - (childLeft - margin);
let scrollLeft = null;
if (leftOverflow > 0) {
scrollLeft = parentScroll - leftOverflow;
} else if (rightOverflow > 0) {
scrollLeft = parentScroll + rightOverflow;
}
if (scrollLeft !== null) {
/**
* The optional chaining is used here to avoid unit test failures.
* It can be removed when JSDOM supports `Element` scroll methods.
* See: https://github.com/WordPress/gutenberg/pull/66498#issuecomment-2441146096
*/
parent.scroll?.({
left: scrollLeft
});
}
}, [margin, parent, rect]);
}
export const TabList = forwardRef(function TabList({
children,
...otherProps
}, ref) {
var _useTabsContext;
const {
store
} = (_useTabsContext = useTabsContext()) !== null && _useTabsContext !== void 0 ? _useTabsContext : {};
const selectedId = Ariakit.useStoreState(store, 'selectedId');
const activeId = Ariakit.useStoreState(store, 'activeId');
const selectOnMove = Ariakit.useStoreState(store, 'selectOnMove');
const items = Ariakit.useStoreState(store, 'items');
const [parent, setParent] = useState();
const refs = useMergeRefs([ref, setParent]);
const selectedItem = store?.item(selectedId);
const renderedItems = Ariakit.useStoreState(store, 'renderedItems');
const selectedItemIndex = renderedItems && selectedItem ? renderedItems.indexOf(selectedItem) : -1;
// Use selectedItemIndex as a dependency to force recalculation when the
// selected item index changes (elements are swapped / added / removed).
const selectedRect = useTrackElementOffsetRect(selectedItem?.element, [selectedItemIndex]);
// Track overflow to show scroll hints.
const overflow = useTrackOverflow(parent, {
first: items?.at(0)?.element,
last: items?.at(-1)?.element
});
// Size, position, and animate the indicator.
useAnimatedOffsetRect(parent, selectedRect, {
prefix: 'selected',
dataAttribute: 'indicator-animated',
transitionEndFilter: event => event.pseudoElement === '::before',
roundRect: true
});
// Make sure selected tab is scrolled into view.
useScrollRectIntoView(parent, selectedRect);
const onBlur = () => {
if (!selectOnMove) {
return;
}
// When automatic tab selection is on, make sure that the active tab is up
// to date with the selected tab when leaving the tablist. This makes sure
// that the selected tab will receive keyboard focus when tabbing back into
// the tablist.
if (selectedId !== activeId) {
store?.setActiveId(selectedId);
}
};
if (!store) {
globalThis.SCRIPT_DEBUG === true ? warning('`Tabs.TabList` must be wrapped in a `Tabs` component.') : void 0;
return null;
}
return /*#__PURE__*/_jsx(StyledTabList, {
ref: refs,
store: store,
render: props => {
var _props$tabIndex;
return /*#__PURE__*/_jsx("div", {
...props,
// Fallback to -1 to prevent browsers from making the tablist
// tabbable when it is a scrolling container.
tabIndex: (_props$tabIndex = props.tabIndex) !== null && _props$tabIndex !== void 0 ? _props$tabIndex : -1
});
},
onBlur: onBlur,
"data-select-on-move": selectOnMove ? 'true' : 'false',
...otherProps,
className: clsx(overflow.first && 'is-overflowing-first', overflow.last && 'is-overflowing-last', otherProps.className),
children: children
});
});
//# sourceMappingURL=tablist.js.map