Skip to content

Commit

Permalink
feat(label): add required indicator component
Browse files Browse the repository at this point in the history
  • Loading branch information
andresz1 committed Jun 12, 2023
1 parent e014ed3 commit 62021f8
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 25 deletions.
21 changes: 19 additions & 2 deletions packages/components/label/src/Label.doc.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Meta, Canvas } from '@storybook/addon-docs'
import { ArgTypes } from '@storybook/blocks'
import { ArgTypes } from '@docs/helpers/ArgTypes'
import { Label } from '.'
import * as stories from './Label.stories'

Expand All @@ -23,12 +23,29 @@ import { Label } from '@spark-ui/label'

## Props

<ArgTypes of={Label} />
<ArgTypes
of={Label}
subcomponents={{
'Label.RequiredIndicator': Label.RequiredIndicator,
}}
/>

## Usage

<Canvas of={stories.Default} />

## Required

Use `Label.RequiredIndicator` component to display a required indicator inside the label. By default `*` is used as indicator.

<Canvas of={stories.Required} />

### Custom required indicator

Optionally use the `children` prop to change the indicator.

<Canvas of={stories.RequiredIndicator} />

## Accessibility

This component is based on the native `label` element, it will automatically apply the correct labelling when wrapping controls or using the `htmlFor` attribute. For your own custom controls to work correctly, ensure they use native elements such as `button` or `input` as a base.
40 changes: 37 additions & 3 deletions packages/components/label/src/Label.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,46 @@ export default meta

export const Default: StoryFn = _args => (
<div className="gap-md flex flex-col">
<Label htmlFor="name">First name</Label>
<Label htmlFor="label-default">Title</Label>
<input
type="text"
id="name"
placeholder="Jon Snow"
id="label-default"
placeholder="IPhone 14"
className="p-md border-neutral active:border-primary border-md rounded-sm"
/>
</div>
)

export const Required: StoryFn = _args => (
<div className="gap-md flex flex-col">
<Label className="gap-sm flex items-center" htmlFor="label-required">
Title
<Label.RequiredIndicator />
</Label>

<input
type="text"
id="label-required"
placeholder="IPhone 14"
className="p-md border-neutral active:border-primary border-md rounded-sm"
required
/>
</div>
)

export const RequiredIndicator: StoryFn = _args => (
<div className="gap-md flex flex-col">
<Label className="gap-sm flex items-center" htmlFor="label-indicator">
Title
<Label.RequiredIndicator>Required</Label.RequiredIndicator>
</Label>

<input
type="text"
id="label-indicator"
placeholder="IPhone 14"
className="p-md border-neutral active:border-primary border-md rounded-sm"
required
/>
</div>
)
50 changes: 44 additions & 6 deletions packages/components/label/src/Label.test.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,65 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'

import { Label } from './Label'
import { Label } from '.'

describe('Label', () => {
it('should render applying correct labelling when wrapping controls', () => {
render(
<Label>
Name <input type="text" name="name" />
Title <input type="text" name="title" />
</Label>
)

expect(screen.getByLabelText('Name')).toBeInTheDocument()
expect(screen.getByLabelText('Title')).toBeInTheDocument()
})

it('should render applying correct labelling when not wrapping controls ', () => {
render(
<>
<Label htmlFor="id">Name</Label>
<input id="id" type="text" name="name" />
<Label htmlFor="id">Title</Label>
<input id="id" type="text" name="title" />
</>
)

expect(screen.getByLabelText('Name')).toBeInTheDocument()
expect(screen.getByLabelText('Title')).toBeInTheDocument()
})

it('should render default required indicator', () => {
render(
<>
<Label htmlFor="id">
Title
<Label.RequiredIndicator />
</Label>
<input id="id" type="text" name="title" />
</>
)

expect(screen.getByLabelText('Title')).toBeInTheDocument()

const requiredEl = screen.getByText('*')

expect(requiredEl).toHaveAttribute('role', 'presentation')
expect(requiredEl).toHaveAttribute('aria-hidden', 'true')
})

it('should render custom required indicator', () => {
render(
<>
<Label htmlFor="id">
Title
<Label.RequiredIndicator>Required</Label.RequiredIndicator>
</Label>
<input id="id" type="text" name="title" />
</>
)

expect(screen.getByLabelText('Title')).toBeInTheDocument()

const requiredEl = screen.getByText('Required')

expect(requiredEl).toHaveAttribute('role', 'presentation')
expect(requiredEl).toHaveAttribute('aria-hidden', 'true')
})
})
24 changes: 11 additions & 13 deletions packages/components/label/src/Label.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { Label as LabelPrimitive } from '@radix-ui/react-label'
import { Label as LabelPrimitive, LabelProps as LabelPrimitiveProps } from '@radix-ui/react-label'
import { cx } from 'class-variance-authority'
import { ComponentPropsWithoutRef, forwardRef } from 'react'
import { forwardRef } from 'react'

export interface LabelProps extends ComponentPropsWithoutRef<'label'> {
/**
* Change the component to the HTML tag or custom component of the only child.
*/
asChild?: boolean
/**
* The id of the element the label is associated with.
*/
htmlFor?: string
}
export type LabelProps = LabelPrimitiveProps

export const Label = forwardRef<HTMLLabelElement, LabelProps>(({ className, ...others }, ref) => {
return <LabelPrimitive ref={ref} className={cx('text-body-1', className)} {...others} />
return (
<LabelPrimitive
ref={ref}
data-spark-component="label"
className={cx('text-body-1', className)}
{...others}
/>
)
})
23 changes: 23 additions & 0 deletions packages/components/label/src/LabelRequiredIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { cx } from 'class-variance-authority'
import { ComponentPropsWithoutRef, forwardRef } from 'react'

export type LabelRequiredIndicatorProps = ComponentPropsWithoutRef<'span'>

export const LabelRequiredIndicator = forwardRef<HTMLSpanElement, LabelRequiredIndicatorProps>(
({ className, children = '*', ...others }, ref) => {
return (
<span
ref={ref}
data-spark-component="label-required-indicator"
role="presentation"
aria-hidden="true"
className={cx(className, 'text-on-surface/dim-3 text-caption')}
{...others}
>
{children}
</span>
)
}
)

LabelRequiredIndicator.displayName = 'Label.RequiredIndicator'
12 changes: 11 additions & 1 deletion packages/components/label/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
export * from './Label'
import { Label as Root } from './Label'
import { LabelRequiredIndicator } from './LabelRequiredIndicator'

export type { LabelProps } from './Label'
export type { LabelRequiredIndicatorProps } from './LabelRequiredIndicator'

export const Label: typeof Root & {
RequiredIndicator: typeof LabelRequiredIndicator
} = Object.assign(Root, {
RequiredIndicator: LabelRequiredIndicator,
})

0 comments on commit 62021f8

Please sign in to comment.