diff --git a/.lastsync b/.lastsync deleted file mode 100644 index 94f729137..000000000 --- a/.lastsync +++ /dev/null @@ -1 +0,0 @@ -9014da9bbe05bd45f38841c02e34dcb5a8e3e1d8 diff --git a/package.json b/package.json index ad915344d..b13811f9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@openstax/ui-components", - "version": "1.23.1", + "version": "1.23.2", "license": "MIT", "sideEffects": [ "**/*.css" diff --git a/src/components/Text.spec.tsx b/src/components/Text.spec.tsx index c28cbb009..7189bfcff 100644 --- a/src/components/Text.spec.tsx +++ b/src/components/Text.spec.tsx @@ -1,5 +1,7 @@ -import { Paragraph, H2, H3 } from './Text'; +import { Paragraph, H2, H3, Heading } from './Text'; import renderer from 'react-test-renderer'; +import { render } from '@testing-library/react'; +import React from 'react'; describe('Text', () => { it('matches body snapshot', () => { @@ -24,3 +26,41 @@ describe('Text', () => { }); }); + +describe('Heading', () => { + it('defaults variant to match the semantic level', () => { + const tree = renderer.create( + This is a heading + ).toJSON(); + expect(tree).toMatchObject({ type: 'h2', props: { className: 'text-h2' } }); + }); + + it('renders the element for the given level', () => { + const tree = renderer.create( + This is a heading + ).toJSON(); + expect((tree as unknown as { type: string }).type).toBe('h4'); + }); + + it('decouples visual style from semantic level', () => { + // an

element styled like an h3 + const tree = renderer.create( + This is a heading + ).toJSON(); + expect(tree).toMatchObject({ type: 'h2', props: { className: 'text-h3' } }); + }); + + it('merges a consumer className with the variant class', () => { + const tree = renderer.create( + This is a heading + ).toJSON(); + expect((tree as unknown as { props: { className: string } }).props.className).toBe('text-h2 custom'); + }); + + it('forwards a ref to the underlying element', () => { + const ref = React.createRef(); + render(This is a heading); + expect(ref.current).not.toBeNull(); + expect(ref.current?.tagName).toBe('H2'); + }); +}); diff --git a/src/components/Text.stories.tsx b/src/components/Text.stories.tsx index c83372b30..bab6aad54 100644 --- a/src/components/Text.stories.tsx +++ b/src/components/Text.stories.tsx @@ -1,4 +1,4 @@ -import { H2, H3, Paragraph } from "./Text"; +import { H2, H3, Heading as HeadingComponent, Paragraph } from "./Text"; export const PText = () => ( <> @@ -12,3 +12,11 @@ export const Heading = () => (

H3 Heading text

); + +// Semantic level and visual style are independent: this renders an

+// element (correct document outline) styled like an h3. +export const HeadingStyleOverride = () => ( + + h2 element with h3 style + +); diff --git a/src/components/Text.tsx b/src/components/Text.tsx index 08b815e8d..7381aa8d1 100644 --- a/src/components/Text.tsx +++ b/src/components/Text.tsx @@ -2,18 +2,46 @@ import React from 'react'; import classNames from 'classnames'; import './Text.css'; +export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; + +/** + * Visual style applied to a heading, decoupled from its semantic level. + * Maps to the `text-*` classes in Text.css. + */ +export type HeadingVariant = 'h2' | 'h3'; + +export interface HeadingProps extends React.ComponentPropsWithoutRef<'h2'> { + /** Semantic heading level — controls which `h1`–`h6` element is rendered. */ + level: HeadingLevel; + /** Visual style to apply. Defaults to match `level` (e.g. `level={2}` → `h2`). */ + variant?: HeadingVariant; +} + +/** + * Renders a heading whose semantic level (the rendered `h1`–`h6` tag) is + * independent of its visual style (`variant`). This lets consumers preserve a + * correct document outline while reusing an existing heading style — e.g. + * `` is an `

` styled like an h3. + */ +export const Heading = React.forwardRef( + ({ level, variant, className, ...props }, ref) => { + const Tag = `h${level}` as const; + // Default the visual style to match the semantic level. + const resolvedVariant = variant ?? (`h${level}` as HeadingVariant); + return ; + } +); + +Heading.displayName = 'Heading'; + export const H2 = React.forwardRef>( - ({ className, ...props }, ref) => ( -

- ) + (props, ref) => ); H2.displayName = 'H2'; export const H3 = React.forwardRef>( - ({ className, ...props }, ref) => ( -

- ) + (props, ref) => ); H3.displayName = 'H3';