diff --git a/package-lock.json b/package-lock.json index 330ae864..1efba3da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24707,6 +24707,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, "license": "MIT" }, "node_modules/lodash.sortby": { @@ -33013,7 +33014,6 @@ "class-variance-authority": "0.7.0", "clsx": "2.1.1", "input-otp": "1.2.4", - "lodash.merge": "4.6.2", "react-hook-form": "7.53.0", "react-intl": "6.7.0", "tailwind-merge": "2.5.2", diff --git a/packages/elements-react/api-report/elements-react-theme.api.json b/packages/elements-react/api-report/elements-react-theme.api.json index ed4c47f0..d65dcca1 100644 --- a/packages/elements-react/api-report/elements-react-theme.api.json +++ b/packages/elements-react/api-report/elements-react-theme.api.json @@ -628,6 +628,53 @@ "endIndex": 8 } }, + { + "kind": "Function", + "canonicalReference": "@ory/elements-react!getOryComponents:function(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "declare function getOryComponents(overrides?: " + }, + { + "kind": "Reference", + "text": "OryFlowComponentOverrides", + "canonicalReference": "@ory/elements-react!~OryFlowComponentOverrides:type" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "OryFlowComponents", + "canonicalReference": "@ory/elements-react!~OryFlowComponents:type" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "dist/theme/default/index.d.ts", + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "overrides", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": true + } + ], + "name": "getOryComponents" + }, { "kind": "Function", "canonicalReference": "@ory/elements-react!Login:function(1)", @@ -750,30 +797,6 @@ "endIndex": 8 } }, - { - "kind": "Variable", - "canonicalReference": "@ory/elements-react!OryDefaultComponents:var", - "docComment": "", - "excerptTokens": [ - { - "kind": "Content", - "text": "OryDefaultComponents: " - }, - { - "kind": "Reference", - "text": "OryFlowComponents", - "canonicalReference": "@ory/elements-react!~OryFlowComponents:type" - } - ], - "fileUrlPath": "dist/theme/default/index.d.ts", - "isReadonly": true, - "releaseTag": "Public", - "name": "OryDefaultComponents", - "variableTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } - }, { "kind": "Function", "canonicalReference": "@ory/elements-react!Recovery:function(1)", diff --git a/packages/elements-react/api-report/elements-react-theme.api.md b/packages/elements-react/api-report/elements-react-theme.api.md index fc7c9fbe..c9412205 100644 --- a/packages/elements-react/api-report/elements-react-theme.api.md +++ b/packages/elements-react/api-report/elements-react-theme.api.md @@ -71,6 +71,11 @@ export type ErrorFlowContextProps = { config: OryClientConfiguration; }; +// Warning: (ae-forgotten-export) The symbol "OryFlowComponents" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function getOryComponents(overrides?: OryFlowComponentOverrides): OryFlowComponents; + // @public (undocumented) export function Login({ flow, config, children, components: flowOverrideComponents, }: PropsWithChildren): react_jsx_runtime.JSX.Element; @@ -81,11 +86,6 @@ export type LoginFlowContextProps = { config: OryClientConfiguration; }; -// Warning: (ae-forgotten-export) The symbol "OryFlowComponents" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -export const OryDefaultComponents: OryFlowComponents; - // @public (undocumented) export function Recovery({ flow, config, children, components: flowOverrideComponents, }: PropsWithChildren): react_jsx_runtime.JSX.Element; diff --git a/packages/elements-react/package.json b/packages/elements-react/package.json index e12dad61..9c0ef81b 100644 --- a/packages/elements-react/package.json +++ b/packages/elements-react/package.json @@ -41,7 +41,6 @@ "class-variance-authority": "0.7.0", "clsx": "2.1.1", "input-otp": "1.2.4", - "lodash.merge": "4.6.2", "react-hook-form": "7.53.0", "react-intl": "6.7.0", "tailwind-merge": "2.5.2", diff --git a/packages/elements-react/src/tests/jest/test-utils.tsx b/packages/elements-react/src/tests/jest/test-utils.tsx index 493ba686..4a6dcc12 100644 --- a/packages/elements-react/src/tests/jest/test-utils.tsx +++ b/packages/elements-react/src/tests/jest/test-utils.tsx @@ -2,11 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { render, RenderOptions } from "@testing-library/react" -import { merge } from "lodash" import { ComponentProps, PropsWithChildren, ReactElement } from "react" import { OryFlowComponentOverrides } from "../../components" import { OryComponentProvider } from "../../context/component" -import { OryDefaultComponents } from "../../theme/default" +import { getOryComponents } from "../../theme/default" import { OryClientConfiguration } from "../../util" export const defaultConfiguration: OryClientConfiguration = { name: "test", @@ -30,9 +29,7 @@ const ComponentProvider = ({ children, components, }: PropsWithChildren) => ( - + {children} ) diff --git a/packages/elements-react/src/theme/default/components/default-component.spec.tsx b/packages/elements-react/src/theme/default/components/default-component.spec.tsx new file mode 100644 index 00000000..4ddc0896 --- /dev/null +++ b/packages/elements-react/src/theme/default/components/default-component.spec.tsx @@ -0,0 +1,43 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { getOryComponents } from "./default-components" + +const CustomComponent = jest.fn() + +describe("DefaultComponent", () => { + test("can override card component", () => { + const result = getOryComponents({ + Card: { AuthMethodListItem: CustomComponent }, + }) + expect(result.Card.AuthMethodListItem).toBe(CustomComponent) + expect(result.Card.Content).toBeDefined() + }) + test("can override form component", () => { + const result = getOryComponents({ + Form: { OidcSettings: CustomComponent }, + }) + expect(result.Form.OidcSettings).toBe(CustomComponent) + expect(result.Form.Root).toBeDefined() + }) + test("can override message component", () => { + const result = getOryComponents({ + Message: { Content: CustomComponent }, + }) + expect(result.Message.Content).toBe(CustomComponent) + expect(result.Message.Root).toBeDefined() + }) + test("can override page component", () => { + const result = getOryComponents({ + Page: { Header: CustomComponent }, + }) + expect(result.Page.Header).toBe(CustomComponent) + }) + test("can override node component", () => { + const result = getOryComponents({ + Node: { Button: CustomComponent }, + }) + expect(result.Node.Button).toBe(CustomComponent) + expect(result.Node.Input).toBeDefined() + }) +}) diff --git a/packages/elements-react/src/theme/default/components/default-components.tsx b/packages/elements-react/src/theme/default/components/default-components.tsx index 6e1d7265..5db2b700 100644 --- a/packages/elements-react/src/theme/default/components/default-components.tsx +++ b/packages/elements-react/src/theme/default/components/default-components.tsx @@ -29,7 +29,10 @@ import { } from "./form/social" import { DefaultText } from "./form/text" import { DefaultCurrentIdentifierButton } from "./card/current-identifier-button" -import { OryFlowComponents } from "@ory/elements-react" +import { + OryFlowComponentOverrides, + OryFlowComponents, +} from "@ory/elements-react" import { DefaultFormSection, DefaultFormSectionContent, @@ -42,47 +45,60 @@ import { DefaultSettingsWebauthn } from "./settings/settings-webauthn" import { DefaultSettingsPasskey } from "./settings/settings-passkey" import { DefaultPageHeader } from "./generic/page-header" -export const OryDefaultComponents: OryFlowComponents = { - Card: { - Root: DefaultCard, - Footer: DefaultCardFooter, - Header: DefaultCardHeader, - Content: DefaultCardContent, - Logo: DefaultCardLogo, - Divider: DefaultHorizontalDivider, - AuthMethodListItem: DefaultAuthMethodListItem, - - SettingsSection: DefaultFormSection, - SettingsSectionContent: DefaultFormSectionContent, - SettingsSectionFooter: DefaultFormSectionFooter, - }, - Node: { - Button: DefaultButton, - OidcButton: DefaultButtonSocial, - CurrentIdentifierButton: DefaultCurrentIdentifierButton, - Input: DefaultInput, - CodeInput: DefaultPinCodeInput, - Image: DefaultImage, - Label: DefaultLabel, - Checkbox: DefaultCheckbox, - Text: DefaultText, - Anchor: DefaultLinkButton, - }, - Form: { - Root: DefaultFormContainer, - Group: DefaultGroupContainer, - OidcRoot: DefaultSocialButtonContainer, - RecoveryCodesSettings: DefaultSettingsRecoveryCodes, - TotpSettings: DefaultSettingsTotp, - OidcSettings: DefaultSettingsOidc, - WebauthnSettings: DefaultSettingsWebauthn, - PasskeySettings: DefaultSettingsPasskey, - }, - Message: { - Root: DefaultMessageContainer, - Content: DefaultMessage, - }, - Page: { - Header: DefaultPageHeader, - }, +export function getOryComponents( + overrides?: OryFlowComponentOverrides, +): OryFlowComponents { + // Yes, this could probably be easier by using lodash or a custom merge function. + // But, this makes it very explicit what can be overridden, and does not introduce issues with merging nested fields. + return { + Card: { + Root: overrides?.Card?.Root ?? DefaultCard, + Footer: overrides?.Card?.Footer ?? DefaultCardFooter, + Header: overrides?.Card?.Header ?? DefaultCardHeader, + Content: overrides?.Card?.Content ?? DefaultCardContent, + Logo: overrides?.Card?.Logo ?? DefaultCardLogo, + Divider: overrides?.Card?.Divider ?? DefaultHorizontalDivider, + AuthMethodListItem: + overrides?.Card?.AuthMethodListItem ?? DefaultAuthMethodListItem, + SettingsSection: overrides?.Card?.SettingsSection ?? DefaultFormSection, + SettingsSectionContent: + overrides?.Card?.SettingsSectionContent ?? DefaultFormSectionContent, + SettingsSectionFooter: + overrides?.Card?.SettingsSectionFooter ?? DefaultFormSectionFooter, + }, + Node: { + Button: overrides?.Node?.Button ?? DefaultButton, + OidcButton: overrides?.Node?.OidcButton ?? DefaultButtonSocial, + CurrentIdentifierButton: + overrides?.Node?.CurrentIdentifierButton ?? + DefaultCurrentIdentifierButton, + Input: overrides?.Node?.Input ?? DefaultInput, + CodeInput: overrides?.Node?.CodeInput ?? DefaultPinCodeInput, + Image: overrides?.Node?.Image ?? DefaultImage, + Label: overrides?.Node?.Label ?? DefaultLabel, + Checkbox: overrides?.Node?.Checkbox ?? DefaultCheckbox, + Text: overrides?.Node?.Text ?? DefaultText, + Anchor: overrides?.Node?.Anchor ?? DefaultLinkButton, + }, + Form: { + Root: overrides?.Form?.Root ?? DefaultFormContainer, + Group: overrides?.Form?.Group ?? DefaultGroupContainer, + OidcRoot: overrides?.Form?.OidcRoot ?? DefaultSocialButtonContainer, + RecoveryCodesSettings: + overrides?.Form?.RecoveryCodesSettings ?? DefaultSettingsRecoveryCodes, + TotpSettings: overrides?.Form?.TotpSettings ?? DefaultSettingsTotp, + OidcSettings: overrides?.Form?.OidcSettings ?? DefaultSettingsOidc, + WebauthnSettings: + overrides?.Form?.WebauthnSettings ?? DefaultSettingsWebauthn, + PasskeySettings: + overrides?.Form?.PasskeySettings ?? DefaultSettingsPasskey, + }, + Message: { + Root: overrides?.Message?.Root ?? DefaultMessageContainer, + Content: overrides?.Message?.Content ?? DefaultMessage, + }, + Page: { + Header: overrides?.Page?.Header ?? DefaultPageHeader, + }, + } } diff --git a/packages/elements-react/src/theme/default/flows/login.tsx b/packages/elements-react/src/theme/default/flows/login.tsx index 9142d606..1a430d6d 100644 --- a/packages/elements-react/src/theme/default/flows/login.tsx +++ b/packages/elements-react/src/theme/default/flows/login.tsx @@ -9,9 +9,8 @@ import { OryProvider, OryTwoStepCard, } from "@ory/elements-react" -import merge from "lodash.merge" import { PropsWithChildren } from "react" -import { OryDefaultComponents } from "../components" +import { getOryComponents } from "../components" export type LoginFlowContextProps = { flow: LoginFlow @@ -25,9 +24,7 @@ export function Login({ children, components: flowOverrideComponents, }: PropsWithChildren) { - const components = flowOverrideComponents - ? merge({}, OryDefaultComponents, flowOverrideComponents) - : OryDefaultComponents + const components = getOryComponents(flowOverrideComponents) return ( ) { - const components = flowOverrideComponents - ? merge({}, OryDefaultComponents, flowOverrideComponents) - : OryDefaultComponents + const components = getOryComponents(flowOverrideComponents) return ( ) { - const components = flowOverrideComponents - ? merge({}, OryDefaultComponents, flowOverrideComponents) - : OryDefaultComponents + const components = getOryComponents(flowOverrideComponents) return ( ) { - const components = flowOverrideComponents - ? merge({}, OryDefaultComponents, flowOverrideComponents) - : OryDefaultComponents + const components = getOryComponents(flowOverrideComponents) return ( ) { - const components = flowOverrideComponents - ? merge({}, OryDefaultComponents, flowOverrideComponents) - : OryDefaultComponents + const components = getOryComponents(flowOverrideComponents) return (