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..651214938f5 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,31 @@ 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 toggle = screen.getByRole('button').parentElement; + expect(toggle?.tagName).toBe('DIV'); +}); + +test('Renders with h2 wrapper when toggleWrapper="h2"', () => { + render( + + Test content + + ); + + expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument(); +}); + +test('Renders with div wrapper when toggleWrapper="div"', () => { + render( + + Test content + + ); + + 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 332b3149050..9757132e6fa 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,31 @@ 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 toggle = screen.getByRole('button').parentElement; + expect(toggle?.tagName).toBe('DIV'); +}); + +test('Renders with h2 wrapper when toggleWrapper="h2"', () => { + render( + + Toggle test + + ); + + expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument(); +}); + +test('Renders with div wrapper when toggleWrapper="div"', () => { + render( + + Toggle test + + ); + + const toggle = screen.getByRole('button').parentElement; + 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..9f72c4e851a --- /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 Heading Content with Icon + + } + onToggle={onToggle2} + isExpanded={isExpanded2} + > +

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

+
+
+
+ ); +};