diff --git a/.changeset/blue-eggs-suffer.md b/.changeset/blue-eggs-suffer.md new file mode 100644 index 00000000000..7f2aba3a444 --- /dev/null +++ b/.changeset/blue-eggs-suffer.md @@ -0,0 +1,5 @@ +--- +"@primer/react": minor +--- + +feat(LabelGroup): render as list by default diff --git a/packages/react/src/LabelGroup/LabelGroup.docs.json b/packages/react/src/LabelGroup/LabelGroup.docs.json index 34fafdde94f..51e64eef176 100644 --- a/packages/react/src/LabelGroup/LabelGroup.docs.json +++ b/packages/react/src/LabelGroup/LabelGroup.docs.json @@ -17,6 +17,12 @@ "description": "How many tokens to show. `'auto'` truncates the tokens to fit in the parent container. Passing a number will truncate after that number tokens. If this is undefined, tokens will never be truncated.", "defaultValue": "", "type": "'auto' | number" + }, + { + "name": "as", + "description": "Customize the element type of the rendered container.", + "defaultValue": "ul", + "type": "React.ElementType" } ], "subcomponents": [] diff --git a/packages/react/src/LabelGroup/LabelGroup.stories.tsx b/packages/react/src/LabelGroup/LabelGroup.stories.tsx index 639446335d2..d123757c6ed 100644 --- a/packages/react/src/LabelGroup/LabelGroup.stories.tsx +++ b/packages/react/src/LabelGroup/LabelGroup.stories.tsx @@ -69,6 +69,10 @@ export const Playground: StoryFn = ({ ) } +Playground.args = { + as: 'ul', +} + Playground.argTypes = { overflowStyle: { control: { diff --git a/packages/react/src/LabelGroup/LabelGroup.tsx b/packages/react/src/LabelGroup/LabelGroup.tsx index e39d389ddd1..5aaf6f8edd8 100644 --- a/packages/react/src/LabelGroup/LabelGroup.tsx +++ b/packages/react/src/LabelGroup/LabelGroup.tsx @@ -12,6 +12,8 @@ import type {SxProp} from '../sx' import sx from '../sx' export type LabelGroupProps = { + /** Customize the element type of the rendered container */ + as?: React.ElementType /** How hidden tokens should be shown. `'inline'` shows the hidden tokens after the visible tokens. `'overlay'` shows all tokens in an overlay that appears on top of the visible tokens. */ overflowStyle?: 'inline' | 'overlay' /** How many tokens to show. `'auto'` truncates the tokens to fit in the parent container. Passing a number will truncate after that number tokens. If this is undefined, tokens will never be truncated. */ @@ -30,6 +32,13 @@ const StyledLabelGroupContainer = styled.div` flex-wrap: wrap; } + &[data-list] { + padding-inline-start: 0; + margin-block-start: 0; + margin-block-end: 0; + list-style-type: none; + } + ${sx}; ` @@ -54,7 +63,7 @@ const ItemWrapper = styled.div` // Calculates the width of the overlay to cover the labels/tokens and the expand button. const getOverlayWidth = ( buttonClientRect: DOMRect, - containerRef: React.RefObject, + containerRef: React.RefObject, overlayPaddingPx: number, ) => overlayPaddingPx + buttonClientRect.right - (containerRef.current?.getBoundingClientRect().left || 0) @@ -148,8 +157,9 @@ const LabelGroup: React.FC> = ({ visibleChildCount, overflowStyle = 'overlay', sx: sxProp, + as = 'ul', }) => { - const containerRef = React.useRef(null) + const containerRef = React.useRef(null) const collapseButtonRef = React.useRef(null) const firstHiddenIndexRef = React.useRef(undefined) const [visibilityMap, setVisibilityMap] = React.useState>({}) @@ -317,50 +327,63 @@ const LabelGroup: React.FC> = ({ } }, [overflowStyle, isOverflowShown]) + const isList = as === 'ul' || as === 'ol' + const ToggleWrapper = isList ? 'li' : React.Fragment + // If truncation is enabled, we need to render based on truncation logic. return visibleChildCount ? ( {React.Children.map(children, (child, index) => ( {child} ))} - {overflowStyle === 'inline' ? ( - - ) : ( - - {children} - - )} + + {overflowStyle === 'inline' ? ( + + ) : ( + + {children} + + )} + ) : ( - - {children} + + {isList + ? React.Children.map(children, (child, index) => { + return
  • {child}
  • + }) + : children}
    ) } diff --git a/packages/react/src/__tests__/LabelGroup.test.tsx b/packages/react/src/__tests__/LabelGroup.test.tsx index b2e8f6192f5..77a89f65581 100644 --- a/packages/react/src/__tests__/LabelGroup.test.tsx +++ b/packages/react/src/__tests__/LabelGroup.test.tsx @@ -177,4 +177,127 @@ describe('LabelGroup', () => { expect(document.activeElement).toEqual(getByText('+2').closest('button')) }) + + describe('should render as ul by default', () => { + it('without truncation', () => { + const {getByRole} = HTMLRender( + + + + + + + + + , + ) + const list = getByRole('list') + expect(list).not.toBeNull() + expect(list.tagName).toBe('UL') + expect(list).toHaveAttribute('data-list', 'true') + expect(list.querySelectorAll('li')).toHaveLength(5) + }) + + it('with truncation', () => { + const {getByRole} = HTMLRender( + + + + + + + + + , + ) + const list = getByRole('list') + expect(list).not.toBeNull() + expect(list.tagName).toBe('UL') + expect(list).toHaveAttribute('data-list', 'true') + // account for "show more" button + expect(list.querySelectorAll('li')).toHaveLength(6) + }) + }) + + describe('should render as custom element when `as` is provided', () => { + it('without truncation', () => { + const {queryByRole, container} = HTMLRender( + + + + + + + + + , + ) + const list = queryByRole('list') + expect(list).toBeNull() + const labelGroupDiv = container.querySelectorAll('div')[1] + expect(labelGroupDiv.querySelectorAll('li')).toHaveLength(0) + expect(labelGroupDiv.querySelectorAll('span')).toHaveLength(5) + expect(labelGroupDiv).not.toHaveAttribute('data-list') + }) + + it('with truncation', () => { + const {queryByRole, container} = HTMLRender( + + + + + + + + + , + ) + const list = queryByRole('list') + expect(list).toBeNull() + const labelGroupDiv = container.querySelectorAll('div')[1] + expect(labelGroupDiv.querySelectorAll('li')).toHaveLength(0) + expect(labelGroupDiv.querySelectorAll(':scope > span')).toHaveLength(5) + expect(labelGroupDiv).not.toHaveAttribute('data-list') + }) + }) + + describe('should render children as list items when rendered as ol', () => { + it('without truncation', () => { + const {getByRole} = HTMLRender( + + + + + + + + + , + ) + const list = getByRole('list') + expect(list).not.toBeNull() + expect(list.tagName).toBe('OL') + expect(list).toHaveAttribute('data-list', 'true') + expect(list.querySelectorAll('li')).toHaveLength(5) + }) + it('with truncation', () => { + const {getByRole} = HTMLRender( + + + + + + + + + , + ) + const list = getByRole('list') + expect(list).not.toBeNull() + expect(list.tagName).toBe('OL') + expect(list).toHaveAttribute('data-list', 'true') + // account for "show more" button + expect(list.querySelectorAll('li')).toHaveLength(6) + }) + }) })