diff --git a/.changeset/pretty-trainers-rhyme.md b/.changeset/pretty-trainers-rhyme.md new file mode 100644 index 00000000000..ac91ac100d0 --- /dev/null +++ b/.changeset/pretty-trainers-rhyme.md @@ -0,0 +1,5 @@ +--- +"@primer/react": patch +--- + +ActionList: Fix bug that did not allow both inline and block description at the same time diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-colorblind-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-colorblind-linux.png index e0cb829ee47..3045185cd7b 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-colorblind-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-dimmed-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-dimmed-linux.png index ab8676464eb..83bf0ca97ad 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-dimmed-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-high-contrast-linux.png index f71598b43f6..372937d63dd 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-high-contrast-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-linux.png index 56c0f22b260..80f3fc01a28 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-tritanopia-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-tritanopia-linux.png index e0cb829ee47..cbad2d02913 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-tritanopia-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-light-colorblind-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-light-colorblind-linux.png index e405bf18c67..ad822ecceb0 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-light-colorblind-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-light-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-light-high-contrast-linux.png index ec4576b172f..aa613944f89 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-light-high-contrast-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-light-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-light-linux.png index 5a85c2be797..964bc494b17 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-light-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-light-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-light-tritanopia-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-light-tritanopia-linux.png index e405bf18c67..75c9dd2458b 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-light-tritanopia-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inline-Description-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-colorblind-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-colorblind-linux.png index 78db1bf3fbd..abef84c40af 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-colorblind-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-dimmed-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-dimmed-linux.png index b0778b1eaf4..1ed9cd1b9e3 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-dimmed-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-high-contrast-linux.png index a86c1417c5e..ad17432b616 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-high-contrast-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-linux.png index 7fe52960dc9..89772f78161 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-tritanopia-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-tritanopia-linux.png index 78db1bf3fbd..abef84c40af 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-tritanopia-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-light-colorblind-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-light-colorblind-linux.png index 640f22417fe..eca1feb3806 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-light-colorblind-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-light-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-light-high-contrast-linux.png index 8f476e592ce..1131cccae0c 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-light-high-contrast-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-light-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-light-linux.png index dab3620e85d..62cac07dde7 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-light-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-light-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-light-tritanopia-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-light-tritanopia-linux.png index 640f22417fe..eca1feb3806 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-light-tritanopia-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Text-Wrap-And-Truncation-light-tritanopia-linux.png differ diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index f7bde505817..97d3eaea55c 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -36,8 +36,10 @@ export const Item = React.forwardRef( const [slots, childrenWithoutSlots] = useSlots(props.children, { leadingVisual: LeadingVisual, trailingVisual: TrailingVisual, - description: Description, + blockDescription: [Description, props => props.variant === 'block'], + inlineDescription: [Description, props => props.variant !== 'block'], }) + const { variant: listVariant, role: listRole, @@ -204,10 +206,8 @@ export const Item = React.forwardRef( onKeyPress: keyPressHandler, 'aria-disabled': disabled ? true : undefined, tabIndex: disabled ? undefined : 0, - 'aria-labelledby': `${labelId} ${ - slots.description && slots.description.props.variant !== 'block' ? inlineDescriptionId : '' - }`, - 'aria-describedby': slots.description?.props.variant === 'block' ? blockDescriptionId : undefined, + 'aria-labelledby': `${labelId} ${slots.inlineDescription ? inlineDescriptionId : ''}`, + 'aria-describedby': slots.blockDescription ? blockDescriptionId : undefined, ...(selectionAttribute && {[selectionAttribute]: selected}), role: role || itemRole, id: itemId, @@ -235,26 +235,25 @@ export const Item = React.forwardRef( > {childrenWithoutSlots} - {slots.description?.props.variant !== 'block' ? slots.description : null} + {slots.inlineDescription} {slots.trailingVisual} - {slots.description?.props.variant === 'block' ? slots.description : null} + {slots.blockDescription} diff --git a/src/hooks/__tests__/useSlots.test.tsx b/src/hooks/__tests__/useSlots.test.tsx index d491f90c475..3a71ad40f1f 100644 --- a/src/hooks/__tests__/useSlots.test.tsx +++ b/src/hooks/__tests__/useSlots.test.tsx @@ -2,7 +2,8 @@ import {renderHook} from '@testing-library/react-hooks' import React from 'react' import {useSlots} from '../useSlots' -function TestComponentA(props: React.PropsWithChildren) { +type TestComponentAProps = React.PropsWithChildren<{variant?: 'a' | 'b'}> +function TestComponentA(props: TestComponentAProps) { return
} @@ -114,3 +115,35 @@ test('warns about duplicate slots', () => { `) expect(warnSpy).toHaveBeenCalledTimes(1) }) + +test('extracts elements based on condition in config object', () => { + const children = [ + , + , +
Hello World
, + ] + + const {result} = renderHook(() => + useSlots(children, { + a: [TestComponentA, (props: TestComponentAProps) => props.variant === 'a'], + b: [TestComponentA, (props: TestComponentAProps) => props.variant === 'b'], + }), + ) + expect(result.current).toMatchInlineSnapshot(` + [ + { + "a": , + "b": , + }, + [ +
+ Hello World +
, + ], + ] + `) +}) diff --git a/src/hooks/useSlots.ts b/src/hooks/useSlots.ts index 8b432cc6b30..29aaed481af 100644 --- a/src/hooks/useSlots.ts +++ b/src/hooks/useSlots.ts @@ -1,29 +1,48 @@ import React from 'react' import {warning} from '../utils/warning' +// slot config allows 2 options: +// 1. Component to match, example: { leadingVisual: LeadingVisual } +type ComponentMatcher = React.ElementType +// 2. Component to match + a test function, example: { blockDescription: [Description, props => props.variant === 'block'] } +type ComponentAndPropsMatcher = [ComponentMatcher, (props: Props) => boolean] + +export type SlotConfig = Record + +// We don't know what the props are yet, we set them later based on slot config // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type SlotConfig = Record> +type Props = any -type SlotElements = { - [Property in keyof Type]: React.ReactElement, Type[Property]> +type SlotElements = { + [Property in keyof Config]: SlotValue } +type SlotValue = Config[Property] extends React.ElementType // config option 1 + ? React.ReactElement, Config[Property]> + : Config[Property] extends readonly [ + infer ElementType extends React.ElementType, // config option 2, infer array[0] as component + // eslint-disable-next-line @typescript-eslint/no-unused-vars + infer _testFn, // even though we don't use testFn, we need to infer it to support types for slots.*.props + ] + ? React.ReactElement, ElementType> + : never // useful for narrowing types, third option is not possible + /** * Extract components from `children` so we can render them in different places, * allowing us to implement components with SSR-compatible slot APIs. * Note: We can only extract direct children, not nested ones. */ -export function useSlots( +export function useSlots( children: React.ReactNode, - config: T, -): [Partial>, React.ReactNode[]] { + config: Config, +): [Partial>, React.ReactNode[]] { // Object mapping slot names to their elements - const slots: Partial> = mapValues(config, () => undefined) + const slots: Partial> = mapValues(config, () => undefined) // Array of elements that are not slots const rest: React.ReactNode[] = [] - const keys = Object.keys(config) as Array + const keys = Object.keys(config) as Array const values = Object.values(config) // eslint-disable-next-line github/array-foreach @@ -34,7 +53,12 @@ export function useSlots( } const index = values.findIndex(value => { - return child.type === value + if (Array.isArray(value)) { + const [component, testFn] = value + return child.type === component && testFn(child.props) + } else { + return child.type === value + } }) // If the child is not a slot, add it to the `rest` array @@ -52,7 +76,8 @@ export function useSlots( } // If the child is a slot, add it to the `slots` object - slots[slotKey] = child as React.ReactElement, T[keyof T]> + + slots[slotKey] = child as SlotValue }) return [slots, rest]