Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC(web-react, web-twig): elementType in Heading #DS-1482 #1691

Merged
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
26 changes: 26 additions & 0 deletions docs/migrations/web-react/MIGRATION-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,32 @@ Manually replace the props in your project.
- `<ButtonLink isSquare … />` → `<ButtonLink isSymmetrical … />`
</details>

### Heading elementType prop is now mandatory

The `Heading` component previously had a default `elementType` of `"div"`.
We've removed this default to encourage developers to explicitly choose a more appropriate semantic HTML element.

#### Migration Guide

🪄 Use codemods to automatically update your codebase:

```sh
npx @lmc-eu/spirit-codemods -p <path> -t v3/web-react/heading-elementType-prop
```

👉 See [Codemods documentation][readme-codemods] for more details.

⚠️ This codemod will add `elementType="div"` where it's missing.
**We highly recommend reviewing these changes and updating them to use the most appropriate semantic HTML elements.**

<details>
<summary>🔧 Manual Migration Steps</summary>

Manually replace the props in your project.

- `<Heading … />` → `<Heading elementType="{/* Your semantic HTML element here */}" … />`
</details>

---

Please refer back to these instructions or reach out to our team if you encounter any issues during migration.
Expand Down
9 changes: 9 additions & 0 deletions docs/migrations/web-twig/MIGRATION-v4.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ Button `isSquare` prop was renamed to `isSymmetrical`.
- `<Button isSquare … />` → `<Button isSymmetrical … />`
- `<ButtonLink isSquare … />` → `<ButtonLink isSymmetrical … />`

### Heading elementType prop is now mandatory

The `Heading` component previously had a default `elementType` of `"div"`.
We've removed this default to encourage developers to explicitly choose a more appropriate semantic HTML element.

#### Migration Guide

- `<Heading … />` → `<Heading elementType="{/* Your semantic HTML element here */}" … />`

---

Please refer back to this guide or reach out to our team if you encounter any issues during migration.
Expand Down
16 changes: 16 additions & 0 deletions packages/codemods/src/transforms/v3/web-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,19 @@ npx @lmc-eu/spirit-codemods -p <path> -t v3/web-react/buttonLink-isSquare-prop-n
- <ButtonLink isSquare … />
+ <ButtonLink isSymmetrical … />
```

### `v3/web-react/heading-elementType-prop` — Add `elementType="div"` to Heading component

The `elementType` prop is now mandatory in the `Heading` component.
This codemod updates the `Heading` component by adding `elementType="div"` if the `elementType` prop is missing.

```sh
npx @lmc-eu/spirit-codemods -p <path> -t v3/web-react/heading-elementType-prop
```

#### Example

```diff
- <Heading … />
+ <Heading elementType="div" … />
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// @ts-ignore: No declaration -- The library is not installed; we don't need to install it for fixtures.
import { Heading } from '@lmc-eu/spirit-web-react';

