feature/IO-3624-Shop-Config-UX-Refresh - Add Quick Select Jump
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import { Select } from "antd";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./shop-info.section-navigator.styles.scss";
|
||||
|
||||
const HIGHLIGHT_CLASS = "shop-info-section-navigator__target--active";
|
||||
|
||||
export default function ShopInfoSectionNavigator({ tabsRef, activeTabKey }) {
|
||||
const { t } = useTranslation();
|
||||
const targetMapRef = useRef(new Map());
|
||||
const highlightedTargetRef = useRef(null);
|
||||
const [options, setOptions] = useState([]);
|
||||
const [selectedSection, setSelectedSection] = useState(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const tabsContainer = tabsRef.current;
|
||||
if (!tabsContainer) return undefined;
|
||||
|
||||
let animationFrameId = 0;
|
||||
|
||||
const refreshOptions = () => {
|
||||
const activePane = tabsContainer.querySelector(".ant-tabs-tabpane-active");
|
||||
if (!activePane) {
|
||||
targetMapRef.current = new Map();
|
||||
setOptions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextTargetMap = new Map();
|
||||
const nextOptions = Array.from(activePane.querySelectorAll(".imex-form-row"))
|
||||
.filter((card) => {
|
||||
const titleNode = getOwnCardTitleNode(card);
|
||||
if (!titleNode?.textContent?.trim()) return false;
|
||||
|
||||
const ancestorCard = card.parentElement?.closest(".imex-form-row");
|
||||
return !ancestorCard || !activePane.contains(ancestorCard);
|
||||
})
|
||||
.map((card, index) => {
|
||||
const label = getOwnCardTitleNode(card)?.textContent?.trim();
|
||||
const value = `${activeTabKey}-shop-info-section-${index}`;
|
||||
|
||||
nextTargetMap.set(value, card);
|
||||
|
||||
return {
|
||||
label,
|
||||
value
|
||||
};
|
||||
});
|
||||
|
||||
targetMapRef.current = nextTargetMap;
|
||||
setOptions((currentOptions) => (areOptionsEqual(currentOptions, nextOptions) ? currentOptions : nextOptions));
|
||||
};
|
||||
|
||||
const scheduleRefresh = () => {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = requestAnimationFrame(refreshOptions);
|
||||
};
|
||||
|
||||
scheduleRefresh();
|
||||
|
||||
const observer = new MutationObserver(scheduleRefresh);
|
||||
observer.observe(tabsContainer, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["class"]
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [activeTabKey, tabsRef]);
|
||||
|
||||
useEffect(() => {
|
||||
clearHighlightedTarget(highlightedTargetRef);
|
||||
setSelectedSection(undefined);
|
||||
}, [activeTabKey]);
|
||||
|
||||
const handleSectionChange = (value) => {
|
||||
setSelectedSection(value);
|
||||
|
||||
clearHighlightedTarget(highlightedTargetRef);
|
||||
if (!value) return;
|
||||
|
||||
const target = targetMapRef.current.get(value);
|
||||
if (target) {
|
||||
target.classList.add(HIGHLIGHT_CLASS);
|
||||
highlightedTargetRef.current = target;
|
||||
target.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start"
|
||||
});
|
||||
}
|
||||
|
||||
window.setTimeout(() => {
|
||||
setSelectedSection(undefined);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="shop-info-section-navigator">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch={{ optionFilterProp: "label" }}
|
||||
value={selectedSection}
|
||||
placeholder={t("bodyshop.labels.jump_to_section")}
|
||||
options={options}
|
||||
popupMatchSelectWidth={false}
|
||||
disabled={options.length === 0}
|
||||
onChange={handleSectionChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getOwnCardTitleNode(card) {
|
||||
const headNode = Array.from(card.children).find((child) => child.classList?.contains("ant-card-head"));
|
||||
return headNode?.querySelector(".ant-card-head-title");
|
||||
}
|
||||
|
||||
function clearHighlightedTarget(highlightedTargetRef) {
|
||||
if (highlightedTargetRef.current) {
|
||||
highlightedTargetRef.current.classList.remove(HIGHLIGHT_CLASS);
|
||||
highlightedTargetRef.current = null;
|
||||
}
|
||||
}
|
||||
|
||||
function areOptionsEqual(currentOptions, nextOptions) {
|
||||
if (currentOptions.length !== nextOptions.length) return false;
|
||||
|
||||
return currentOptions.every((option, index) => {
|
||||
const nextOption = nextOptions[index];
|
||||
return option.label === nextOption.label && option.value === nextOption.value;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user