Skip to content

Commit ffa5f67

Browse files
authored
feat: Implement ULThemeLogo component with Auth0 theme support and using UDS Avatar component (#34)
* feat: Implement ULThemeLogo component with Auth0 theme support and added using UDS * feat: Fixed formatting suggested via lint * feat: Added useful comments for custom tailwind utilities
1 parent 0ceec51 commit ffa5f67

File tree

7 files changed

+199
-14
lines changed

7 files changed

+199
-14
lines changed

src/components/ULThemeLogo.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Avatar, AvatarImage } from "./ui/avatar";
2+
import { cn } from "@/lib/utils";
3+
import { extractTokenValue } from "@/utils/helpers/tokenUtils";
4+
5+
export interface ULThemeLogoProps
6+
extends React.HTMLAttributes<HTMLSpanElement> {
7+
/**
8+
* Optional image url of the logo.
9+
*/
10+
imageUrl?: string;
11+
/**
12+
* Alt Text for the logo image
13+
*/
14+
altText: string;
15+
/**
16+
* Optional Classes for custom overrides
17+
*/
18+
className?: string;
19+
}
20+
21+
const ULThemeLogo = ({
22+
imageUrl,
23+
altText,
24+
className,
25+
...rest
26+
}: ULThemeLogoProps) => {
27+
// Using extractTokenValue utility to extract the logo URL, Logo Visible flags from CSS variable
28+
const themedLogoSrcValue = extractTokenValue("--ul-theme-widget-logo-url");
29+
const isLogoHidden =
30+
extractTokenValue("--ul-theme-widget-logo-position") === "none";
31+
const themedStylesAvatar = "flex flex-wrap justify-widget-logo";
32+
const themedStylesAvatarImg = "h-(--height-widget-logo)";
33+
const logoSrc = themedLogoSrcValue || imageUrl;
34+
35+
return (
36+
!isLogoHidden && (
37+
<div className={cn(themedStylesAvatar, className)}>
38+
<Avatar className="size-auto rounded-none">
39+
<AvatarImage
40+
src={logoSrc}
41+
alt={altText}
42+
className={cn(themedStylesAvatarImg)}
43+
loading="eager" // Default should load an image immediately
44+
decoding="async" // Decode the image asynchronously
45+
fetchPriority="high" // Fetch the image at a high priority
46+
{...rest}
47+
/>
48+
</Avatar>
49+
</div>
50+
)
51+
);
52+
};
53+
export default ULThemeLogo;
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { render } from "@testing-library/react";
2+
import ULThemeLogo from "../ULThemeLogo";
3+
import { extractTokenValue } from "@/utils/helpers/tokenUtils";
4+
5+
jest.mock("@/utils/helpers/tokenUtils", () => ({
6+
extractTokenValue: jest.fn(),
7+
}));
8+
9+
describe("ULThemeLogo", () => {
10+
const mockExtractTokenValue = extractTokenValue as jest.Mock;
11+
12+
beforeEach(() => {
13+
mockExtractTokenValue.mockReset();
14+
});
15+
16+
// Snapshot Test: Default Props
17+
it("renders correctly with default props", () => {
18+
mockExtractTokenValue.mockReturnValue("");
19+
const { container } = render(<ULThemeLogo altText="Default Logo" />);
20+
expect(container).toMatchSnapshot();
21+
});
22+
23+
// Functional Test: Uses imageUrl prop
24+
it("uses the imageUrl prop when provided", () => {
25+
render(
26+
<ULThemeLogo
27+
imageUrl="https://example.com/logo.png"
28+
altText="Custom Logo"
29+
/>
30+
);
31+
const avatar = document.querySelector('[data-slot="avatar"]');
32+
expect(avatar).toHaveClass(
33+
"relative flex shrink-0 overflow-hidden rounded-none size-auto"
34+
);
35+
});
36+
37+
// Functional Test: Uses fallback when imageUrl is not provided
38+
it("uses fallback when imageUrl is not provided", () => {
39+
mockExtractTokenValue.mockReturnValue(
40+
"https://example.com/fallback-logo.png"
41+
);
42+
render(<ULThemeLogo altText="Fallback Logo" />);
43+
const avatar = document.querySelector('[data-slot="avatar"]');
44+
expect(avatar).toHaveClass(
45+
"relative flex shrink-0 overflow-hidden rounded-none size-auto"
46+
);
47+
});
48+
49+
// Functional Test: Applies custom className
50+
it("applies custom className", () => {
51+
render(
52+
<ULThemeLogo altText="Custom Class Logo" className="custom-class" />
53+
);
54+
const avatar = document.querySelector('[data-slot="avatar"]');
55+
expect(avatar).toHaveClass(
56+
"relative flex shrink-0 overflow-hidden rounded-none size-auto"
57+
);
58+
});
59+
60+
// Snapshot Test: With custom className
61+
it("matches snapshot with custom className", () => {
62+
const { container } = render(
63+
<ULThemeLogo altText="Snapshot Logo" className="snapshot-class" />
64+
);
65+
expect(container).toMatchSnapshot();
66+
});
67+
68+
// Functional Test: Renders with correct alt text
69+
it("renders with correct alt text", () => {
70+
render(
71+
<ULThemeLogo
72+
imageUrl="https://example.com/logo.png"
73+
altText="Accessible Logo"
74+
/>
75+
);
76+
const avatar = document.querySelector('[data-slot="avatar"]');
77+
expect(avatar).toBeInTheDocument();
78+
});
79+
80+
// Functional Test: Handles empty imageUrl and fallback gracefully
81+
it("handles empty imageUrl and fallback gracefully", () => {
82+
mockExtractTokenValue.mockReturnValue("");
83+
render(<ULThemeLogo altText="Empty Fallback Logo" />);
84+
const avatar = document.querySelector('[data-slot="avatar"]');
85+
expect(avatar).toBeInTheDocument();
86+
});
87+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2+
3+
exports[`ULThemeLogo matches snapshot with custom className 1`] = `
4+
<div>
5+
<div
6+
class="flex flex-wrap justify-widget-logo snapshot-class"
7+
>
8+
<span
9+
class="relative flex shrink-0 overflow-hidden size-auto rounded-none"
10+
data-slot="avatar"
11+
/>
12+
</div>
13+
</div>
14+
`;
15+
16+
exports[`ULThemeLogo renders correctly with default props 1`] = `
17+
<div>
18+
<div
19+
class="flex flex-wrap justify-widget-logo"
20+
>
21+
<span
22+
class="relative flex shrink-0 overflow-hidden size-auto rounded-none"
23+
data-slot="avatar"
24+
/>
25+
</div>
26+
</div>
27+
`;

src/index.css

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,24 @@
1111
}
1212
}
1313

