+
diff --git a/explorer/src/lib/components/__tests__/__snapshots__/TransactionsCard.spec.js.snap b/explorer/src/lib/components/__tests__/__snapshots__/TransactionsCard.spec.js.snap
index c21b955d63..711dfea952 100644
--- a/explorer/src/lib/components/__tests__/__snapshots__/TransactionsCard.spec.js.snap
+++ b/explorer/src/lib/components/__tests__/__snapshots__/TransactionsCard.spec.js.snap
@@ -72,7 +72,7 @@ exports[`Transactions Card > should disable the \`Show More\` button if there is
class="data-card__content"
>
should disable the \`Show More\` button if there is
- {#each displayedTxns as txn (txn)}
-
- {/each}
-
+ {#if isSmallScreen}
+
+ {#each displayedTxns as txn (txn)}
+
+ {/each}
+
+ {:else}
+
+ {/if}
diff --git a/explorer/src/lib/dusk/mocks/MediaQueryList.js b/explorer/src/lib/dusk/mocks/MediaQueryList.js
new file mode 100644
index 0000000000..28fca92a1a
--- /dev/null
+++ b/explorer/src/lib/dusk/mocks/MediaQueryList.js
@@ -0,0 +1,68 @@
+import { afterAll } from "vitest";
+
+const controllers = new Set();
+
+afterAll(() => {
+ controllers.forEach((controller) => {
+ controller.abort();
+ controllers.delete(controller);
+ });
+});
+
+/**
+ * Mocks the `MediaQueryList` object and listens to the
+ * "DuskMediaQueryMatchesChange" custom event.
+ * Fire one manually or with the `changeMediaQueryMatches`
+ * helper function to simulate media query changes.
+ */
+export default class MediaQueryList extends EventTarget {
+ #matches;
+
+ #media;
+
+ /**
+ * @param {string} mediaQuery
+ * @param {boolean} initialMatches
+ */
+ constructor(mediaQuery, initialMatches) {
+ super();
+
+ this.#matches = initialMatches;
+ this.#media = mediaQuery;
+
+ const abortController = new AbortController();
+
+ controllers.add(abortController);
+
+ global.addEventListener("DuskMediaQueryMatchesChange", this, {
+ signal: abortController.signal,
+ });
+ }
+
+ get matches() {
+ return this.#matches;
+ }
+
+ get media() {
+ return this.#media;
+ }
+
+ /** @param {CustomEvent<{ media: string, matches: boolean }>} evt */
+ handleEvent(evt) {
+ const { detail, type } = evt;
+
+ if (
+ type === "DuskMediaQueryMatchesChange" &&
+ detail.media === this.#media
+ ) {
+ this.#matches = detail.matches;
+
+ this.dispatchEvent(
+ new MediaQueryListEvent("change", {
+ matches: this.#matches,
+ media: this.#media,
+ })
+ );
+ }
+ }
+}
diff --git a/explorer/src/lib/dusk/mocks/MediaQueryListEvent.js b/explorer/src/lib/dusk/mocks/MediaQueryListEvent.js
new file mode 100644
index 0000000000..6f076c1137
--- /dev/null
+++ b/explorer/src/lib/dusk/mocks/MediaQueryListEvent.js
@@ -0,0 +1,26 @@
+import { pickIn } from "lamb";
+
+export default class MediaQueryListEvent extends Event {
+ #matches;
+
+ #media;
+
+ /**
+ * @param {string} type
+ * @param {MediaQueryListEventInit} options
+ */
+ constructor(type, options) {
+ super(type, pickIn(options, ["bubbles", "cancelable", "composed"]));
+
+ this.#matches = options.matches;
+ this.#media = options.media;
+ }
+
+ get matches() {
+ return this.#matches;
+ }
+
+ get media() {
+ return this.#media;
+ }
+}
diff --git a/explorer/src/lib/dusk/mocks/index.js b/explorer/src/lib/dusk/mocks/index.js
index bc6d671e61..f31648fc8d 100644
--- a/explorer/src/lib/dusk/mocks/index.js
+++ b/explorer/src/lib/dusk/mocks/index.js
@@ -1 +1,3 @@
export { default as IntersectionObserver } from "./IntersectionObserver";
+export { default as MediaQueryList } from "./MediaQueryList";
+export { default as MediaQueryListEvent } from "./MediaQueryListEvent";
diff --git a/explorer/src/lib/dusk/test-helpers/__tests__/changeMediaQueryMatches.spec.js b/explorer/src/lib/dusk/test-helpers/__tests__/changeMediaQueryMatches.spec.js
new file mode 100644
index 0000000000..1c0e6a3aaa
--- /dev/null
+++ b/explorer/src/lib/dusk/test-helpers/__tests__/changeMediaQueryMatches.spec.js
@@ -0,0 +1,27 @@
+import { describe, expect, it } from "vitest";
+
+import { changeMediaQueryMatches } from "..";
+
+describe("changeMediaQueryMatches", () => {
+ it('should dispatch "DuskMediaQueryMatchesChange" custom events', () => {
+ const media = "(max-width: 1024px)";
+ const matches = true;
+
+ /** @param {Event} evt */
+ const handler = (evt) => {
+ expect(evt).toBeInstanceOf(CustomEvent);
+ expect(evt.type).toBe("DuskMediaQueryMatchesChange");
+
+ // @ts-ignore see https://github.com/Microsoft/TypeScript/issues/28357
+ expect(evt.detail).toStrictEqual({ matches, media });
+ };
+
+ global.addEventListener("DuskMediaQueryMatchesChange", handler);
+
+ changeMediaQueryMatches(media, matches);
+
+ global.removeEventListener("DuskMediaQueryMatchesChange", handler);
+
+ expect.assertions(3);
+ });
+});
diff --git a/explorer/src/lib/dusk/test-helpers/changeMediaQueryMatches.js b/explorer/src/lib/dusk/test-helpers/changeMediaQueryMatches.js
new file mode 100644
index 0000000000..ec0b6e16ad
--- /dev/null
+++ b/explorer/src/lib/dusk/test-helpers/changeMediaQueryMatches.js
@@ -0,0 +1,14 @@
+/**
+ * Helper to fire "DuskMediaQueryMatchesChange" custom
+ * events that are listened by our `MediaQueryList` mock.
+ *
+ * @param {string} media
+ * @param {boolean} matches
+ */
+export default function changeMediaQueryMatches(media, matches) {
+ dispatchEvent(
+ new CustomEvent("DuskMediaQueryMatchesChange", {
+ detail: { matches, media },
+ })
+ );
+}
diff --git a/explorer/src/lib/dusk/test-helpers/index.js b/explorer/src/lib/dusk/test-helpers/index.js
index 307c8f5ed6..1a8924b35f 100644
--- a/explorer/src/lib/dusk/test-helpers/index.js
+++ b/explorer/src/lib/dusk/test-helpers/index.js
@@ -1,3 +1,4 @@
+export { default as changeMediaQueryMatches } from "./changeMediaQueryMatches";
export { default as mockReadableStore } from "./mockReadableStore";
export { default as renderWithSimpleContent } from "./renderWithSimpleContent";
export { default as renderWithSlots } from "./renderWithSlots";
diff --git a/explorer/src/lib/stores/__tests__/appStore.spec.js b/explorer/src/lib/stores/__tests__/appStore.spec.js
index 3085cba90f..1314b714d7 100644
--- a/explorer/src/lib/stores/__tests__/appStore.spec.js
+++ b/explorer/src/lib/stores/__tests__/appStore.spec.js
@@ -1,6 +1,8 @@
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import { get } from "svelte/store";
+import { changeMediaQueryMatches } from "$lib/dusk/test-helpers";
+
describe("appStore", () => {
const originalTouchStart = window.ontouchstart;
const originalMaxTouchPoints = navigator.maxTouchPoints;
@@ -40,6 +42,7 @@ describe("appStore", () => {
darkMode: false,
fetchInterval: Number(env.VITE_REFETCH_INTERVAL),
hasTouchSupport: false,
+ isSmallScreen: false,
marketDataFetchInterval: Number(env.VITE_MARKET_DATA_REFETCH_INTERVAL),
network: expectedNetworks[0].value,
networks: expectedNetworks,
@@ -101,4 +104,32 @@ describe("appStore", () => {
expect(get(appStore).darkMode).toBe(true);
});
+
+ it("should set the `isSmallScreen` property to `false` when the related media query doesn't match", async () => {
+ const { appStore } = await import("..");
+
+ expect(get(appStore).isSmallScreen).toBe(false);
+ });
+
+ it("should set the `isSmallScreen` property to `true` when the related media query matches", async () => {
+ const mqMatchesSpy = vi
+ .spyOn(MediaQueryList.prototype, "matches", "get")
+ .mockReturnValue(true);
+
+ const { appStore } = await import("..");
+
+ expect(get(appStore).isSmallScreen).toBe(true);
+
+ mqMatchesSpy.mockRestore();
+ });
+
+ it("should update the `isSmallScreen` property when the media query match changes", async () => {
+ const { appStore } = await import("..");
+
+ expect(get(appStore).isSmallScreen).toBe(false);
+
+ changeMediaQueryMatches("(max-width: 1024px)", true);
+
+ expect(get(appStore).isSmallScreen).toBe(true);
+ });
});
diff --git a/explorer/src/lib/stores/appStore.js b/explorer/src/lib/stores/appStore.js
index 366c540ee2..5acb8f1f8c 100644
--- a/explorer/src/lib/stores/appStore.js
+++ b/explorer/src/lib/stores/appStore.js
@@ -6,6 +6,8 @@ const networks = [
{ label: "Testnet", value: import.meta.env.VITE_DUSK_TESTNET_NODE },
];
+const maxWidthMediaQuery = window.matchMedia("(max-width: 1024px)");
+
const browserDefaults = browser
? {
darkMode: window.matchMedia("(prefers-color-scheme: dark)").matches,
@@ -21,6 +23,7 @@ const initialState = {
chainInfoEntries: Number(import.meta.env.VITE_CHAIN_INFO_ENTRIES),
fetchInterval: Number(import.meta.env.VITE_REFETCH_INTERVAL) || 1000,
hasTouchSupport: "ontouchstart" in window || navigator.maxTouchPoints > 0,
+ isSmallScreen: maxWidthMediaQuery.matches,
marketDataFetchInterval:
Number(import.meta.env.VITE_MARKET_DATA_REFETCH_INTERVAL) || 120000,
network: networks[0].value,
@@ -35,6 +38,13 @@ const initialState = {
const store = writable(initialState);
const { set, subscribe } = store;
+maxWidthMediaQuery.addEventListener("change", (event) => {
+ set({
+ ...get(store),
+ isSmallScreen: event.matches,
+ });
+});
+
/** @param {string} network */
const setNetwork = (network) =>
set({
diff --git a/explorer/src/lib/stores/stores.d.ts b/explorer/src/lib/stores/stores.d.ts
index fb1b5c6efe..a4e15d35dd 100644
--- a/explorer/src/lib/stores/stores.d.ts
+++ b/explorer/src/lib/stores/stores.d.ts
@@ -8,6 +8,7 @@ type AppStoreContent = {
chainInfoEntries: number;
darkMode: boolean;
fetchInterval: number;
+ isSmallScreen: boolean;
hasTouchSupport: boolean;
marketDataFetchInterval: number;
network: string;
diff --git a/explorer/src/routes/+page.svelte b/explorer/src/routes/+page.svelte
index 125e4cb05a..46f2e489a1 100644
--- a/explorer/src/routes/+page.svelte
+++ b/explorer/src/routes/+page.svelte
@@ -21,7 +21,7 @@
onDestroy(pollingDataStore.stop);
$: ({ data, error, isLoading } = $pollingDataStore);
- $: ({ chainInfoEntries, network } = $appStore);
+ $: ({ chainInfoEntries, isSmallScreen, network } = $appStore);
const retry = () => {
pollingDataStore.start(network, chainInfoEntries);
@@ -38,6 +38,7 @@
className="tables-layout"
blocks={data?.blocks}
{error}
+ {isSmallScreen}
loading={isLoading}
/>
@@ -46,6 +47,7 @@
className="tables-layout"
txns={data?.transactions}
{error}
+ {isSmallScreen}
loading={isLoading}
/>
diff --git a/explorer/src/routes/__tests__/layout.spec.js b/explorer/src/routes/__tests__/layout.spec.js
index 90c4cdadf4..9302e384ee 100644
--- a/explorer/src/routes/__tests__/layout.spec.js
+++ b/explorer/src/routes/__tests__/layout.spec.js
@@ -112,7 +112,7 @@ describe("Main layout", () => {
vi.useRealTimers();
});
- it("should use the default delay for the tooltip if the device ha touch support", async () => {
+ it("should use the default delay for the tooltip if the device has touch support", async () => {
const defaultDelayShow = 500;
vi.useFakeTimers();
diff --git a/explorer/src/routes/blocks/+page.svelte b/explorer/src/routes/blocks/+page.svelte
index 95b2d4c966..f2c77070d2 100644
--- a/explorer/src/routes/blocks/+page.svelte
+++ b/explorer/src/routes/blocks/+page.svelte
@@ -19,7 +19,11 @@
onDestroy(pollingDataStore.stop);
$: ({ data, error, isLoading } = $pollingDataStore);
- $: ({ blocksListEntries, network: currentNetwork } = $appStore);
+ $: ({
+ blocksListEntries,
+ isSmallScreen,
+ network: currentNetwork,
+ } = $appStore);
@@ -28,5 +32,6 @@
blocks={data}
{error}
loading={isLoading}
+ {isSmallScreen}
/>
diff --git a/explorer/src/routes/blocks/__tests__/__snapshots__/page.spec.js.snap b/explorer/src/routes/blocks/__tests__/__snapshots__/page.spec.js.snap
index 2ed48dfe30..d2de0352fd 100644
--- a/explorer/src/routes/blocks/__tests__/__snapshots__/page.spec.js.snap
+++ b/explorer/src/routes/blocks/__tests__/__snapshots__/page.spec.js.snap
@@ -1,52 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[`Blocks page > should render the Blocks page, start polling for blocks and stop the polling when unmounted 1`] = `
-
-
-
-
- Blocks — 0 Displayed Items
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`Blocks page > should render the Blocks page, start polling for blocks and stop the polling when unmounted 2`] = `
+exports[`Blocks page > should render the Blocks page with the mobile layout 1`] = `
@@ -79,943 +33,424 @@ exports[`Blocks page > should render the Blocks page, start polling for blocks a
class="data-card__content"
>
should render the Blocks page, start polling for blocks a
>
- 1,365,454
+ 1,365,450
@@ -1050,7 +485,7 @@ exports[`Blocks page > should render the Blocks page, start polling for blocks a
>
-
-
+
+
+
+
+
+
+
+`;
+
+exports[`Blocks page > should render the Blocks page, start polling for blocks and stop the polling when unmounted 1`] = `
+
+
+
+
+ Blocks — 0 Displayed Items
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Blocks page > should render the Blocks page, start polling for blocks and stop the polling when unmounted 2`] = `
+
+
diff --git a/explorer/src/routes/blocks/block/__tests__/page.spec.js b/explorer/src/routes/blocks/block/__tests__/page.spec.js
index eab8654a56..39050fd756 100644
--- a/explorer/src/routes/blocks/block/__tests__/page.spec.js
+++ b/explorer/src/routes/blocks/block/__tests__/page.spec.js
@@ -1,9 +1,11 @@
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, render } from "@testing-library/svelte";
+import { get } from "svelte/store";
import { duskAPI } from "$lib/services";
import { transformBlock } from "$lib/chain-info";
import { gqlBlock } from "$lib/mock-data";
+import { changeMediaQueryMatches } from "$lib/dusk/test-helpers";
import BlockDetails from "../+page.svelte";
@@ -37,4 +39,19 @@ describe("Block Details", () => {
// snapshot with received data from APIs
expect(container.firstChild).toMatchSnapshot();
});
+
+ it("should render the Transaction section of the Block Details page with the mobile layout", async () => {
+ const { appStore } = await import("$lib/stores");
+ const { container } = render(BlockDetails);
+
+ changeMediaQueryMatches("(max-width: 1024px)", true);
+
+ expect(get(appStore).isSmallScreen).toBe(true);
+
+ expect(getBlockSpy).toHaveBeenCalledTimes(1);
+
+ await vi.advanceTimersByTimeAsync(1);
+
+ expect(container.firstChild).toMatchSnapshot();
+ });
});
diff --git a/explorer/src/routes/transactions/+page.svelte b/explorer/src/routes/transactions/+page.svelte
index dd969691bd..dc2a0823b7 100644
--- a/explorer/src/routes/transactions/+page.svelte
+++ b/explorer/src/routes/transactions/+page.svelte
@@ -19,7 +19,11 @@
onDestroy(pollingDataStore.stop);
$: ({ data, error, isLoading } = $pollingDataStore);
- $: ({ network: currentNetwork, transactionsListEntries } = $appStore);
+ $: ({
+ isSmallScreen,
+ network: currentNetwork,
+ transactionsListEntries,
+ } = $appStore);
@@ -29,5 +33,6 @@
txns={data}
{error}
loading={isLoading}
+ {isSmallScreen}
/>
diff --git a/explorer/src/routes/transactions/__tests__/__snapshots__/page.spec.js.snap b/explorer/src/routes/transactions/__tests__/__snapshots__/page.spec.js.snap
index 56f67198f4..4de3b3ca4c 100644
--- a/explorer/src/routes/transactions/__tests__/__snapshots__/page.spec.js.snap
+++ b/explorer/src/routes/transactions/__tests__/__snapshots__/page.spec.js.snap
@@ -1,52 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[`Transactions page > should render the Transactions page, start polling for blocks and stop the polling when unmounted 1`] = `
-
-
-
-
- Transactions — 0 Displayed Items
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`Transactions page > should render the Transactions page, start polling for blocks and stop the polling when unmounted 2`] = `
+exports[`Transactions page > should render the Transactions page with the mobile layout 1`] = `
@@ -79,1175 +33,710 @@ exports[`Transactions page > should render the Transactions page, start polling
class="data-card__content"
>