Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .lastsync

This file was deleted.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openstax/ui-components",
"version": "1.23.1",
"version": "1.23.2",
"license": "MIT",
"sideEffects": [
"**/*.css"
Expand Down
42 changes: 41 additions & 1 deletion src/components/Text.spec.tsx
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -24,3 +26,41 @@ describe('Text', () => {
});

});

describe('Heading', () => {
it('defaults variant to match the semantic level', () => {
const tree = renderer.create(
<Heading level={2}>This is a heading</Heading>
).toJSON();
expect(tree).toMatchObject({ type: 'h2', props: { className: 'text-h2' } });
});

it('renders the element for the given level', () => {
const tree = renderer.create(
<Heading level={4}>This is a heading</Heading>
).toJSON();
expect((tree as unknown as { type: string }).type).toBe('h4');
});

it('decouples visual style from semantic level', () => {
// an <h2> element styled like an h3
const tree = renderer.create(
<Heading level={2} variant="h3">This is a heading</Heading>
).toJSON();
expect(tree).toMatchObject({ type: 'h2', props: { className: 'text-h3' } });
});

it('merges a consumer className with the variant class', () => {
const tree = renderer.create(
<Heading level={2} className="custom">This is a heading</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<HTMLHeadingElement>();
render(<Heading level={2} ref={ref}>This is a heading</Heading>);
expect(ref.current).not.toBeNull();
expect(ref.current?.tagName).toBe('H2');
});
});
10 changes: 9 additions & 1 deletion src/components/Text.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { H2, H3, Paragraph } from "./Text";
import { H2, H3, Heading as HeadingComponent, Paragraph } from "./Text";

export const PText = () => (
<>
Expand All @@ -12,3 +12,11 @@ export const Heading = () => (
<H3>H3 Heading text</H3>
</>
);

// Semantic level and visual style are independent: this renders an <h2>
// element (correct document outline) styled like an h3.
export const HeadingStyleOverride = () => (
<HeadingComponent level={2} variant="h3">
h2 element with h3 style
</HeadingComponent>
);
40 changes: 34 additions & 6 deletions src/components/Text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* `<Heading level={2} variant="h3">` is an `<h2>` styled like an h3.
*/
export const Heading = React.forwardRef<HTMLHeadingElement, HeadingProps>(
({ 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 <Tag ref={ref} className={classNames(`text-${resolvedVariant}`, className)} {...props} />;
Comment thread
RoyEJohnson marked this conversation as resolved.
}
);
Comment thread
RoyEJohnson marked this conversation as resolved.

Heading.displayName = 'Heading';

export const H2 = React.forwardRef<HTMLHeadingElement, React.ComponentPropsWithoutRef<'h2'>>(
({ className, ...props }, ref) => (
<h2 ref={ref} className={classNames('text-h2', className)} {...props} />
)
(props, ref) => <Heading level={2} ref={ref} {...props} />
Comment thread
RoyEJohnson marked this conversation as resolved.
);

H2.displayName = 'H2';

export const H3 = React.forwardRef<HTMLHeadingElement, React.ComponentPropsWithoutRef<'h3'>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={classNames('text-h3', className)} {...props} />
)
(props, ref) => <Heading level={3} ref={ref} {...props} />
Comment thread
RoyEJohnson marked this conversation as resolved.
);

H3.displayName = 'H3';
Expand Down
Loading