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

Feat(web-react): Introduce Label component #DS-1566 #1905

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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: 1 addition & 0 deletions packages/web-react/scripts/entryPoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const entryPoints = [
{ dirs: ['components', 'Heading'] },
{ dirs: ['components', 'Icon'] },
{ dirs: ['components', 'Item'] },
{ dirs: ['components', 'Label'] },
{ dirs: ['components', 'Link'] },
{ dirs: ['components', 'Modal'] },
{ dirs: ['components', 'NoSsr'] },
Expand Down
13 changes: 10 additions & 3 deletions packages/web-react/src/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useStyleProps } from '../../hooks';
import { SpiritCheckboxProps } from '../../types';
import { HelperText, ValidationText, useAriaIds } from '../Field';
import { useValidationTextRole } from '../Field/useValidationTextRole';
import { Label } from '../Label';
import { useCheckboxStyleProps } from './useCheckboxStyleProps';

/* We need an exception for components exported with forwardRef */
Expand Down Expand Up @@ -33,7 +34,11 @@ const _Checkbox = (props: SpiritCheckboxProps, ref: ForwardedRef<HTMLInputElemen
});

return (
<label {...styleProps} htmlFor={id} className={classNames(classProps.root, styleProps.className)}>
<Label
htmlFor={id}
UNSAFE_style={styleProps.style}
UNSAFE_className={classNames(classProps.root, styleProps.className)}
>
<input
{...otherProps}
aria-describedby={ids.join(' ')}
Expand All @@ -47,7 +52,9 @@ const _Checkbox = (props: SpiritCheckboxProps, ref: ForwardedRef<HTMLInputElemen
ref={ref}
/>
<span className={classProps.text}>
<span className={classProps.label}>{label}</span>
<Label elementType="span" UNSAFE_className={classProps.label}>
{label}
</Label>
<HelperText
className={classProps.helperText}
elementType="span"
Expand All @@ -66,7 +73,7 @@ const _Checkbox = (props: SpiritCheckboxProps, ref: ForwardedRef<HTMLInputElemen
/>
)}
</span>
</label>
</Label>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { SpiritFileUploaderInputProps } from '../../types';
import { HelperText, ValidationText, useAriaIds } from '../Field';
import { useValidationTextRole } from '../Field/useValidationTextRole';
import { Icon } from '../Icon';
import { Label } from '../Label';
import { DEFAULT_FILE_QUEUE_LIMIT, DEFAULT_FILE_SIZE_LIMIT } from './constants';
import { useFileUploaderInput } from './useFileUploaderInput';
import { useFileUploaderStyleProps } from './useFileUploaderStyleProps';
Expand Down Expand Up @@ -85,9 +86,9 @@ const FileUploaderInput = (props: SpiritFileUploaderInputProps) => {
onDrop={!isDisabled && isDragAndDropSupported ? onDrop : undefined}
className={classNames(classProps.input.root, styleProps.className)}
>
<label htmlFor={id} className={classProps.input.label}>
<Label htmlFor={id} UNSAFE_className={classProps.input.label}>
{label}
</label>
</Label>
<input
aria-describedby={ids.join(' ')}
type="file"
Expand All @@ -102,11 +103,11 @@ const FileUploaderInput = (props: SpiritFileUploaderInputProps) => {
/>
<div ref={dropZoneRef} className={classProps.input.dropZone.root}>
<Icon name={iconName} aria-hidden="true" />
<label htmlFor={id} className={classProps.input.dropZone.label}>
<Label htmlFor={id} UNSAFE_className={classProps.input.dropZone.label}>
<span className={classProps.input.link}>{linkText}</span>
&nbsp;
<span className={classProps.input.dropLabel}>{labelText}</span>
</label>
</Label>
<HelperText
className={classProps.input.helper}
id={`${id}__helperText`}
Expand Down
18 changes: 18 additions & 0 deletions packages/web-react/src/components/Label/Label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client';

import React, { ElementType } from 'react';
import { useStyleProps } from '../../hooks';
import { SpiritLabelProps } from '../../types';

const Label = <T extends ElementType = 'label'>(props: SpiritLabelProps<T>): JSX.Element => {
const { elementType: ElementTag = 'label', children, ...restProps } = props;
const { styleProps, props: otherProps } = useStyleProps(restProps);

return (
<ElementTag {...otherProps} {...styleProps}>
{children}
</ElementTag>
);
};

export default Label;
37 changes: 37 additions & 0 deletions packages/web-react/src/components/Label/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Label

The `Label` component is used to associate text with a form control, such as an input, checkbox, or radio button.
It improves accessibility by allowing users to click the label to interact with the corresponding input.
This component can be customized using various props to fit different use cases.

Simple Item example:

```jsx
import { Label } from '@lmc-eu/spirit-web-react';

<Label>Label</Label>
```

## Full Example

```jsx
import { Label } from '@lmc-eu/spirit-web-react';

<Label elementType="span" htmlFor="input-id">Label</Label>
```

## API

| Name | Type | Default | Required | Description |
| ------------- | ------------- | ------- | -------- | ----------------------------------------------- |
| `elementType` | `ElementType` | `label` | ✕ | Type of element used as wrapper |
| `htmlFor` | `string` | — | ✕ | ID of the associated form element (e.g., input) |

On top of the API options, the components accept [additional attributes][readme-additional-attributes].
If you need more control over the styling of a component, you can use [style props][readme-style-props]
and [escape hatches][readme-escape-hatches].

[readme-additional-attributes]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#additional-attributes
[readme-escape-hatches]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#escape-hatches
[readme-style-props]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#style-props

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { restPropsTest, stylePropsTest } from '@local/tests';
import { SpiritLabelProps } from '../../../types';
import Label from '../Label';

describe('Item', () => {
stylePropsTest(Label);

restPropsTest((props: SpiritLabelProps) => <Label {...props} />, 'label');

it('should render children', () => {
const label = 'Item label';
render(<Label data-testid="test">{label}</Label>);

expect(screen.getByTestId('test')).toHaveTextContent(label);
});

it('should render as span', () => {
render(<Label data-testid="test" elementType="span" />);

expect(screen.getByTestId('test').tagName).toBe('SPAN');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react';
import Label from '../Label';

const DefaultLabel = () => {
return <Label>Label</Label>;
};

export default DefaultLabel;
12 changes: 12 additions & 0 deletions packages/web-react/src/components/Label/demo/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import DocsSection from '../../../../docs/DocsSections';
import DefaultLabel from './DefaultLabel';

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<DocsSection title="Default">
<DefaultLabel />
</DocsSection>
</React.StrictMode>,
);
1 change: 1 addition & 0 deletions packages/web-react/src/components/Label/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{> web-react/demo title="Label" parentPageName="Components" }}
3 changes: 3 additions & 0 deletions packages/web-react/src/components/Label/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use client';

export { default as Label } from './Label';
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Markdown } from '@storybook/blocks';
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';
import ReadMe from '../README.md';
import { Label } from '..';

const meta: Meta<typeof Label> = {
title: 'Components/Label',
component: Label,
parameters: {
docs: {
page: () => <Markdown>{ReadMe}</Markdown>,
},
},
argTypes: {
elementType: {
control: 'text',
table: {
defaultValue: { summary: 'label' },
},
},
children: {
control: 'text',
},
},
args: {
elementType: 'label',
children: 'Label',
},
};

export default meta;
type Story = StoryObj<typeof Label>;

export const Playground: Story = {
name: 'Label',
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Checkbox,
Grid,
GridItem,
Label,
Modal,
ModalBody,
ModalDialog,
Expand Down Expand Up @@ -237,17 +238,17 @@ const ModalScrollingLongContent = () => {
}
/>
<Grid UNSAFE_style={{ columnGap: 'var(--spirit-space-600)' }}>
<label
className="GridItem"
<Label
UNSAFE_className="GridItem"
htmlFor={`custom-height-${breakpoint}`}
style={{
UNSAFE_style={{
['--grid-item-column-start' as string]: 1,
['--grid-item-column-end' as string]: 6,
['--grid-item-column-end-tablet' as string]: 4,
}}
>
Height
</label>
</Label>
<GridItem
columnStart={{ mobile: 6, tablet: 4 }}
columnEnd={{ mobile: 13, tablet: 7 }}
Expand All @@ -274,17 +275,17 @@ const ModalScrollingLongContent = () => {
/>
</Grid>
<Grid UNSAFE_style={{ columnGap: 'var(--spirit-space-600)' }}>
<label
className="GridItem"
<Label
UNSAFE_className="GridItem"
htmlFor={`custom-max-height-${breakpoint}`}
style={{
UNSAFE_style={{
['--grid-item-column-start' as string]: 1,
['--grid-item-column-end' as string]: 6,
['--grid-item-column-end-tablet' as string]: 4,
}}
>
Max height
</label>
</Label>
<GridItem
columnStart={{ mobile: 6, tablet: 4 }}
columnEnd={{ mobile: 13, tablet: 7 }}
Expand Down
15 changes: 11 additions & 4 deletions packages/web-react/src/components/Radio/Radio.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use client';

import classNames from 'classnames';
import React, { forwardRef, ForwardedRef } from 'react';
import React, { ForwardedRef, forwardRef } from 'react';
import { useStyleProps } from '../../hooks';
import { SpiritRadioProps } from '../../types';
import { HelperText, useAriaIds } from '../Field';
import { Label } from '../Label';
import { useRadioStyleProps } from './useRadioStyleProps';

/* We need an exception for components exported with forwardRef */
Expand All @@ -27,7 +28,11 @@ const _Radio = (props: SpiritRadioProps, ref: ForwardedRef<HTMLInputElement>): J
const [ids, register] = useAriaIds(ariaDescribedBy);

return (
<label htmlFor={id} {...styleProps} className={classNames(classProps.root, styleProps.className)}>
<Label
htmlFor={id}
UNSAFE_style={styleProps.style}
UNSAFE_className={classNames(classProps.root, styleProps.className)}
>
<input
{...otherProps}
aria-describedby={ids.join(' ')}
Expand All @@ -41,7 +46,9 @@ const _Radio = (props: SpiritRadioProps, ref: ForwardedRef<HTMLInputElement>): J
ref={ref}
/>
<span className={classProps.text}>
<span className={classProps.label}>{label}</span>
<Label elementType="span" UNSAFE_className={classProps.label}>
{label}
</Label>
<HelperText
className={classProps.helperText}
elementType="span"
Expand All @@ -50,7 +57,7 @@ const _Radio = (props: SpiritRadioProps, ref: ForwardedRef<HTMLInputElement>): J
helperText={helperText}
/>
</span>
</label>
</Label>
);
};

Expand Down
5 changes: 3 additions & 2 deletions packages/web-react/src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { SpiritSelectProps } from '../../types';
import { HelperText, ValidationText, useAriaIds } from '../Field';
import { useValidationTextRole } from '../Field/useValidationTextRole';
import { Icon } from '../Icon';
import { Label } from '../Label';
import { useSelectStyleProps } from './useSelectStyleProps';

/* We need an exception for components exported with forwardRef */
Expand Down Expand Up @@ -37,9 +38,9 @@ const _Select = (props: SpiritSelectProps, ref: ForwardedRef<HTMLSelectElement>)

return (
<div {...styleProps} className={classNames(classProps.root, styleProps.className)}>
<label htmlFor={id} className={classProps.label}>
<Label htmlFor={id} UNSAFE_className={classProps.label}>
{label}
</label>
</Label>
<div className={classProps.container}>
<select
{...transferProps}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useStyleProps } from '../../hooks';
import { SpiritTextFieldBaseProps, TextFieldBasePasswordToggleProps } from '../../types';
import { HelperText, ValidationText, useAriaIds } from '../Field';
import { useValidationTextRole } from '../Field/useValidationTextRole';
import { Label } from '../Label';
import TextFieldBaseInput from './TextFieldBaseInput';
import { useTextFieldBaseStyleProps } from './useTextFieldBaseStyleProps';
import withPasswordToggle from './withPasswordToggle';
Expand Down Expand Up @@ -36,9 +37,9 @@ const _TextFieldBase = (props: SpiritTextFieldBaseProps, ref: ForwardedRef<HTMLI

return (
<div {...styleProps} className={classNames(classProps.root, styleProps.className)}>
<label htmlFor={id} className={classProps.label}>
<Label htmlFor={id} UNSAFE_className={classProps.label}>
{label}
</label>
</Label>
<TextFieldBaseInputWithPasswordToggle id={id} ref={ref} aria-describedby={ids.join(' ')} {...otherProps} />
<HelperText
className={classProps.helperText}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useStyleProps } from '../../hooks';
import { SpiritSliderProps } from '../../types';
import { HelperText, ValidationText, useAriaIds } from '../Field';
import { useValidationTextRole } from '../Field/useValidationTextRole';
import { Label } from '../Label';
import { SLIDER_DEFAULT_PROPS } from './constants';
import { useSliderStyleProps } from './useSliderStyleProps';

Expand Down Expand Up @@ -53,9 +54,9 @@ const _UnstableSlider = (props: SpiritSliderProps, ref: ForwardedRef<HTMLInputEl

return (
<div {...styleProps} {...otherProps} className={classNames(classProps.root, styleProps.className)}>
<label htmlFor={id} className={classProps.label}>
<Label htmlFor={id} UNSAFE_className={classProps.label}>
{label}
</label>
</Label>
<input
aria-describedby={ids.join(' ')}
className={classProps.input}
Expand Down
Loading
Loading