Skip to content
Merged
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
7 changes: 7 additions & 0 deletions examples/form/app/layout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ import { PrimerRoot } from '@barso/ui';
import '@barso/ui/css';

import { DefaultLayout } from '../components/DefaultLayout.jsx';
import { DefaultHead } from '../components/Head/Head.App';
import { NotificationsProvider } from '../components/Notifications';

export const metadata = {
title: 'App Router · Default Title',
description: 'App Router · Default Description',
};

export default function Layout({ children }) {
return (
<PrimerRoot defaultColorMode="light" lang="pt-BR">
<DefaultHead />
<NotificationsProvider>
<DefaultLayout containerWidth="medium">{children}</DefaultLayout>
</NotificationsProvider>
Expand Down
10 changes: 10 additions & 0 deletions examples/form/components/Head/Head.App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* eslint-disable @next/next/no-head-element */
import { DefaultTags } from './Tags';

export function DefaultHead() {
return (
<head>
<DefaultTags />
</head>
);
}
20 changes: 20 additions & 0 deletions examples/form/components/Head/Head.Pages.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import NextHead from 'next/head';

import { DefaultTags } from './Tags';

export function DefaultHead() {
// next/head does not mount child components like a normal React tree.
// Using <DefaultTags /> results in the SSR output being reused on the client,
// so hooks inside the component never run.
// Calling DefaultTags() forces evaluation instead of relying on component mounting.
return <NextHead>{DefaultTags()}</NextHead>;
}

export function Head({ title, description }) {
return (
<NextHead>
{title && <title>{title}</title>}
{description && <meta name="description" content={description} key="description" />}
</NextHead>
);
}
13 changes: 13 additions & 0 deletions examples/form/components/Head/Tags.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use client';
import { useMediaQuery } from '@barso/hooks';

export function DefaultTags() {
const isDark = useMediaQuery('(prefers-color-scheme: dark)');

return (
<>
<link rel="icon" href={isDark ? '/favicon-dark.png' : '/favicon-light.png'} key="favicon" />
{/* Add other SEO tags or links here as needed */}
</>
);
}
3 changes: 3 additions & 0 deletions examples/form/pages/_app.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { AutoThemeProvider } from '@barso/ui';
import '@barso/ui/css';

import { DefaultHead, Head } from '../components/Head/Head.Pages';
import { NotificationsProvider } from '../components/Notifications';

export default function MyApp({ Component, pageProps }) {
return (
<AutoThemeProvider defaultColorMode="dark">
<DefaultHead />
<Head title="Pages Router · Default Title" description="Pages Router · Default Description" />
<NotificationsProvider>
<Component {...pageProps} />
</NotificationsProvider>
Expand Down
2 changes: 2 additions & 0 deletions examples/form/pages/pages_router.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Checkout } from '../components/Checkout.jsx';
import { DefaultLayout } from '../components/DefaultLayout.jsx';
import { Head } from '../components/Head/Head.Pages.jsx';
import { checkoutFields, product, store } from '../form-config.js';

export default function Home() {
return (
<DefaultLayout containerWidth="medium">
<Head description="Pages Router · Custom Description" />
<Checkout fields={checkoutFields} product={product} store={store} />
</DefaultLayout>
);
Expand Down
2 changes: 2 additions & 0 deletions examples/form/pages/registration.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { DefaultLayout } from '../components/DefaultLayout.jsx';
import { Head } from '../components/Head/Head.Pages.jsx';
import { Registration } from '../components/Registration.jsx';
import { registrationFields, store } from '../form-config.js';

export default function RegistrationPage() {
return (
<DefaultLayout containerWidth="small">
<Head title="Pages Router · Custom Title" />
<Registration fields={registrationFields} store={store} />
</DefaultLayout>
);
Expand Down
Binary file added examples/form/public/favicon-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/form/public/favicon-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/hooks/src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './useConfig/index.js';
export * from './useMediaQuery/index.js';
export * from './useMergedState/index.js';
export * from './useTreeCollapse/index.js';
1 change: 1 addition & 0 deletions packages/hooks/src/useMediaQuery/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useMediaQuery.js';
17 changes: 17 additions & 0 deletions packages/hooks/src/useMediaQuery/useMediaQuery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useEffect, useState } from 'react';

export function useMediaQuery(query) {
const [matches, setMatches] = useState(() => typeof window !== 'undefined' && !!window.matchMedia(query).matches);

useEffect(() => {
const media = window.matchMedia(query);
const listener = () => setMatches(media.matches);

listener();

media.addEventListener('change', listener);
return () => media.removeEventListener('change', listener);
}, [query]);

return matches;
}
60 changes: 60 additions & 0 deletions packages/hooks/src/useMediaQuery/useMediaQuery.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { act, renderHook } from '@testing-library/react';
import { renderToString } from 'react-dom/server';

import { useMediaQuery } from './index.js';

describe('useMediaQuery', () => {
describe('renderHook', () => {
let matches = false;
const listeners = new Set();

beforeAll(() => {
vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
get matches() {
return matches;
},
media: query,
addEventListener: (event, cb) => listeners.add(cb),
removeEventListener: (event, cb) => listeners.delete(cb),
}));
});

beforeEach(() => {
matches = false;
listeners.clear();
});

afterAll(() => {
window.matchMedia.mockRestore();
});

it('should return true if the media query matches', () => {
matches = true;
const { result } = renderHook(() => useMediaQuery('(min-width: 600px)'));
expect(result.current).toBe(true);
});

it('should return false if the media query does not match', () => {
const { result } = renderHook(() => useMediaQuery('(min-width: 1200px)'));
expect(result.current).toBe(false);
});

it('should update when the media query changes', () => {
const { result } = renderHook(() => useMediaQuery('(min-width: 800px)'));
expect(result.current).toBe(false);

act(() => {
matches = true;
listeners.forEach((cb) => cb());
});
expect(result.current).toBe(true);
});
});

describe('SSR', () => {
it('should not throw during SSR and return false', () => {
const TestComponent = () => String(useMediaQuery('(min-width: 600px)'));
expect(renderToString(<TestComponent />)).toBe('false');
});
});
});
Loading