diff --git a/src/__storyshots__/Updated Components/Breadcrumb-BasicBreadcrumbs.snap b/src/__storyshots__/Updated Components/Breadcrumb-BasicBreadcrumbs.snap index 3a304fd2f..843c0cedc 100644 --- a/src/__storyshots__/Updated Components/Breadcrumb-BasicBreadcrumbs.snap +++ b/src/__storyshots__/Updated Components/Breadcrumb-BasicBreadcrumbs.snap @@ -162,6 +162,7 @@ > @@ -196,6 +197,7 @@ > @@ -230,6 +232,7 @@ > @@ -264,6 +267,7 @@ > diff --git a/src/__storyshots__/Updated Components/Breadcrumb-GoBackTo.snap b/src/__storyshots__/Updated Components/Breadcrumb-GoBackTo.snap index 9cf8c3b24..c88b556fc 100644 --- a/src/__storyshots__/Updated Components/Breadcrumb-GoBackTo.snap +++ b/src/__storyshots__/Updated Components/Breadcrumb-GoBackTo.snap @@ -113,6 +113,7 @@ > diff --git a/src/__storyshots__/Updated Components/Breadcrumb-Playground.snap b/src/__storyshots__/Updated Components/Breadcrumb-Playground.snap index dce45bafc..950c54492 100644 --- a/src/__storyshots__/Updated Components/Breadcrumb-Playground.snap +++ b/src/__storyshots__/Updated Components/Breadcrumb-Playground.snap @@ -253,6 +253,7 @@ > diff --git a/src/__storyshots__/Updated Components/Breadcrumb-ThirdPartyRoutingLibrary.snap b/src/__storyshots__/Updated Components/Breadcrumb-ThirdPartyRoutingLibrary.snap index 3a304fd2f..843c0cedc 100644 --- a/src/__storyshots__/Updated Components/Breadcrumb-ThirdPartyRoutingLibrary.snap +++ b/src/__storyshots__/Updated Components/Breadcrumb-ThirdPartyRoutingLibrary.snap @@ -162,6 +162,7 @@ > @@ -196,6 +197,7 @@ > @@ -230,6 +232,7 @@ > @@ -264,6 +267,7 @@ > diff --git a/src/__storyshots__/Updated Components/Link-LinkStyles.snap b/src/__storyshots__/Updated Components/Link-LinkStyles.snap index a78246246..697675328 100644 --- a/src/__storyshots__/Updated Components/Link-LinkStyles.snap +++ b/src/__storyshots__/Updated Components/Link-LinkStyles.snap @@ -142,6 +142,7 @@ > @@ -159,6 +160,7 @@ > diff --git a/src/__storyshots__/Updated Components/Link-LinkWithIcon.snap b/src/__storyshots__/Updated Components/Link-LinkWithIcon.snap index 349acb065..773e5239a 100644 --- a/src/__storyshots__/Updated Components/Link-LinkWithIcon.snap +++ b/src/__storyshots__/Updated Components/Link-LinkWithIcon.snap @@ -234,6 +234,7 @@ > @@ -263,6 +264,7 @@ > @@ -292,6 +294,7 @@ > diff --git a/src/__storyshots__/Updated Components/Link-Placement.snap b/src/__storyshots__/Updated Components/Link-Placement.snap index 81f2b47ca..06f0a189a 100644 --- a/src/__storyshots__/Updated Components/Link-Placement.snap +++ b/src/__storyshots__/Updated Components/Link-Placement.snap @@ -132,6 +132,7 @@ @@ -149,6 +150,7 @@ This is a diff --git a/src/__storyshots__/Updated Components/Link-Playground.snap b/src/__storyshots__/Updated Components/Link-Playground.snap index 5e965c3af..b736dba3f 100644 --- a/src/__storyshots__/Updated Components/Link-Playground.snap +++ b/src/__storyshots__/Updated Components/Link-Playground.snap @@ -74,6 +74,7 @@ > diff --git a/src/__storyshots__/Updated Components/Link-Sizes.snap b/src/__storyshots__/Updated Components/Link-Sizes.snap index cf06f5d24..caffa5864 100644 --- a/src/__storyshots__/Updated Components/Link-Sizes.snap +++ b/src/__storyshots__/Updated Components/Link-Sizes.snap @@ -180,6 +180,7 @@ > @@ -193,6 +194,7 @@ > @@ -206,6 +208,7 @@ > diff --git a/src/__storyshots__/Updated Components/Link-ThirdPartyRoutingLibrary.snap b/src/__storyshots__/Updated Components/Link-ThirdPartyRoutingLibrary.snap index 01d769daa..a3e8a102a 100644 --- a/src/__storyshots__/Updated Components/Link-ThirdPartyRoutingLibrary.snap +++ b/src/__storyshots__/Updated Components/Link-ThirdPartyRoutingLibrary.snap @@ -54,6 +54,7 @@ diff --git a/src/__storyshots__/Updated Components/Menu-MenuTriggers.snap b/src/__storyshots__/Updated Components/Menu-MenuTriggers.snap index c61c80481..63801fb50 100644 --- a/src/__storyshots__/Updated Components/Menu-MenuTriggers.snap +++ b/src/__storyshots__/Updated Components/Menu-MenuTriggers.snap @@ -275,6 +275,7 @@ aria-haspopup="true" aria-label="Menu" class="emotion-8" + data-slot="link" data-testid="_link" href="#" > diff --git a/src/__storyshots__/Updated Components/Notifications/Inline Alert-Error.snap b/src/__storyshots__/Updated Components/Notifications/Inline Alert-Error.snap index 2b3e762b1..677e6483b 100644 --- a/src/__storyshots__/Updated Components/Notifications/Inline Alert-Error.snap +++ b/src/__storyshots__/Updated Components/Notifications/Inline Alert-Error.snap @@ -252,6 +252,7 @@ > diff --git a/src/__storyshots__/Updated Components/Notifications/Inline Alert-Informational.snap b/src/__storyshots__/Updated Components/Notifications/Inline Alert-Informational.snap index 9bf3283a1..706a653a4 100644 --- a/src/__storyshots__/Updated Components/Notifications/Inline Alert-Informational.snap +++ b/src/__storyshots__/Updated Components/Notifications/Inline Alert-Informational.snap @@ -254,6 +254,7 @@ > diff --git a/src/__storyshots__/Updated Components/Notifications/Inline Alert-Neutral.snap b/src/__storyshots__/Updated Components/Notifications/Inline Alert-Neutral.snap index 50f670f02..b43b0fac2 100644 --- a/src/__storyshots__/Updated Components/Notifications/Inline Alert-Neutral.snap +++ b/src/__storyshots__/Updated Components/Notifications/Inline Alert-Neutral.snap @@ -214,6 +214,7 @@ > diff --git a/src/__storyshots__/Updated Components/Notifications/Inline Alert-Playground.snap b/src/__storyshots__/Updated Components/Notifications/Inline Alert-Playground.snap index 50f670f02..b43b0fac2 100644 --- a/src/__storyshots__/Updated Components/Notifications/Inline Alert-Playground.snap +++ b/src/__storyshots__/Updated Components/Notifications/Inline Alert-Playground.snap @@ -214,6 +214,7 @@ > diff --git a/src/__storyshots__/Updated Components/Notifications/Inline Alert-Success.snap b/src/__storyshots__/Updated Components/Notifications/Inline Alert-Success.snap index 31cd1eab9..d893e34a4 100644 --- a/src/__storyshots__/Updated Components/Notifications/Inline Alert-Success.snap +++ b/src/__storyshots__/Updated Components/Notifications/Inline Alert-Success.snap @@ -252,6 +252,7 @@ > diff --git a/src/__storyshots__/Updated Components/Notifications/Inline Alert-Warning.snap b/src/__storyshots__/Updated Components/Notifications/Inline Alert-Warning.snap index 237b4d2f3..48f758420 100644 --- a/src/__storyshots__/Updated Components/Notifications/Inline Alert-Warning.snap +++ b/src/__storyshots__/Updated Components/Notifications/Inline Alert-Warning.snap @@ -254,6 +254,7 @@ > diff --git a/src/components/ButtonBase/ButtonBase.tsx b/src/components/ButtonBase/ButtonBase.tsx index 55ed3d7e0..dbf62cfe0 100644 --- a/src/components/ButtonBase/ButtonBase.tsx +++ b/src/components/ButtonBase/ButtonBase.tsx @@ -7,6 +7,7 @@ import { generateTestDataId } from 'utils/helpers'; import type { ComponentSizes, TestProps } from 'utils/types'; import { buttonBaseStyle, buttonWrapperStyle } from './ButtonBase.style'; +import { useSlotProps } from '../utils/Slots'; import type { ButtonTypes } from 'components/Button/Button.types'; import ButtonLoader from 'components/Button/ButtonLoader'; import type { IconButtonShape } from 'components/IconButton'; @@ -43,6 +44,7 @@ export type ButtonBaseProps = { //@TODO fix props to not overwrite button props const ButtonBase = React.forwardRef((props, ref) => { + props = useSlotProps(props, 'button'); const { type = 'primary', size = 'normal', @@ -70,7 +72,6 @@ const ButtonBase = React.forwardRef((props, ref={ref} type={htmlType} data-testid={generateTestDataId(testIdName, dataTestId)} - data-slot="button" css={buttonBaseStyle({ type, size, diff --git a/src/components/InlineAlert/InlineAlert.stories.tsx b/src/components/InlineAlert/InlineAlert.stories.tsx index 429db8752..d93147133 100644 --- a/src/components/InlineAlert/InlineAlert.stories.tsx +++ b/src/components/InlineAlert/InlineAlert.stories.tsx @@ -30,7 +30,7 @@ type Story = StoryObj; export const Neutral: Story = { render: (args) => ( - Single Action} onDismiss={() => {}} {...args}> + Single Action} onDismiss={() => {}} {...args}> Alert copy should be short, easy to understand and actionable. ), @@ -40,7 +40,7 @@ export const Informational: Story = { render: (args) => ( Single Action} + actions={Single Action} onDismiss={() => {}} {...args} > @@ -51,12 +51,7 @@ export const Informational: Story = { export const Error: Story = { render: (args) => ( - Single Action} - onDismiss={() => {}} - {...args} - > + Single Action} onDismiss={() => {}} {...args}> Alert copy should be short, easy to understand and actionable. ), @@ -66,7 +61,7 @@ export const Warning: Story = { render: (args) => ( Single Action} + actions={Single Action} onDismiss={() => {}} {...args} > @@ -79,7 +74,7 @@ export const Success: Story = { render: (args) => ( Single Action} + actions={Single Action} onDismiss={() => {}} {...args} > @@ -92,12 +87,7 @@ export const WithButtons: Story = { render: (args) => ( - Tertiary - , - Primary, - ]} + actions={[Tertiary, Primary]} onDismiss={() => {}} {...args} > @@ -141,7 +131,7 @@ export const WithTrigger: Story = { export const Playground: Story = { render: (args) => ( - Single Action} onDismiss={() => {}} {...args}> + Single Action} onDismiss={() => {}} {...args}> Alert copy should be short, easy to understand and actionable. ), diff --git a/src/components/InlineAlert/InlineAlert.tsx b/src/components/InlineAlert/InlineAlert.tsx index cc3a61148..e723cb577 100644 --- a/src/components/InlineAlert/InlineAlert.tsx +++ b/src/components/InlineAlert/InlineAlert.tsx @@ -5,12 +5,14 @@ import { useId } from 'react-aria'; import { getIconColor, styles } from './InlineAlert.style'; import type { InlineAlertProps } from './InlineAlert.types'; import Icon from '../Icon'; +import { SlotProvider, useSlotProps } from '../utils/Slots'; import { useDOMRef } from '../utils/useDOMRef'; import useTheme from '~/hooks/useTheme'; export const InlineAlert = forwardRef( (props: InlineAlertProps, ref: RefObject) => { + props = useSlotProps(props, 'inline-alert'); const { status = 'neutral', actions, @@ -46,7 +48,6 @@ export const InlineAlert = forwardRef( tabIndex={0} role={status === 'warning' || status === 'error' ? 'alert' : 'status'} aria-describedby={onDismiss ? dismissId : undefined} - data-slot="inline-alert" data-testid={`${dataTestPrefixId}_inline_alert`} > {status !== 'neutral' ? ( @@ -59,7 +60,18 @@ export const InlineAlert = forwardRef( /> ) : null} {children} - {actionsArray.length > 0 ? {actionsArray} : null} + {actionsArray.length > 0 ? ( + + + {actionsArray} + + + ) : null} {onDismiss ? ( ((props, ref) => { + props = useSlotProps(props, 'link'); const { type = 'primary', placement = 'block', diff --git a/src/components/utils/Slots.tsx b/src/components/utils/Slots.tsx new file mode 100644 index 000000000..cb6e7a34c --- /dev/null +++ b/src/components/utils/Slots.tsx @@ -0,0 +1,109 @@ +import type { ReactNode } from 'react'; +import { createContext, useContext, useMemo } from 'react'; +import { mergeProps } from 'react-aria'; + +interface SlotProps { + 'data-slot'?: string; +} + +const SlotContext = createContext(null); + +export function useSlotProps(props: T, defaultSlot?: string) { + const slot = (props as SlotProps)['data-slot'] || defaultSlot; + const { [slot]: slotProps = {} } = useContext(SlotContext) || {}; + + return mergeProps(props, mergeProps(slotProps, { 'data-slot': slot })); +} + +/** + * SlotProvider manages and provides slot props to its descendants. + * It allows for the definition and inheritance of props for named slots in a component tree. + * + * @component + * @param {Object} [props.slots={}] - An object containing slot names as keys and their corresponding props as values. + * @param {ReactNode} props.children - The child components to be wrapped by the SlotProvider. + * + * * @example + * // Register component with the useSlotProps() hook + * function Button(props) { + * props = useSlotProps(props, 'button'); + * + * return ( + * {props.children} + * ); + * } + * + * function ButtonsWithPadding(props) { + * return ( + * + * {props.children} + * + * ) + * } + * + * + * Button 1 // Gets 1rem padding + * Button 2 // Gets 1rem padding + * + * + * @example + * // Nested usage + * + * + * + * + * + * + */ +export function SlotProvider({ slots = {}, children }: { slots?: object; children: ReactNode }) { + // eslint-disable-next-line react-hooks/exhaustive-deps + const parentSlots = useContext(SlotContext) || {}; + + const value = useMemo(() => { + return Object.keys(parentSlots) + .concat(Object.keys(slots)) + .reduce( + (acc, slot) => ({ + ...acc, + [slot]: mergeProps(parentSlots[slot] || {}, slots[slot] || {}), + }), + {} + ); + }, [parentSlots, slots]); + + return {children}; +} + +/** + * ClearSlots resets the SlotContext to an empty object for its children. + * It's used to remove any inherited slot props from parent SlotProviders. + * + * @component + * @param {React.ReactNode} props.children - The child components to be wrapped by ClearSlots. + * + * @example + * // Basic usage + * + * + * This text is blue + * + * This text is black + * + * + * + * + * @example + * // Usage within nested SlotProviders + * + * This text is blue + * + * + * This text is red, not blue + * + * + * + * + */ +export function ClearSlots({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/src/components/utils/tests/Slots.test.tsx b/src/components/utils/tests/Slots.test.tsx new file mode 100644 index 000000000..af1d02f3a --- /dev/null +++ b/src/components/utils/tests/Slots.test.tsx @@ -0,0 +1,91 @@ +import { render, renderHook } from '~/test'; +import { ClearSlots, SlotProvider, useSlotProps } from '../Slots'; + +describe('useSlotProps()', () => { + it('merges props with slot props', () => { + const wrapper = ({ children }) => ( + {children} + ); + + const { result } = renderHook(() => useSlotProps({ id: 'test-id' }, 'test'), { wrapper }); + + expect(result.current).toEqual({ + id: 'test-id', + className: 'test-class', + 'data-slot': 'test', + }); + }); + + it('uses the data-slot attribute if provided', () => { + const wrapper = ({ children }) => ( + {children} + ); + + const { result } = renderHook(() => useSlotProps({ id: 'test-id', 'data-slot': 'custom' }), { + wrapper, + }); + + expect(result.current).toEqual({ + id: 'test-id', + className: 'test-class', + 'data-slot': 'custom', + }); + }); +}); + +describe('', () => { + it('provides slot props to children', () => { + const Component = () => { + const props = useSlotProps({}, 'test'); + return ; + }; + + const { container } = render( + + + + ); + + expect(container.firstChild).toHaveClass('test-class'); + expect(container.firstChild).toHaveAttribute('data-slot', 'test'); + }); + + it('merges parent and child slot props', () => { + const Component = () => { + const props = useSlotProps({}, 'test'); + return ; + }; + + const { container } = render( + + + + + + ); + + expect(container.firstChild).toHaveClass('test-class'); + expect(container.firstChild).toHaveAttribute('id', 'test-id'); + expect(container.firstChild).toHaveAttribute('data-slot', 'test'); + }); +}); + +describe('', () => { + it('clears slot context for children', () => { + const Component = () => { + const props = useSlotProps({}, 'test'); + return ; + }; + + const { container } = render( + + + + + + ); + + expect(container.firstChild).not.toHaveClass('test-class'); + expect(container.firstChild).toHaveAttribute('data-slot', 'test'); + }); +});
This text is blue
This text is black
This text is red, not blue