14-
/** UL Custom Utilities **/
14+
/** UL Custom Utilities:
15+
Created to support alignment related utlities like justify, text-align etc with dynamic theming values
16+
**/
1517
@layer utilities {
18+
/** Custom Utility to handle page layout alignment **/
1619
.justify-page-layout {
1720
justify-content: var(--ul-theme-page-bg-page-layout);
1821
}
22+
23+
/** Custom Utility to handle text header alignment **/
1924
.justify-text-header {
2025
text-align: var(--ul-theme-widget-header-text-alignment);
2126
}
27+
28+
/** Custom Utility to handle logo alignment(left, right, center) **/
29+
.justify-widget-logo {
30+
justify-content: var(--ul-theme-widget-logo-position);
31+
}
2232
}
2333

2434
/** UDS Default theme variables **/
@@ -115,7 +125,6 @@
115125

116126
/* Widget */
117127
--height-widget-logo: var(--ul-theme-widget-logo-height);
118-
--justify-widget-logo: var(--ul-theme-widget-logo-position);
119128
--social-buttons-layout: var(--ul-theme-widget-social-buttons-layout);
120129

121130
/* Layout */

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import Logo from "@/common/Logo";
2-
import ULThemeSubtitle from "@/components/ULThemeSubtitle";
1+
import ULThemeLogo from "@/components/ULThemeLogo";
32
import ULThemeTitle from "@/components/ULThemeTitle";
3+
import ULThemeSubtitle from "@/components/ULThemeSubtitle";
44

55
import { useLoginIdManager } from "../hooks/useLoginIdManager";
66

@@ -12,7 +12,7 @@ function Header() {
1212

1313
return (
1414
<>
15-
<Logo imageClassName="h-13" altText={logoAltText} />
15+
<ULThemeLogo altText={logoAltText}></ULThemeLogo>
1616
<ULThemeTitle>{texts?.title || "Welcome"}</ULThemeTitle>
1717
<ULThemeSubtitle>
1818
{texts?.description ||

src/utils/helpers/tokenUtils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Extract and return the Token CSS Variable data from the token string
3+
*/
4+
export const extractTokenValue = (varName: string): string => {
5+
return getComputedStyle(document.documentElement)
6+
.getPropertyValue(varName)
7+
.trim()
8+
.replace(/^"(.*)"$/, "$1"); // Remove quotes
9+
};

src/utils/theme/themeFlatteners.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -290,26 +290,26 @@ export function flattenPageBackground(pageBackground: {
290290
export function flattenWidget(widget: WidgetData): Record<string, string> {
291291
const result: Record<string, string> = {};
292292

293+
// Logo Source URL
294+
if (widget.logo_url)
295+
result["--ul-theme-widget-logo-url"] = `"${widget.logo_url}"`;
296+
297+
// Logo height needs px units
298+
if (widget.logo_height)
299+
result["--ul-theme-widget-logo-height"] = `${widget.logo_height}px`;
300+
293301
// Logo position: convert Auth0 values to Tailwind justify values
294302
if (widget.logo_position) {
295-
result["--ul-theme-widget-logo-position"] = widget.logo_position;
296-
297303
// Convert to Tailwind semantic variable
298304
const positionMap: Record<string, string> = {
299305
center: "center",
300306
left: "flex-start",
301307
right: "flex-end",
302308
none: "none",
303309
};
304-
result["--justify-widget-logo"] =
310+
result["--ul-theme-widget-logo-position"] =
305311
positionMap[widget.logo_position] || "center";
306312
}
307-
if (widget.logo_url)
308-
result["--ul-theme-widget-logo-url"] = `"${widget.logo_url}"`;
309-
310-
// Logo height needs px units
311-
if (widget.logo_height)
312-
result["--ul-theme-widget-logo-height"] = `${widget.logo_height}px`;
313313

314314
// Header text alignment: convert Auth0 values to CSS text-align values
315315
if (widget.header_text_alignment) {

0 commit comments

Comments
 (0)