From fb373444c024f81664cb55e77180ffdf518fba0f Mon Sep 17 00:00:00 2001 From: gitdallas <5322142+gitdallas@users.noreply.github.com> Date: Fri, 3 Oct 2025 13:28:17 -0500 Subject: [PATCH 1/8] fix(ExpandableSection): allow toggles to have proper dom structured headings Signed-off-by: gitdallas <5322142+gitdallas@users.noreply.github.com> --- .../ExpandableSection/ExpandableSection.tsx | 11 ++- .../ExpandableSectionToggle.tsx | 86 ++++++++++-------- .../__tests__/ExpandableSection.test.tsx | 33 +++++++ .../ExpandableSectionToggle.test.tsx | 32 +++++++ .../examples/ExpandableSection.md | 8 ++ .../ExpandableSectionCustomToggle.tsx | 47 ++++++---- .../examples/ExpandableSectionWithHeading.tsx | 89 +++++++++++++++++++ 7 files changed, 250 insertions(+), 56 deletions(-) create mode 100644 packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx diff --git a/packages/react-core/src/components/ExpandableSection/ExpandableSection.tsx b/packages/react-core/src/components/ExpandableSection/ExpandableSection.tsx index 8b5cd727118..e6331b70b35 100644 --- a/packages/react-core/src/components/ExpandableSection/ExpandableSection.tsx +++ b/packages/react-core/src/components/ExpandableSection/ExpandableSection.tsx @@ -74,6 +74,11 @@ export interface ExpandableSectionProps extends Omit + - + ); return ( diff --git a/packages/react-core/src/components/ExpandableSection/ExpandableSectionToggle.tsx b/packages/react-core/src/components/ExpandableSection/ExpandableSectionToggle.tsx index e44ba385eda..2d3edeba4d6 100644 --- a/packages/react-core/src/components/ExpandableSection/ExpandableSectionToggle.tsx +++ b/packages/react-core/src/components/ExpandableSection/ExpandableSectionToggle.tsx @@ -34,6 +34,11 @@ export interface ExpandableSectionToggleProps extends Omit = ({ @@ -48,45 +53,50 @@ export const ExpandableSectionToggle: React.FunctionComponent ( -
-
- +}: ExpandableSectionToggleProps) => { + const ToggleWrapper = toggleWrapper as any; + + return ( +
+ + +
-
-); + ); +}; ExpandableSectionToggle.displayName = 'ExpandableSectionToggle'; diff --git a/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSection.test.tsx b/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSection.test.tsx index 64a5c9ce36a..38bd9f8ae35 100644 --- a/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSection.test.tsx +++ b/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSection.test.tsx @@ -208,6 +208,7 @@ test('Renders with aria-labelledby when toggleAriaLabelledBy is passed', () => { expect(screen.getByRole('button')).toHaveAccessibleName('Test label'); }); + test('Renders toggleContent as a function in uncontrolled mode (collapsed)', () => { render( (isExpanded ? 'Hide details' : 'Show details')}> @@ -242,3 +243,35 @@ test('Renders toggleContent as a function in controlled mode', () => { expect(screen.getByRole('button', { name: 'Collapse' })).toBeInTheDocument(); }); + +test('Renders with default div wrapper when toggleWrapper is not specified', () => { + render(Test content); + + const wrapper = screen.getByTestId('test-id'); + const toggle = wrapper.querySelector('.pf-v6-c-expandable-section__toggle'); + expect(toggle?.tagName).toBe('DIV'); +}); + +test('Renders with h2 wrapper when toggleWrapper="h2"', () => { + render( + + Test content + + ); + + const wrapper = screen.getByTestId('test-id'); + const toggle = wrapper.querySelector('.pf-v6-c-expandable-section__toggle'); + expect(toggle?.tagName).toBe('H2'); +}); + +test('Renders with div wrapper when toggleWrapper="div"', () => { + render( + + Test content + + ); + + const wrapper = screen.getByTestId('test-id'); + const toggle = wrapper.querySelector('.pf-v6-c-expandable-section__toggle'); + expect(toggle?.tagName).toBe('DIV'); +}); diff --git a/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSectionToggle.test.tsx b/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSectionToggle.test.tsx index 332b3149050..6c996c1660c 100644 --- a/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSectionToggle.test.tsx +++ b/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSectionToggle.test.tsx @@ -47,3 +47,35 @@ test('Renders with aria-labelledby when toggleAriaLabelledBy is passed', () => { expect(screen.getByRole('button')).toHaveAccessibleName('Test label'); }); + +test('Renders with default div wrapper when toggleWrapper is not specified', () => { + render(Toggle test); + + const wrapper = screen.getByTestId('test-id'); + const toggle = wrapper.querySelector('.pf-v6-c-expandable-section__toggle'); + expect(toggle?.tagName).toBe('DIV'); +}); + +test('Renders with h2 wrapper when toggleWrapper="h2"', () => { + render( + + Toggle test + + ); + + const wrapper = screen.getByTestId('test-id'); + const toggle = wrapper.querySelector('.pf-v6-c-expandable-section__toggle'); + expect(toggle?.tagName).toBe('H2'); +}); + +test('Renders with div wrapper when toggleWrapper="div"', () => { + render( + + Toggle test + + ); + + const wrapper = screen.getByTestId('test-id'); + const toggle = wrapper.querySelector('.pf-v6-c-expandable-section__toggle'); + expect(toggle?.tagName).toBe('DIV'); +}); diff --git a/packages/react-core/src/components/ExpandableSection/examples/ExpandableSection.md b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSection.md index a2d3eb06544..647a8998c75 100644 --- a/packages/react-core/src/components/ExpandableSection/examples/ExpandableSection.md +++ b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSection.md @@ -70,6 +70,14 @@ By using the `toggleContent` prop, you can pass in content other than a simple s ``` +### With heading semantics + +When the toggle text should function as a heading in the document structure, use the `toggleWrapper` prop to specify a heading element (h1-h6). This ensures proper semantic structure for screen readers and other assistive technologies. The component automatically uses a native button element when heading wrappers are used, allowing the heading styles to display properly. + +```ts file="ExpandableSectionWithHeading.tsx" + +``` + ### Truncate expansion By passing in `variant="truncate"`, the expandable content will be visible up to a maximum number of lines before being truncated, with the toggle revealing or hiding the truncated content. By default the expandable content will truncate after 3 lines, and this can be customized by also passing in the `truncateMaxLines` prop. diff --git a/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionCustomToggle.tsx b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionCustomToggle.tsx index 3f9048722da..8de69f13678 100644 --- a/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionCustomToggle.tsx +++ b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionCustomToggle.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { ExpandableSection, Badge } from '@patternfly/react-core'; +import { ExpandableSection, Badge, Stack, StackItem } from '@patternfly/react-core'; import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon'; export const ExpandableSectionCustomToggle: React.FunctionComponent = () => { @@ -10,20 +10,35 @@ export const ExpandableSectionCustomToggle: React.FunctionComponent = () => { }; return ( - - You can also use icons - - or badges - 4 - ! -
- } - onToggle={onToggle} - isExpanded={isExpanded} - > - This content is visible only when the component is expanded. - + + +

Custom Toggle Content

+

You can use custom content such as icons and badges in the toggle:

+ + You can also use icons + + or badges + 4 + ! + + } + onToggle={onToggle} + isExpanded={isExpanded} + > + This content is visible only when the component is expanded. + +
+ + +

Accessibility Note

+

+ Important: If you need the toggle text to function as a heading in the document structure, do + NOT put heading elements (h1-h6) inside the toggleContent prop, as this creates invalid HTML + structure. Instead, use the toggleWrapper prop. +

+
+
); }; diff --git a/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx new file mode 100644 index 00000000000..8b0dc0439b7 --- /dev/null +++ b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx @@ -0,0 +1,89 @@ +import { useState, MouseEvent } from 'react'; +import { ExpandableSection, ExpandableSectionToggle, Stack, StackItem } from '@patternfly/react-core'; +import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon'; + +export const ExpandableSectionWithHeading = () => { + const [isExpanded1, setIsExpanded1] = useState(false); + const [isExpanded2, setIsExpanded2] = useState(false); + const [isExpandedDetached, setIsExpandedDetached] = useState(false); + + const onToggle1 = (_event: MouseEvent, isExpanded: boolean) => { + setIsExpanded1(isExpanded); + }; + + const onToggle2 = (_event: MouseEvent, isExpanded: boolean) => { + setIsExpanded2(isExpanded); + }; + + const onToggleDetached = (isExpanded: boolean) => { + setIsExpandedDetached(isExpanded); + }; + + return ( + + +

Document with Expandable Sections

+

This demonstrates how to use expandable sections with proper heading semantics.

+ + {/* Using toggleWrapper prop for proper heading semantics */} + +

+ This content is visible only when the component is expanded. The toggle text above functions as a proper + heading in the document structure, which is important for screen readers and other assistive technologies. +

+

+ When using the toggleWrapper prop with heading elements (h1-h6), the button is rendered inside + the heading element, maintaining proper semantic structure. +

+
+
+ + +

Detached Variant with Heading

+

You can also use the detached variant with heading semantics:

+ + + Detached Toggle with Heading + + + +

This is detached content that can be positioned anywhere in the DOM.

+
+
+ + +

Custom Content with Heading

+

You can also use custom content within heading wrappers:

+ + + Custom Content with Icon + + } + onToggle={onToggle2} + isExpanded={isExpanded2} + > +

This expandable section uses custom content with an icon inside a heading wrapper.

+
+
+
+ ); +}; From 371c8024a8b4e9064f1e2b3db49faaebaf33fc12 Mon Sep 17 00:00:00 2001 From: gitdallas <5322142+gitdallas@users.noreply.github.com> Date: Thu, 9 Oct 2025 08:09:11 -0500 Subject: [PATCH 2/8] pr review fixes Signed-off-by: gitdallas <5322142+gitdallas@users.noreply.github.com> --- .../__tests__/ExpandableSection.test.tsx | 10 +++------- .../__tests__/ExpandableSectionToggle.test.tsx | 10 +++------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSection.test.tsx b/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSection.test.tsx index 38bd9f8ae35..651214938f5 100644 --- a/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSection.test.tsx +++ b/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSection.test.tsx @@ -247,8 +247,7 @@ test('Renders toggleContent as a function in controlled mode', () => { test('Renders with default div wrapper when toggleWrapper is not specified', () => { render(Test content); - const wrapper = screen.getByTestId('test-id'); - const toggle = wrapper.querySelector('.pf-v6-c-expandable-section__toggle'); + const toggle = screen.getByRole('button').parentElement; expect(toggle?.tagName).toBe('DIV'); }); @@ -259,9 +258,7 @@ test('Renders with h2 wrapper when toggleWrapper="h2"', () => { ); - const wrapper = screen.getByTestId('test-id'); - const toggle = wrapper.querySelector('.pf-v6-c-expandable-section__toggle'); - expect(toggle?.tagName).toBe('H2'); + expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument(); }); test('Renders with div wrapper when toggleWrapper="div"', () => { @@ -271,7 +268,6 @@ test('Renders with div wrapper when toggleWrapper="div"', () => { ); - const wrapper = screen.getByTestId('test-id'); - const toggle = wrapper.querySelector('.pf-v6-c-expandable-section__toggle'); + const toggle = screen.getByRole('button').parentElement; expect(toggle?.tagName).toBe('DIV'); }); diff --git a/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSectionToggle.test.tsx b/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSectionToggle.test.tsx index 6c996c1660c..9757132e6fa 100644 --- a/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSectionToggle.test.tsx +++ b/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSectionToggle.test.tsx @@ -51,8 +51,7 @@ test('Renders with aria-labelledby when toggleAriaLabelledBy is passed', () => { test('Renders with default div wrapper when toggleWrapper is not specified', () => { render(Toggle test); - const wrapper = screen.getByTestId('test-id'); - const toggle = wrapper.querySelector('.pf-v6-c-expandable-section__toggle'); + const toggle = screen.getByRole('button').parentElement; expect(toggle?.tagName).toBe('DIV'); }); @@ -63,9 +62,7 @@ test('Renders with h2 wrapper when toggleWrapper="h2"', () => { ); - const wrapper = screen.getByTestId('test-id'); - const toggle = wrapper.querySelector('.pf-v6-c-expandable-section__toggle'); - expect(toggle?.tagName).toBe('H2'); + expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument(); }); test('Renders with div wrapper when toggleWrapper="div"', () => { @@ -75,7 +72,6 @@ test('Renders with div wrapper when toggleWrapper="div"', () => { ); - const wrapper = screen.getByTestId('test-id'); - const toggle = wrapper.querySelector('.pf-v6-c-expandable-section__toggle'); + const toggle = screen.getByRole('button').parentElement; expect(toggle?.tagName).toBe('DIV'); }); From 0f718ea9b0c98302dd18b565a80563c6a4fbcfd8 Mon Sep 17 00:00:00 2001 From: Dallas <5322142+gitdallas@users.noreply.github.com> Date: Wed, 15 Oct 2025 09:52:11 -0500 Subject: [PATCH 3/8] Update packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx Co-authored-by: Eric Olkowski <70952936+thatblindgeye@users.noreply.github.com> --- .../examples/ExpandableSectionWithHeading.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx index 8b0dc0439b7..02843212d18 100644 --- a/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx +++ b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx @@ -72,10 +72,10 @@ export const ExpandableSectionWithHeading = () => {

You can also use custom content within heading wrappers:

- Custom Content with Icon + Custom Heading Content with Icon } onToggle={onToggle2} From 1ce94d124e230e165cf13a7a9e0e89ee78e8f68d Mon Sep 17 00:00:00 2001 From: Dallas <5322142+gitdallas@users.noreply.github.com> Date: Wed, 15 Oct 2025 09:52:18 -0500 Subject: [PATCH 4/8] Update packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx Co-authored-by: Eric Olkowski <70952936+thatblindgeye@users.noreply.github.com> --- .../ExpandableSection/examples/ExpandableSectionWithHeading.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx index 02843212d18..6cdb0d0f134 100644 --- a/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx +++ b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx @@ -68,7 +68,7 @@ export const ExpandableSectionWithHeading = () => { -

Custom Content with Heading

+

Custom Content with Heading

You can also use custom content within heading wrappers:

Date: Wed, 15 Oct 2025 09:52:25 -0500 Subject: [PATCH 5/8] Update packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx Co-authored-by: Eric Olkowski <70952936+thatblindgeye@users.noreply.github.com> --- .../ExpandableSection/examples/ExpandableSectionWithHeading.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx index 6cdb0d0f134..16af97bb491 100644 --- a/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx +++ b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx @@ -48,7 +48,7 @@ export const ExpandableSectionWithHeading = () => {

You can also use the detached variant with heading semantics:

Date: Wed, 15 Oct 2025 09:52:31 -0500 Subject: [PATCH 6/8] Update packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx Co-authored-by: Eric Olkowski <70952936+thatblindgeye@users.noreply.github.com> --- .../ExpandableSection/examples/ExpandableSectionWithHeading.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx index 16af97bb491..2682f852992 100644 --- a/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx +++ b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx @@ -44,7 +44,7 @@ export const ExpandableSectionWithHeading = () => {
-

Detached Variant with Heading

+

Detached Variant with Heading

You can also use the detached variant with heading semantics:

Date: Wed, 15 Oct 2025 09:52:38 -0500 Subject: [PATCH 7/8] Update packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx Co-authored-by: Eric Olkowski <70952936+thatblindgeye@users.noreply.github.com> --- .../examples/ExpandableSectionWithHeading.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx index 2682f852992..b9401b3eee1 100644 --- a/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx +++ b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx @@ -27,8 +27,8 @@ export const ExpandableSectionWithHeading = () => { {/* Using toggleWrapper prop for proper heading semantics */} From 8680deee4fdec5f6429803f4dbd7097b852f2626 Mon Sep 17 00:00:00 2001 From: Dallas <5322142+gitdallas@users.noreply.github.com> Date: Wed, 15 Oct 2025 09:52:44 -0500 Subject: [PATCH 8/8] Update packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx Co-authored-by: Eric Olkowski <70952936+thatblindgeye@users.noreply.github.com> --- .../ExpandableSection/examples/ExpandableSectionWithHeading.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx index b9401b3eee1..9f72c4e851a 100644 --- a/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx +++ b/packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionWithHeading.tsx @@ -22,7 +22,7 @@ export const ExpandableSectionWithHeading = () => { return ( -

Document with Expandable Sections

+

Document with Expandable Sections

This demonstrates how to use expandable sections with proper heading semantics.

{/* Using toggleWrapper prop for proper heading semantics */}