@regardio/react 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -16
- package/package.json +17 -38
- package/src/background-slideshow/background-slideshow.test.tsx +146 -0
- package/src/carousel/carousel.test.tsx +86 -0
- package/src/countdown/countdown.test.tsx +46 -0
- package/src/grid/grid.test.tsx +94 -0
- package/src/markdown-container/markdown-container.test.tsx +124 -0
- package/src/password-input/password-input.test.tsx +53 -0
- package/src/protected-email/protected-email.test.tsx +82 -0
- package/dist/generic-error/index.d.mts +0 -47
- package/dist/generic-error/index.mjs +0 -56
- package/dist/hooks/use-current-route-data.d.mts +0 -8
- package/dist/hooks/use-current-route-data.mjs +0 -19
- package/dist/hooks/use-matches-data.d.mts +0 -10
- package/dist/hooks/use-matches-data.mjs +0 -20
- package/dist/hooks/use-user.d.mts +0 -53
- package/dist/hooks/use-user.mjs +0 -32
- package/dist/link/index.d.mts +0 -71
- package/dist/link/index.mjs +0 -127
- package/src/generic-error/generic-error.stories.tsx +0 -45
- package/src/generic-error/generic-error.tsx +0 -105
- package/src/generic-error/index.ts +0 -2
- package/src/hooks/use-current-route-data.ts +0 -20
- package/src/hooks/use-matches-data.ts +0 -21
- package/src/hooks/use-user.tsx +0 -73
- package/src/link/index.ts +0 -2
- package/src/link/link.stories.tsx +0 -109
- package/src/link/link.test.tsx +0 -169
- package/src/link/link.tsx +0 -218
package/dist/link/index.mjs
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { t as lowerCaseSzett } from "../text-BFTwzxPw.mjs";
|
|
2
|
-
import { createContext, useCallback, useContext } from "react";
|
|
3
|
-
import { tv } from "@regardio/tailwind/utils";
|
|
4
|
-
import { Fragment as Fragment$1, jsx } from "react/jsx-runtime";
|
|
5
|
-
import { NavLink } from "react-router";
|
|
6
|
-
//#region src/link/link.tsx
|
|
7
|
-
const PathResolverContext = createContext(null);
|
|
8
|
-
const PathResolverProvider = PathResolverContext.Provider;
|
|
9
|
-
function usePathResolver() {
|
|
10
|
-
return useContext(PathResolverContext);
|
|
11
|
-
}
|
|
12
|
-
const LinkBase = ({ className, to, routeKey, children, onClick, viewTransition = true, ...props }) => {
|
|
13
|
-
const pathResolver = usePathResolver();
|
|
14
|
-
let path;
|
|
15
|
-
if (routeKey && pathResolver) path = pathResolver(routeKey);
|
|
16
|
-
else if (typeof to === "string") path = to;
|
|
17
|
-
else {
|
|
18
|
-
path = to?.pathname ?? "";
|
|
19
|
-
if (to?.search) path += to.search;
|
|
20
|
-
if (to?.hash) path += to.hash;
|
|
21
|
-
}
|
|
22
|
-
const isExternal = path.startsWith("tel:") || path.startsWith("mailto:") || path.startsWith("#") || path.startsWith("http");
|
|
23
|
-
const handleClick = useCallback((event) => {
|
|
24
|
-
onClick?.(event);
|
|
25
|
-
if (event.defaultPrevented) return;
|
|
26
|
-
if (path.startsWith("tel:") || path.startsWith("mailto:")) return;
|
|
27
|
-
if (path.startsWith("#")) {
|
|
28
|
-
event.preventDefault();
|
|
29
|
-
const element = document.getElementById(path.substring(1));
|
|
30
|
-
if (element) element.scrollIntoView({ behavior: "smooth" });
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
if (path.startsWith("http")) {
|
|
34
|
-
event.preventDefault();
|
|
35
|
-
window.open(path, "_blank", "noopener,noreferrer");
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
}, [onClick, path]);
|
|
39
|
-
if (!path) return /* @__PURE__ */ jsx(Fragment$1, { children: typeof children === "function" ? null : children });
|
|
40
|
-
if (isExternal) {
|
|
41
|
-
const externalState = {
|
|
42
|
-
isActive: false,
|
|
43
|
-
isPending: false,
|
|
44
|
-
isTransitioning: false
|
|
45
|
-
};
|
|
46
|
-
const resolvedClassName = typeof className === "function" ? className(externalState) : className;
|
|
47
|
-
const resolvedStyle = typeof props.style === "function" ? props.style(externalState) : props.style;
|
|
48
|
-
return /* @__PURE__ */ jsx("a", {
|
|
49
|
-
className: resolvedClassName,
|
|
50
|
-
href: path,
|
|
51
|
-
onClick: handleClick,
|
|
52
|
-
style: resolvedStyle,
|
|
53
|
-
children: typeof children === "function" ? children(externalState) : children
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return /* @__PURE__ */ jsx(NavLink, {
|
|
57
|
-
...props,
|
|
58
|
-
className,
|
|
59
|
-
onClick: handleClick,
|
|
60
|
-
to: path,
|
|
61
|
-
viewTransition,
|
|
62
|
-
children
|
|
63
|
-
});
|
|
64
|
-
};
|
|
65
|
-
const link = tv({
|
|
66
|
-
base: [],
|
|
67
|
-
defaultVariants: { variant: "primary" },
|
|
68
|
-
variants: {
|
|
69
|
-
arrow: {
|
|
70
|
-
darr: "darr",
|
|
71
|
-
larr: "larr",
|
|
72
|
-
rarr: "rarr",
|
|
73
|
-
uarr: "uarr"
|
|
74
|
-
},
|
|
75
|
-
variant: {
|
|
76
|
-
button: [
|
|
77
|
-
"block",
|
|
78
|
-
"button",
|
|
79
|
-
"mt-s",
|
|
80
|
-
"relative",
|
|
81
|
-
"rarr",
|
|
82
|
-
"text-right",
|
|
83
|
-
"text-sm",
|
|
84
|
-
"tracking-wider",
|
|
85
|
-
"uppercase"
|
|
86
|
-
],
|
|
87
|
-
code: ["font-monospace"],
|
|
88
|
-
link: [
|
|
89
|
-
"rarr",
|
|
90
|
-
"!bg-transparent",
|
|
91
|
-
"uppercase",
|
|
92
|
-
"!tracking-wider"
|
|
93
|
-
],
|
|
94
|
-
navtitle: [
|
|
95
|
-
"block",
|
|
96
|
-
"uppercase",
|
|
97
|
-
"tracking-wider"
|
|
98
|
-
],
|
|
99
|
-
primary: [],
|
|
100
|
-
subtitle: ["text-lg"]
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
const Link = ({ arrow, children, className, routeKey, to, variant, viewTransition, ...props }) => {
|
|
105
|
-
return /* @__PURE__ */ jsx(LinkBase, {
|
|
106
|
-
...props,
|
|
107
|
-
className: link({
|
|
108
|
-
arrow,
|
|
109
|
-
className: typeof className === "string" ? className : void 0,
|
|
110
|
-
variant
|
|
111
|
-
}),
|
|
112
|
-
routeKey,
|
|
113
|
-
to,
|
|
114
|
-
viewTransition,
|
|
115
|
-
children: lowerCaseSzett(children)
|
|
116
|
-
});
|
|
117
|
-
};
|
|
118
|
-
const MarkdownLink = ({ children, href, ...props }) => {
|
|
119
|
-
if (href) return /* @__PURE__ */ jsx(Link, {
|
|
120
|
-
to: href,
|
|
121
|
-
...props,
|
|
122
|
-
children
|
|
123
|
-
});
|
|
124
|
-
return null;
|
|
125
|
-
};
|
|
126
|
-
//#endregion
|
|
127
|
-
export { Link, LinkBase, MarkdownLink, PathResolverProvider, usePathResolver };
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
-
import { GenericError, getErrorDescriptor } from './generic-error';
|
|
3
|
-
|
|
4
|
-
const meta: Meta<typeof GenericError> = {
|
|
5
|
-
component: GenericError,
|
|
6
|
-
parameters: {
|
|
7
|
-
layout: 'fullscreen',
|
|
8
|
-
},
|
|
9
|
-
tags: ['autodocs'],
|
|
10
|
-
title: 'Components/GenericError',
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export default meta;
|
|
14
|
-
type Story = StoryObj<typeof GenericError>;
|
|
15
|
-
|
|
16
|
-
const ErrorWrapper = ({ status }: { status: number }) => {
|
|
17
|
-
const error = new Response('Not Found', { status, statusText: 'Not Found' });
|
|
18
|
-
|
|
19
|
-
return <GenericError error={error} />;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const Error404: Story = {
|
|
23
|
-
render: () => <ErrorWrapper status={404} />,
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export const Error500: Story = {
|
|
27
|
-
render: () => <ErrorWrapper status={500} />,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export const GetErrorDescriptorDemo: Story = {
|
|
31
|
-
render: () => {
|
|
32
|
-
const httpError = getErrorDescriptor(new Response('Not Found', { status: 404 }));
|
|
33
|
-
const runtimeError = getErrorDescriptor(new Error('Something went wrong'));
|
|
34
|
-
const unknownError = getErrorDescriptor('unknown');
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<div style={{ padding: '24px' }}>
|
|
38
|
-
<h3>Error Descriptor Examples</h3>
|
|
39
|
-
<pre style={{ background: '#f5f5f5', padding: '16px' }}>
|
|
40
|
-
{JSON.stringify({ httpError, runtimeError, unknownError }, null, 2)}
|
|
41
|
-
</pre>
|
|
42
|
-
</div>
|
|
43
|
-
);
|
|
44
|
-
},
|
|
45
|
-
};
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { isRouteErrorResponse } from 'react-router';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Descriptor returned from getErrorDescriptor to help apps localize messages.
|
|
5
|
-
*/
|
|
6
|
-
export type ErrorDescriptor =
|
|
7
|
-
| {
|
|
8
|
-
type: 'http';
|
|
9
|
-
status: number;
|
|
10
|
-
statusText: string;
|
|
11
|
-
defaultId: string; // e.g. "errors.http"
|
|
12
|
-
defaultMessage: string; // e.g. "Error {status}"
|
|
13
|
-
}
|
|
14
|
-
| {
|
|
15
|
-
type: 'error';
|
|
16
|
-
defaultId: string; // e.g. "errors.runtime"
|
|
17
|
-
defaultMessage: string; // e.g. "An unexpected error occurred."
|
|
18
|
-
message?: string;
|
|
19
|
-
stack?: string;
|
|
20
|
-
}
|
|
21
|
-
| {
|
|
22
|
-
type: 'unknown';
|
|
23
|
-
defaultId: string; // e.g. "errors.unknown"
|
|
24
|
-
defaultMessage: string; // e.g. "An unexpected error occurred."
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Compute a normalized error descriptor from a React Router error.
|
|
29
|
-
* Apps can use this to map to i18n keys.
|
|
30
|
-
*/
|
|
31
|
-
export function getErrorDescriptor(error: unknown): ErrorDescriptor {
|
|
32
|
-
if (isRouteErrorResponse(error)) {
|
|
33
|
-
const status = error.status;
|
|
34
|
-
const statusText = error.statusText || 'Error';
|
|
35
|
-
return {
|
|
36
|
-
defaultId: status === 404 ? 'errors.404' : 'errors.http',
|
|
37
|
-
defaultMessage: status === 404 ? 'Not found' : `Error ${status}`,
|
|
38
|
-
status,
|
|
39
|
-
statusText,
|
|
40
|
-
type: 'http',
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
if (error instanceof Error) {
|
|
44
|
-
return {
|
|
45
|
-
defaultId: 'errors.runtime',
|
|
46
|
-
defaultMessage: 'An unexpected error occurred.',
|
|
47
|
-
message: error.message,
|
|
48
|
-
type: 'error',
|
|
49
|
-
...(error.stack ? { stack: error.stack } : {}),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
return {
|
|
53
|
-
defaultId: 'errors.unknown',
|
|
54
|
-
defaultMessage: 'An unexpected error occurred.',
|
|
55
|
-
type: 'unknown',
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* GenericError
|
|
61
|
-
*
|
|
62
|
-
* A reusable error boundary component for React Router apps.
|
|
63
|
-
* - Displays status-based messages for route responses
|
|
64
|
-
* - Shows stack traces in development for non-response errors
|
|
65
|
-
* - SSR-safe: accepts optional error prop for server-side rendering
|
|
66
|
-
*
|
|
67
|
-
* For localization, apps may either:
|
|
68
|
-
* - Wrap this component and use `getErrorDescriptor(error)` to map to i18n keys
|
|
69
|
-
* - Or provide a custom `renderMessage` to override the displayed details
|
|
70
|
-
*/
|
|
71
|
-
export function GenericError({
|
|
72
|
-
error,
|
|
73
|
-
renderMessage,
|
|
74
|
-
}: {
|
|
75
|
-
error: unknown;
|
|
76
|
-
renderMessage?: (descriptor: ErrorDescriptor) => React.JSX.Element;
|
|
77
|
-
}): React.JSX.Element {
|
|
78
|
-
const descriptor = getErrorDescriptor(error);
|
|
79
|
-
|
|
80
|
-
const title = descriptor.type === 'http' ? `Error ${descriptor.status}` : 'Error';
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<main className="pt-2xl p-sm container mx-auto">
|
|
84
|
-
<h1>{title}</h1>
|
|
85
|
-
{renderMessage ? (
|
|
86
|
-
renderMessage(descriptor)
|
|
87
|
-
) : (
|
|
88
|
-
<>
|
|
89
|
-
<p>
|
|
90
|
-
{descriptor.type === 'http'
|
|
91
|
-
? descriptor.defaultMessage
|
|
92
|
-
: descriptor.type === 'error'
|
|
93
|
-
? descriptor.message || descriptor.defaultMessage
|
|
94
|
-
: descriptor.defaultMessage}
|
|
95
|
-
</p>
|
|
96
|
-
{import.meta.env.DEV && descriptor.type === 'error' && descriptor.stack && (
|
|
97
|
-
<pre className="w-full p-sm overflow-x-auto">
|
|
98
|
-
<code>{descriptor.stack}</code>
|
|
99
|
-
</pre>
|
|
100
|
-
)}
|
|
101
|
-
</>
|
|
102
|
-
)}
|
|
103
|
-
</main>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useMemo } from 'react';
|
|
4
|
-
import { useLocation, useMatches } from 'react-router';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* This base hook is used to access data related to the current route
|
|
8
|
-
* @returns {JSON|undefined} The router data or undefined if not found
|
|
9
|
-
*/
|
|
10
|
-
export function useCurrentRouteData<HeaderData>(): HeaderData | undefined {
|
|
11
|
-
const location = useLocation();
|
|
12
|
-
const matchingRoutes = useMatches();
|
|
13
|
-
const route = useMemo(() => {
|
|
14
|
-
return matchingRoutes.find((route) => {
|
|
15
|
-
return route.pathname === location.pathname;
|
|
16
|
-
});
|
|
17
|
-
}, [matchingRoutes, location]);
|
|
18
|
-
|
|
19
|
-
return (route?.loaderData as HeaderData) || undefined;
|
|
20
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useMemo } from 'react';
|
|
4
|
-
import { useMatches } from 'react-router';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* This base hook is used in other hooks to quickly search for specific data
|
|
8
|
-
* across all loader data using useMatches.
|
|
9
|
-
* @param {string} id The route id
|
|
10
|
-
* @returns {JSON|undefined} The router data or undefined if not found
|
|
11
|
-
*/
|
|
12
|
-
export function useMatchesData<T>(id: string): T | undefined {
|
|
13
|
-
const matchingRoutes = useMatches();
|
|
14
|
-
const route = useMemo(() => {
|
|
15
|
-
return matchingRoutes.find((route) => {
|
|
16
|
-
return route.id === id;
|
|
17
|
-
});
|
|
18
|
-
}, [matchingRoutes, id]);
|
|
19
|
-
|
|
20
|
-
return (route?.data as T) || undefined;
|
|
21
|
-
}
|
package/src/hooks/use-user.tsx
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import type { User } from '@supabase/supabase-js';
|
|
2
|
-
import { createContext, type ReactNode, useContext } from 'react';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Context for storing and accessing the current authenticated user
|
|
6
|
-
*/
|
|
7
|
-
export interface UserContextType {
|
|
8
|
-
/**
|
|
9
|
-
* Whether the user data is currently loading
|
|
10
|
-
*/
|
|
11
|
-
isLoading: boolean;
|
|
12
|
-
/**
|
|
13
|
-
* The current authenticated user, or null if not authenticated
|
|
14
|
-
*/
|
|
15
|
-
user: User | null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Default context value when no provider is present
|
|
20
|
-
*/
|
|
21
|
-
const defaultContextValue: UserContextType = {
|
|
22
|
-
isLoading: false,
|
|
23
|
-
user: null,
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Context for storing and accessing the current authenticated user
|
|
28
|
-
*/
|
|
29
|
-
export const UserContext: React.Context<UserContextType> =
|
|
30
|
-
createContext<UserContextType>(defaultContextValue);
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Props for the UserContextProvider component
|
|
34
|
-
*/
|
|
35
|
-
export interface UserContextProviderProps {
|
|
36
|
-
/**
|
|
37
|
-
* Child components that will have access to the user context
|
|
38
|
-
*/
|
|
39
|
-
children: ReactNode;
|
|
40
|
-
/**
|
|
41
|
-
* Whether the user data is currently loading
|
|
42
|
-
*/
|
|
43
|
-
isLoading?: boolean;
|
|
44
|
-
/**
|
|
45
|
-
* The current authenticated user, or null if not authenticated
|
|
46
|
-
*/
|
|
47
|
-
user: User | null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Provider component for the UserContext
|
|
52
|
-
*/
|
|
53
|
-
export function UserContextProvider({
|
|
54
|
-
user,
|
|
55
|
-
isLoading = false,
|
|
56
|
-
children,
|
|
57
|
-
}: UserContextProviderProps): React.JSX.Element {
|
|
58
|
-
return <UserContext.Provider value={{ isLoading, user }}>{children}</UserContext.Provider>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Hook to access the current authenticated user from the UserContext
|
|
63
|
-
* @returns The current user context containing the user object and loading state
|
|
64
|
-
*/
|
|
65
|
-
export function useUser(): UserContextType {
|
|
66
|
-
const context = useContext(UserContext);
|
|
67
|
-
|
|
68
|
-
if (context === undefined) {
|
|
69
|
-
throw new Error('useUser must be used within a UserContextProvider');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return context;
|
|
73
|
-
}
|
package/src/link/index.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
-
import { MemoryRouter } from 'react-router';
|
|
3
|
-
import { Link } from './link';
|
|
4
|
-
|
|
5
|
-
const meta: Meta<typeof Link> = {
|
|
6
|
-
component: Link,
|
|
7
|
-
decorators: [
|
|
8
|
-
(Story) => (
|
|
9
|
-
<MemoryRouter>
|
|
10
|
-
<Story />
|
|
11
|
-
</MemoryRouter>
|
|
12
|
-
),
|
|
13
|
-
],
|
|
14
|
-
parameters: {
|
|
15
|
-
layout: 'padded',
|
|
16
|
-
},
|
|
17
|
-
tags: ['autodocs'],
|
|
18
|
-
title: 'Components/Link',
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export default meta;
|
|
22
|
-
type Story = StoryObj<typeof Link>;
|
|
23
|
-
|
|
24
|
-
export const Default: Story = {
|
|
25
|
-
args: {
|
|
26
|
-
children: 'Default Link',
|
|
27
|
-
to: '/example',
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export const External: Story = {
|
|
32
|
-
args: {
|
|
33
|
-
children: 'External Link',
|
|
34
|
-
to: 'https://example.com',
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export const WithGermanText: Story = {
|
|
39
|
-
args: {
|
|
40
|
-
children: 'Größenübersicht',
|
|
41
|
-
to: '/sizes',
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export const AllVariants: Story = {
|
|
46
|
-
render: () => (
|
|
47
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
48
|
-
<Link to="/internal">Internal Link</Link>
|
|
49
|
-
<Link to="https://example.com">External Link</Link>
|
|
50
|
-
<Link
|
|
51
|
-
className="text-blue-600 underline"
|
|
52
|
-
to="/styled"
|
|
53
|
-
>
|
|
54
|
-
Styled Link
|
|
55
|
-
</Link>
|
|
56
|
-
</div>
|
|
57
|
-
),
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
export const TelephoneLink: Story = {
|
|
61
|
-
args: {
|
|
62
|
-
children: 'Call Us',
|
|
63
|
-
to: 'tel:+1234567890',
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
export const MailtoLink: Story = {
|
|
68
|
-
args: {
|
|
69
|
-
children: 'Email Us',
|
|
70
|
-
to: 'mailto:hello@example.com',
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export const HashLink: Story = {
|
|
75
|
-
args: {
|
|
76
|
-
children: 'Jump to Section',
|
|
77
|
-
to: '#section-id',
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
export const WithSearchAndHash: Story = {
|
|
82
|
-
args: {
|
|
83
|
-
children: 'Link with Query',
|
|
84
|
-
to: { hash: '#results', pathname: '/search', search: '?q=test' },
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export const EmptyPath: Story = {
|
|
89
|
-
args: {
|
|
90
|
-
children: 'No destination',
|
|
91
|
-
to: '',
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
export const WithArrow: Story = {
|
|
96
|
-
args: {
|
|
97
|
-
arrow: 'rarr',
|
|
98
|
-
children: 'Link with Arrow',
|
|
99
|
-
to: '/arrow',
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
export const ButtonVariant: Story = {
|
|
104
|
-
args: {
|
|
105
|
-
children: 'Button Style Link',
|
|
106
|
-
to: '/button',
|
|
107
|
-
variant: 'button',
|
|
108
|
-
},
|
|
109
|
-
};
|
package/src/link/link.test.tsx
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
-
import { MemoryRouter } from 'react-router';
|
|
3
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
|
|
5
|
-
import { Link, LinkBase, MarkdownLink, PathResolverProvider } from './link';
|
|
6
|
-
|
|
7
|
-
afterEach(() => {
|
|
8
|
-
cleanup();
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
const renderWithRouter = (ui: React.ReactNode) => {
|
|
12
|
-
return render(<MemoryRouter>{ui}</MemoryRouter>);
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
describe('LinkBase', () => {
|
|
16
|
-
it('renders internal link with NavLink', () => {
|
|
17
|
-
renderWithRouter(<LinkBase to="/about">About</LinkBase>);
|
|
18
|
-
expect(screen.getByText('About')).toBeDefined();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('renders external http link as anchor', () => {
|
|
22
|
-
renderWithRouter(<LinkBase to="https://example.com">External</LinkBase>);
|
|
23
|
-
const link = screen.getByText('External');
|
|
24
|
-
expect(link.tagName).toBe('A');
|
|
25
|
-
expect(link.getAttribute('href')).toBe('https://example.com');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('renders mailto link as anchor', () => {
|
|
29
|
-
renderWithRouter(<LinkBase to="mailto:test@example.com">Email</LinkBase>);
|
|
30
|
-
const link = screen.getByText('Email');
|
|
31
|
-
expect(link.tagName).toBe('A');
|
|
32
|
-
expect(link.getAttribute('href')).toBe('mailto:test@example.com');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('renders tel link as anchor', () => {
|
|
36
|
-
renderWithRouter(<LinkBase to="tel:+1234567890">Call</LinkBase>);
|
|
37
|
-
const link = screen.getByText('Call');
|
|
38
|
-
expect(link.tagName).toBe('A');
|
|
39
|
-
expect(link.getAttribute('href')).toBe('tel:+1234567890');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('uses pathResolver when routeKey is provided', () => {
|
|
43
|
-
const resolver = vi.fn().mockReturnValue('/resolved-path');
|
|
44
|
-
renderWithRouter(
|
|
45
|
-
<PathResolverProvider value={resolver}>
|
|
46
|
-
<LinkBase routeKey="home">Home</LinkBase>
|
|
47
|
-
</PathResolverProvider>,
|
|
48
|
-
);
|
|
49
|
-
expect(resolver).toHaveBeenCalledWith('home');
|
|
50
|
-
expect(screen.getByText('Home')).toBeDefined();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('handles object to prop with pathname, search, and hash', () => {
|
|
54
|
-
renderWithRouter(
|
|
55
|
-
<LinkBase to={{ hash: '#section', pathname: '/page', search: '?q=test' }}>Complex</LinkBase>,
|
|
56
|
-
);
|
|
57
|
-
expect(screen.getByText('Complex')).toBeDefined();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('renders children only when path is empty', () => {
|
|
61
|
-
renderWithRouter(<LinkBase>No Link</LinkBase>);
|
|
62
|
-
expect(screen.getByText('No Link')).toBeDefined();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('calls onClick handler', () => {
|
|
66
|
-
const handleClick = vi.fn();
|
|
67
|
-
renderWithRouter(
|
|
68
|
-
<LinkBase
|
|
69
|
-
onClick={handleClick}
|
|
70
|
-
to="/test"
|
|
71
|
-
>
|
|
72
|
-
Click Me
|
|
73
|
-
</LinkBase>,
|
|
74
|
-
);
|
|
75
|
-
fireEvent.click(screen.getByText('Click Me'));
|
|
76
|
-
expect(handleClick).toHaveBeenCalled();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('opens external http link in new window', () => {
|
|
80
|
-
const windowOpen = vi.spyOn(window, 'open').mockImplementation(() => null);
|
|
81
|
-
renderWithRouter(<LinkBase to="https://example.com">External</LinkBase>);
|
|
82
|
-
fireEvent.click(screen.getByText('External'));
|
|
83
|
-
expect(windowOpen).toHaveBeenCalledWith('https://example.com', '_blank', 'noopener,noreferrer');
|
|
84
|
-
windowOpen.mockRestore();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('scrolls to element for hash links', () => {
|
|
88
|
-
const scrollIntoView = vi.fn();
|
|
89
|
-
const element = document.createElement('div');
|
|
90
|
-
element.id = 'section';
|
|
91
|
-
element.scrollIntoView = scrollIntoView;
|
|
92
|
-
document.body.appendChild(element);
|
|
93
|
-
|
|
94
|
-
renderWithRouter(<LinkBase to="#section">Jump</LinkBase>);
|
|
95
|
-
fireEvent.click(screen.getByText('Jump'));
|
|
96
|
-
expect(scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth' });
|
|
97
|
-
|
|
98
|
-
document.body.removeChild(element);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('handles hash link when element not found', () => {
|
|
102
|
-
renderWithRouter(<LinkBase to="#nonexistent">Jump</LinkBase>);
|
|
103
|
-
fireEvent.click(screen.getByText('Jump'));
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('does not prevent default for tel links', () => {
|
|
107
|
-
renderWithRouter(<LinkBase to="tel:+1234567890">Call</LinkBase>);
|
|
108
|
-
const link = screen.getByText('Call');
|
|
109
|
-
const event = fireEvent.click(link);
|
|
110
|
-
expect(event).toBe(true);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('respects defaultPrevented in onClick', () => {
|
|
114
|
-
const handleClick = vi.fn((e: React.MouseEvent) => e.preventDefault());
|
|
115
|
-
const windowOpen = vi.spyOn(window, 'open').mockImplementation(() => null);
|
|
116
|
-
|
|
117
|
-
renderWithRouter(
|
|
118
|
-
<LinkBase
|
|
119
|
-
onClick={handleClick}
|
|
120
|
-
to="https://example.com"
|
|
121
|
-
>
|
|
122
|
-
External
|
|
123
|
-
</LinkBase>,
|
|
124
|
-
);
|
|
125
|
-
fireEvent.click(screen.getByText('External'));
|
|
126
|
-
expect(handleClick).toHaveBeenCalled();
|
|
127
|
-
expect(windowOpen).not.toHaveBeenCalled();
|
|
128
|
-
|
|
129
|
-
windowOpen.mockRestore();
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe('Link', () => {
|
|
134
|
-
it('renders with variant', () => {
|
|
135
|
-
renderWithRouter(
|
|
136
|
-
<Link
|
|
137
|
-
to="/test"
|
|
138
|
-
variant="button"
|
|
139
|
-
>
|
|
140
|
-
Button Link
|
|
141
|
-
</Link>,
|
|
142
|
-
);
|
|
143
|
-
expect(screen.getByText('Button Link')).toBeDefined();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('renders with arrow', () => {
|
|
147
|
-
renderWithRouter(
|
|
148
|
-
<Link
|
|
149
|
-
arrow="rarr"
|
|
150
|
-
to="/test"
|
|
151
|
-
>
|
|
152
|
-
Arrow Link
|
|
153
|
-
</Link>,
|
|
154
|
-
);
|
|
155
|
-
expect(screen.getByText('Arrow Link')).toBeDefined();
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
describe('MarkdownLink', () => {
|
|
160
|
-
it('renders Link when href is provided', () => {
|
|
161
|
-
renderWithRouter(<MarkdownLink href="/page">Markdown</MarkdownLink>);
|
|
162
|
-
expect(screen.getByText('Markdown')).toBeDefined();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('returns null when href is not provided', () => {
|
|
166
|
-
const { container } = renderWithRouter(<MarkdownLink>No Link</MarkdownLink>);
|
|
167
|
-
expect(container.innerHTML).toBe('');
|
|
168
|
-
});
|
|
169
|
-
});
|