diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 60dcf5d7b2a5..d21d5f54c135 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -1452,6 +1452,9 @@ Map { "danger": Object { "type": "bool", }, + "decorator": Object { + "type": "node", + }, "isFullWidth": Object { "type": "bool", }, @@ -1510,9 +1513,7 @@ Map { ], "type": "oneOf", }, - "slug": Object { - "type": "node", - }, + "slug": [Function], }, "render": [Function], }, diff --git a/packages/react/src/components/ComposedModal/ComposedModal-test.js b/packages/react/src/components/ComposedModal/ComposedModal-test.js index 52a9550ce027..8a9208e5e4ea 100644 --- a/packages/react/src/components/ComposedModal/ComposedModal-test.js +++ b/packages/react/src/components/ComposedModal/ComposedModal-test.js @@ -344,9 +344,9 @@ describe('ComposedModal', () => { expect(screen.getByRole('button', { name: 'Cancel' })).toBeDisabled(); }); - it('should respect slug prop', () => { + it('should respect decorator prop', () => { const { container } = render( - }> + }> Modal header This is the modal body content { ); - expect(container.firstChild).toHaveClass(`${prefix}--modal--slug`); + expect(container.firstChild).toHaveClass(`${prefix}--modal--decorator`); }); }); + it('should respect the deprecated slug prop', () => { + const spy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + render( + }> + Modal header + This is the modal body content + + + ); + + expect( + screen.getByRole('button', { name: 'AI - Show information' }) + ).toBeInTheDocument(); + spy.mockRestore(); + }); + it('should handle onClick events', async () => { const onClick = jest.fn(); render( diff --git a/packages/react/src/components/ComposedModal/ComposedModal.stories.js b/packages/react/src/components/ComposedModal/ComposedModal.stories.js index b0743d05e225..157d8d883ace 100644 --- a/packages/react/src/components/ComposedModal/ComposedModal.stories.js +++ b/packages/react/src/components/ComposedModal/ComposedModal.stories.js @@ -16,6 +16,7 @@ import Select from '../Select'; import SelectItem from '../SelectItem'; import TextInput from '../TextInput'; import Button from '../Button'; +import { Information } from '@carbon/icons-react'; import { StructuredListWrapper, StructuredListHead, @@ -391,7 +392,7 @@ export const _withAILabel = { setOpen(false)} - slug={aiLabel}> + decorator={aiLabel}>

diff --git a/packages/react/src/components/ComposedModal/ComposedModal.tsx b/packages/react/src/components/ComposedModal/ComposedModal.tsx index b0019f672f82..6195b1c93ae9 100644 --- a/packages/react/src/components/ComposedModal/ComposedModal.tsx +++ b/packages/react/src/components/ComposedModal/ComposedModal.tsx @@ -8,6 +8,8 @@ import React, { type ReactNode, type ReactElement, type RefObject, + useMemo, + isValidElement, } from 'react'; import { isElement } from 'react-is'; import PropTypes from 'prop-types'; @@ -28,6 +30,7 @@ import { usePrefix } from '../../internal/usePrefix'; import { keys, match } from '../../internal/keyboard'; import { useFeatureFlag } from '../FeatureFlags'; import { composeEventHandlers } from '../../tools/events'; +import deprecate from '../../prop-types/deprecate'; export interface ModalBodyProps extends HTMLAttributes { /** Specify the content to be placed in the ModalBody. */ @@ -172,6 +175,11 @@ export interface ComposedModalProps extends HTMLAttributes { */ danger?: boolean; + /** + * **Experimental**: Provide a `decorator` component to be rendered inside the `ComposedModal` component + */ + decorator?: ReactNode; + /** * Specify whether the Modal content should have any inner padding. */ @@ -212,6 +220,7 @@ export interface ComposedModalProps extends HTMLAttributes { size?: 'xs' | 'sm' | 'md' | 'lg'; /** + * @deprecated please use `decorator` instead. * **Experimental**: Provide a `Slug` component to be rendered inside the `ComposedModal` component */ slug?: ReactNode; @@ -226,6 +235,7 @@ const ComposedModal = React.forwardRef( className: customClassName, containerClassName, danger, + decorator, isFullWidth, onClose, onKeyDown, @@ -335,6 +345,7 @@ const ComposedModal = React.forwardRef( 'is-visible': isOpen, [`${prefix}--modal--danger`]: danger, [`${prefix}--modal--slug`]: slug, + [`${prefix}--modal--decorator`]: decorator, }, customClassName ); @@ -421,12 +432,20 @@ const ComposedModal = React.forwardRef( } }, [open, selectorPrimaryFocus, isOpen]); - // Slug is always size `sm` - let normalizedSlug; - if (slug && slug['type']?.displayName === 'AILabel') { - normalizedSlug = React.cloneElement(slug as React.ReactElement, { - size: 'sm', - }); + // AILabel is always size `sm` + let normalizedDecorator = React.isValidElement(slug ?? decorator) + ? (slug ?? decorator) + : null; + if ( + normalizedDecorator && + normalizedDecorator['type']?.displayName === 'AILabel' + ) { + normalizedDecorator = React.cloneElement( + normalizedDecorator as React.ReactElement, + { + size: 'sm', + } + ); } return ( @@ -456,7 +475,15 @@ const ComposedModal = React.forwardRef( )}

- {normalizedSlug} + {slug ? ( + normalizedDecorator + ) : decorator ? ( +
+ {normalizedDecorator} +
+ ) : ( + '' + )} {childrenWithProps}
{/* Non-translatable: Focus-wrap code makes this `