Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/prs/react/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export function App() {
<GoabxWorkSideMenuItem label="3407 Tabs Orientation" url="/features/3407-stack-on-mobile" />
<GoabxWorkSideMenuItem label="3398 Group open prop" url="/features/3398" />
<GoabxWorkSideMenuItem label="3478 Popover API Rewrite" url="/features/3478" />
<GoabxWorkSideMenuItem label="3347 Sticky Container" url="/features/3347" />
</GoabxWorkSideMenuGroup>
<GoabxWorkSideMenuItem icon="list" label="Everything" url="/everything" />
</>
Expand Down
2 changes: 2 additions & 0 deletions apps/prs/react/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import { Feat3398Route } from "./routes/features/feat3398";
import { Feat3478Route } from "./routes/features/feat3478";
import { Feat2885Route } from "./routes/features/feat2885";
import { Feat2885NavigationTabsRoute } from "./routes/features/feat2885-navigation-tabs";
import { Feat3347Route } from "./routes/features/feat3347";

const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);

Expand Down Expand Up @@ -191,6 +192,7 @@ root.render(
path="features/2885-navigation-tabs"
element={<Feat2885NavigationTabsRoute />}
/>
<Route path="features/3347" element={<Feat3347Route />} />
</Route>

{/* Standalone route without App wrapper for full-page layout demos */}
Expand Down
243 changes: 243 additions & 0 deletions apps/prs/react/src/routes/features/feat3347.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/**
* Feature #3347 - Sticky Container Component
*
* Playground test page for the new goa-sticky-container component.
* Demonstrates sticky header/footer with scrollable content.
*/

import { useState } from "react";
import {
GoabBlock,
GoabText,
GoabDivider,
GoabDetails,
GoabLink,
GoabButton,
GoabButtonGroup,
GoabBadge,
GoabStickyContainer,
} from "@abgov/react-components";

const bodyItems = Array.from({ length: 30 }, (_, i) => `Item ${i + 1}`);

export function Feat3347Route() {
const [selected, setSelected] = useState<string[]>([]);

const toggleSelect = (item: string) => {
setSelected((prev) =>
prev.includes(item) ? prev.filter((x) => x !== item) : [...prev, item],
);
};

const clearSelection = () => setSelected([]);

return (
<div>
<GoabText tag="h1" mt="m">
Feature #3347: Sticky Container
</GoabText>

<GoabBlock>
<GoabLink trailingIcon="open">
<a
href="https://github.com/GovAlta/ui-components/issues/3347"
target="_blank"
rel="noopener"
>
View on GitHub
</a>
</GoabLink>

<GoabDetails heading="Issue Description">
<GoabText tag="p">
Create reusable components (or a single component with
header/footer positioning capability) to support "sticky" UI regions
within scrollable pages. The component should support an optional
sticky header, a scrollable content/body area, and an optional
sticky footer.
</GoabText>
</GoabDetails>
</GoabBlock>

<GoabDivider mt="l" mb="l" />

<GoabText tag="h2">Test Cases</GoabText>

{/* Test 1: Full featured (header + footer + content) */}
<GoabText tag="h3">Test 1: Header + Scrollable Content + Footer</GoabText>
<GoabText tag="p">
The header stays visible at the top; the list scrolls; the footer
(bulk-action bar, visible only when items are selected) stays at the
bottom. Click items to select them and show the footer.
</GoabText>

<div style={{ height: "400px", border: "1px solid #ccc" }}>
<GoabStickyContainer
height="100%"
testId="sticky-full"
header={
<div
style={{
padding: "12px 16px",
display: "flex",
alignItems: "center",
gap: "8px",
}}
>
<GoabText tag="h4" mt="none" mb="none">
Cases list
</GoabText>
<input
type="search"
placeholder="Search cases…"
style={{
marginLeft: "auto",
padding: "6px 10px",
border: "1px solid #ccc",
borderRadius: "4px",
}}
/>
</div>
}
footer={
selected.length > 0 ? (
<div
style={{
padding: "12px 16px",
display: "flex",
alignItems: "center",
gap: "12px",
}}
>
<GoabBadge type="information" content={`${selected.length} selected`} />
<GoabButtonGroup alignment="end">
<GoabButton type="secondary" size="compact" onClick={clearSelection}>
Clear selection
</GoabButton>
<GoabButton type="primary" size="compact" onClick={clearSelection}>
Assign
</GoabButton>
</GoabButtonGroup>
</div>
) : null
}
>
<ul style={{ margin: 0, padding: "8px 0", listStyle: "none" }}>
{bodyItems.map((item) => (
<li
key={item}
onClick={() => toggleSelect(item)}
style={{
padding: "10px 16px",
cursor: "pointer",
backgroundColor: selected.includes(item)
? "#e8f0fe"
: "transparent",
borderBottom: "1px solid #eee",
}}
>
{item}
</li>
))}
</ul>
</GoabStickyContainer>
</div>

<GoabDivider mt="l" mb="l" />

{/* Test 2: Header only */}
<GoabText tag="h3">Test 2: Sticky Header Only (no footer)</GoabText>
<GoabText tag="p">
The header stays pinned at the top while content scrolls. No footer is
rendered.
</GoabText>

<div style={{ height: "300px", border: "1px solid #ccc" }}>
<GoabStickyContainer
height="100%"
testId="sticky-header-only"
header={
<div style={{ padding: "12px 16px", fontWeight: "bold" }}>
Sticky Header
</div>
}
>
<ul style={{ margin: 0, padding: "8px 0", listStyle: "none" }}>
{bodyItems.map((item) => (
<li
key={item}
style={{ padding: "10px 16px", borderBottom: "1px solid #eee" }}
>
{item}
</li>
))}
</ul>
</GoabStickyContainer>
</div>

<GoabDivider mt="l" mb="l" />

{/* Test 3: Footer only */}
<GoabText tag="h3">Test 3: Sticky Footer Only (no header)</GoabText>
<GoabText tag="p">
No header. A sticky footer stays pinned at the bottom while content
scrolls above it.
</GoabText>

<div style={{ height: "300px", border: "1px solid #ccc" }}>
<GoabStickyContainer
height="100%"
testId="sticky-footer-only"
footer={
<div
style={{
padding: "12px 16px",
fontWeight: "bold",
textAlign: "center",
}}
>
Sticky Footer
</div>
}
>
<ul style={{ margin: 0, padding: "8px 0", listStyle: "none" }}>
{bodyItems.map((item) => (
<li
key={item}
style={{ padding: "10px 16px", borderBottom: "1px solid #eee" }}
>
{item}
</li>
))}
</ul>
</GoabStickyContainer>
</div>

<GoabDivider mt="l" mb="l" />

{/* Test 4: Content only (no header/footer) */}
<GoabText tag="h3">Test 4: Content Only (no header or footer)</GoabText>
<GoabText tag="p">
No header or footer - just a scrollable content region. The component
should render cleanly.
</GoabText>

<div style={{ height: "200px", border: "1px solid #ccc" }}>
<GoabStickyContainer height="100%" testId="sticky-content-only">
<ul style={{ margin: 0, padding: "8px 0", listStyle: "none" }}>
{bodyItems.map((item) => (
<li
key={item}
style={{ padding: "10px 16px", borderBottom: "1px solid #eee" }}
>
{item}
</li>
))}
</ul>
</GoabStickyContainer>
</div>
</div>
);
}

export default Feat3347Route;
1 change: 1 addition & 0 deletions libs/angular-components/src/lib/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export * from "./side-menu-group/side-menu-group";
export * from "./side-menu-heading/side-menu-heading";
export * from "./skeleton/skeleton";
export * from "./spacer/spacer";
export * from "./sticky-container/sticky-container";
export * from "./tab/tab";
export * from "./table/table";
export * from "./table-sort-header/table-sort-header";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing";
import { GoabStickyContainer } from "./sticky-container";
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { By } from "@angular/platform-browser";

@Component({
standalone: true,
imports: [GoabStickyContainer],
template: `
<goab-sticky-container
[height]="height"
[testId]="testId"
[header]="header"
[footer]="footer"
>
<ng-template #header>
<div class="test-header">Header Content</div>
</ng-template>
<ng-template #footer>
<div class="test-footer">Footer Content</div>
</ng-template>
<div class="test-body">Body Content</div>
</goab-sticky-container>
`,
})
class TestStickyContainerComponent {
height?: string = "400px";
testId?: string = "sticky-test";
}

describe("GoabStickyContainer", () => {
let fixture: ComponentFixture<TestStickyContainerComponent>;
let component: TestStickyContainerComponent;

beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [GoabStickyContainer, TestStickyContainerComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();

fixture = TestBed.createComponent(TestStickyContainerComponent);
component = fixture.componentInstance;

fixture.detectChanges();
tick();
fixture.detectChanges();
}));

it("should render the goa-sticky-container element", () => {
const el = fixture.debugElement.query(By.css("goa-sticky-container"))
?.nativeElement;
expect(el).toBeTruthy();
});

it("should set the height attribute", () => {
const el = fixture.debugElement.query(By.css("goa-sticky-container"))
?.nativeElement;
expect(el?.getAttribute("height")).toBe("400px");
// height is also applied as inline style so shadow-DOM flex layout is constrained
expect(el?.style.height).toBe("400px");
});

it("should set the testid attribute", () => {
const el = fixture.debugElement.query(By.css("goa-sticky-container"))
?.nativeElement;
expect(el?.getAttribute("testid")).toBe("sticky-test");
});

it("should render header slot content", () => {
const el = fixture.debugElement.query(By.css("goa-sticky-container"))
?.nativeElement;
const headerSlot = el?.querySelector("[slot='header']");
expect(headerSlot).toBeTruthy();
expect(headerSlot?.innerHTML).toContain("Header Content");
});

it("should render footer slot content", () => {
const el = fixture.debugElement.query(By.css("goa-sticky-container"))
?.nativeElement;
const footerSlot = el?.querySelector("[slot='footer']");
expect(footerSlot).toBeTruthy();
expect(footerSlot?.innerHTML).toContain("Footer Content");
});

it("should render body content", () => {
const el = fixture.debugElement.query(By.css("goa-sticky-container"))
?.nativeElement;
expect(el?.innerHTML).toContain("Body Content");
});
});
Loading
Loading