Skip to content

Commit f89cce9

Browse files
authored
feat: Implement ULThemeLink component with Auth0 theme support and using UDS Link component (#43)
* feat: Implement ULThemeLink component with Auth0 theme support and using UDS Link component * feat: Removed React Forward ref, and passed ref as prop to link component
1 parent fecb1c4 commit f89cce9

File tree

7 files changed

+154
-22
lines changed

7 files changed

+154
-22
lines changed

src/components/ULThemeLink.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as React from "react";
2+
import { Link, type LinkProps } from "@/components/ui/link";
3+
import { cn } from "@/lib/utils";
4+
import { extractTokenValue } from "@/utils/helpers/tokenUtils";
5+
6+
export interface ULThemeLinkProps extends LinkProps {
7+
/**
8+
* The content of the link element.
9+
*/
10+
children: React.ReactNode;
11+
/**
12+
* Additional class names for custom styling.
13+
*/
14+
className?: string;
15+
/**
16+
* Optional flag to disable the link.
17+
*/
18+
disabled?: boolean;
19+
/**
20+
* Optional ref for the link element.
21+
*/
22+
ref?: React.Ref<HTMLAnchorElement>;
23+
}
24+
25+
const ULThemeLink = ({
26+
children,
27+
className,
28+
disabled = false,
29+
ref,
30+
...props
31+
}: ULThemeLinkProps) => {
32+
// Base component styles
33+
const baseStyles =
34+
"text-link-focus text-(length:--ul-theme-font-links-size) font-(weight:--ul-theme-font-links-weight) focus:rounded-(--ul-theme-border-links-border-radius) hover:text-link-focus/80";
35+
36+
// Disabled state styles
37+
const disabledStyles = disabled
38+
? "pointer-events-none text-muted cursor-not-allowed"
39+
: "";
40+
41+
// UL theme overrides
42+
const variantThemeOverrides =
43+
"theme-universal:focus:outline-none theme-universal:focus:ring-4 theme-universal:focus:ring-base-focus/15 theme-universal:focus:bg-base-focus/15"; // focus base color
44+
45+
// Using extractTokenValue utility to extract the link style variant type from the CSS variable
46+
const linkStyleValue =
47+
extractTokenValue("--ul-theme-font-links-style") === "normal"
48+
? "none"
49+
: "always";
50+
51+
return (
52+
<Link
53+
ref={ref}
54+
className={cn(
55+
baseStyles,
56+
disabledStyles,
57+
variantThemeOverrides,
58+
className
59+
)}
60+
underline={linkStyleValue}
61+
aria-disabled={disabled}
62+
{...props}
63+
>
64+
{children}
65+
</Link>
66+
);
67+
};
68+
69+
export default ULThemeLink;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { render, screen, fireEvent } from "@testing-library/react";
2+
import ULThemeLink from "../ULThemeLink";
3+
4+
describe("ULThemeLink Component", () => {
5+
const defaultProps = {
6+
href: "https://example.com",
7+
children: "Example Link",
8+
className: "custom-class",
9+
};
10+
11+
it("renders correctly and matches snapshot", () => {
12+
const { container } = render(<ULThemeLink {...defaultProps} />);
13+
expect(container).toMatchSnapshot();
14+
});
15+
16+
it("renders the correct text", () => {
17+
render(<ULThemeLink {...defaultProps} />);
18+
expect(screen.getByText("Example Link")).toBeInTheDocument();
19+
});
20+
21+
it("applies additional class names", () => {
22+
render(<ULThemeLink {...defaultProps} />);
23+
const link = screen.getByRole("link");
24+
expect(link).toHaveClass("custom-class");
25+
});
26+
27+
it("renders the correct href attribute", () => {
28+
render(<ULThemeLink {...defaultProps} />);
29+
const link = screen.getByRole("link");
30+
expect(link).toHaveAttribute("href", "https://example.com");
31+
});
32+
33+
it("calls the onClick handler when clicked", () => {
34+
const mockOnClick = jest.fn();
35+
render(<ULThemeLink {...defaultProps} onClick={mockOnClick} />);
36+
const link = screen.getByRole("link");
37+
fireEvent.click(link);
38+
expect(mockOnClick).toHaveBeenCalledTimes(1);
39+
});
40+
41+
it("disables the link when the disabled prop is true", () => {
42+
render(<ULThemeLink {...defaultProps} disabled />);
43+
const link = screen.getByRole("link");
44+
expect(link).toHaveClass("pointer-events-none cursor-not-allowed");
45+
expect(link).toHaveAttribute("aria-disabled", "true");
46+
});
47+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2+
3+
exports[`ULThemeLink Component renders correctly and matches snapshot 1`] = `
4+
<div>
5+
<a
6+
aria-disabled="false"
7+
class="focus-within:ring-ring inline-flex items-center gap-2 rounded-md py-0.5 underline-offset-4 transition-colors focus:ring-3 focus-visible:outline-hidden underline text-link-focus text-(length:--ul-theme-font-links-size) font-(weight:--ul-theme-font-links-weight) focus:rounded-(--ul-theme-border-links-border-radius) hover:text-link-focus/80 theme-universal:focus:outline-none theme-universal:focus:ring-4 theme-universal:focus:ring-base-focus/15 theme-universal:focus:bg-base-focus/15 custom-class"
8+
href="https://example.com"
9+
>
10+
Example Link
11+
</a>
12+
</div>
13+
`;

src/components/ui/link.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,21 @@ const linkVariants = cva(
2828

2929
export interface LinkProps
3030
extends React.AnchorHTMLAttributes<HTMLAnchorElement>,
31-
VariantProps<typeof linkVariants> {}
31+
VariantProps<typeof linkVariants> {
32+
/**
33+
* Optional ref for the link element.
34+
*/
35+
ref?: React.Ref<HTMLAnchorElement>;
36+
}
3237

33-
function Link(
34-
{ className, children, variant, underline, ...props }: LinkProps,
35-
ref: React.Ref<HTMLAnchorElement> | undefined
36-
) {
38+
function Link({
39+
className,
40+
children,
41+
variant,
42+
underline,
43+
ref,
44+
...props
45+
}: LinkProps) {
3746
return (
3847
<a
3948
ref={ref}

src/index.css

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,6 @@ Created to support alignment related utlities like justify, text-align etc with
119119
--font-weight-input-label: var(--ul-theme-font-input-labels-weight);
120120
--font-weight-link: var(--ul-theme-font-links-weight);
121121

122-
/* Text Decoration */
123-
--text-decoration-link: var(--ul-theme-font-links-style);
124-
125122
/* Widget */
126123
--height-widget-logo: var(--ul-theme-widget-logo-height);
127124
--social-buttons-layout: var(--ul-theme-widget-social-buttons-layout);
@@ -185,6 +182,7 @@ Created to support alignment related utlities like justify, text-align etc with
185182
--ul-theme-border-widget-corner-radius: 5px;
186183
--ul-theme-border-widget-border-weight: 0px;
187184
--ul-theme-border-show-widget-shadow: 1;
185+
--ul-theme-border-links-border-radius: 3px;
188186

189187
/* Fonts */
190188
--ul-theme-font-reference-text-size: 16px;

src/screens/login-id/components/Footer.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { rebaseLinkToCurrentOrigin } from "@/utils/helpers/urlUtils";
1+
import ULThemeLink from "@/components/ULThemeLink";
22

3+
import { rebaseLinkToCurrentOrigin } from "@/utils/helpers/urlUtils";
34
import { useLoginIdManager } from "../hooks/useLoginIdManager";
45

56
function Footer() {
@@ -17,14 +18,11 @@ function Footer() {
1718

1819
return (
1920
<div className="mt-4 text-left">
20-
<span className="text-sm pr-1">{footerText}</span>
21+
<span className="pr-1 text-body-text text-(length:--ul-theme-font-body-text-size) font-body">
22+
{footerText}
23+
</span>
2124
{localizedSignupLink && (
22-
<a
23-
href={localizedSignupLink}
24-
className="text-sm font-bold text-link hover:text-link/80 focus:bg-link/15 focus:rounded"
25-
>
26-
{footerLinkText}
27-
</a>
25+
<ULThemeLink href={localizedSignupLink}>{footerLinkText}</ULThemeLink>
2826
)}
2927
</div>
3028
);

src/screens/login-id/components/IdentifierForm.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ULThemeFloatingLabelField } from "@/components/form/ULThemeFloatingLabe
99
import { ULThemeFormMessage } from "@/components/form/ULThemeFormMessage";
1010
import { Form, FormField, FormItem } from "@/components/ui/form";
1111
import { ULThemePrimaryButton } from "@/components/ULThemePrimaryButton";
12+
import ULThemeLink from "@/components/ULThemeLink";
1213
import {
1314
isPhoneNumberSupported,
1415
transformAuth0CountryCode,
@@ -166,14 +167,11 @@ function IdentifierForm() {
166167
)}
167168

168169
{/* Forgot Password link */}
169-
<div className="text-left">
170+
<div className="text-left mb-4">
170171
{isForgotPasswordEnabled && localizedResetPasswordLink && (
171-
<a
172-
href={localizedResetPasswordLink}
173-
className="text-sm text-link font-bold hover:text-link/80 focus:bg-link/15 focus:rounded"
174-
>
172+
<ULThemeLink href={localizedResetPasswordLink}>
175173
{forgotPasswordText}
176-
</a>
174+
</ULThemeLink>
177175
)}
178176
</div>
179177

0 commit comments

Comments
 (0)