Implemented the customer rate card view, including SKU search

This commit is contained in:
2026-09-22 20:25:41 -03:00
parent 9ac5ffb11d
commit b4d8d9dbc1
23 changed files with 1854 additions and 130 deletions

View File

@@ -0,0 +1,54 @@
function createConsumptionHeaderView({ createCloseIcon, formatOracleResponseDate, environment }) {
const { document } = environment;
function createHeader({ titleId, customerLabel, closeLabel, onClose, titleText = "Cloud Consumption" }) {
const header = document.createElement("header");
header.className = "opportunities-extension-account-plans-header";
const heading = document.createElement("div");
const title = document.createElement("h2");
title.id = titleId;
title.textContent = titleText;
const subtitle = document.createElement("p");
subtitle.textContent = customerLabel;
heading.append(title, subtitle);
const closeButton = document.createElement("button");
closeButton.type = "button";
closeButton.className = "opportunities-extension-icon-button";
closeButton.setAttribute("aria-label", closeLabel);
closeButton.title = "Close";
closeButton.append(createCloseIcon());
closeButton.addEventListener("click", onClose);
header.append(heading, closeButton);
return header;
}
function createDetails(plan, { contractAction = null } = {}) {
const details = document.createElement("dl");
details.className = "opportunities-extension-account-plan-details";
const fields = [
{ label: "Booking Customer", value: plan.bookingCustomer },
{ label: "Sold To Party Name", value: plan.soldToPartyName },
{ label: "Contract ID", value: plan.contractId },
{ label: "Header Status", value: plan.hdrStatusCode },
{ label: "Start Date", value: formatOracleResponseDate(plan.startDate) },
{ label: "End Date", value: formatOracleResponseDate(plan.endDate) }
];
fields.forEach(field => {
const group = document.createElement("div");
const term = document.createElement("dt");
const description = document.createElement("dd");
term.textContent = field.label;
description.textContent = field.value === null || field.value === undefined || field.value === "" ? "-" : String(field.value);
if (field.label === "Contract ID" && contractAction) {
group.className = "opportunities-extension-contract-row";
const value = document.createElement("span");
value.textContent = description.textContent;
description.textContent = "";
description.className = "opportunities-extension-contract-ratecard";
description.append(value, contractAction);
}
group.append(term, description);
details.append(group);
});
return details;
}
return { createHeader, createDetails };
}

View File

