From 622454cc31511da20b6a3639e80d1314680c9220 Mon Sep 17 00:00:00 2001 From: Dani Sandoval Date: Wed, 27 May 2026 11:18:03 -0600 Subject: [PATCH 1/9] test(Label): add visual regression baseline before CSS Modules migration --- src/components/Label/Label.stories.tsx | 33 ++++ tests/utils/label.spec.ts | 144 ++++++++++++++++++ .../label-default-dark-chromium-linux.png | Bin 0 -> 1354 bytes .../label-default-light-chromium-linux.png | Bin 0 -> 1367 bytes .../label-disabled-dark-chromium-linux.png | Bin 0 -> 1281 bytes .../label-disabled-light-chromium-linux.png | Bin 0 -> 1248 bytes .../label-error-dark-chromium-linux.png | Bin 0 -> 1344 bytes .../label-error-light-chromium-linux.png | Bin 0 -> 1241 bytes ...label-focus-within-dark-chromium-linux.png | Bin 0 -> 1506 bytes ...abel-focus-within-light-chromium-linux.png | Bin 0 -> 1394 bytes .../label-hover-dark-chromium-linux.png | Bin 0 -> 1352 bytes .../label-hover-light-chromium-linux.png | Bin 0 -> 1366 bytes 12 files changed, 177 insertions(+) create mode 100644 tests/utils/label.spec.ts create mode 100644 tests/utils/label.spec.ts-snapshots/label-default-dark-chromium-linux.png create mode 100644 tests/utils/label.spec.ts-snapshots/label-default-light-chromium-linux.png create mode 100644 tests/utils/label.spec.ts-snapshots/label-disabled-dark-chromium-linux.png create mode 100644 tests/utils/label.spec.ts-snapshots/label-disabled-light-chromium-linux.png create mode 100644 tests/utils/label.spec.ts-snapshots/label-error-dark-chromium-linux.png create mode 100644 tests/utils/label.spec.ts-snapshots/label-error-light-chromium-linux.png create mode 100644 tests/utils/label.spec.ts-snapshots/label-focus-within-dark-chromium-linux.png create mode 100644 tests/utils/label.spec.ts-snapshots/label-focus-within-light-chromium-linux.png create mode 100644 tests/utils/label.spec.ts-snapshots/label-hover-dark-chromium-linux.png create mode 100644 tests/utils/label.spec.ts-snapshots/label-hover-light-chromium-linux.png diff --git a/src/components/Label/Label.stories.tsx b/src/components/Label/Label.stories.tsx index a12286ff4..14fbdb7bf 100644 --- a/src/components/Label/Label.stories.tsx +++ b/src/components/Label/Label.stories.tsx @@ -1,5 +1,6 @@ import { Meta, StoryObj } from '@storybook/react-vite'; import { Label } from '@/components/Label'; +import type { LabelProps } from './Label.types'; const meta: Meta = { component: Label, @@ -11,6 +12,26 @@ export default meta; type Story = StoryObj; +const LabelHarness = ({ disabled, error, children }: LabelProps) => ( +
+ +
+); + export const Playground: Story = { args: { children: 'Form Field label', @@ -24,3 +45,15 @@ export const Playground: Story = { ), }; + +export const Default: Story = { + render: () => Form Field label, +}; + +export const Disabled: Story = { + render: () => Form Field label, +}; + +export const Error: Story = { + render: () => Form Field label, +}; diff --git a/tests/utils/label.spec.ts b/tests/utils/label.spec.ts new file mode 100644 index 000000000..d57ebdf85 --- /dev/null +++ b/tests/utils/label.spec.ts @@ -0,0 +1,144 @@ +import { test as it, expect } from '@playwright/test'; +import { getStoryUrl } from './index'; + +const { describe, use } = it; + +const harnessLocator = '[data-testid="label-harness"]'; + +describe('Label Visual Regression', () => { + describe('Light Theme (Storybook Global)', () => { + describe('Variants', () => { + it('default matches snapshot', async ({ page }) => { + await page.goto(getStoryUrl('forms-label--default', 'light'), { + waitUntil: 'networkidle', + }); + const harness = page.locator(harnessLocator).first(); + await expect(harness).toBeVisible({ timeout: 10000 }); + await expect(harness).toHaveScreenshot('label-default-light.png', { + maxDiffPixels: 100, + }); + }); + + it('disabled matches snapshot', async ({ page }) => { + await page.goto(getStoryUrl('forms-label--disabled', 'light'), { + waitUntil: 'networkidle', + }); + const harness = page.locator(harnessLocator).first(); + await expect(harness).toBeVisible({ timeout: 10000 }); + await expect(harness).toHaveScreenshot('label-disabled-light.png', { + maxDiffPixels: 100, + }); + }); + + it('error matches snapshot', async ({ page }) => { + await page.goto(getStoryUrl('forms-label--error', 'light'), { + waitUntil: 'networkidle', + }); + const harness = page.locator(harnessLocator).first(); + await expect(harness).toBeVisible({ timeout: 10000 }); + await expect(harness).toHaveScreenshot('label-error-light.png', { + maxDiffPixels: 100, + }); + }); + }); + + describe('Interactive States', () => { + it('hover state matches snapshot', async ({ page }) => { + await page.goto(getStoryUrl('forms-label--default', 'light'), { + waitUntil: 'networkidle', + }); + const harness = page.locator(harnessLocator).first(); + await expect(harness).toBeVisible({ timeout: 10000 }); + const label = harness.locator('label').first(); + await label.hover(); + await page.waitForTimeout(100); + await expect(harness).toHaveScreenshot('label-hover-light.png', { + maxDiffPixels: 100, + }); + }); + + it('focus-within state matches snapshot', async ({ page }) => { + await page.goto(getStoryUrl('forms-label--default', 'light'), { + waitUntil: 'networkidle', + }); + const harness = page.locator(harnessLocator).first(); + await expect(harness).toBeVisible({ timeout: 10000 }); + const input = harness.locator('input').first(); + await input.focus(); + await page.waitForTimeout(100); + await expect(harness).toHaveScreenshot('label-focus-within-light.png', { + maxDiffPixels: 100, + }); + }); + }); + }); + + describe('Dark Theme (System prefers-color-scheme)', () => { + use({ colorScheme: 'dark' }); + + describe('Variants', () => { + it('default matches snapshot', async ({ page }) => { + await page.goto(getStoryUrl('forms-label--default'), { + waitUntil: 'networkidle', + }); + const harness = page.locator(harnessLocator).first(); + await expect(harness).toBeVisible({ timeout: 10000 }); + await expect(harness).toHaveScreenshot('label-default-dark.png', { + maxDiffPixels: 100, + }); + }); + + it('disabled matches snapshot', async ({ page }) => { + await page.goto(getStoryUrl('forms-label--disabled'), { + waitUntil: 'networkidle', + }); + const harness = page.locator(harnessLocator).first(); + await expect(harness).toBeVisible({ timeout: 10000 }); + await expect(harness).toHaveScreenshot('label-disabled-dark.png', { + maxDiffPixels: 100, + }); + }); + + it('error matches snapshot', async ({ page }) => { + await page.goto(getStoryUrl('forms-label--error'), { + waitUntil: 'networkidle', + }); + const harness = page.locator(harnessLocator).first(); + await expect(harness).toBeVisible({ timeout: 10000 }); + await expect(harness).toHaveScreenshot('label-error-dark.png', { + maxDiffPixels: 100, + }); + }); + }); + + describe('Interactive States', () => { + it('hover state matches snapshot', async ({ page }) => { + await page.goto(getStoryUrl('forms-label--default'), { + waitUntil: 'networkidle', + }); + const harness = page.locator(harnessLocator).first(); + await expect(harness).toBeVisible({ timeout: 10000 }); + const label = harness.locator('label').first(); + await label.hover(); + await page.waitForTimeout(100); + await expect(harness).toHaveScreenshot('label-hover-dark.png', { + maxDiffPixels: 100, + }); + }); + + it('focus-within state matches snapshot', async ({ page }) => { + await page.goto(getStoryUrl('forms-label--default'), { + waitUntil: 'networkidle', + }); + const harness = page.locator(harnessLocator).first(); + await expect(harness).toBeVisible({ timeout: 10000 }); + const input = harness.locator('input').first(); + await input.focus(); + await page.waitForTimeout(100); + await expect(harness).toHaveScreenshot('label-focus-within-dark.png', { + maxDiffPixels: 100, + }); + }); + }); + }); +}); diff --git a/tests/utils/label.spec.ts-snapshots/label-default-dark-chromium-linux.png b/tests/utils/label.spec.ts-snapshots/label-default-dark-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..58d0a7227ff02e88584397849aca1290f56eebfd GIT binary patch literal 1354 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 KMNUMnLSTY6K~;?a literal 0 HcmV?d00001 diff --git a/tests/utils/label.spec.ts-snapshots/label-error-dark-chromium-linux.png b/tests/utils/label.spec.ts-snapshots/label-error-dark-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..ae0c93c207d15c60b0a1f1f4e24fe76dbc770b00 GIT binary patch literal 1344 zcmV-G1;6@bC90%~XONLqJFmo_2hDHxbp}{tXw8ZB>wgM$R%JjGInfhB;U5IF!tA#wnQLgWApRWx#Rb>rM?ZpI%y2w$llx2{#4yXD1N%h?z}hwh4{ zs;<5nk0h1D7ki?ZA)mkpRV1=L#mhUAN&r4P`Zo#Gz0Gw#nL3@cs+w;?ExsELb4Fa5 zo1KX)?<)C=z+9+a5oD{wvJ?CDme3Fjxi*%h%WmgoB1}xqXX;zpoMs}eM5CkO^#lDX ztnEs>&y^4s;;F1wuBXN4by{e7D;^u|S=c?jr`7gdPr&D`rGzDZgVH&W3Rgo*udm)? zVTiOe@N6Qwd6;mj$`zOk*4I%YKR*-xGbv;X$EZ^jpd6*m)xh9qG z?CtAWp}c|iw(66e+3LojP>V-hj!eduD3`aEDK_q9YZ`j}*XWh;*8bs%v@_Jv?5-&E zxyjA0&X0%RN>}||PyIIbyrKYQ(mk0d=K7sS!kzqcb?tuds+U$XdA2nG{{yV8`uVx0%S&hRS?(QaY{emG4`6M^2MkhNDbGw~wAI#+nk6A@d@5xKy{*md_$=0mRb=*oQwn__$mSDRcwAhm^7Wt+br^b&CR&=?i?RUPzBbiUI9;F!*Nf)z^uLt$VEU?@Zmz)*-BfT0jM07D^i z0ER;301So50T}8(00030|LuGF#Q*>R21!IgR09A)U0+GjIR2CX0000WLk7;d@E#~i&|Zu6}f ztZJ$X5i}u85hbB3eN=RNLx_4=7UmEFg%LxPVNKQlryOja@%N1U=d%q-{P6IQ06Fjc6OEkgqoY1>otVP^(V+%$WIfX zBjh~6g(X0w5IKNIA#wnbLgWA<)iZLfT5*{T%@3Wt=#uTV$bam@Dve5hN!2$h^1^Rv zR`ZKo+od~m`;#1ct;T-V3iujEbM&Op<1){W$2#-8ZuO}EzRdq+(`T~6yy7;m?!U{%pp zvgv1>&MW%5Q`$7tg`uGZExmO??>b+lB`Q04`;S^@&p|ouF&$b^^fl*vn{8BztGc?t zl*Mf6U04V5_Z@3lLci~p?kUyR-^n>wVPneja-SOY;EbsJKU{j?LEIU=jGp=+Gy6E zGL`+Dv0_)7mlXvVQ0RrFU;q7^JM*38TVH?fl~}nbeSNZ!zpD^pXr!vYcB&IeJ;+#$ zJs7ugpXn^vwkd*DWz0;EX>7zyiJE^Wle)>}J)sy7r)%APODNU%;P$8z?x!3oh5 zUUJPTADCJqv?#!Uu6!>?`6%-nLbRxwV4YEx2rdF^qIKLWX`H^Fq|?>dal&Yf(koO6 z`b=lQZwm3(&QCAC7=F>+cG#1x40&WkYo07c9sa zMt);&y2B3-6{yz-OgFUQ^wO;f?SuN9Ao8|IcHBu-_1T#&iv&dp`}Ko zTetG<$>j69Yk&6IPmCTlG=;}%?kl_e_sOl9UzhpZude&Xwl+(Gru3i0BxxbJMF)AcgrMQ19>$n^=zIRz#PzbJgs@eQZ)0p`%%D4&hhEMbF^kit7Qd`0z<`GEp?ZD6I|<+_ z@J<4N{|iJ8AX11NK%@{kfJou-N#F3OV|NjZ!5eEPO00v1!K~w_(P@Xzu<&=sJ00000NkvXXu0mjf DRkLK- literal 0 HcmV?d00001 diff --git a/tests/utils/label.spec.ts-snapshots/label-focus-within-dark-chromium-linux.png b/tests/utils/label.spec.ts-snapshots/label-focus-within-dark-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..273805b7370e489048b74915a5c102a3df1989d7 GIT binary patch literal 1506 zcmV<81s(c{P)CfApx@{vSdB6l&9naq&lCd5v1hdV&JR9P?`>SUtu2L2R05Hl&2moLpNI3uu1t|xBp&;b|FchR50EU8;1He!h5NH}nlHWgkc++{4 zymAErphMc*dnvs9X<|Z>q}twda9^%@)6>zt6f4zR~ z^Z9EBK$UAj?~NNHBO?n73kU!m>aqCw^XJ84@zJA4bq7GowVV+W^1iXZzppm{u2=$KC`dT~45gcL*!E3) z=dHMV_A9?D?=6dT6MgX*} z;klghAroW6eEDj)Qpo2ObOvgo?4i_ryda7)0^onA@xoFmq}Tc7<%ISU(s?ZhNc$i+ zzmnN`v$-@oe%Ds65v3xKj6Yl4+}Voz>{m)PVJ0-oZ@(3{7dg*Y}Z|Z zbutPj9YY@4MQJsa>=_KJ;nZz67NZ{fiPAao!i+;gy^nDC77^#CSz+`S|MGV z^*PDvSWLQ^_{N*u#>z~v&szO!!=M1PrL}W8Obv`j88=#wvTQ6v(36jc4^zlwu@c#t zu@NSoFKRQo%3oryR6{NQcMSxvYZXS=2Rqm$pZRWo^NEu$zcZO)qQf zLxNIE*Dn9;*TlM=dEJU_Rb$`jb_adcbE82xL7dNRt_@k+Lusxu84>^f*rD@($mu8a=TFo~gw<{nC1j^40&8#cp}w+CMmOa#8A}iHmeCN zA1IB30?@9O&gB%5kMXkIKN|Gh9q|000I_L_t&o0LHA^kiP!FVE_OC07*qo IM6N<$f^)RceEyT`2F~^YH65lPRi+$2v!a^ug6PBbM&gh2Z7Puv_T3L*!9tL`E&jH9EYH*el( zn$~Ky5CGTEG#v~E*Vfj?$H)72_yTT)59P&+7mA`F031akkq;j}^lgI34QFz4(lCte z?QKyM5dhch?d{FY&5ezX9UL6=?EsM*&b@p0?4KtmCkTK6oDUMXd;);0AaVe>YH;KZ zOEK^LapvOnAhKCE(Z8)7c{76jc#*IC1*V?x; z0$^m0Hwg%ju$V~(neGPVV@dQGT&psU_^9MzCkO<<|BmyCCB^W9D0W{w$DAe3sH?Ix zJLP>i6;9-|W_OP0Oaf10@p;kvD7IPu)86E#mr{wK|G@-5FDvDo^u)*9_XQG#K6BUA zDkiQLb6b_evo@oqrle_(z3=1amzAs2tg&BO4nOgEzlyBx)GdcEV%8N|3OuqW(g`6^ zyfg-^ug8RkK7Mv-L&v?nI~N6D6hp4%uqI0@DiRj6nMJO(wRkB)?MBV;$8#I0+0JB$ z8&xc%OTQ%9T0XI&^FJuckHPj%HotQ?RJr&{g?L`v`y&@J)YSLtrQ_eMmzJcJ3N@3; zCId#fR2_J4+U_Z5cdTIMckS0WQ;}DN0?Y{TS{H$+^d!>pcyiTVg69|R;R&Bnhev+nyvr|BiH@uXTM9O|^VtbPfNMVtq zSSe6wTG3dLV~lv{y;j9-`v4}Ls)Twi*e+-A$>k_NL87H(NLx{>tr@P@S}j*mAl)5I zkj1rcD+9g<>641O)EQXK&oY+Yz-{EUw+S}1oJq~v=d=P-|2m=S4T)nMivlpBTd(B^ zn)8?#w@kvzP@Ptiq8ZX^VtbnF_8^JNtFQA-^6W))B1RyBxJ4B^Ox*^{z%AU;Ux|~i zdVT$M1DzlJmSwaN)f)^!PcR-dh*2#rzgkrri1m93d%ULAE6p(7A$chK<27tqreje6 zM&E5d*RQVdI@Lv5?5w=R}uR#VHr`>1K<&2_nqy+OfVhB_AoU_?V+K4F5XVkTs% ztMQlXy61U*BR=){64xnLNQ=0zz(1Z%H@L;uu}kGhUo=xl@^~u|ds)E3j6e_A3*n2r zUh?S+QvhIUU#uiO{&0vWEq=F@QgW000I_L_t&o0HF0uE4X$#82|tP07*qoM6N<$f@JuV ABme*a literal 0 HcmV?d00001 diff --git a/tests/utils/label.spec.ts-snapshots/label-hover-dark-chromium-linux.png b/tests/utils/label.spec.ts-snapshots/label-hover-dark-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..390228b04a6d080f0324e13571dca570dd49c78d GIT binary patch literal 1352 zcmV-O1-JT%P)1Qt-}_qqYK1BTF^l{W(yW-VH<30vFxra9=dVo@p^&0((?ON>0fE;x#(FNxD`$5`|RQlety-Lx_{%jwUYDXXV*?C{of44 z^Tt~qY;C!X*9bsW3}-AkPfv$q32pn2oRWF9c`H{XPm*&pS~klG!2gc%O#+@vCzliI z-bFR8;~i(uG#;{ZB|bMAjV*2Eu-FJR<@K!G@l*fcyeh6+N}Y8z@xi18k@s|b*ygS&9kbVKRVKoNvudrs_hWy3&C7}cRLSu2r_6il_Jd2s_dFTCek&!n z$}~AL5SbIbzWH_H_@d=leJm9hT|lm@3!~A=#8wL?*+g$qP0c)6Ueo`aPBuHpZxf4a zSvI!RcBRg4AtY;9WqxOkt)W$BtFhs!l)}mANMl3DE8Ax`N=I&!n_MIrEgg)?YJ24} z=TX=k|KZE=;y(2*MI*A)QVbF2T4LzO$bx9`;yHU?_Yt3MbTw~O6rfrM9?NlUQ==9u zqdcSW;wY=2=X5TKMH1QGg7fEh=DY6*FD)s)ip#xVP(`#erDLnHGApa^>U-F=v#sJ_ zWasLqj1pZf9Sm2M94{9+VyQpb)pPD`JJD2yi`_`6+2VROw|$g)PSc5tF-nPHQGhB{ zHkPB}x@2~nxM;$(^m~Wd&W55zliv@V@cJ6tJ3G8AyOI>sC&onqs#M{>HRl#1 z_rqc~FL<$SYsPXQ96P{Gt$%K-Ttt++($`6}^{0;`V`-i2Jdv!hITaqTcKDBnF1IQ3 z;|pnL!~UOA*qn{dH1u@ed`NgEwUW_G9aj;=%+o(e^M&3zl1WU5haVTeJ`9TjROvN5 zRIeiVB>@}+en|lEUxCO0m$>8fj;7KGS22Oa{$cHE((b182F`^pY0^J)=C)bfWytsQ z(j@2H{LU@+%em)-gqxX}ApoG2*9ib<2$2KO6e0(pDMSuHQ-~aZrVu#*O(Ajsn&Jqg zsj;z<0H|`gT&bE6x%9;Hrt;qeNKm;>aAOIeDMSuHQ-~aZrVu#*O{LsEVWHmdTH1Ze zLQl)i&yQ?2Qt~pru*#EQu)$FCm$I5Al_Zr@Q~o=20DfR|sT3;D+l4TodX)VF6v(^}*3gGXbWdCR-i zLhsM+$CLC;Ywi9$bM1uX#f(t#a)m-Ozlj`GZoq?|g zQ@srv?r-l9S*hdb(f7CL0;s)sCk1xq)jJ&vXy6rHTS%NOhKDQ{L9E zKWj~Nv>rJ2k?pBRWW|o%b(;*7RIXqnXM64p)mm)vjPbeaw%66nuD)0}c8{1_R$Xbc z@CLiNCrsY4mzfQ6&Dw6?(}`(9460SC=2}92k0_h#8`c+_XmP{#0e@Sce{x-6qEV6j zVt^Do6N6c6oIY_uOmQBx$liS19xo*gj_R|Be=;nSf_S-{ik;4;^4J7x6T&CY^#?|) zS7xgf1<2I0*K&NGNnnyR#i&uBv$tQAHwc^}MPx>1pL6tYOTD?Ll#BnRiC4H~R3Rji zXi%o5DM=o>+;aT#Twl|eOy;isX*o*x`N8N4bX=;)5oxgN>Xo*KlSG$qqEUsg`O<=r z7*gy(78!{sjHwp|$WVG~IYr2!@_w8_AmED|8{wwivG;s5}Cm_ioudC&YQ!bx8 z-zr#aMT*ZIAIy_`*E_x8l=~;~$(8bgK8J<^UEKkBq2rYA){P$TlfGZvu7^=Wi8*$Q zs$LWzLuqcr3uVracey({FQ2_jV>_1nob}82Q0w1|mvh86NY8xP&h+MrZFO5>b{4U-f&6nTRW>tgC%=<@{I237211V&tW{7Mfr27I*K`g&@f< zx9_UowvsMY6d*&d;Yan-f^QPQa^RZ;0RI<=9Dt?}IRH%|asZmb_DS0C+b6Ijl|42@ z4nR}5u>{Z*A_t%;L=Hexh#Y{X5IF!%A#wnkLgWB6^%npD0RR7K_eOXC000I_L_t&o Y0F#a}UbwPo?*IS*07*qoM6N<$f`v__B>(^b literal 0 HcmV?d00001 From 65846b8d1341be3d185ac3a2a129d5203bed3695 Mon Sep 17 00:00:00 2001 From: Dani Sandoval Date: Wed, 27 May 2026 11:21:49 -0600 Subject: [PATCH 2/9] chore(Label): migrate styling from styled-components to CSS Modules --- .changeset/migrate-label-to-css-modules.md | 5 ++ src/components/Label/Label.module.css | 36 ++++++++++++ src/components/Label/Label.tsx | 68 +++++++++------------- 3 files changed, 69 insertions(+), 40 deletions(-) create mode 100644 .changeset/migrate-label-to-css-modules.md create mode 100644 src/components/Label/Label.module.css diff --git a/.changeset/migrate-label-to-css-modules.md b/.changeset/migrate-label-to-css-modules.md new file mode 100644 index 000000000..aa427b8c6 --- /dev/null +++ b/.changeset/migrate-label-to-css-modules.md @@ -0,0 +1,5 @@ +--- +'@clickhouse/click-ui': patch +--- + +Migrate Label from styled-components to css modules with no change in behavior diff --git a/src/components/Label/Label.module.css b/src/components/Label/Label.module.css new file mode 100644 index 000000000..8cacc00ed --- /dev/null +++ b/src/components/Label/Label.module.css @@ -0,0 +1,36 @@ +.label { + color: var(--click-field-color-label-default); + font: var(--click-field-typography-label-default); +} + +.label:hover { + color: var(--click-field-color-label-hover); + font: var(--click-field-typography-label-hover); +} + +.label:focus, +.label:focus-within { + color: var(--click-field-color-label-active); + font: var(--click-field-typography-label-active); +} + +/* stylelint-disable no-descending-specificity -- error/disabled modifiers + intentionally override :hover/:focus/:focus-within to mirror the original + ternary, which emitted no hover/focus rules when disabled or error were + set. */ +.label.label_error, +.label.label_error:hover, +.label.label_error:focus, +.label.label_error:focus-within { + color: var(--click-field-color-label-error); + font: var(--click-field-typography-label-error); +} + +.label.label_disabled, +.label.label_disabled:hover, +.label.label_disabled:focus, +.label.label_disabled:focus-within { + color: var(--click-field-color-label-disabled); + font: var(--click-field-typography-label-disabled); +} +/* stylelint-enable no-descending-specificity */ diff --git a/src/components/Label/Label.tsx b/src/components/Label/Label.tsx index eb34f27b0..0af490949 100644 --- a/src/components/Label/Label.tsx +++ b/src/components/Label/Label.tsx @@ -1,47 +1,35 @@ -import { styled } from 'styled-components'; +import { cn, cva } from '@/lib/cva'; import { LabelProps } from './Label.types'; +import styles from './Label.module.css'; -interface FormFieldLableProps { - disabled?: boolean; - $error?: boolean; - htmlFor?: string; -} +const labelVariants = cva(styles.label, { + variants: { + disabled: { + true: styles['label_disabled'], + false: '', + }, + error: { + true: styles['label_error'], + false: '', + }, + }, + defaultVariants: { + disabled: false, + error: false, + }, +}); -const FormFieldLabel = styled.label` - ${({ theme, disabled, $error }) => ` - ${ - disabled - ? ` - color: ${theme.click.field.color.label.disabled}; - font: ${theme.click.field.typography.label.disabled}; - ` - : $error - ? ` - color: ${theme.click.field.color.label.error}; - font: ${theme.click.field.typography.label.error}; - ` - : ` - color: ${theme.click.field.color.label.default}; - font: ${theme.click.field.typography.label.default}; - &:hover { - color: ${theme.click.field.color.label.hover}; - font: ${theme.click.field.typography.label.hover}; - } - &:focus, &:focus-within { - color: ${theme.click.field.color.label.active}; - font: ${theme.click.field.typography.label.active}; - } - ` - }; - `} -`; - -export const Label = ({ disabled, error, children, ...props }: LabelProps) => ( - ( + + ); From 5879c5f53d514d0e11a77c6a7adae8e7bb732ca3 Mon Sep 17 00:00:00 2001 From: Dani Sandoval Date: Wed, 27 May 2026 11:41:10 -0600 Subject: [PATCH 3/9] test(TextField): adapt label-color assertion to CSS Modules migration The previous assertion relied on styled-components injecting the default label color (rgb(179, 182, 189)) into the jsdom stylesheet at render time. Now that Label uses CSS Modules, jsdom does not load the `.label { color: var(--click-field-color-label-default) }` rule, and the computed color falls back to canvastext. The test's actual intent is to verify that no `labelColor` override is applied when the prop is unset. Assert that directly via the absence of an inline color style. Default-color rendering is covered by visual regression in tests/display/label.spec.ts. The second test (custom labelColor) is unchanged: the InputWrapper's StyledLabel wrapper is still styled-components and continues to inject its color rule into jsdom. --- src/components/TextField/TextField.test.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/TextField/TextField.test.tsx b/src/components/TextField/TextField.test.tsx index ad58ef5f8..08b3035a7 100644 --- a/src/components/TextField/TextField.test.tsx +++ b/src/components/TextField/TextField.test.tsx @@ -26,7 +26,7 @@ const TextFieldWrapper = ({ describe('TextField', () => { describe('label color', () => { - it('is the default color when labelColor is not set', () => { + it('renders the label without an explicit color override when labelColor is not set', () => { const label = 'Hello there!'; const text = 'General Kenobi'; @@ -39,7 +39,13 @@ describe('TextField', () => { const labelElement = getByText(label); expect(labelElement).toBeInTheDocument(); - expect(labelElement).toHaveStyle('color: rgb(179, 182, 189)'); + // The Label's default color is set via the CSS Module rule + // `.label { color: var(--click-field-color-label-default) }`, which + // jsdom does not load. Visual parity is covered by the Playwright + // suite at tests/display/label.spec.ts. The assertion below verifies + // the InputWrapper does not inject a styled-components color override + // when no labelColor prop is passed. + expect(labelElement.getAttribute('style')).not.toContain('color'); }); it('is the color of the passed in labelColor', () => { From ea67c424f22be23cbf435ffae981534c0510fd32 Mon Sep 17 00:00:00 2001 From: Dani Sandoval Date: Wed, 27 May 2026 11:50:05 -0600 Subject: [PATCH 4/9] fix(Label, TextField test): resolve unit-tests and code-quality CI failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Prettier wants the Label component destructure on one line; reformat to match. - The earlier `getAttribute('style')` assertion returned `null` when no style attribute was present, which `.not.toContain('color')` rejected. Use `element.style.color` instead, which returns `''` for "no inline color set" — the actual semantic we want to verify. --- src/components/Label/Label.tsx | 8 +------- src/components/TextField/TextField.test.tsx | 5 +++-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/components/Label/Label.tsx b/src/components/Label/Label.tsx index 0af490949..e82364df0 100644 --- a/src/components/Label/Label.tsx +++ b/src/components/Label/Label.tsx @@ -19,13 +19,7 @@ const labelVariants = cva(styles.label, { }, }); -export const Label = ({ - disabled, - error, - children, - className, - ...props -}: LabelProps) => ( +export const Label = ({ disabled, error, children, className, ...props }: LabelProps) => (