export const MyComponent = () => (
<>
<Heading>Heading</Heading>
<Heading elementType="div">Heading</Heading>
<Heading elementType="h1">Heading</Heading>
<Heading size="small" elementType="h1">
Heading
</Heading>
<Heading elementType="h2" emphasis="bold">
Heading
</Heading>
<Heading emphasis="bold">Heading</Heading>
<Heading emphasis="bold" size="large">
Heading
</Heading>
</>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// @ts-ignore: No declaration -- The library is not installed; we don't need to install it for fixtures.
import { Heading } from '@lmc-eu/spirit-web-react';

export const MyComponent = () => (
<>
<Heading elementType="div">Heading</Heading>
<Heading elementType="div">Heading</Heading>
<Heading elementType="h1">Heading</Heading>
<Heading size="small" elementType="h1">
Heading
</Heading>
<Heading elementType="h2" emphasis="bold">
Heading
</Heading>
<Heading emphasis="bold" elementType="div">Heading</Heading>
<Heading emphasis="bold" size="large" elementType="div">
Heading
</Heading>
</>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { testTransform } from '../../../../../tests/testUtils';

testTransform(__dirname, 'heading-elementType-prop');
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { API, FileInfo } from 'jscodeshift';

const transform = (fileInfo: FileInfo, api: API) => {
const j = api.jscodeshift;
const root = j(fileInfo.source);

// Find import statements for the specific module and Heading specifier
const importStatements = root.find(j.ImportDeclaration, {
source: {
value: (value: string) => /^@lmc-eu\/spirit-web-react(\/.*)?$/.test(value),
},
});

// Check if the module is imported
if (importStatements.length > 0) {
const componentSpecifier = importStatements.find(j.ImportSpecifier, {
imported: {
type: 'Identifier',
name: 'Heading',
},
});

// Check if Heading specifier is present
if (componentSpecifier.length > 0) {
// Find Heading components in the module
const components = root.find(j.JSXOpeningElement, {
name: {
type: 'JSXIdentifier',
name: 'Heading',
},
});

// Add 'elementType' prop if it's not already present
components.forEach((component) => {
const elementTypeProp = component.node.attributes?.find(
(attribute) => attribute.type === 'JSXAttribute' && attribute.name.name === 'elementType',
);

if (!elementTypeProp) {
component.node.attributes?.push(j.jsxAttribute(j.jsxIdentifier('elementType'), j.stringLiteral('div')));
}
});
}
}

return root.toSource();
};

export default transform;
13 changes: 4 additions & 9 deletions packages/web-react/src/components/Heading/Heading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,15 @@ import { useStyleProps } from '../../hooks';
import { SpiritHeadingProps } from '../../types';
import { useHeadingStyleProps } from './useHeadingStyleProps';

const defaultProps: Partial<SpiritHeadingProps> = {
elementType: 'div',
const defaultProps: Partial<SpiritHeadingProps<ElementType, void, void>> = {
emphasis: 'bold',
size: 'medium',
};

export const Heading = <T extends ElementType = 'div', S = void>(props: SpiritHeadingProps<T, S>): JSX.Element => {
export const Heading = <T extends ElementType, S = void, E = void>(props: SpiritHeadingProps<T, S, E>): JSX.Element => {
const propsWithDefaults = { ...defaultProps, ...props };
const {
elementType: ElementTag = defaultProps.elementType as ElementType,
children,
...restProps
} = propsWithDefaults;
const { classProps, props: modifiedProps } = useHeadingStyleProps(restProps);
const { elementType: ElementTag, children, ...restProps } = propsWithDefaults;
const { classProps, props: modifiedProps } = useHeadingStyleProps({ ...restProps, elementType: ElementTag });
const { styleProps, props: otherProps } = useStyleProps(modifiedProps);

return (
Expand Down
16 changes: 7 additions & 9 deletions packages/web-react/src/components/Heading/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@ The Heading component provides helper classes to render headings.

## Basic Usage

```jsx
<Heading>This is a heading</Heading>
```

## Element Type

Use the `elementType` prop to set the HTML tag of the Heading component.

```jsx
Expand All @@ -21,7 +15,9 @@ Use the `elementType` prop to set the HTML tag of the Heading component.
Use the `size` prop to set the size of the text.

```jsx
<Heading size="large">Heading</Heading>
<Heading elementType="h1" size="large">
Heading
</Heading>
```

## Emphasis
Expand All @@ -31,7 +27,9 @@ Use the `emphasis` prop to set the emphasis of the text.
⚠️ This prop only affects styling, not the semantics of the element.

```jsx
<Heading emphasis="semibold">Semibold heading</Heading>
<Heading elementType="h1" emphasis="semibold">
Semibold heading
</Heading>
```

## Full Example
Expand All @@ -46,7 +44,7 @@ Use the `emphasis` prop to set the emphasis of the text.

| Name | Type | Default | Required | Description |
| ------------- | ------------------------------------------- | -------- | -------- | -------------------- |
| `elementType` | `React.Element` | `div` | ✕ | HTML tag |
| `elementType` | `React.Element` | - | ✓ | HTML tag |
| `emphasis` | [Emphasis dictionary][dictionary-emphasis] | `bold` | ✕ | Emphasis of the text |
| `size` | [Size Extended dictionary][dictionary-size] | `medium` | ✕ | Size of the text |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@ import Heading from '../Heading';
import headingSizeDataProvider from './headingSizeDataProvider';

describe('Heading', () => {
classNamePrefixProviderTest(Heading, 'typography-heading-medium-bold');
classNamePrefixProviderTest(() => <Heading elementType="h1" />, 'typography-heading-medium-bold');

stylePropsTest(Heading);
stylePropsTest((props) => <Heading elementType="h1" data-testid="heading-test-id" {...props} />, 'heading-test-id');

sizePropsTest(Heading);
sizePropsTest((props) => <Heading elementType="h1" data-testid="heading-test-id" {...props} />, 'heading-test-id');

sizeExtendedPropsTest(Heading);
sizeExtendedPropsTest(
(props) => <Heading elementType="h1" data-testid="heading-test-id" {...props} />,
'heading-test-id',
);

restPropsTest(Heading, 'div');
restPropsTest((props) => <Heading elementType="h1" {...props} />, 'h1');

it.each(headingSizeDataProvider)('should have classname', (size, emphasis, expectedClassName) => {
render(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { renderHook } from '@testing-library/react';
import { ElementType } from 'react';
import { SpiritHeadingProps } from '../../../types';
import { useHeadingStyleProps } from '../useHeadingStyleProps';
import headingSizeDataProvider from './headingSizeDataProvider';

describe('useHeadingStyleProps', () => {
it.each(headingSizeDataProvider)('should return typography heading class', (size, emphasis, expectedClassName) => {
const props = { size, emphasis } as SpiritHeadingProps;
const props = { size, emphasis } as SpiritHeadingProps<ElementType, void, void>;
const { result } = renderHook(() => useHeadingStyleProps(props));

expect(result.current.classProps).toBe(expectedClassName);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import React from 'react';
import Heading from '../Heading';

const HeadingDefault = () => (
<>
<Heading>Heading</Heading>
<Heading elementType="h2">Heading H2</Heading>
</>
);
const HeadingDefault = () => <Heading elementType="h2">Heading</Heading>;

export default HeadingDefault;
16 changes: 12 additions & 4 deletions packages/web-react/src/components/Heading/demo/HeadingEmphasis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@ import Heading from '../Heading';

const HeadingEmphasis = () => (
<>
<Heading emphasis="regular">Heading regular</Heading>
<Heading emphasis="semibold">Heading semibold</Heading>
<Heading emphasis="bold">Heading bold</Heading>
<Heading emphasis="italic">Heading italic</Heading>
<Heading elementType="h2" emphasis="regular">
Heading regular
</Heading>
<Heading elementType="h2" emphasis="semibold">
Heading semibold
</Heading>
<Heading elementType="h2" emphasis="bold">
Heading bold
</Heading>
<Heading elementType="h2" emphasis="italic">
Heading italic
</Heading>
</>
);

Expand Down
20 changes: 15 additions & 5 deletions packages/web-react/src/components/Heading/demo/HeadingSizes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@ import Heading from '../Heading';

const HeadingSizes = () => (
<>
<Heading size="xsmall">Heading xsmall</Heading>
<Heading size="small">Heading small</Heading>
<Heading size="medium">Heading medium</Heading>
<Heading size="large">Heading large</Heading>
<Heading size="xlarge">Heading xlarge</Heading>
<Heading elementType="h2" size="xsmall">
Heading xsmall
</Heading>
<Heading elementType="h2" size="small">
Heading small
</Heading>
<Heading elementType="h2" size="medium">
Heading medium
</Heading>
<Heading elementType="h2" size="large">
Heading large
</Heading>
<Heading elementType="h2" size="xlarge">
Heading xlarge
</Heading>
</>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { ElementType } from 'react';
import { useClassNamePrefix } from '../../hooks';
import { SpiritHeadingProps, HeadingProps } from '../../types';

export interface HeadingStyles<T extends ElementType = 'p'> {
export interface HeadingStyles<T extends ElementType> {
/** className props */
classProps: string | null;
/** props to be passed to the input element */
props: HeadingProps<T>;
}

export function useHeadingStyleProps<T extends ElementType = 'div', S = void>(
props: SpiritHeadingProps<T, S>,
export function useHeadingStyleProps<T extends ElementType, S = void, E = void>(
props: SpiritHeadingProps<T, S, E>,
): HeadingStyles<T> {
const { size, emphasis, ...restProps } = props;

Expand Down
12 changes: 5 additions & 7 deletions packages/web-react/src/types/heading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,20 @@ import {
TransferProps,
} from './shared';

export interface HeadingElementTypeProps<T extends ElementType = 'div'> {
export interface HeadingElementTypeProps<T extends ElementType> {
/**
* The HTML element or React element used to render the Heading, e.g. 'div'.
*
* @default 'div'
* The HTML element or React element used to render the Heading, e.g. 'h2'.
*/
elementType?: T | JSXElementConstructor<unknown>;
elementType: T | JSXElementConstructor<unknown>;
}

export interface HeadingProps<T extends ElementType = 'div'>
export interface HeadingProps<T extends ElementType>
extends HeadingElementTypeProps<T>,
ChildrenProps,
StyleProps,
TransferProps {}

export interface SpiritHeadingProps<T extends ElementType = 'div', S = void, E = void>
export interface SpiritHeadingProps<T extends ElementType, S = void, E = void>
extends HeadingProps<T>,
SizeProps<SizeExtendedDictionaryType<S>>,
EmphasisProps<EmphasisDictionaryType<E>> {}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{%- set props = props | default([]) -%}
{%- set _emphasis = props.emphasis | default('bold') -%}
{%- set _size = props.size | default('medium') -%}
{%- set _elementType = props.elementType | default('div') -%}
{%- set _elementType = props.elementType -%}

{# Class names #}
{%- set _rootClassName = _spiritClassPrefix ~ 'typography-heading-' ~ _size ~ '-' ~ _emphasis -%}
Expand Down
Loading
Loading