@@ -2,12 +2,14 @@ function createConsumptionPlansView({
ACCOUNT_PLANS_MODAL_ID,
DEFAULT_CONSUMPTION_TREND_PERIOD,
MODAL_ID,
createCloseIcon,
createConsumptionHeader,
createConsumptionTrendSection,
loadPlanDetailForPanel,
closePlanDetailPopover,
createRatecardButton,
closeRatecardDrawer,
createSvgElement,
formatOracleResponseDate,
createPlanDetails,
getNestedValue,
loadConsumptionTrendForPanel,
environment
@@ -31,12 +33,14 @@ function createConsumptionPlansView({
}
closePlanDetailPopover();
closeRatecardDrawer({ immediate: true, restoreFocus: false });
existingModal?.remove();
const modal = document.createElement("section");
modal.id = ACCOUNT_PLANS_MODAL_ID;
modal.className = "opportunities-extension-account-plans-modal";
modal.dataset.optyNumber = optyNumber;
modal.dataset.customerLabel = getNestedValue(opportunity, ["CustomerAccount", "PartyUniqueName"]) || opportunity.Name || opportunity.OptyNumber || "Opportunity";
modal.setAttribute("role", "dialog");
modal.setAttribute("aria-modal", "true");
modal.setAttribute("aria-labelledby", "opportunities-extension-account-plans-title");
@@ -45,26 +49,12 @@ function createConsumptionPlansView({
const dialog = document.createElement("div");
dialog.className = "opportunities-extension-account-plans-dialog";
const header = document.createElement("header");
header.className = "opportunities-extension-account-plans-header";
const heading = document.createElement("div");
const title = document.createElement("h2");
title.id = "opportunities-extension-account-plans-title";
title.textContent = "Cloud Consumption";
const subtitle = document.createElement("p");
subtitle.textContent = getNestedValue(opportunity, ["CustomerAccount", "PartyUniqueName"]) || opportunity.Name || opportunity.OptyNumber || "Opportunity";
heading.append(title, subtitle);
const closeButton = document.createElement("button");
closeButton.type = "button";
closeButton.className = "opportunities-extension-icon-button";
closeButton.setAttribute("aria-label", "Close cloud consumption");
closeButton.title = "Close";
closeButton.append(createCloseIcon());
closeButton.addEventListener("click", closeAccountPlansModal);
header.append(heading, closeButton);
const header = createConsumptionHeader({
titleId: "opportunities-extension-account-plans-title",
customerLabel: modal.dataset.customerLabel,
closeLabel: "Close cloud consumption",
onClose: closeAccountPlansModal
});
const content = document.createElement("div");
content.className = "opportunities-extension-account-plans-content";
@@ -123,6 +113,7 @@ function createConsumptionPlansView({
tab.addEventListener("click", () => {
closePlanDetailPopover();
closeRatecardDrawer({ immediate: true, restoreFocus: false });
tabList.querySelectorAll("[role='tab']").forEach((candidate) => {
const isSelected = candidate === tab;
candidate.setAttribute("aria-selected", String(isSelected));
@@ -329,31 +320,12 @@ function createConsumptionPlansView({
}
function createAccountPlanDetails(plan) {
const details = document.createElement("dl");
details.className = "opportunities-extension-account-plan-details";
const fields = [
{ label: "Booking Customer", value: plan.bookingCustomer },
{ label: "Sold To Party Name", value: plan.soldToPartyName },
{ label: "Contract ID", value: plan.contractId },
{ label: "Header Status", value: plan.hdrStatusCode },
{ label: "Start Date", value: formatOracleResponseDate(plan.startDate) },
{ label: "End Date", value: formatOracleResponseDate(plan.endDate) }
];
fields.forEach((field) => {
const group = document.createElement("div");
const term = document.createElement("dt");
const description = document.createElement("dd");
term.textContent = field.label;
description.textContent = field.value === null || field.value === undefined || field.value === "" ? "-" : String(field.value);
group.append(term, description);
details.append(group);
});
return details;
return createPlanDetails(plan, { contractAction: createRatecardButton(plan) });
}
function closeAccountPlansModal() {
closePlanDetailPopover();
closeRatecardDrawer({ immediate: true, restoreFocus: false });
const modal = document.getElementById(ACCOUNT_PLANS_MODAL_ID);
if (modal) {

View File

@@ -24,7 +24,7 @@ function createDebugContentView({
if (entry.name === "requestGenerateAiSummary") return "ai";
if (entry.name === "requestInstallBase") return "installBase";
if (entry.name === "requestCustomerServiceRequest") return "serviceRequests";
if (/^request(?:AccountSummary|AccountPlans|ConsumptionService|ConsumptionPlanDetail)/.test(entry.name)) return "consumption";
if (/^request(?:AccountSummary|AccountPlans|ConsumptionService|ConsumptionPlanDetail|RatecardByPlan)/.test(entry.name)) return "consumption";
return "other";
}

View File

@@ -0,0 +1,38 @@
function createRatecardController({ messageTypes, requestConsumerTokenServiceOnce, sendRuntimeMessage,
appendBackgroundRequestLog, openRatecardDrawer, closePlanDetailPopover, environment }) {
const pending = new Map();
async function request(planNumber) {
const authorizations = await requestConsumerTokenServiceOnce();
const response = await sendRuntimeMessage({ type: messageTypes.requestRatecardByPlan, planNumber,
authorization: authorizations && authorizations.accountPlans });
appendBackgroundRequestLog(response?.debugRequests);
if (!response?.ok) throw new Error(response?.error || "Unable to load rate card.");
return response.items;
}
async function showRatecard(plan, trigger) {
closePlanDetailPopover();
const handle = openRatecardDrawer(plan, trigger);
if (!handle) return;
try {
const planNumber = String(plan.subPlanNum ?? "").trim();
if (!planNumber) throw new Error("Plan number is unavailable for this account plan.");
if (!pending.has(planNumber)) pending.set(planNumber, request(planNumber).finally(() => pending.delete(planNumber)));
handle.showItems(await pending.get(planNumber));
} catch (error) {
handle.showError(error.message || "Unable to load rate card.");
}
}
function createRatecardButton(plan) {
const button = environment.document.createElement("button");
button.type = "button";
button.className = "opportunities-extension-ratecard-button";
button.textContent = "Rate Card";
button.title = "View rate card";
button.setAttribute("aria-haspopup", "dialog");
button.addEventListener("click", () => showRatecard(plan, button));
return button;
}
return { createRatecardButton, showRatecard };
}

View File

@@ -0,0 +1,65 @@
function createRatecardModel() {
const columns = [
{ key: "orderNumber", label: "Order Number", width: 7 },
{ key: "partNum", label: "Part Number", width: 6 },
{ key: "productName", label: "Product Name", width: 22 },
{ key: "uom", label: "UOM", width: 10 },
{ key: "subLnStartSt", label: "Start Date", type: "date", width: 8 },
{ key: "subLnEndDt", label: "End Date", type: "date", width: 8 },
{ key: "usageMax", label: "Usage Max", type: "number", width: 5 },
{ key: "rateCardDiscountPerc", label: "Discount (%)", type: "percent", width: 7 },
{ key: "listPriceUsd", label: "List price (USD)", type: "price", width: 9 },
{ key: "netUnitPriceUsd", label: "Net Unit Price (USD)", type: "price", width: 9 },
{ key: "overagePriceUsd", label: "Overage Net Unit Price (USD)", type: "price", width: 9 }
];
const collator = new Intl.Collator("en", { numeric: true, sensitivity: "base" });
const prices = new Intl.NumberFormat("en-US", { minimumFractionDigits: 4, maximumFractionDigits: 20 });
const numbers = new Intl.NumberFormat("en-US", { maximumFractionDigits: 20 });
const months = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
const missing = value => value === null || value === undefined || String(value).trim() === "" || String(value).trim() === "-";
const numeric = value => !missing(value) && Number.isFinite(Number(value)) ? Number(value) : null;
const normalize = value => String(value ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim();
function formatValue(item, column) {
const value = item[column.key];
if (missing(value)) return "-";
const number = numeric(value);
if (number !== null) {
if (column.type === "price") return prices.format(number);
if (column.type === "percent") return `${numbers.format(number)}%`;
if (column.type === "number") return numbers.format(number);
}
return String(value).trim();
}
function dateValue(value) {
const match = String(value).match(/^(\d{1,2})-([a-z]{3})-(\d{4})$/i);
if (match) {
const month = months.indexOf(match[2].toLowerCase());
return month < 0 ? null : Date.UTC(Number(match[3]), month, Number(match[1]));
}
const iso = String(value).match(/^(\d{4})-(\d{2})-(\d{2})(?:T.*)?$/);
return iso ? Date.UTC(Number(iso[1]), Number(iso[2]) - 1, Number(iso[3])) : null;
}
function selectItems(items, search = "", sortKey = "", direction = "asc") {
const query = normalize(search);
const filtered = items.filter(item => !query || columns.some(column =>
normalize(item[column.key]).includes(query) || normalize(formatValue(item, column)).includes(query)));
const column = columns.find(candidate => candidate.key === sortKey);
if (!column) return filtered;
const sortValue = item => {
const value = item[column.key];
if (missing(value)) return null;
if (column.type === "date") return dateValue(value);
return ["price", "number", "percent"].includes(column.type) ? numeric(value) : String(value);
};
return filtered.sort((a, b) => {
const left = sortValue(a), right = sortValue(b);
if (left === null || right === null) return left === right ? 0 : left === null ? 1 : -1;
const compared = typeof left === "number" ? left - right : collator.compare(left, right);
return direction === "desc" ? -compared : compared;
});
}
return { columns, formatValue, selectItems };
}

View File

@@ -0,0 +1,141 @@
.opportunities-extension-account-plan-details > .opportunities-extension-contract-row {
align-items: center;
}
.opportunities-extension-contract-row > dt,
.opportunities-extension-contract-row > dd,
.opportunities-extension-contract-ratecard > span {
line-height: 20px;
}
.opportunities-extension-contract-ratecard {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px 10px;
}
.opportunities-extension-ratecard-button {
display: inline-flex;
align-items: center;
justify-content: center;
align-self: center;
box-sizing: border-box;
height: 28px;
margin: 0;
flex: 0 0 auto;
padding: 0 8px;
border: 1px solid #8aaeb9;
border-radius: 4px;
background: #fff;
color: #006b80;
font: inherit;
font-size: 12px;
font-weight: 600;
line-height: 20px;
cursor: pointer;
}
.opportunities-extension-ratecard-button:hover { background: #e8f4f6; }
.opportunities-extension-ratecard-overlay {
position: absolute;
inset: 0;
z-index: 8;
display: flex;
justify-content: flex-end;
overflow: hidden;
overflow: clip;
background: rgba(0, 0, 0, 0);
transition: background-color 320ms ease;
}
.opportunities-extension-ratecard-overlay[data-open="true"] { background: rgba(0, 0, 0, .32); }
.opportunities-extension-ratecard-drawer {
--ratecard-bg: #fff;
--ratecard-heading: #faf9f8;
--ratecard-text: #312d2a;
--ratecard-muted: #615a55;
--ratecard-border: #dedbd8;
--ratecard-accent: #00758f;
--ratecard-skeleton: #eae7e4;
display: grid;
grid-template-rows: auto auto auto minmax(0, 1fr);
width: min(1520px, calc(100% - 48px));
height: 100%;
min-width: 0;
box-sizing: border-box;
overflow: hidden;
background: var(--ratecard-bg);
color: var(--ratecard-text);
box-shadow: -6px 0 24px rgba(0, 0, 0, .2);
transform: translateX(100%);
transition: transform 320ms cubic-bezier(.22, .61, .36, 1);
}
.opportunities-extension-ratecard-overlay[data-open="true"] .opportunities-extension-ratecard-drawer { transform: translateX(0); }
.opportunities-extension-ratecard-header > div { min-width: 0; overflow-wrap: anywhere; }
.opportunities-extension-ratecard-plan-details { padding: 12px 20px 0; }
.opportunities-extension-ratecard-toolbar { display: flex; align-items: center; gap: 16px; padding: 12px 20px; }
.opportunities-extension-ratecard-search {
width: min(360px, 100%);
min-width: 0;
box-sizing: border-box;
padding: 9px 12px;
border: 1px solid var(--ratecard-muted);
border-radius: 4px;
background: var(--ratecard-bg);
color: var(--ratecard-text);
font: inherit;
font-size: 13px;
}
.opportunities-extension-ratecard-search::placeholder { color: var(--ratecard-muted); opacity: 1; }
.opportunities-extension-ratecard-count { color: var(--ratecard-muted); font-size: 12px; white-space: nowrap; }
.opportunities-extension-ratecard-viewport { overflow: auto; min-height: 0; min-width: 0; scrollbar-gutter: stable; }
.opportunities-extension-ratecard-table { width: 100%; min-width: 1100px; table-layout: fixed; border-collapse: separate; border-spacing: 0; font-size: 12px; }
.opportunities-extension-ratecard-table th, .opportunities-extension-ratecard-table td {
padding: 11px 10px;
border-bottom: 1px solid var(--ratecard-border);
vertical-align: top;
overflow-wrap: anywhere;
text-align: left;
}
.opportunities-extension-ratecard-table th { position: sticky; top: 0; z-index: 1; background: var(--ratecard-heading); }
.opportunities-extension-ratecard-sort {
display: grid;
grid-template-columns: minmax(0, 1fr) 12px;
align-items: center;
gap: 5px;
width: 100%;
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
font-weight: 600;
text-align: left;
cursor: pointer;
}
.opportunities-extension-ratecard-sort-icon { color: var(--ratecard-accent); text-align: center; }
.opportunities-extension-ratecard-table .opportunities-extension-ratecard-numeric { text-align: right; font-variant-numeric: tabular-nums; }
.opportunities-extension-ratecard-table .opportunities-extension-ratecard-notice { padding: 28px 20px; text-align: center; }
.opportunities-extension-ratecard-notice [role="alert"] { color: #b3261e; }
.opportunities-extension-ratecard-skeleton { display: block; width: 85%; height: 14px; border-radius: 3px; background: var(--ratecard-skeleton); animation: opportunities-ratecard-pulse 1.1s ease-in-out infinite alternate; }
.opportunities-extension-ratecard-drawer :focus-visible, .opportunities-extension-ratecard-button:focus-visible { outline: 2px solid #00758f; outline-offset: 2px; }
.opportunities-extension-ratecard-overlay[data-closing="true"] { pointer-events: none; background: rgba(0, 0, 0, 0); transition-duration: 180ms; }
.opportunities-extension-ratecard-overlay[data-closing="true"] .opportunities-extension-ratecard-drawer { transform: translateX(100%); transition-duration: 180ms; }
[data-theme="dark"] .opportunities-extension-ratecard-drawer {
--ratecard-bg: #292725;
--ratecard-heading: #35322f;
--ratecard-text: #f5f3f1;
--ratecard-muted: #c2bcb6;
--ratecard-border: #514c47;
--ratecard-accent: #59d1e5;
--ratecard-skeleton: #514c47;
}
[data-theme="dark"] .opportunities-extension-ratecard-button { color: #59d1e5; border-color: #6298a2; background: #292725; }
[data-theme="dark"] .opportunities-extension-ratecard-button:hover { background: #313f40; }
[data-theme="dark"] .opportunities-extension-ratecard-notice [role="alert"] { color: #ffb4ab; }
[data-theme="dark"] .opportunities-extension-ratecard-drawer :focus-visible, [data-theme="dark"] .opportunities-extension-ratecard-button:focus-visible { outline-color: #59d1e5; }
@keyframes opportunities-ratecard-pulse { from { opacity: .45; } to { opacity: 1; } }
@media (max-width: 720px) {
.opportunities-extension-ratecard-drawer { width: 100%; }
.opportunities-extension-ratecard-toolbar { flex-wrap: wrap; gap: 8px; }
}
@media (prefers-reduced-motion: reduce) {
.opportunities-extension-ratecard-overlay, .opportunities-extension-ratecard-drawer, .opportunities-extension-ratecard-skeleton,
.opportunities-extension-ratecard-overlay[data-closing="true"], .opportunities-extension-ratecard-overlay[data-closing="true"] .opportunities-extension-ratecard-drawer { animation: none; transition: none; }
}

View File

@@ -0,0 +1,208 @@
function createRatecardView({ ACCOUNT_PLANS_MODAL_ID, columns, formatValue, selectItems, createConsumptionHeader, createPlanDetails, environment }) {
const { document, setTimeout, clearTimeout, requestAnimationFrame, cancelAnimationFrame } = environment;
let active = null;
let closing = null;
let closeTimer = null;
function closeRatecardDrawer({ immediate = false, restoreFocus = true } = {}) {
if (closeTimer !== null) clearTimeout(closeTimer);
closeTimer = null;
closing?.remove();
closing = null;
if (!active) return;
const previous = active;
active = null;
if (previous.openFrame !== null) cancelAnimationFrame(previous.openFrame);
previous.background.inert = previous.wasInert;
if (immediate) previous.root.remove();
else {
closing = previous.root;
closing.dataset.closing = "true";
closing.inert = true;
closeTimer = setTimeout(() => {
closing?.remove();
closing = null;
closeTimer = null;
}, 200);
}
if (restoreFocus && previous.trigger?.isConnected) previous.trigger.focus({ preventScroll: true });
}
function openRatecardDrawer(plan, trigger) {
const parent = document.getElementById(ACCOUNT_PLANS_MODAL_ID);
const background = parent?.querySelector(".opportunities-extension-account-plans-dialog");
if (!parent || parent.hidden || !background) return null;
closeRatecardDrawer({ immediate: true, restoreFocus: false });
const element = (tag, className, text) => {
const node = document.createElement(tag);
if (className) node.className = `opportunities-extension-ratecard-${className}`;
if (text !== undefined) node.textContent = text;
return node;
};
const root = element("div", "overlay");
root.dataset.open = "false";
const panel = element("section", "drawer");
panel.setAttribute("role", "dialog");
panel.setAttribute("aria-modal", "true");
panel.setAttribute("aria-labelledby", "opportunities-extension-ratecard-title");
panel.setAttribute("aria-busy", "true");
const header = createConsumptionHeader({
titleId: "opportunities-extension-ratecard-title",
titleText: "Rate Card",
customerLabel: parent.dataset.customerLabel,
closeLabel: "Close rate card",
onClose: () => closeRatecardDrawer()
});
header.classList.add("opportunities-extension-ratecard-header");
const close = header.querySelector("button");
const details = createPlanDetails(plan);
details.classList.add("opportunities-extension-ratecard-plan-details");
const toolbar = element("div", "toolbar");
const search = element("input", "search");
search.type = "search";
search.placeholder = "Search rate card";
search.setAttribute("aria-label", "Search rate card");
const count = element("span", "count", "Loading rate card...");
count.setAttribute("role", "status");
toolbar.append(search, count);
const viewport = element("div", "viewport");
viewport.tabIndex = 0;
viewport.setAttribute("role", "region");
viewport.setAttribute("aria-label", "Rate card items");
const table = element("table", "table");
const caption = element("caption", null, "Rate card items");
caption.className = "opportunities-extension-visually-hidden";
const colgroup = element("colgroup");
const head = element("thead");
const headRow = element("tr");
const sortHeaders = [];
const current = { root, background, wasInert: background.inert, trigger, items: [], status: "loading", sortKey: "", direction: "asc", openFrame: null };
active = current;
background.inert = true;
const alive = () => active === current && root.isConnected && !parent.hidden;
columns.forEach(column => {
const col = element("col");
col.style.width = `${column.width}%`;
colgroup.append(col);
const th = element("th");
th.scope = "col";
th.setAttribute("aria-sort", "none");
const sort = element("button", "sort");
sort.type = "button";
sort.title = `Sort by ${column.label}`;
const label = element("span", null, column.label);
const icon = element("span", "sort-icon", "\u2195");
icon.setAttribute("aria-hidden", "true");
sort.append(label, icon);
sort.addEventListener("click", () => {
current.direction = current.sortKey === column.key && current.direction === "asc" ? "desc" : "asc";
current.sortKey = column.key;
updateSortHeaders();
if (current.status === "ready") renderRows();
});
sortHeaders.push({ th, icon, key: column.key });
th.append(sort);
headRow.append(th);
});
head.append(headRow);
const body = element("tbody");
table.append(caption, colgroup, head, body);
viewport.append(table);
panel.append(header, details, toolbar, viewport);
root.append(panel);
parent.append(root);
function updateSortHeaders() {
sortHeaders.forEach(({ th, icon, key }) => {
const selected = key === current.sortKey;
th.setAttribute("aria-sort", selected ? current.direction === "asc" ? "ascending" : "descending" : "none");
icon.textContent = !selected ? "\u2195" : current.direction === "asc" ? "\u2191" : "\u2193";
});
}
function showNotice(message, error = false) {
body.textContent = "";
const row = element("tr");
const cell = element("td", "notice");
cell.colSpan = columns.length;
const text = element("p", null, message);
if (error) text.setAttribute("role", "alert");
cell.append(text);
row.append(cell);
body.append(row);
}
function renderRows() {
if (!alive()) return;
body.textContent = "";
const visible = selectItems(current.items, search.value, current.sortKey, current.direction);
count.textContent = `${visible.length} of ${current.items.length} items`;
if (!visible.length) {
showNotice(current.items.length ? "No matching rate card items." : "No rate card items found for this plan.");
}
visible.forEach(item => {
const row = element("tr");
columns.forEach(column => {
const cell = element("td", null, formatValue(item, column));
if (["price", "number", "percent"].includes(column.type)) cell.className = "opportunities-extension-ratecard-numeric";
row.append(cell);
});
body.append(row);
});
viewport.scrollTop = 0;
}
for (let index = 0; index < 7; index += 1) {
const row = element("tr");
row.setAttribute("aria-hidden", "true");
columns.forEach(() => {
const cell = element("td");
const skeleton = element("span", "skeleton");
cell.append(skeleton);
row.append(cell);
});
body.append(row);
}
search.addEventListener("input", () => { if (current.status === "ready") renderRows(); });
root.addEventListener("click", event => { if (event.target === root) closeRatecardDrawer(); });
root.addEventListener("keydown", event => {
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
closeRatecardDrawer();
} else if (event.key === "Tab") {
const controls = [close, search, viewport, ...headRow.querySelectorAll("button")];
const index = controls.indexOf(document.activeElement);
if (index < 0 || (event.shiftKey && index === 0) || (!event.shiftKey && index === controls.length - 1)) {
event.preventDefault();
controls[event.shiftKey ? controls.length - 1 : 0].focus({ preventScroll: true });
}
}
});
// Focusing offscreen content must not scroll the clipped overlay and cancel the slide.
search.focus({ preventScroll: true });
// Paint the offscreen position before transitioning to the fully open position.
current.openFrame = requestAnimationFrame(() => {
if (!alive()) return;
current.openFrame = requestAnimationFrame(() => {
current.openFrame = null;
if (alive()) root.dataset.open = "true";
});
});
return {
showItems(items) {
if (!alive()) return;
current.items = items;
current.status = "ready";
panel.setAttribute("aria-busy", "false");
renderRows();
},
showError(message) {
if (!alive()) return;
current.status = "error";
panel.setAttribute("aria-busy", "false");
count.textContent = "Rate card unavailable";
showNotice(message, true);
}
};
}
return { openRatecardDrawer, closeRatecardDrawer };
}