@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.12",
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
+ });