Skip to content

Commit 58ff9a5

Browse files
feat: field data attrs (#309)
* feat: field data attrs * feat: correct prop spread
1 parent ac5a477 commit 58ff9a5

File tree

3 files changed

+230
-2
lines changed

3 files changed

+230
-2
lines changed

libs/components/src/field/field-error.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const FieldError = component$((props: PropsOf<"div">) => {
1717
}, errorRef);
1818

1919
return (
20-
<Render fallback="div" {...props} id={errorId}>
20+
<Render {...props} internalRef={errorRef} fallback="div" id={errorId}>
2121
<Slot />
2222
</Render>
2323
);

libs/components/src/field/field-root.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,15 @@ export const FieldRoot = component$((props: FieldRootProps) => {
8181
useContextProvider(fieldContextId, context);
8282

8383
return (
84-
<Render fallback="div" {...props}>
84+
<Render
85+
{...props}
86+
fallback="div"
87+
data-qds-root
88+
data-disabled={isDisabled.value}
89+
data-required={isRequired.value}
90+
data-readonly={isReadOnly.value}
91+
data-empty={!rootValue.value || rootValue.value === "" ? "true" : undefined}
92+
>
8593
<Slot />
8694
</Render>
8795
);

libs/components/src/field/field.browser.tsx

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,3 +555,223 @@ test("textarea with local value prop changes independently from root", async ()
555555

556556
await expect.element(Textarea).toHaveValue("value changed");
557557
});
558+
559+
test("field root has data-qds-root attribute", async () => {
560+
render(<BasicInput />);
561+
562+
await expect.element(Root).toBeVisible();
563+
564+
const rootElement = await Root.element();
565+
expect(rootElement?.hasAttribute("data-qds-root")).toBe(true);
566+
});
567+
568+
test("field root has data-disabled when disabled", async () => {
569+
render(<BasicInput disabled />);
570+
571+
await expect.element(Root).toBeVisible();
572+
573+
const rootElement = await Root.element();
574+
expect(rootElement?.hasAttribute("data-disabled")).toBe(true);
575+
});
576+
577+
test("field root does not have data-disabled when not disabled", async () => {
578+
render(<BasicInput />);
579+
580+
await expect.element(Root).toBeVisible();
581+
582+
const rootElement = await Root.element();
583+
expect(rootElement?.hasAttribute("data-disabled")).toBe(false);
584+
});
585+
586+
test("field root has data-required when required", async () => {
587+
render(<BasicInput required />);
588+
589+
await expect.element(Root).toBeVisible();
590+
591+
const rootElement = await Root.element();
592+
expect(rootElement?.hasAttribute("data-required")).toBe(true);
593+
});
594+
595+
test("field root does not have data-required when not required", async () => {
596+
render(<BasicInput />);
597+
598+
await expect.element(Root).toBeVisible();
599+
600+
const rootElement = await Root.element();
601+
expect(rootElement?.hasAttribute("data-required")).toBe(false);
602+
});
603+
604+
test("field root has data-readonly when readonly", async () => {
605+
render(<BasicInput readOnly />);
606+
607+
await expect.element(Root).toBeVisible();
608+
609+
const rootElement = await Root.element();
610+
expect(rootElement?.hasAttribute("data-readonly")).toBe(true);
611+
});
612+
613+
test("field root does not have data-readonly when not readonly", async () => {
614+
render(<BasicInput />);
615+
616+
await expect.element(Root).toBeVisible();
617+
618+
const rootElement = await Root.element();
619+
expect(rootElement?.hasAttribute("data-readonly")).toBe(false);
620+
});
621+
622+
test("field root has data-empty when value is empty", async () => {
623+
render(<BasicInput />);
624+
625+
await expect.element(Root).toBeVisible();
626+
627+
const rootElement = await Root.element();
628+
expect(rootElement?.hasAttribute("data-empty")).toBe(true);
629+
});
630+
631+
const FilledValueInput = component$(() => {
632+
const initialValue = useSignal("test value");
633+
return (
634+
<Field.Root data-testid="root" bind:value={initialValue}>
635+
<Field.Label data-testid="label">Username</Field.Label>
636+
<Field.Input data-testid="input" />
637+
</Field.Root>
638+
);
639+
});
640+
641+
test("field root does not have data-empty when value exists", async () => {
642+
render(<FilledValueInput />);
643+
644+
await expect.element(Root).toBeVisible();
645+
646+
const rootElement = await Root.element();
647+
expect(rootElement?.hasAttribute("data-empty")).toBe(false);
648+
});
649+
650+
test("field root data-empty updates when input changes from empty to filled", async () => {
651+
render(<BasicInput />);
652+
653+
await expect.element(Root).toBeVisible();
654+
655+
const rootElement = await Root.element();
656+
expect(rootElement?.hasAttribute("data-empty")).toBe(true);
657+
658+
await userEvent.fill(Input, "test value");
659+
660+
expect(rootElement?.hasAttribute("data-empty")).toBe(false);
661+
});
662+
663+
test("field root data-empty updates when input changes from filled to empty", async () => {
664+
render(<FilledValueInput />);
665+
666+
await expect.element(Root).toBeVisible();
667+
668+
const rootElement = await Root.element();
669+
expect(rootElement?.hasAttribute("data-empty")).toBe(false);
670+
671+
await userEvent.clear(Input);
672+
673+
expect(rootElement?.hasAttribute("data-empty")).toBe(true);
674+
});
675+
676+
const DynamicStateInput = component$(() => {
677+
const disabled = useSignal(false);
678+
const required = useSignal(false);
679+
const readOnly = useSignal(false);
680+
681+
return (
682+
<div>
683+
<Field.Root
684+
data-testid="root"
685+
bind:disabled={disabled}
686+
bind:required={required}
687+
bind:readOnly={readOnly}
688+
>
689+
<Field.Label data-testid="label">Username</Field.Label>
690+
<Field.Input data-testid="input" />
691+
</Field.Root>
692+
<button
693+
type="button"
694+
data-testid="toggle-disabled"
695+
onClick$={() => (disabled.value = !disabled.value)}
696+
>
697+
Toggle Disabled
698+
</button>
699+
<button
700+
type="button"
701+
data-testid="toggle-required"
702+
onClick$={() => (required.value = !required.value)}
703+
>
704+
Toggle Required
705+
</button>
706+
<button
707+
type="button"
708+
data-testid="toggle-readonly"
709+
onClick$={() => (readOnly.value = !readOnly.value)}
710+
>
711+
Toggle Readonly
712+
</button>
713+
</div>
714+
);
715+
});
716+
717+
test("field root data-disabled updates dynamically", async () => {
718+
render(<DynamicStateInput />);
719+
720+
await expect.element(Root).toBeVisible();
721+
await expect.element(Root).not.toHaveAttribute("data-disabled");
722+
723+
await userEvent.click(page.getByTestId("toggle-disabled"));
724+
await expect.element(Root).toHaveAttribute("data-disabled");
725+
726+
await userEvent.click(page.getByTestId("toggle-disabled"));
727+
await expect.element(Root).not.toHaveAttribute("data-disabled");
728+
});
729+
730+
test("field root data-required updates dynamically", async () => {
731+
render(<DynamicStateInput />);
732+
733+
await expect.element(Root).toBeVisible();
734+
await expect.element(Root).not.toHaveAttribute("data-required");
735+
736+
await userEvent.click(page.getByTestId("toggle-required"));
737+
await expect.element(Root).toHaveAttribute("data-required");
738+
739+
await userEvent.click(page.getByTestId("toggle-required"));
740+
await expect.element(Root).not.toHaveAttribute("data-required");
741+
});
742+
743+
test("field root data-readonly updates dynamically", async () => {
744+
render(<DynamicStateInput />);
745+
746+
await expect.element(Root).toBeVisible();
747+
await expect.element(Root).not.toHaveAttribute("data-readonly");
748+
749+
await userEvent.click(page.getByTestId("toggle-readonly"));
750+
await expect.element(Root).toHaveAttribute("data-readonly");
751+
752+
await userEvent.click(page.getByTestId("toggle-readonly"));
753+
await expect.element(Root).not.toHaveAttribute("data-readonly");
754+
});
755+
756+
const AllAttributesInput = component$(() => {
757+
const fieldValue = useSignal("");
758+
return (
759+
<Field.Root data-testid="root" bind:value={fieldValue} disabled required readOnly>
760+
<Field.Label data-testid="label">Username</Field.Label>
761+
<Field.Input data-testid="input" />
762+
</Field.Root>
763+
);
764+
});
765+
766+
test("field root has all data attributes simultaneously", async () => {
767+
render(<AllAttributesInput />);
768+
769+
await expect.element(Root).toBeVisible();
770+
771+
const rootElement = await Root.element();
772+
expect(rootElement?.hasAttribute("data-qds-root")).toBe(true);
773+
expect(rootElement?.hasAttribute("data-disabled")).toBe(true);
774+
expect(rootElement?.hasAttribute("data-required")).toBe(true);
775+
expect(rootElement?.hasAttribute("data-readonly")).toBe(true);
776+
expect(rootElement?.hasAttribute("data-empty")).toBe(true);
777+
});

0 commit comments

Comments
 (0)