Skip to content

Commit

Permalink
fix(#2109): tabs query string and initial tab issue
Browse files Browse the repository at this point in the history
  • Loading branch information
syedszeeshan committed Nov 15, 2024
1 parent 73d7625 commit 135f0f8
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 15 deletions.
39 changes: 31 additions & 8 deletions libs/react-components/src/lib/tab/tab.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,44 @@
import { render } from "@testing-library/react";
import { GoATab } from "./tab";
import { GoABadge } from "../badge/badge";

describe("GoATab", () => {
it("should render successfully", () => {
it("should render string heading as attribute", () => {
const { container } = render(
<GoATab heading="Profile">
<p>
<b>Profile:</b> Lorem ipsum dolor sit amet, consectetur adipiscing
elit, sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua.
<b>Profile:</b> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.
</p>
</GoATab>
</GoATab>,
);
expect(container.querySelector("goa-tab")).toBeTruthy();
const heading = container.querySelector("[slot='heading']");
expect(heading?.innerHTML).toContain("Profile");

const tabElement = container.querySelector("goa-tab");
expect(tabElement).toBeTruthy();
expect(tabElement?.getAttribute("heading")).toBe("Profile");
const content = container.querySelector("p");
expect(content?.innerHTML).toContain("Lorem ipsum dolor sit amet");
});

it("should render complex heading content using slot", () => {
const { container } = render(
<GoATab
heading={
<>
Completed <GoABadge type="midtone" content="1" />
</>
}
>
<p>Test content</p>
</GoATab>,
);

const tabElement = container.querySelector("goa-tab");
expect(tabElement).toBeTruthy();
const heading = container.querySelector("[slot='heading']");
expect(heading).toBeTruthy();
expect(heading?.textContent?.trim()).toContain("Completed");
const badge = container.querySelector("goa-badge");
expect(badge).toBeTruthy();
});
});
15 changes: 14 additions & 1 deletion libs/react-components/src/lib/tab/tab.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
interface WCProps {
heading?: React.ReactNode;
heading?: string | undefined; // for string headings only
}

declare global {
Expand All @@ -19,10 +19,23 @@ export interface GoATabItemProps {
export type TabItemProps = GoATabItemProps;

export function GoATab({ heading, children }: GoATabItemProps): JSX.Element {
//If heading is a simple string (not a fragment or element), use it as an attribute
const isSimpleString =
typeof heading === "string" ||
typeof heading === "number" ||
typeof heading === "boolean";

if (isSimpleString) {
return <goa-tab heading={heading?.toString()}>{children}</goa-tab>;
}

// For fragments, elements, or complex content, use slots
return (
<goa-tab>
{heading && <span slot="heading">{heading}</span>}
{children}
</goa-tab>
);
}

export default GoATab;
57 changes: 51 additions & 6 deletions libs/web-components/src/components/tabs/Tabs.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<svelte:options customElement="goa-tabs" />

<script lang="ts">
import { onDestroy, onMount } from "svelte";
import { onDestroy, onMount, tick } from "svelte";
import { clamp, fromBoolean } from "../../common/utils";
import { GoATabProps } from "../tab/Tab.svelte";
Expand All @@ -24,16 +24,22 @@
onMount(() => {
addChildMountListener();
addKeyboardEventListeners();
window.addEventListener("hashchange", handleHashChange);
});
onDestroy(() => {
removeKeyboardEventListeners();
window.removeEventListener("hashchange", handleHashChange);
});
// =========
// Functions
// =========
function handleHashChange() {
setCurrentTab(getTabNumberFromHash());
}
function addChildMountListener() {
_rootEl.addEventListener("tab:mounted", (e: Event) => {
const detail = (e as CustomEvent<GoATabProps>).detail;
Expand All @@ -46,14 +52,16 @@
}
_bindTimeoutId = setTimeout(() => {
bindChildren();
setCurrentTab(initialtab || 1);
setCurrentTab(getTabNumberFromHash());
}, 1);
e.stopPropagation();
});
}
function bindChildren() {
const path = window.location.pathname;
const url = new URL(window.location.href);
const path = url.pathname;
const searchParams = url.search;
// create buttons (tabs) for each of the tab contents elements
_tabProps.forEach((tabProps, index) => {
Expand Down Expand Up @@ -94,7 +102,7 @@
link.setAttribute("id", `tab-${index + 1}`);
link.setAttribute("data-testid", `tab-${index + 1}`);
link.setAttribute("role", "tab");
link.setAttribute("href", path + "#" + tabSlug);
link.setAttribute("href", `${path}${searchParams}#${tabSlug}`);
link.addEventListener("click", () => setCurrentTab(index + 1));
link.setAttribute("aria-controls", `tabpanel-${index + 1}`);
link.appendChild(headingEl);
Expand All @@ -111,6 +119,42 @@
_rootEl.removeEventListener("focus", handleKeydownEvents, true);
}
function getTabNumberFromHash(): number {
const urlhash = window.location.hash;
if (!urlhash || !_tabsEl) return initialtab;
// Remove the # from the hash for comparison
const cleanedHash = decodeURIComponent(urlhash.replace("#", ""));
const tabs = [
..._tabsEl.querySelectorAll("[role=tab]"),
] as HTMLAnchorElement[];
const matchingTab = tabs.find((tab) => {
const href = tab.getAttribute("href");
if (!href) return false;
const hrefHash = decodeURIComponent(href.split("#")[1]);
return hrefHash === cleanedHash;
});
if (matchingTab) {
const testId = matchingTab.getAttribute("data-testid");
if (testId) {
const tabNumber = parseInt(testId.replace("tab-", ""));
return isNaN(tabNumber) ? initialtab : tabNumber;
}
}
if (cleanedHash.startsWith("tab-")) {
const tabNumber = parseInt(cleanedHash.replace("tab-", "")) + 1; // Add 1 since the hash uses 0-based index
if (!isNaN(tabNumber) && tabNumber >= 1 && tabNumber <= tabs.length) {
return tabNumber;
}
}
return initialtab;
}
function setCurrentTab(tab: number) {
if (!_tabsEl) return;
Expand All @@ -131,10 +175,11 @@
let currentLocation = "";
// @ts-expect-error
[..._tabsEl.querySelectorAll("[role=tab]")].map((el, index) => {
[..._tabsEl.querySelectorAll("[role=tab]")].map(async (el, index) => {
const isCurrent = index + 1 === +_currentTab; // currentTab is 1-based
el.setAttribute("aria-selected", fromBoolean(isCurrent));
el.setAttribute("tabindex", isCurrent ? "0" : "-1");
await tick(); // some tests fail without this
if (isCurrent) {
currentLocation = (el as HTMLLinkElement).href;
el.focus();
Expand All @@ -157,7 +202,7 @@
// update the browswers url with the new hash
if (currentLocation) {
document.location = currentLocation;
history.pushState(null, "", currentLocation);
}
}
Expand Down

0 comments on commit 135f0f8

Please sign in to comment.