Skip to content
Open
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
4 changes: 2 additions & 2 deletions apps/www/src/components/playground/headline-examples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ export function HeadlineExamples() {
<PlaygroundLayout title='Headline'>
<Flex direction='column' gap='large'>
<Flex direction='column' gap='large'>
<Headline size='large' as='h1'>
<Headline size='large' render={<h1 />}>
Large Headline
</Headline>

<Headline size='medium'>Medium Headline</Headline>

<Headline size='small' as='h3'>
<Headline size='small' render={<h3 />}>
Small Headline
</Headline>
</Flex>
Expand Down
6 changes: 3 additions & 3 deletions apps/www/src/content/docs/components/headline/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ export const playground = {
options: ['regular', 'medium'],
defaultValue: 'medium'
},
as: {
render: {
type: 'select',
options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
defaultValue: 'h2'
options: ['<h1 />', '<h2 />', '<h3 />', '<h4 />', '<h5 />', '<h6 />'],
defaultValue: '<h2 />'
},
align: {
type: 'select',
Expand Down
2 changes: 1 addition & 1 deletion apps/www/src/content/docs/components/headline/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { Headline } from '@raystack/apsara'

## API Reference

Renders a heading element with configurable size and weight.
Renders a heading element with configurable size and weight. Use the `render` prop to change the heading level or provide a custom render function.

<auto-type-table path="./props.ts" name="HeadlineProps" />

Expand Down
7 changes: 5 additions & 2 deletions apps/www/src/content/docs/components/headline/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ export interface HeadlineProps {
weight?: 'regular' | 'medium';

/**
* HTML heading element to render.
* Custom render element or function. Accepts a JSX element (e.g. `<h1 />`)
* or a render function for full control.
* @default "h2"
*/
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
render?:
| React.ReactElement
| ((props: React.ComponentPropsWithRef<'h2'>) => React.ReactElement);

/**
* Text alignment.
Expand Down
6 changes: 3 additions & 3 deletions apps/www/src/content/docs/components/text/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export const playground = {
],
defaultValue: 'primary'
},
as: {
render: {
type: 'select',
options: ['span', 'p', 'div', 'label', 'a'],
defaultValue: 'span'
options: ['<span />', '<p />', '<div />', '<label />', '<a />'],
defaultValue: '<span />'
},
size: {
type: 'select',
Expand Down
2 changes: 1 addition & 1 deletion apps/www/src/content/docs/components/text/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { Text } from '@raystack/apsara'

## API Reference

According to the element rendered using `as`, Text will extend over the default HTML Attributes
Use the `render` prop to change the rendered element or provide a custom render function.

<auto-type-table path="./props.ts" name="TextProps" />

Expand Down
7 changes: 5 additions & 2 deletions apps/www/src/content/docs/components/text/props.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
export interface TextProps {
/**
* Text element to render as.
* Custom render element or function. Accepts a JSX element (e.g. `<p />`)
* or a render function for full control.
* @default "span"
*/
as?: 'span' | 'p' | 'div' | 'label' | 'a';
render?:
| React.ReactElement
| ((props: React.ComponentPropsWithRef<'span'>) => React.ReactElement);

/**
* The visual style variant.
Expand Down
38 changes: 33 additions & 5 deletions packages/raystack/components/headline/__tests__/headline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,48 @@ describe('Headline', () => {
});
});

describe('As Prop', () => {
const headingLevels = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const;
describe('Render Prop', () => {
it('renders as h1 via render prop', () => {
render(<Headline render={<h1 />}>Heading</Headline>);
const heading = screen.getByText('Heading');
expect(heading.tagName).toBe('H1');
});

it('renders as h3 via render prop', () => {
render(<Headline render={<h3 />}>Heading</Headline>);
const heading = screen.getByText('Heading');
expect(heading.tagName).toBe('H3');
});

it('renders as h4 via render prop', () => {
render(<Headline render={<h4 />}>Heading</Headline>);
const heading = screen.getByText('Heading');
expect(heading.tagName).toBe('H4');
});

it('renders as h5 via render prop', () => {
render(<Headline render={<h5 />}>Heading</Headline>);
const heading = screen.getByText('Heading');
expect(heading.tagName).toBe('H5');
});

it.each(headingLevels)('renders as %s element', level => {
render(<Headline as={level}>Heading</Headline>);
it('renders as h6 via render prop', () => {
render(<Headline render={<h6 />}>Heading</Headline>);
const heading = screen.getByText('Heading');
expect(heading.tagName).toBe(level.toUpperCase());
expect(heading.tagName).toBe('H6');
});

it('renders as h2 by default', () => {
render(<Headline>Heading</Headline>);
const heading = screen.getByText('Heading');
expect(heading.tagName).toBe('H2');
});

it('supports render function', () => {
render(<Headline render={props => <h1 {...props} />}>Heading</Headline>);
const heading = screen.getByText('Heading');
expect(heading.tagName).toBe('H1');
});
});

describe('Sizes', () => {
Expand Down
27 changes: 15 additions & 12 deletions packages/raystack/components/headline/headline.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mergeProps, useRender } from '@base-ui/react';
import { cva, type VariantProps } from 'class-variance-authority';
import { ComponentProps } from 'react';
import styles from './headline.module.css';

const headline = cva(styles.headline, {
Expand Down Expand Up @@ -41,26 +41,29 @@ export type HeadlineBaseProps = VariantProps<typeof headline> & {
size?: 't1' | 't2' | 't3' | 't4' | 'small' | 'medium' | 'large';
};

type HeadlineProps = HeadlineBaseProps &
ComponentProps<'h1'> & {
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
};
export type HeadlineProps = HeadlineBaseProps & useRender.ComponentProps<'h2'>;

export function Headline({
className,
size,
weight,
align,
truncate,
as: Component = 'h2',
render,
ref,
...props
}: HeadlineProps) {
return (
<Component
className={headline({ size, weight, align, truncate, className })}
{...props}
/>
);
const element = useRender({
defaultTagName: 'h2',
ref,
render,
props: mergeProps<'h2'>(
{ className: headline({ size, weight, align, truncate, className }) },
props
)
});

return element;
}

Headline.displayName = 'Headline';
2 changes: 1 addition & 1 deletion packages/raystack/components/link/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function Link({
{...externalProps}
{...downloadProps}
{...props}
as='a'
render={<a />}
>
{children}
</Text>
Expand Down
44 changes: 27 additions & 17 deletions packages/raystack/components/text/__tests__/text.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,50 +206,60 @@ describe('Text', () => {
});
});

describe('As Rendering', () => {
it('renders as div when specified', () => {
render(<Text as='div'>Div text</Text>);
describe('Render Prop', () => {
it('renders as div via render prop', () => {
render(<Text render={<div />}>Div text</Text>);

const div = screen.getByText('Div text');
expect(div.tagName.toLowerCase()).toBe('div');
});

it('renders as paragraph when specified', () => {
render(<Text as='p'>Paragraph text</Text>);
it('renders as paragraph via render prop', () => {
render(<Text render={<p />}>Paragraph text</Text>);

const p = screen.getByText('Paragraph text');
expect(p.tagName.toLowerCase()).toBe('p');
});

it('renders as label when specified', () => {
render(<Text as='label'>Label text</Text>);
it('renders as label via render prop', () => {
render(<Text render={<label />}>Label text</Text>);

const label = screen.getByText('Label text');
expect(label.tagName.toLowerCase()).toBe('label');
});

it('renders as anchor when specified', () => {
render(
<Text as='a' href='#test'>
Link text
</Text>
);
it('renders as anchor via render prop', () => {
render(<Text render={<a href='#test' />}>Link text</Text>);

const a = screen.getByText('Link text');
expect(a.tagName.toLowerCase()).toBe('a');
expect(a).toHaveAttribute('href', '#test');
});

it('forwards props to the correct element type', () => {
it('renders as h1 via render prop', () => {
render(<Text render={<h1 />}>Heading text</Text>);

const h1 = screen.getByText('Heading text');
expect(h1.tagName.toLowerCase()).toBe('h1');
});

it('forwards props to the rendered element', () => {
render(
<Text as='label' htmlFor='test-input'>
Label for input
</Text>
<Text render={<label htmlFor='test-input' />}>Label for input</Text>
);

const label = screen.getByText('Label for input');
expect(label).toHaveAttribute('for', 'test-input');
});

it('supports render function', () => {
render(
<Text render={props => <section {...props} />}>Section text</Text>
);

const section = screen.getByText('Section text');
expect(section.tagName.toLowerCase()).toBe('section');
});
});

describe('HTML Attributes', () => {
Expand Down
30 changes: 13 additions & 17 deletions packages/raystack/components/text/text.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mergeProps, useRender } from '@base-ui/react';
import { cva, type VariantProps } from 'class-variance-authority';
import { ComponentProps } from 'react';
import styles from './text.module.css';

export const textVariants = cva(styles.text, {
Expand Down Expand Up @@ -131,13 +131,7 @@ export type TextBaseProps = VariantProps<typeof textVariants> & {
| 900;
};

type TextSpanProps = { as?: 'span' } & ComponentProps<'span'>;
type TextDivProps = { as: 'div' } & ComponentProps<'div'>;
type TextLabelProps = { as: 'label' } & ComponentProps<'label'>;
type TextPProps = { as: 'p' } & ComponentProps<'p'>;
type TextAProps = { as: 'a' } & ComponentProps<'a'>;
export type TextProps = TextBaseProps &
(TextSpanProps | TextDivProps | TextLabelProps | TextPProps | TextAProps);
export type TextProps = TextBaseProps & useRender.ComponentProps<'span'>;

export function Text({
className,
Expand All @@ -150,9 +144,9 @@ export function Text({
underline,
strikeThrough,
italic,
as: Component = 'span',
children,
...rest
render,
ref,
...props
}: TextProps) {
const textClassName = textVariants({
size,
Expand All @@ -167,12 +161,14 @@ export function Text({
italic
});

return (
// @ts-expect-error polymorphic ref
<Component className={textClassName} {...rest}>
{children}
</Component>
);
const element = useRender({
defaultTagName: 'span',
ref,
render,
props: mergeProps<'span'>({ className: textClassName }, props)
});

return element;
}

Text.displayName = 'Text';
Loading