Skip to content

pos-navigation redesign #137

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Jul 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
11a5474
BREAKING CHANGE: rename pos-navigation-bar to pos-navigation
angelo-v Jul 7, 2025
ef044b2
feat(elements): add new pos-navigation-bar component based on a button
angelo-v Jul 7, 2025
a9d0633
feat(elements): emit navigation event on button click in pos-navigati…
angelo-v Jul 7, 2025
feb6ae9
feat(elements): move search input & suggestions to modal dialog
angelo-v Jul 7, 2025
ef28594
refactor(elements): replace ion-searchbar with native input in pos-na…
angelo-v Jul 7, 2025
93c9037
fix(elements): suggestions no longer need to be relatively positioned…
angelo-v Jul 7, 2025
8c03632
update type definitions
angelo-v Jul 7, 2025
0065dad
feat(pos-navigation): move pos-make-findable to navigation bar; allow…
angelo-v Jul 8, 2025
852858a
feat(pos-app-browser): make the new navbar appear inline with the res…
angelo-v Jul 8, 2025
86f466e
refactor(pos-navigation): remove obsolete event parameter and type cast
angelo-v Jul 9, 2025
80e9922
feat(pos-navigation-bar): show button to trigger search if no current…
angelo-v Jul 9, 2025
6aedd37
feat(pos-navigation-bar): style nav bar to look more like a browser a…
angelo-v Jul 9, 2025
4ac636e
feat(pos-app-browser, pos-navigation-bar): ensure navbar can shrink a…
angelo-v Jul 9, 2025
171529b
feat(pos-navigation): position the search dialog seamlessly above the…
angelo-v Jul 11, 2025
218d496
test: fix type issues
angelo-v Jul 14, 2025
8dce3dc
feat: debounce search so that it waits until the user finished typing
angelo-v Jul 14, 2025
01387be
refactor: remove unnecessary css styles
angelo-v Jul 14, 2025
f87df21
feat: close dialog when clicked or navigated elsewhere; clear suggest…
angelo-v Jul 14, 2025
8b23087
feat(pos-navigation): search for the current resource when the dialog…
angelo-v Jul 14, 2025
b8f1ac3
feat(pos-make-findable): align board radius with navigation bar
angelo-v Jul 14, 2025
9bb189d
feat(pos-navigation): full width mobile navigation bar & larger font …
angelo-v Jul 14, 2025
0d88a50
refactor(pos-navigation): make state private and clarify meaning of v…
angelo-v Jul 14, 2025
fb613c0
refactor(pos-navigation): apply focus styles for better accessibility…
angelo-v Jul 14, 2025
0c946c4
feat(pos-navigation): add html search element for better accessibility
angelo-v Jul 14, 2025
162a9eb
feat(pos-navigation): animation & border radius
angelo-v Jul 14, 2025
8c8fdb2
doc: update changelog for pos-navigation changes
angelo-v Jul 14, 2025
eb8a9a8
feat: improve aria label for search button
angelo-v Jul 14, 2025
e5653ba
test: adjust e2e tests to new pos-navigation
angelo-v Jul 14, 2025
aeaa877
feat: scroll suggestions, not whole dialog
angelo-v Jul 14, 2025
a846802
fix: unset resource if URI is invalid
angelo-v Jul 14, 2025
bc81f0d
doc: update component readmes
angelo-v Jul 14, 2025
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
13 changes: 8 additions & 5 deletions apps/tests/accessibility.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import { test, expect } from "@playwright/test";
import { expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";

import { test } from "./fixtures";

test.describe("accessibility", () => {
["light", "dark"].forEach((colorScheme: "light" | "dark") => {
test(`no contrast issues in ${colorScheme} mode`, async ({ page }) => {
test(`no contrast issues in ${colorScheme} mode`, async ({
page,
navigationBar,
}) => {
await test.step(`given the preferred color scheme is set to ${colorScheme}`, async () => {
await page.emulateMedia({ colorScheme });
});

await test.step("when viewing a typical page in PodOS Browser", async () => {
await page.goto("/");

const navigationBar = page.getByPlaceholder("Enter URI");
await navigationBar.fill(
await navigationBar.fillAndSubmit(
"http://localhost:4000/alice/public/generic/resource#it",
);
await navigationBar.press("Enter");

const heading = page.getByRole("heading");
await expect(heading).toHaveText("Something");
Expand Down
10 changes: 5 additions & 5 deletions apps/tests/add-literal-value.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { expect, test } from "@playwright/test";
import { expect } from "@playwright/test";
import { signIn } from "./actions/signIn";
import { alice } from "./fixtures/credentials";

test("can add a literal value", async ({ page }) => {
import { test } from "./fixtures";

test("can add a literal value", async ({ page, navigationBar }) => {
// when opening PodOS Browser
await page.goto("/");

// and navigating to a generic resource
const navigationBar = page.getByPlaceholder("Enter URI");
await navigationBar.fill(
await navigationBar.fillAndSubmit(
"http://localhost:4000/alice/public/generic/resource#it",
);
await navigationBar.press("Enter");

// then I cannot see any input to add literal values
const missingAddLiteralField = page.getByPlaceholder("Add literal");
Expand Down
11 changes: 5 additions & 6 deletions apps/tests/add-new-thing.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { expect, test } from "@playwright/test";
import { expect } from "@playwright/test";
import { signIn } from "./actions/signIn";
import { alice } from "./fixtures/credentials";
import { test } from "./fixtures";

test("can add a new thing", async ({ page }) => {
test("can add a new thing", async ({ page, navigationBar }) => {
// when opening PodOS Browser
await page.goto("/");

// and navigating to a container
const navigationBar = page.getByPlaceholder("Enter URI");
await navigationBar.fill(
await navigationBar.fillAndSubmit(
"http://localhost:4000/alice/acb50d31-42af-4d4c-9ead-e2d5e70d7317/",
);
await navigationBar.press("Enter");

// when signing in as the pod owner
await signIn(page, alice);
Expand All @@ -37,7 +36,7 @@ test("can add a new thing", async ({ page }) => {
await page.keyboard.press("Enter");

// then I am at the page showing the new thing
await expect(navigationBar).toHaveValue(
expect(await navigationBar.inputValue()).toEqual(
"http://localhost:4000/alice/acb50d31-42af-4d4c-9ead-e2d5e70d7317/my-new-thing#it",
);

Expand Down
10 changes: 10 additions & 0 deletions apps/tests/fixtures/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { test as base } from "playwright/test";
import { NavigationBar } from "../page-objects/NavigationBar";

export const test = base.extend<{
navigationBar: NavigationBar;
}>({
navigationBar: async ({ page }, use) => {
await use(new NavigationBar(page));
},
});
9 changes: 5 additions & 4 deletions apps/tests/generic.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { test, expect } from "@playwright/test";
import { expect } from "@playwright/test";

import { test } from "./fixtures";

test("show generic information about unknown types of things", async ({
page,
navigationBar,
}) => {
// when opening PodOS Browser
await page.goto("/");

// and navigating to a generic resource
const navigationBar = page.getByPlaceholder("Enter URI");
await navigationBar.fill(
await navigationBar.fillAndSubmit(
"http://localhost:4000/alice/public/generic/resource#it",
);
await navigationBar.press("Enter");

// then page shows a heading with the resource name
const heading = page.getByRole("heading");
Expand Down
10 changes: 5 additions & 5 deletions apps/tests/ldp-container.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { test, expect } from "@playwright/test";
import { expect } from "@playwright/test";

test("show contents of an LDP container", async ({ page }) => {
import { test } from "./fixtures";

test("show contents of an LDP container", async ({ page, navigationBar }) => {
// when opening PodOS Browser
await page.goto("/");

// and navigating to a ldp container resource
const navigationBar = page.getByPlaceholder("Enter URI");
await navigationBar.fill("http://localhost:4000/alice/container/");
await navigationBar.press("Enter");
await navigationBar.fillAndSubmit("http://localhost:4000/alice/container/");

// then page shows the full container URL as heading
const heading = await page.getByRole("heading", { level: 1 });
Expand Down
37 changes: 37 additions & 0 deletions apps/tests/page-objects/NavigationBar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Locator, Page } from "@playwright/test";

export class NavigationBar {
private readonly nav: Locator;
private readonly close: () => Promise<void>;

constructor(private page: Page) {
this.nav = page.getByRole("navigation");
this.close = () => page.keyboard.press("Escape");
}

async fill(text: string) {
const input = await this.activateInput();
await input.fill(text);
return input;
}

async fillAndSubmit(text: string) {
const input = await this.fill(text);
await input.press("Enter");
}

async activateInput() {
const button = this.nav.getByRole("button", {
name: "Search or enter URI",
});
await button.click();
return this.page.getByPlaceholder("Search or enter URI");
}

async inputValue() {
const input = await this.activateInput();
const value = await input.inputValue();
await this.close();
return value;
}
}
12 changes: 7 additions & 5 deletions apps/tests/person.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { test, expect } from "@playwright/test";
import { expect } from "@playwright/test";

test("show name as heading for a person", async ({ page }) => {
import { test } from "./fixtures";

test("show name as heading for a person", async ({ page, navigationBar }) => {
// when opening PodOS Browser
await page.goto("/");

// and navigating to Alice's WebID
const navigationBar = page.getByPlaceholder("Enter URI");
await navigationBar.fill("http://localhost:4000/alice/profile/card#me");
await navigationBar.press("Enter");
await navigationBar.fillAndSubmit(
"http://localhost:4000/alice/profile/card#me",
);

// then the heading shows Alice's name as heading
const label = await page.getByRole("heading");
Expand Down
13 changes: 6 additions & 7 deletions apps/tests/reading-while-offline.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { test, expect } from "@playwright/test";
import { expect } from "@playwright/test";

import { test } from "./fixtures";

test("can access cached resources while offline", async ({
page,
context,
browserName,
navigationBar,
}) => {
test.skip(
browserName === "webkit",
Expand All @@ -17,11 +20,9 @@ test("can access cached resources while offline", async ({
});

await test.step("and the they have visited a resource", async () => {
const navigationBar = page.getByPlaceholder("Enter URI");
await navigationBar.fill(
await navigationBar.fillAndSubmit(
"http://localhost:4000/alice/public/generic/resource#it",
);
await navigationBar.press("Enter");

const heading = page.getByRole("heading");
await expect(heading).toHaveText("Something");
Expand All @@ -36,11 +37,9 @@ test("can access cached resources while offline", async ({
});

await test.step("and visits the previously loaded resource", async () => {
const navigationBar = page.getByPlaceholder("Enter URI");
await navigationBar.fill(
await navigationBar.fillAndSubmit(
"http://localhost:4000/alice/public/generic/resource#it",
);
await navigationBar.press("Enter");
});

await test.step("then the cached content is still accessible", async () => {
Expand Down
33 changes: 18 additions & 15 deletions apps/tests/text-search.spec.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import { test, expect } from "@playwright/test";
import { expect } from "@playwright/test";
import { signIn } from "./actions/signIn";
import { alice } from "./fixtures/credentials";

import { test } from "./fixtures";
test.describe("Text search", () => {
test("finds a thing based on the label index", async ({ page }) => {
test("finds a thing based on the label index", async ({
page,
navigationBar,
}) => {
await test.step("given a user is signed in", async () => {
await page.goto("/");
await signIn(page, alice);
});

await test.step("when they search for a text and select the first result", async () => {
const navigationBar = page.getByPlaceholder("Search or enter URI");
await navigationBar.fill("ometh");
const input = await navigationBar.fill("ometh");

const result = page
.getByRole("listitem")
.filter({ hasText: "Something" });
await expect(result).toBeVisible();
await navigationBar.press("ArrowDown");
await navigationBar.press("Enter");
await input.press("ArrowDown");
await input.press("Enter");
});

await test.step("then the page shows the selected resource", async () => {
Expand All @@ -29,26 +32,27 @@ test.describe("Text search", () => {
});
});

test("can find a thing, after making it findable", async ({ page }) => {
test("can find a thing, after making it findable", async ({
page,
navigationBar,
}) => {
await test.step("given a user is signed in", async () => {
await page.goto("/");
await signIn(page, alice);
});

const input = await navigationBar.activateInput();

await test.step("and there is no search result for 'Banana'", async () => {
const navigationBar = page.getByPlaceholder("Search or enter URI");
await navigationBar.fill("Banana");
await input.fill("Banana");

const result = page.getByRole("listitem").filter({ hasText: "Banana" });
await expect(result).not.toBeVisible();
});

await test.step("when they navigate to the thing", async () => {
const navigationBar = page.getByPlaceholder("Enter URI");
await navigationBar.fill(
"http://localhost:4000/alice/make-findable/banana#it",
);
await navigationBar.press("Enter");
await input.fill("http://localhost:4000/alice/make-findable/banana#it");
await input.press("Enter");
await expect(page.getByRole("heading", { name: "Banana" })).toBeVisible();
});

Expand All @@ -57,7 +61,6 @@ test.describe("Text search", () => {
});

await test.step("then they will find it when retrying the search", async () => {
const navigationBar = page.getByPlaceholder("Search or enter URI");
await navigationBar.fill("Banana");

const result = page.getByRole("listitem").filter({ hasText: "Banana" });
Expand Down
17 changes: 8 additions & 9 deletions docs/elements/apps/pos-app-browser/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

## Properties

| Property | Attribute | Description | Type | Default |
| ------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -------------- |
| `mode` | `mode` | The mode the app is running in: - standalone: use this when you deploy it as a standalone web application - pod: use this when you host this app as a default interface for you pod | `"pod" \| "standalone"` | `'standalone'` |
| `restorePreviousSession` | `restore-previous-session` | | `boolean` | `false` |
| Property | Attribute | Description | Type | Default |
| ------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- | -------------- |
| `mode` | `mode` | The mode the app is running in: - standalone: use this when you deploy it as a standalone web application - pod: use this when you host this app as a default interface for you pod | `"pod" \| "standalone"` | `'standalone'` |
| `restorePreviousSession` | `restore-previous-session` | | `boolean` | `false` |


## Dependencies
Expand All @@ -21,7 +21,7 @@
- [pos-error-toast](../../components/pos-error-toast)
- [pos-router](../../components/pos-router)
- [pos-add-new-thing](../../components/pos-add-new-thing)
- [pos-navigation-bar](../../components/pos-navigation-bar)
- [pos-navigation](../../components/pos-navigation)
- [pos-login](../../components/pos-login)
- [pos-internal-router](../../components/pos-internal-router)
- [pos-resource](../../components/pos-resource)
Expand All @@ -34,7 +34,7 @@ graph TD;
pos-app-browser --> pos-error-toast
pos-app-browser --> pos-router
pos-app-browser --> pos-add-new-thing
pos-app-browser --> pos-navigation-bar
pos-app-browser --> pos-navigation
pos-app-browser --> pos-login
pos-app-browser --> pos-internal-router
pos-app-browser --> pos-resource
Expand All @@ -48,13 +48,12 @@ graph TD;
pos-add-new-thing --> pos-new-thing-form
pos-dialog --> ion-icon
pos-new-thing-form --> pos-select-term
pos-navigation --> pos-navigation-bar
pos-navigation --> pos-rich-link
pos-navigation-bar --> pos-make-findable
pos-navigation-bar --> ion-searchbar
pos-navigation-bar --> pos-rich-link
pos-make-findable --> pos-resource
pos-make-findable --> pos-label
pos-resource --> ion-progress-bar
ion-searchbar --> ion-icon
pos-rich-link --> pos-resource
pos-rich-link --> pos-label
pos-rich-link --> pos-description
Expand Down
13 changes: 8 additions & 5 deletions docs/elements/components/pos-container-contents/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,29 @@

| Event | Description | Type |
| ----------------- | ----------- | ------------------ |
| `pod-os:link` | | `CustomEvent<any>` |
| `pod-os:resource` | | `CustomEvent<any>` |


## Dependencies

### Used by

- [pos-container-contents](.)
- [pos-app-ldp-container](../../apps/pos-app-ldp-container)

### Depends on

- ion-icon
- [pos-resource](../pos-resource)
- [pos-container-item](.)

### Graph
```mermaid
graph TD;
pos-container-item --> ion-icon
pos-container-contents --> pos-resource
pos-container-contents --> pos-container-item
style pos-container-item fill:#f9f,stroke:#333,stroke-width:4px
pos-resource --> ion-progress-bar
pos-container-item --> ion-icon
pos-app-ldp-container --> pos-container-contents
style pos-container-contents fill:#f9f,stroke:#333,stroke-width:4px
```

----------------------------------------------
Expand Down
Loading