@primitiv-ui/react 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primitiv-ui/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Headless, accessible React components built on the WAI-ARIA authoring patterns. Zero styles ship with this package — bring your own (CSS, Tailwind, CSS-in-JS, design tokens).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { render } from "@testing-library/react";
|
|
6
|
+
|
|
7
|
+
import { Field } from "../Field";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Auto-verification of the Field styling contract (RFC 0004 §3.4 / D15). The
|
|
11
|
+
* `data-*` half of `registry/components/field/contract.json` is *derived from and
|
|
12
|
+
* asserted against the rendered headless `Field.Root`* so it can never drift from
|
|
13
|
+
* what the component emits. The root always carries `data-field`, plus
|
|
14
|
+
* `data-field-invalid` / `data-field-disabled` / `data-field-required` when the
|
|
15
|
+
* matching flag is set. The authored half (part classes, custom properties) is a
|
|
16
|
+
* styling convention the headless layer does not emit and is checked by the
|
|
17
|
+
* generator drift-guards instead.
|
|
18
|
+
*/
|
|
19
|
+
const contractPath = resolve(
|
|
20
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
21
|
+
"../../../../../registry/components/field/contract.json",
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const contract = JSON.parse(readFileSync(contractPath, "utf8"));
|
|
25
|
+
|
|
26
|
+
/** Every `data-*` attribute name present on an element. */
|
|
27
|
+
function dataAttributeNames(element: Element): string[] {
|
|
28
|
+
return [...element.attributes]
|
|
29
|
+
.map((attribute) => attribute.name)
|
|
30
|
+
.filter((name) => name.startsWith("data-"))
|
|
31
|
+
.sort();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Render a `Field.Root` with the given flags and return its `[data-field]` element. */
|
|
35
|
+
function renderRoot(props: Record<string, boolean>): Element {
|
|
36
|
+
const { container } = render(<Field.Root {...props} />);
|
|
37
|
+
return container.querySelector("[data-field]")!;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("Field styling contract", () => {
|
|
41
|
+
const autoAttributes = contract.dataAttributes.filter(
|
|
42
|
+
(attribute: { source: string }) => attribute.source === "auto",
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
it("declares every data-* attribute as auto-verified against the component", () => {
|
|
46
|
+
// The Field has no authored data-* surface; the whole contract is derived.
|
|
47
|
+
expect(autoAttributes).toHaveLength(contract.dataAttributes.length);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("emits exactly the contracted data-* attribute names across its states", () => {
|
|
51
|
+
const emitted = new Set<string>();
|
|
52
|
+
for (const props of [{}, { invalid: true, disabled: true, required: true }]) {
|
|
53
|
+
for (const name of dataAttributeNames(renderRoot(props))) {
|
|
54
|
+
emitted.add(name);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const contracted = new Set(
|
|
59
|
+
autoAttributes.map((attribute: { name: string }) => attribute.name),
|
|
60
|
+
);
|
|
61
|
+
expect([...emitted].sort()).toEqual([...contracted].sort());
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("always emits data-field regardless of state", () => {
|
|
65
|
+
expect(renderRoot({})).toHaveAttribute("data-field", "");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("emits each conditional attribute exactly as the contract documents it", () => {
|
|
69
|
+
const conditional = autoAttributes.filter(
|
|
70
|
+
(attribute: { when: string }) => attribute.when !== "always",
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
for (const entry of conditional) {
|
|
74
|
+
const root = renderRoot({ [entry.when]: true });
|
|
75
|
+
expect(root).toHaveAttribute(entry.name, entry.value);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("omits each conditional attribute when its flag is not set", () => {
|
|
80
|
+
const root = renderRoot({});
|
|
81
|
+
for (const entry of autoAttributes.filter(
|
|
82
|
+
(attribute: { when: string }) => attribute.when !== "always",
|
|
83
|
+
)) {
|
|
84
|
+
expect(root).not.toHaveAttribute(entry.name);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { render, screen } from "@testing-library/react";
|
|
6
|
+
|
|
7
|
+
import { Input } from "../Input";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Auto-verification of the Input styling contract (RFC 0004 §3.4 / D15). The
|
|
11
|
+
* `data-*` half of `registry/components/input/contract.json` is *derived from and
|
|
12
|
+
* asserted against the rendered headless component* so it can never drift from
|
|
13
|
+
* what the component actually emits. Input's only data-* surface is
|
|
14
|
+
* `data-disabled=""` when disabled — `invalid` is an `aria-invalid` concern, not
|
|
15
|
+
* a data attribute. The authored half (root class, custom properties) is a
|
|
16
|
+
* styling convention the headless layer does not emit and is checked by the
|
|
17
|
+
* generator drift-guards instead.
|
|
18
|
+
*/
|
|
19
|
+
const contractPath = resolve(
|
|
20
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
21
|
+
"../../../../../registry/components/input/contract.json",
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const contract = JSON.parse(readFileSync(contractPath, "utf8"));
|
|
25
|
+
|
|
26
|
+
/** Every `data-*` attribute name present on an element. */
|
|
27
|
+
function dataAttributeNames(element: Element): string[] {
|
|
28
|
+
return [...element.attributes]
|
|
29
|
+
.map((attribute) => attribute.name)
|
|
30
|
+
.filter((name) => name.startsWith("data-"))
|
|
31
|
+
.sort();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("Input styling contract", () => {
|
|
35
|
+
const autoAttributes = contract.dataAttributes.filter(
|
|
36
|
+
(attribute: { source: string }) => attribute.source === "auto",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
it("declares every data-* attribute as auto-verified against the component", () => {
|
|
40
|
+
// The Input has no authored data-* surface; the whole contract is derived.
|
|
41
|
+
expect(autoAttributes).toHaveLength(contract.dataAttributes.length);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("emits exactly the contracted data-* attribute names across its states", () => {
|
|
45
|
+
const emitted = new Set<string>();
|
|
46
|
+
for (const ui of [
|
|
47
|
+
<Input key="default" aria-label="x" />,
|
|
48
|
+
<Input key="disabled" aria-label="x" disabled />,
|
|
49
|
+
]) {
|
|
50
|
+
const { unmount } = render(ui);
|
|
51
|
+
for (const name of dataAttributeNames(screen.getByRole("textbox"))) {
|
|
52
|
+
emitted.add(name);
|
|
53
|
+
}
|
|
54
|
+
unmount();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const contracted = new Set(
|
|
58
|
+
autoAttributes.map((attribute: { name: string }) => attribute.name),
|
|
59
|
+
);
|
|
60
|
+
expect([...emitted].sort()).toEqual([...contracted].sort());
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("emits data-disabled exactly as the contract documents it", () => {
|
|
64
|
+
const entry = autoAttributes.find(
|
|
65
|
+
(attribute: { name: string }) => attribute.name === "data-disabled",
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
render(<Input aria-label="x" disabled />);
|
|
69
|
+
expect(screen.getByRole("textbox")).toHaveAttribute(entry.name, entry.value);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("omits data-disabled when the documented condition does not hold", () => {
|
|
73
|
+
render(<Input aria-label="x" />);
|
|
74
|
+
|
|
75
|
+
expect(screen.getByRole("textbox")).not.toHaveAttribute("data-disabled");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { render } from "@testing-library/react";
|
|
6
|
+
|
|
7
|
+
import { InputGroup } from "../InputGroup";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Auto-verification of the InputGroup styling contract (RFC 0004 §3.4 / D15). The
|
|
11
|
+
* `data-*` half of `registry/components/input-group/contract.json` is *derived
|
|
12
|
+
* from and asserted against the rendered headless `InputGroup.Root`* so it can
|
|
13
|
+
* never drift. The root always carries `data-input-group`; the adornment slots
|
|
14
|
+
* are styled through their part classes, not a contracted data attribute. The
|
|
15
|
+
* authored half (part classes, custom properties) is a styling convention the
|
|
16
|
+
* headless layer does not emit and is checked by the generator drift-guards.
|
|
17
|
+
*/
|
|
18
|
+
const contractPath = resolve(
|
|
19
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
20
|
+
"../../../../../registry/components/input-group/contract.json",
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const contract = JSON.parse(readFileSync(contractPath, "utf8"));
|
|
24
|
+
|
|
25
|
+
/** Every `data-*` attribute name present on an element. */
|
|
26
|
+
function dataAttributeNames(element: Element): string[] {
|
|
27
|
+
return [...element.attributes]
|
|
28
|
+
.map((attribute) => attribute.name)
|
|
29
|
+
.filter((name) => name.startsWith("data-"))
|
|
30
|
+
.sort();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("InputGroup styling contract", () => {
|
|
34
|
+
const autoAttributes = contract.dataAttributes.filter(
|
|
35
|
+
(attribute: { source: string }) => attribute.source === "auto",
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
it("declares every data-* attribute as auto-verified against the component", () => {
|
|
39
|
+
// The InputGroup root has no authored data-* surface; the contract is derived.
|
|
40
|
+
expect(autoAttributes).toHaveLength(contract.dataAttributes.length);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("emits exactly the contracted data-* attribute names on the root", () => {
|
|
44
|
+
const { container } = render(<InputGroup.Root />);
|
|
45
|
+
const root = container.querySelector("[data-input-group]")!;
|
|
46
|
+
|
|
47
|
+
const contracted = autoAttributes
|
|
48
|
+
.map((attribute: { name: string }) => attribute.name)
|
|
49
|
+
.sort();
|
|
50
|
+
expect(dataAttributeNames(root)).toEqual(contracted);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("always emits data-input-group", () => {
|
|
54
|
+
const { container } = render(<InputGroup.Root />);
|
|
55
|
+
expect(container.querySelector("[data-input-group]")).toHaveAttribute(
|
|
56
|
+
"data-input-group",
|
|
57
|
+
"",
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
});
|