diff --git a/.changeset/loud-wolves-lead.md b/.changeset/loud-wolves-lead.md new file mode 100644 index 0000000000..372b743565 --- /dev/null +++ b/.changeset/loud-wolves-lead.md @@ -0,0 +1,6 @@ +--- +"@frontify/fondue-components": patch +"@frontify/fondue": patch +--- + +feat: add inheritance to theme provider diff --git a/packages/components/src/components/Dialog/Dialog.tsx b/packages/components/src/components/Dialog/Dialog.tsx index 6b846c81a1..d7123df266 100644 --- a/packages/components/src/components/Dialog/Dialog.tsx +++ b/packages/components/src/components/Dialog/Dialog.tsx @@ -229,7 +229,7 @@ export const DialogContent = ( }: DialogContentProps, ref: ForwardedRef, ) => { - const { theme, dir, locale } = useFondueTheme(); + const { dir } = useFondueTheme(); const contentRef = useRef(null); const { dismissable } = useContext(DialogContext); @@ -271,7 +271,7 @@ export const DialogContent = ( return ( - + , ) => { const localRef = useRef(null); - const { theme, dir, locale } = useFondueTheme(); + const { dir } = useFondueTheme(); const actualRef = ref || localRef; return ( - + , ) => { - const { theme, dir, locale } = useFondueTheme(); + const { dir } = useFondueTheme(); return ( - + , ) => { - const { theme, dir, locale } = useFondueTheme(); + const { dir } = useFondueTheme(); const getAdjustedSide = (side?: 'top' | 'right' | 'bottom' | 'left') => { if (!side || dir === 'ltr') { @@ -173,7 +173,7 @@ export const FlyoutContent = ( return ( - +
{ if (dir === 'ltr') { @@ -115,7 +115,7 @@ export const SelectMenu = ({ return ( - + ({ theme: 'light', dir: 'ltr', locale: enUS, + className: '', }); ThemeContext.displayName = 'ThemeContext'; @@ -68,23 +76,30 @@ export const useFondueTheme = () => { export const ThemeProvider = forwardRef( ( - { children, theme = 'light', dir = 'ltr', translations = enUS, locale, asChild = false }, + { children, theme, dir, translations, locale, className, asChild = false }, forwardedRef: ForwardedRef, ) => { const Comp = asChild ? Slot : 'div'; + const existingContext = useFondueTheme(); + const contextValue = useMemo( () => ({ - theme, - dir, - locale: locale ? locale : translations, + theme: theme ?? existingContext.theme, + dir: dir ?? existingContext.dir, + locale: locale ?? translations ?? existingContext.locale, + className: className ?? existingContext.className, }), - [dir, theme, locale, translations], + [dir, theme, locale, translations, className, existingContext], ); return ( - + {children} diff --git a/packages/components/src/components/ThemeProvider/__tests__/ThemeProvider.spec.tsx b/packages/components/src/components/ThemeProvider/__tests__/ThemeProvider.spec.tsx new file mode 100644 index 0000000000..44af185756 --- /dev/null +++ b/packages/components/src/components/ThemeProvider/__tests__/ThemeProvider.spec.tsx @@ -0,0 +1,137 @@ +/* (c) Copyright Frontify Ltd., all rights reserved. */ + +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import { deDE, enUS, frFR } from '../../../locales'; +import { ThemeProvider, useFondueTheme } from '../ThemeProvider'; + +const ContextProbe = ({ id }: { id: string }) => { + const { theme, dir, locale, className } = useFondueTheme(); + return ( +
+ ); +}; + +describe('ThemeProvider context inheritance', () => { + it('exposes default context values when no props are provided', () => { + render( + + + , + ); + + const probe = screen.getByTestId('probe'); + expect(probe.dataset.theme).toBe('light'); + expect(probe.dataset.dir).toBe('ltr'); + expect(probe.dataset.locale).toBe(enUS.translationStrings.Dialog_close); + expect(probe.dataset.scope).toBe(''); + }); + + it('applies all explicitly provided props', () => { + render( + + + , + ); + + const probe = screen.getByTestId('probe'); + expect(probe.dataset.theme).toBe('dark'); + expect(probe.dataset.dir).toBe('rtl'); + expect(probe.dataset.locale).toBe(deDE.translationStrings.Dialog_close); + expect(probe.dataset.scope).toBe('my-scope'); + }); + + it('inherits all values from the parent provider when no props are provided', () => { + render( + + + + + , + ); + + const probe = screen.getByTestId('probe'); + expect(probe.dataset.theme).toBe('dark'); + expect(probe.dataset.dir).toBe('rtl'); + expect(probe.dataset.locale).toBe(deDE.translationStrings.Dialog_close); + expect(probe.dataset.scope).toBe('parent-scope'); + }); + + it('only overrides values explicitly set on the nested provider', () => { + render( + + + + + , + ); + + const probe = screen.getByTestId('probe'); + expect(probe.dataset.theme).toBe('light'); + expect(probe.dataset.dir).toBe('rtl'); + expect(probe.dataset.locale).toBe(deDE.translationStrings.Dialog_close); + expect(probe.dataset.scope).toBe('parent-scope'); + }); + + it('merges overrides across multiple levels of nesting', () => { + render( + + + + + + + , + ); + + const probe = screen.getByTestId('probe'); + expect(probe.dataset.theme).toBe('dark'); + expect(probe.dataset.dir).toBe('ltr'); + expect(probe.dataset.locale).toBe(frFR.translationStrings.Dialog_close); + expect(probe.dataset.scope).toBe('outer-scope'); + }); + + it('falls back to deprecated translations prop when locale is not provided', () => { + render( + + + , + ); + + const probe = screen.getByTestId('probe'); + expect(probe.dataset.locale).toBe(deDE.translationStrings.Dialog_close); + }); + + it('prefers locale over the deprecated translations prop', () => { + render( + + + , + ); + + const probe = screen.getByTestId('probe'); + expect(probe.dataset.locale).toBe(frFR.translationStrings.Dialog_close); + }); + + it('renders the parent className alongside the inherited scope on a nested provider', () => { + const { container } = render( + + + + + , + ); + + const providers = container.querySelectorAll('.fondue-theme-provider'); + expect(providers).toHaveLength(2); + // The nested provider should re-apply the inherited scope class on its DOM node + expect(providers[1]?.className).toContain('parent-scope'); + }); +}); diff --git a/packages/components/src/components/Tooltip/Tooltip.tsx b/packages/components/src/components/Tooltip/Tooltip.tsx index 64a05636cf..8b5d5dd82a 100644 --- a/packages/components/src/components/Tooltip/Tooltip.tsx +++ b/packages/components/src/components/Tooltip/Tooltip.tsx @@ -89,7 +89,7 @@ export const TooltipContent = ( }: TooltipContentProps, ref: ForwardedRef, ) => { - const { theme, dir, locale } = useFondueTheme(); + const { dir } = useFondueTheme(); const getAdjustedSide = (side?: 'top' | 'right' | 'bottom' | 'left') => { if (!side || dir === 'ltr') { @@ -108,7 +108,7 @@ export const TooltipContent = ( return ( - +