Skip to content

Commit f4191c3

Browse files
committed
feat: headless label
1 parent 2afd090 commit f4191c3

File tree

13 files changed

+250
-6
lines changed

13 files changed

+250
-6
lines changed

apps/website/src/_state/component-statuses.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const statusByComponent: ComponentKitsStatuses = {
3737
Carousel: ComponentStatus.Draft,
3838
Collapsible: ComponentStatus.Draft,
3939
Combobox: ComponentStatus.Beta,
40+
Label: ComponentStatus.Draft,
4041
Modal: ComponentStatus.Beta,
4142
Pagination: ComponentStatus.Draft,
4243
Popover: ComponentStatus.Beta,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { component$, useStyles$ } from '@builder.io/qwik';
2+
import { Label } from '@qwik-ui/headless';
3+
import styles from './label.css?inline';
4+
5+
export default component$(() => {
6+
useStyles$(styles);
7+
8+
return (
9+
<div
10+
style={{
11+
display: 'flex',
12+
padding: '0 20px',
13+
flexWrap: 'wrap',
14+
gap: 15,
15+
alignItems: 'center',
16+
}}
17+
data-testid="root"
18+
>
19+
<Label class="LabelRoot" for="firstName" data-testid="label">
20+
First name
21+
</Label>
22+
<input
23+
class="Input"
24+
type="text"
25+
id="firstName"
26+
placeholder="John Doe"
27+
data-testid="input"
28+
/>
29+
</div>
30+
);
31+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/* reset */
2+
input {
3+
all: unset;
4+
}
5+
6+
.LabelRoot {
7+
font-size: 15px;
8+
font-weight: 500;
9+
line-height: 35px;
10+
color: black;
11+
}
12+
13+
.Input {
14+
width: 200px;
15+
display: inline-flex;
16+
align-items: center;
17+
justify-content: center;
18+
border-radius: 4px;
19+
padding: 0 10px;
20+
height: 35px;
21+
font-size: 15px;
22+
line-height: 1;
23+
background-color: white;
24+
box-shadow: 0 0 0 1px black;
25+
}
26+
.Input:focus {
27+
box-shadow: 0 0 0 2px black;
28+
}
29+
.Input::selection {
30+
background-color: var(--black-a9);
31+
color: white;
32+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
title: Qwik UI | Progress
3+
---
4+
5+
import { statusByComponent } from '~/_state/component-statuses';
6+
import { FeatureList } from '~/components/feature-list/feature-list';
7+
import { Note } from '~/components/note/note';
8+
9+
<StatusBanner status={statusByComponent.headless.Label} />
10+
11+
# Label
12+
13+
Renders an accessible label associated with controls.
14+
15+
<Showcase name="hero" />
16+
17+
### Features
18+
19+
<FeatureList
20+
features={[
21+
'Text selection is prevented when double clicking label.',
22+
'Supports nested controls.',
23+
]}
24+
/>
25+
26+
<div class="mb-6 flex flex-col gap-2">
27+
[View Source Code ↗️](https://github.com/qwikifiers/qwik-ui/tree/main/packages/kit-headless/src/components/label)
28+
29+
[Report an issue 🚨](https://github.com/qwikifiers/qwik-ui/issues)
30+
31+
[Edit This Page 🗒️](<https://github.com/qwikifiers/qwik-ui/edit/main/apps/website/src/routes/docs/headless/(components)/label/index.mdx>)
32+
33+
</div>
34+
35+
## Building blocks
36+
37+
```tsx
38+
import { component$, useStyles$ } from '@builder.io/qwik';
39+
import { Label } from '@qwik-ui/headless';
40+
41+
export default component$(() => {
42+
return (
43+
<div>
44+
<Label for="firstName">First name</Label>
45+
<input type="text" id="firstName" placeholder="John Doe" />
46+
</div>
47+
);
48+
});
49+
```
50+
51+
## Accessibility
52+
53+
This component is based on the native label element, it will automatically apply the correct labelling when wrapping controls or using the for attribute. For your own custom controls to work correctly, ensure they use native elements such as button or input as a base.
54+
55+
### 🎨 Anatomy
56+
57+
<AnatomyTable
58+
propDescriptors={[
59+
{
60+
name: 'Label',
61+
description: 'The root container for the label',
62+
},
63+
]}
64+
/>

apps/website/src/routes/docs/headless/menu.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- [Carousel](/docs/headless/carousel)
1818
- [Collapsible](/docs/headless/collapsible)
1919
- [Combobox](/docs/headless/combobox)
20+
- [Label](/docs/headless/label)
2021
- [Modal](/docs/headless/modal)
2122
- [Pagination](/docs/headless/pagination)
2223
- [Popover](/docs/headless/popover)

apps/website/src/routes/docs/headless/progress/index.mdx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,37 @@ Adheres to the [progressbar role requirements](https://www.w3.org/WAI/ARIA/apg/p
5757

5858
## API
5959

60-
### Root
60+
### Progress
61+
62+
Contains all of the progress parts.
63+
64+
<APITable
65+
propDescriptors={[
66+
{
67+
name: 'class',
68+
type: 'string',
69+
description: 'CSS classes to apply to the Progress.',
70+
},
71+
{
72+
name: 'value',
73+
type: 'number',
74+
description: 'The progress value.',
75+
},
76+
{
77+
name: 'max',
78+
type: 'number',
79+
description: 'The maximum progress value',
80+
},
81+
{
82+
name: 'getValueLabel',
83+
type: 'function',
84+
description:
85+
'A function to get the accessible label text representing the current value in a human-readable format. If not provided, the value label will be read as the numeric value as a percentage of the max value.',
86+
},
87+
]}
88+
/>
89+
90+
### Progress Indicator
6191

6292
Contains all of the progress parts.
6393

apps/website/src/routes/docs/styled/label/index.mdx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,22 @@ qwik-ui add label
2222

2323
```tsx
2424
import { component$, Slot, PropsOf } from '@builder.io/qwik';
25+
import { Label as QwikUILabel } from '@qwik-ui/headless';
2526
import { cn } from '@qwik-ui/utils';
2627

2728
type LabelProps = PropsOf<'label'>;
2829

2930
export const Label = component$<LabelProps>((props) => {
3031
return (
31-
<label
32+
<QwikUILabel
3233
{...props}
3334
class={cn(
3435
'font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
3536
props.class,
3637
)}
3738
>
3839
<Slot />
39-
</label>
40+
</QwikUILabel>
4041
);
4142
});
4243
```
@@ -48,5 +49,5 @@ import { Label } from '@/components/ui/label';
4849
```
4950

5051
```tsx
51-
<Label htmlFor="email">Your email address</Label>
52+
<Label for="email">Your email address</Label>
5253
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './label';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { type Locator, type Page } from '@playwright/test';
2+
export type DriverLocator = Locator | Page;
3+
4+
export function createTestDriver<T extends DriverLocator>(rootLocator: T) {
5+
const getRoot = () => {
6+
return rootLocator;
7+
};
8+
9+
const getLabel = () => {
10+
return rootLocator.getByTestId('label');
11+
};
12+
13+
const getForElement = () => {
14+
return rootLocator.getByTestId('input');
15+
};
16+
17+
return {
18+
...rootLocator,
19+
getRoot,
20+
getLabel,
21+
getForElement,
22+
};
23+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { expect, test, type Page } from '@playwright/test';
2+
import { createTestDriver } from './label.driver';
3+
4+
async function setup(page: Page, exampleName: string) {
5+
await page.goto(`/headless/label/${exampleName}`);
6+
const driver = createTestDriver(page.getByTestId('root'));
7+
const { getLabel, getForElement } = driver;
8+
9+
return { driver, getLabel, getForElement };
10+
}
11+
12+
test.describe('Label usaged', () => {
13+
test('should have a label', async ({ page }) => {
14+
const { getLabel } = await setup(page, 'hero');
15+
await expect(getLabel()).toBeVisible();
16+
});
17+
18+
test('should focus on for', async ({ page }) => {
19+
const { getLabel, getForElement } = await setup(page, 'hero');
20+
const label = getLabel();
21+
const forElement = getForElement();
22+
23+
await label.click();
24+
await expect(forElement).toBeFocused();
25+
});
26+
27+
test('should not select the text on label', async ({ page }) => {
28+
const { getLabel } = await setup(page, 'hero');
29+
const label = getLabel();
30+
31+
await label.dblclick();
32+
const selection = await page.evaluate(() => window.getSelection()?.toString());
33+
expect(selection).toBeFalsy();
34+
});
35+
});

0 commit comments

Comments
 (0)