diff --git a/docs/COMPONENT_CUSTOMIZATION.md b/docs/COMPONENT_CUSTOMIZATION.md index c70d04d1a..88ad4ec88 100644 --- a/docs/COMPONENT_CUSTOMIZATION.md +++ b/docs/COMPONENT_CUSTOMIZATION.md @@ -106,7 +106,9 @@ Available component types include: - `statement` - Statement/information display - `table` - Table components - `drawer` - Drawer components +- `forcedValue` - Forced value components - `zendeskDrawer` - Zendesk drawer components +- `zendeskTriggerButton` - Zendesk trigger button components - `pdfViewer` - PDF viewer component - `tel` - Tel field component - `time` - Time field component @@ -117,6 +119,7 @@ and their typescript definitions - `ButtonComponentProps`: For custom button components - `StatementComponentProps`: For custom statement components - `ZendeskDrawerComponentProps`: For custom zendesk drawer +- `ZendeskTriggerButtonComponentProps`: For custom zendesk trigger button - `FileComponentProps`: for custom file field components - `CountryComponentProps`: for country field components - `TextFieldComponentProps`: for textfield components @@ -125,6 +128,7 @@ and their typescript definitions - `PDFPreviewComponentProps`: for the pdf viewer component - `TelFieldComponentProps`: for the tel component - `TimeFieldComponentProps`: for the timefield component +- `ForcedValueComponentProps`: for the forced value component > **Tip:** Check [src/default-components.ts](../src/default-components.ts) to see the default implementations. You can use these as a starting point or reference when building your own custom components. @@ -393,6 +397,76 @@ type JSFCustomComponentProps = { }; ``` +### ZendeskTriggerButtonComponentProps + +For custom Zendesk trigger button components: + +```tsx +import { ZendeskTriggerButtonComponentProps } from '@remoteoss/remote-flows'; + +type ZendeskTriggerButtonComponentProps = { + zendeskId: number; + className?: string; + onClick?: (zendeskId: number) => void; + children?: React.ReactNode; + external?: boolean; +} & Record; +``` + +**Example: Custom Zendesk Trigger Button** + +```tsx +import { + RemoteFlows, + ZendeskTriggerButtonComponentProps, +} from '@remoteoss/remote-flows'; + +const CustomZendeskTriggerButton = ({ + zendeskId, + onClick, + children, + className, + external, +}: ZendeskTriggerButtonComponentProps) => { + const handleClick = () => { + onClick?.(zendeskId); + }; + + if (external) { + return ( + + {children} + + ); + } + + return ( + + ); +}; + +function App() { + return ( + + {/* All flows will use this custom Zendesk trigger button */} + + ); +} +``` + ## When to Use Each Method ### Use Global Override When: diff --git a/example/src/Components.tsx b/example/src/Components.tsx index 511ad33fd..598cf91a9 100644 --- a/example/src/Components.tsx +++ b/example/src/Components.tsx @@ -1,15 +1,17 @@ -import type { - ButtonComponentProps, - Components, - FieldComponentProps, - FieldSetToggleComponentProps, - FileComponentProps, - ForcedValueComponentProps, - PDFPreviewComponentProps, - TelFieldComponentProps, - TimeFieldComponentProps, +import { + buildZendeskURL, + type ButtonComponentProps, + type Components, + type FieldComponentProps, + type FieldSetToggleComponentProps, + type FileComponentProps, + type ForcedValueComponentProps, + type PDFPreviewComponentProps, + type TelFieldComponentProps, + type TimeFieldComponentProps, + type ZendeskTriggerButtonComponentProps, } from '@remoteoss/remote-flows'; -import { FileUploader } from '@remoteoss/remote-flows/internals'; +import { cn, FileUploader } from '@remoteoss/remote-flows/internals'; import { splitAccordionDescription } from './utils/transformHtml'; import { Accordion } from './components/Accordion'; //import { ZendeskDialog } from './ZendeskDialog'; @@ -491,6 +493,40 @@ const TimeField = ({ ); }; +const ZendeskTriggerButton = ({ + zendeskId, + onClick, + children, + external, + className, +}: ZendeskTriggerButtonComponentProps) => { + const handleClick = () => { + onClick?.(zendeskId); + }; + + if (external) { + return ( + + {children} + + ); + } + + return ( + + ); +}; + export const components: Components = { button: Button, text: Input, @@ -506,5 +542,6 @@ export const components: Components = { pdfViewer: PDFPreview, tel: TelField, time: TimeField, + zendeskTriggerButton: ZendeskTriggerButton, //zendeskDrawer: ZendeskDialog, }; diff --git a/src/components/shared/zendesk-drawer/ZendeskTriggerButton.tsx b/src/components/shared/zendesk-drawer/ZendeskTriggerButton.tsx index c4594588b..2be94a2de 100644 --- a/src/components/shared/zendesk-drawer/ZendeskTriggerButton.tsx +++ b/src/components/shared/zendesk-drawer/ZendeskTriggerButton.tsx @@ -1,7 +1,6 @@ -import { cn } from '@/src/lib/utils'; import { ZendeskDrawer } from './ZendeskDrawer'; -import { buildZendeskURL } from './utils'; import { useState } from 'react'; +import { useFormFields } from '@/src/context'; interface ZendeskTriggerButtonProps { /** @@ -26,9 +25,6 @@ interface ZendeskTriggerButtonProps { external?: boolean; } -const baseClassName = - 'RemoteFlows__ZendeskTriggerButton text-blue-500 hover:underline inline-block text-xs bg-transparent border-none cursor-pointer p-0'; - export function ZendeskTriggerButton({ zendeskId, className, @@ -36,6 +32,7 @@ export function ZendeskTriggerButton({ children, external = false, }: ZendeskTriggerButtonProps) { + const { components } = useFormFields(); const [isOpen, setIsOpen] = useState(false); const handleClick = () => { @@ -45,18 +42,25 @@ export function ZendeskTriggerButton({ onClick?.(zendeskId); }; + const CustomZendeskTriggerButton = components?.zendeskTriggerButton; + + if (!CustomZendeskTriggerButton) { + throw new Error(`Zendesk trigger button component not found`); + } + + const triggerElement = ( + + {children} + + ); + if (external) { - return ( - - {children} - - ); + return triggerElement; } return ( @@ -64,11 +68,7 @@ export function ZendeskTriggerButton({ zendeskId={zendeskId} open={isOpen} onClose={() => setIsOpen(false)} - Trigger={ - - } + Trigger={triggerElement} /> ); } diff --git a/src/components/shared/zendesk-drawer/default/ZendeskTriggerButtonDefault.tsx b/src/components/shared/zendesk-drawer/default/ZendeskTriggerButtonDefault.tsx new file mode 100644 index 000000000..26543e8d3 --- /dev/null +++ b/src/components/shared/zendesk-drawer/default/ZendeskTriggerButtonDefault.tsx @@ -0,0 +1,38 @@ +import { ZendeskTriggerButtonComponentProps } from '@/src/types/remoteFlows'; +import { cn } from '@/src/lib/utils'; +import { buildZendeskURL } from '../utils'; + +const baseClassName = + 'RemoteFlows__ZendeskTriggerButton text-blue-500 hover:underline inline-block text-xs bg-transparent border-none cursor-pointer p-0'; + +export function ZendeskTriggerButtonDefault({ + zendeskId, + onClick, + children, + className, + external, +}: ZendeskTriggerButtonComponentProps) { + const handleClick = () => { + onClick?.(zendeskId); + }; + + if (external) { + return ( + + {children} + + ); + } + + return ( + + ); +} diff --git a/src/components/shared/zendesk-drawer/tests/ZendeskTriggerButton.test.tsx b/src/components/shared/zendesk-drawer/tests/ZendeskTriggerButton.test.tsx index ae730de4e..bb799ade2 100644 --- a/src/components/shared/zendesk-drawer/tests/ZendeskTriggerButton.test.tsx +++ b/src/components/shared/zendesk-drawer/tests/ZendeskTriggerButton.test.tsx @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event'; import { server } from '@/src/tests/server'; import { ZendeskTriggerButton } from '../ZendeskTriggerButton'; import { queryClient, TestProviders } from '@/src/tests/testHelpers'; +import type { ZendeskTriggerButtonComponentProps } from '@/src/types/remoteFlows'; describe('ZendeskTriggerButton', () => { const mockArticle = { @@ -176,4 +177,105 @@ describe('ZendeskTriggerButton', () => { expect(links).toHaveLength(1); }); }); + + describe('with custom trigger button component', () => { + const CustomTriggerButton = ({ + zendeskId, + onClick, + children, + className, + }: ZendeskTriggerButtonComponentProps) => { + return ( + + ); + }; + + const WrapperWithCustomButton = ({ + children, + }: { + children: React.ReactNode; + }) => ( + + {children} + + ); + + it('renders custom trigger button when provided', () => { + render( + + Open Article + , + { wrapper: WrapperWithCustomButton }, + ); + + const customButton = screen.getByTestId('custom-trigger'); + expect(customButton).toBeInTheDocument(); + expect(customButton).toHaveTextContent('Custom: Open Article'); + }); + + it('calls onClick when custom trigger button is clicked', async () => { + const onClick = vi.fn(); + + render( + + Open Article + , + { wrapper: WrapperWithCustomButton }, + ); + + await userEvent.click(screen.getByTestId('custom-trigger')); + expect(onClick).toHaveBeenCalledWith(123456); + }); + + it('opens drawer when custom trigger button is clicked and external is false', async () => { + render( + + Open Article + , + { wrapper: WrapperWithCustomButton }, + ); + + await userEvent.click(screen.getByTestId('custom-trigger')); + + // Wait for drawer to load and display content + const title = await screen.findByText('Test Article'); + expect(title).toBeInTheDocument(); + }); + + it('does not open drawer when custom trigger button is clicked and external is true', async () => { + render( + + Open Article + , + { wrapper: WrapperWithCustomButton }, + ); + + await userEvent.click(screen.getByTestId('custom-trigger')); + + // Drawer should not appear + const title = screen.queryByText('Test Article'); + expect(title).not.toBeInTheDocument(); + }); + + it('passes className to custom trigger button', () => { + render( + + Open Article + , + { wrapper: WrapperWithCustomButton }, + ); + + const customButton = screen.getByTestId('custom-trigger'); + expect(customButton).toHaveClass('custom-class-from-parent'); + }); + }); }); diff --git a/src/default-components.ts b/src/default-components.ts index 7ef41d213..a8d96b881 100644 --- a/src/default-components.ts +++ b/src/default-components.ts @@ -14,6 +14,7 @@ import { TextFieldDefault } from '@/src/components/form/fields/default/TextField import { FieldsetToggleButtonDefault } from '@/src/components/form/fields/default/FieldsetToggleButtonDefault'; import { DrawerDefault } from '@/src/components/shared/drawer/DrawerDefault'; import { ZendeskDrawerDefault } from '@/src/components/shared/zendesk-drawer/ZendeskDrawerDefault'; +import { ZendeskTriggerButtonDefault } from '@/src/components/shared/zendesk-drawer/default/ZendeskTriggerButtonDefault'; import { TableFieldDefault } from '@/src/components/shared/table/TableFieldDefault'; import { CheckboxFieldDefault } from '@/src/components/form/fields/default/CheckboxFieldDefault'; import { WorkScheduleFieldDefault } from '@/src/components/form/fields/default/WorkScheduleFieldDefault'; @@ -47,6 +48,7 @@ export const defaultComponents: Components = { textarea: TextAreaFieldDefault, text: TextFieldDefault, zendeskDrawer: ZendeskDrawerDefault, + zendeskTriggerButton: ZendeskTriggerButtonDefault, table: TableFieldDefault, 'work-schedule': WorkScheduleFieldDefault, pdfViewer: PDFPreviewDefault, diff --git a/src/index.tsx b/src/index.tsx index 95163a77d..3f05001b2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -152,6 +152,7 @@ export type { FieldSetToggleComponentProps, ButtonComponentProps, ZendeskDrawerComponentProps, + ZendeskTriggerButtonComponentProps, DrawerComponentProps, PDFPreviewComponentProps, Meta, @@ -191,5 +192,8 @@ export type { export type { $TSFixMe, JSFCustomComponentProps } from './types/remoteFlows'; export type { JSFModifyField } from './flows/types'; export type { FieldError, NormalizedFieldError } from './lib/mutations'; -export { zendeskArticles } from './components/shared/zendesk-drawer/utils'; +export { + zendeskArticles, + buildZendeskURL, +} from './components/shared/zendesk-drawer/utils'; export { ZendeskTriggerButton } from './components/shared/zendesk-drawer/ZendeskTriggerButton'; diff --git a/src/lazy-default-components.ts b/src/lazy-default-components.ts index b66fe20b1..5550151f4 100644 --- a/src/lazy-default-components.ts +++ b/src/lazy-default-components.ts @@ -142,4 +142,11 @@ export const lazyDefaultComponents: Components = { }), ), ), + zendeskTriggerButton: lazy(() => + import('./components/shared/zendesk-drawer/default/ZendeskTriggerButtonDefault').then( + (m) => ({ + default: m.ZendeskTriggerButtonDefault, + }), + ), + ), }; diff --git a/src/types/remoteFlows.ts b/src/types/remoteFlows.ts index 509144ac3..33f5c6626 100644 --- a/src/types/remoteFlows.ts +++ b/src/types/remoteFlows.ts @@ -104,6 +104,32 @@ export type ZendeskDrawerComponentProps = { Trigger: React.ReactElement; }; +/** + * Props for custom Zendesk trigger button components. + */ +export type ZendeskTriggerButtonComponentProps = { + /** + * The Zendesk ID for the help article + */ + zendeskId: number; + /** + * The class name for the button + */ + className?: string; + /** + * The callback function to be called when the button is clicked + */ + onClick?: (zendeskId: number) => void; + /** + * The children to be rendered inside the button + */ + children?: React.ReactNode; + /** + * Whether to open the help article in a new tab + */ + external?: boolean; +} & Record; + export type DrawerComponentProps = { open: boolean; onOpenChange: (open: boolean) => void; @@ -156,6 +182,7 @@ export type Components = { button?: React.ComponentType; fieldsetToggle?: React.ComponentType; zendeskDrawer?: React.ComponentType; + zendeskTriggerButton?: React.ComponentType; drawer?: React.ComponentType; table?: React.ComponentType; 'work-schedule'?: React.ComponentType;