@thorprovider/blocks 1.0.0 → 2.0.0
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/CHANGELOG.md +48 -0
- package/README.md +156 -40
- package/package.json +1 -1
- package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
- package/src/blocks/application/login-forms/login-page.tsx +240 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
- package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
- package/src/blocks/application/verification/verify-1.tsx +2 -2
- package/src/blocks/application/viewport-ownership.test.tsx +41 -0
- package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
- package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
- package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
- package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
- package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
- package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
- package/src/blocks/{marketing/navbars/navbar-4-account-menu.test.tsx → e-commerce/navigation/storefront-navbar-account-menu.test.tsx} +8 -8
- package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
- package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +40 -40
- package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +17 -17
- package/src/blocks/e-commerce/product-list/product-card.tsx +63 -40
- package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +2 -2
- package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
- package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
- package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
- package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
- package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
- package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
- package/src/blocks/marketing/footers/footer-section.tsx +532 -0
- package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
- package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
- package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
- package/src/blocks/marketing/navbars/navbar.tsx +735 -0
- package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
- package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
- package/src/index.ts +80 -81
- package/src/lib/contracts-curated.test.ts +5 -5
- package/src/lib/contracts.test.ts +55 -0
- package/src/lib/contracts.ts +64 -2
- package/tsup.config.ts +1 -1
- package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
- package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
- package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
- package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
- package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
- package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
- package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
- package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
- package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
- package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
- package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
- package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
- package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
- package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
- package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
- package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
- package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
- package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
- package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
- package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
- package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# @thorprovider/blocks
|
|
2
2
|
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- dc82f71: Consolidate the category filter family into one canonical `CategoryListing` block.
|
|
8
|
+
|
|
9
|
+
- Adds `CategoryListing` (export + `CategoryListingProps` and the `CategoryListing*` option, facet, status, filter and active-filter types). It is the single controlled listing controller: products, pagination and the selected query are props; `on*Change` / `onClearFilters` / `onRepair` / `onRetry` report intent; the block never fetches, parses the URL, or keeps a private query.
|
|
10
|
+
- Two bounded presentations share one contract: `sidebar` (default; persistent desktop panel plus mobile sheet) and `toolbar` (sticky mobile toolbar, filter/sort drawers and chip rail). Presentation changes chrome only.
|
|
11
|
+
- Results render through the canonical `ProductCard` (`minimal`), so the listing owns no card markup. Facets are generic option-only groups supplied by the host; a group renders only from real metadata, a real `0` count is displayed, and an absent/invalid count is omitted. Price renders only from a real finite range, sort only from advertised options, and no result total is fabricated.
|
|
12
|
+
- `ProductCard` gains `hideAddToCart` for listing cells and renders `data-slot='product-image-placeholder'` instead of an image with an empty source.
|
|
13
|
+
- Runtime: `toFilterProduct` → `toListingProduct` (returns canonical `ProductPresentation` with `Money`), `CategoryFilterPresentation(Input)` → `CategoryListingPresentation(Input)`, `mapCatalogResultsToFilterProps` → `mapCatalogResultsToCategoryListing`, and `resolveFilterPresentationSupport` → `resolveCategoryListingSupport` plus a separate `resolveCategoryFacetSupport`. The old numbered presentation metadata is gone.
|
|
14
|
+
|
|
15
|
+
BREAKING: removes the `CategoryFilter1`, `CategoryFilter2` and `CategoryFilter4` exports, their types, and the `category-filter-4-*` helpers. Consumers must migrate to `CategoryListing`:
|
|
16
|
+
|
|
17
|
+
| Removed | Migration |
|
|
18
|
+
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
19
|
+
| `CategoryFilter1` | `CategoryListing` with `presentation='sidebar'`; replace counted categories/price bands with real `categories` / `priceRange` metadata. |
|
|
20
|
+
| `CategoryFilter2` | `CategoryListing` with `presentation='sidebar'` (the default). Products become canonical `ProductPresentation` items with `Money` prices; `filterPrice` string prices are no longer accepted. |
|
|
21
|
+
| `CategoryFilter4` | `CategoryListing` with `presentation='toolbar'`; replace bike-specific facets/frame/model fields with generic `facets` groups and remove its private grid/state. |
|
|
22
|
+
|
|
23
|
+
- 28a3bd7: Consolidate the numbered checkout-form family into one canonical page-level `Checkout` block.
|
|
24
|
+
|
|
25
|
+
- Adds `Checkout` (export plus `CheckoutProps`, `CheckoutValues`, `CheckoutPresentation`, `CheckoutStepId`, `CheckoutLabels`, `CheckoutValuesFieldErrors`, `CheckoutFieldDescriptions`, `CheckoutChangeHandler`, `CheckoutOrderSummary`, `CheckoutOrderLine`, `CheckoutTotals`, `CheckoutTotalsState`, `CheckoutCountryOption`, `CheckoutShippingOption`) with a required bounded `presentation` (`single-page` | `stepper`) and an orthogonal `showReviewStep` flow option. `single-page` is the former `CheckoutForm2` structure; `stepper` is the former `CheckoutForm1` wizard; `stepper` plus the review option is the former `CheckoutForm3`. Field values are controlled (`values` + `onChange`), the step index is ephemeral block state, and `validateStep(step: CheckoutStepId)` is injected — the block contains no validation logic.
|
|
26
|
+
- Totals are canonical `Money` only and render verbatim from the host; the legacy numeric `price`/`shipping`/`tax`/`discount`/`promoDiscount` fields and their fallback summation are removed. A missing or malformed required total renders as unavailable with recovery, never as zero.
|
|
27
|
+
- Removes raw card capture: `cardNumber`, `expiryDate`, `cvv`, `cardName`, the `collectCardFields` escape hatch, the CVV visibility toggle, and `card-formatting.ts`. Payment methods remain data (`PaymentMethod[]`); card collection belongs to the host payment-provider surface.
|
|
28
|
+
- Removes the numbered exports `CheckoutForm1`, `CheckoutForm2`, `CheckoutForm3` and their `CheckoutForm1*`, `CheckoutForm2*`, `CheckoutForm3*` types.
|
|
29
|
+
|
|
30
|
+
BREAKING: consumers importing a numbered checkout must migrate to `Checkout` with the `presentation` prop (and `showReviewStep` for the review flow). Values lose the card fields and `sameAsBilling`; totals must be supplied as backend `Money`; `validateStep` now receives a `CheckoutStepId` instead of a step number.
|
|
31
|
+
|
|
32
|
+
- 06333a8: Consolidate the navigation family into two canonical components and retire the numbered navbars.
|
|
33
|
+
|
|
34
|
+
- `Navbar` is the canonical page-level header with bounded `bar`, `mega`, and `floating` presentations. Content is serializable (`links`, `items`, `featured`, `signIn`, `cta`, `logoText`, `logoHref`, `currentPath`); icons resolve through a host-bound `iconMap`; `floating` uses `sticky` so it reserves its own space.
|
|
35
|
+
- `StorefrontNavbar` is the canonical commerce shell (formerly `Navbar4`) and keeps its full storefront contract: utility strip, category navigation, optional search, account states, and the controlled cart sheet with canonical `Money`.
|
|
36
|
+
- Removed identifiers: `Navbar1`, `Navbar2`, `Navbar3`, `Navbar4`, `navbar4PropsSchema`, `Navbar4SpecProps`, `Navbar4LikeButton`, `Navbar4ProductGrid`, and the `navbar-4-*` helper modules (now `storefront-navbar-cart-sheet`, `storefront-navbar-account-menu`, `storefront-navbar-search-bar`, `storefront-navbar-mobile-nav`).
|
|
37
|
+
- The page-level header no longer owns cart state: `cartCount` and `onOpenCart` are gone. Use the commerce shell for the cart affordance.
|
|
38
|
+
|
|
39
|
+
- 65fac4b: Remove the numbered compatibility adapters `Testimonial1`, `Testimonial2`, `LoginPage1`, `LoginPage2`, `FooterSection1`, `FooterSection3`, and `FooterSection4`, including their deep module paths and legacy prop types. Migrate testimonials to `TestimonialSection` with `carousel` or `bento`, login pages to `LoginPage` with `card` or `social`, and footers to `FooterSection` with `brand-columns`, `newsletter-grid`, or `contact`. Select native footer link behavior explicitly where browser navigation is intended.
|
|
40
|
+
|
|
41
|
+
### Minor Changes
|
|
42
|
+
|
|
43
|
+
- 5d32763: The auth blocks render into the host page instead of owning it.
|
|
44
|
+
|
|
45
|
+
`LoginPage` (both `card` and `social`), `SignUpPage1`, `ForgotPasswordPage1` and `Verify1` no longer paint a full-viewport surface. They drop the `min-h-dvh` canvas, the page landmark (the `social` presentation no longer renders a root `<main>`), the `bg-linear-to-br` backdrop with `isolate`/`overflow-hidden`, the vertical centring and the page-level padding. Each block now renders a neutral, content-sized root that keeps its width cap (`max-w-md` for login and recovery, `max-w-lg` for registration) and its card styling, including the shadow.
|
|
46
|
+
|
|
47
|
+
Why: a generated storefront mounts these blocks inside the default chrome (notice + banner + navbar + footer). A viewport-height canvas inside that chrome centres the form roughly 100 px low and, for the `social` presentation, emits a second `<main>` landmark inside the page's own `<main>`.
|
|
48
|
+
|
|
49
|
+
No prop, type or export changed, so existing call sites keep compiling. What changes is visual ownership: a host that relied on the block's canvas must now supply one (a route shell, a modal, a split screen, or a template the host owns), using the same theme tokens. `@thorprovider/storefront` does exactly that for the generated `account` view, which keeps the shopper-visible result unchanged.
|
|
50
|
+
|
|
3
51
|
## 1.0.0
|
|
4
52
|
|
|
5
53
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "@thorprovider/blocks"
|
|
3
3
|
purpose: "Reusable, framework-agnostic storefront blocks built from ShadcnStore UI, decoupled from data and backend"
|
|
4
|
-
lastReviewed: "2026-10-
|
|
4
|
+
lastReviewed: "2026-10-21"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# @thorprovider/blocks
|
|
@@ -39,22 +39,18 @@ Blocks follow `src/blocks/<category>/<section>/<block>.tsx`. Each block co-locat
|
|
|
39
39
|
|
|
40
40
|
| Export | Category | Description |
|
|
41
41
|
| --- | --- | --- |
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `Navbar3` | Marketing | Floating navbar that condenses into a rounded pill on scroll |
|
|
45
|
-
| `Navbar4` | Marketing | Storefront navbar: utility bar, category links, search, account menu, cart sheet |
|
|
42
|
+
| `Navbar` | Marketing | Page-level navigation header: `bar`, `mega` (grouped flyouts plus optional featured promo) or `floating` (self-reserving pinned pill) |
|
|
43
|
+
| `StorefrontNavbar` | E-commerce | Commerce navigation shell: utility strip, category mega menu, optional search, account menu, cart sheet |
|
|
46
44
|
| `Banner1` | Marketing | Announcement banner (bar or pill) with dismiss |
|
|
47
45
|
| `HeroSection1` | Marketing | Hero with badge, headline, CTA, reviewer avatars, rating and image |
|
|
48
46
|
| `CtaSection1` | Marketing | Full-width call-to-action with badge, headline and two CTAs |
|
|
49
47
|
| `Feature` | Marketing | Canonical benefits section: `cards` (icon feature grid) or `split` (editorial split with a required image) presentation |
|
|
50
|
-
| `
|
|
51
|
-
| `Testimonial2` | Marketing | Bento testimonials grid |
|
|
48
|
+
| `TestimonialSection` | Marketing | Canonical testimonial collection section with `carousel` and `bento` presentations |
|
|
52
49
|
| `FaqBlock1` | Marketing | FAQ accordion (single or multiple open) |
|
|
53
50
|
| `PricingSection1` | Marketing | Pricing tiers with popular badge |
|
|
54
51
|
| `Statistic1` | Marketing | Metrics grid with progress bars |
|
|
55
52
|
| `LogoCloud1` | Marketing | Logo cloud with skeleton fallback |
|
|
56
|
-
| `
|
|
57
|
-
| `FooterSection3` | Marketing | Newsletter + six-column footer with social links |
|
|
53
|
+
| `FooterSection` | Marketing | Canonical page-level footer with `brand-columns`, `newsletter-grid` or `contact` presentation |
|
|
58
54
|
| `ProductCard` | E-commerce | Canonical product card (item cell of a listing); `presentation` `classic`/`minimal`/`rich`, canonical `ProductPresentation` items with `Money` prices, evidence-only merchandising |
|
|
59
55
|
| `ProductShowcase1` | E-commerce | Combined grid-and-carousel of product cards with manual paging |
|
|
60
56
|
| `ProductOverview` | E-commerce | Canonical product detail; bounded `presentation` `standard`/`gallery`/`rich` (rich adds optional specs/features/reviews tabs) |
|
|
@@ -62,35 +58,138 @@ Blocks follow `src/blocks/<category>/<section>/<block>.tsx`. Each block co-locat
|
|
|
62
58
|
| `ReviewRating1` | E-commerce | Review list with rating summary |
|
|
63
59
|
| `ReviewRating2` | E-commerce | Rating summary with distribution and review carousel |
|
|
64
60
|
| `ProductCategory` | E-commerce | Canonical category block; bounded `presentation` `grid` (image cards) / `icons` (compact icon grid) / `list` (rows with subcategory chips and optional controlled search) |
|
|
65
|
-
| `
|
|
66
|
-
| `CategoryFilter2` | E-commerce | Filter sidebar with mobile sheet |
|
|
61
|
+
| `CategoryListing` | E-commerce | Canonical category listing (results + pagination) with a controlled query and bounded `presentation` `sidebar` (persistent desktop panel + mobile sheet) / `toolbar` (sticky mobile toolbar + drawers); generic facet groups render only from real metadata |
|
|
67
62
|
| `ShoppingCart` | E-commerce | Canonical cart; backend-authoritative `Money` totals and item lines, optional promo entry and recently-viewed suggestions; `presentation` `compact` (collapsible card) or `page` (full-width review) |
|
|
68
|
-
| `
|
|
69
|
-
| `CheckoutForm2` | E-commerce | Single-page checkout with order summary beside |
|
|
63
|
+
| `Checkout` | E-commerce | Canonical page-level checkout; controlled values, backend-authoritative `Money` totals, `PaymentMethod[]` data with no raw card input; `presentation` `single-page` or `stepper`, optional `showReviewStep` |
|
|
70
64
|
| `CheckoutConfirmation` | E-commerce | Order confirmation with fulfilment timeline |
|
|
71
65
|
| `OrderHistory1` | E-commerce | Orders list |
|
|
72
66
|
| `OrderHistory2` | E-commerce | Order cards with carousel items |
|
|
73
67
|
| `StorefrontHero` | E-commerce | Canonical storefront hero; `presentation` `compact` (gradient headline, CTAs, product grid), `showcase` (search + rotating product carousel), `editorial` (departments, featured deals, store locator) or `promotional` (rotating deals bar, category cards) |
|
|
74
68
|
| `AppShell1` | Application | Application shell with navigation slots |
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `SignUpPage1` | Application | Registration card with social options |
|
|
69
|
+
| `LoginPage` | Application | Canonical sign-in form with explicit `card` or `social` presentation |
|
|
70
|
+
| `SignUpPage1` | Application | Registration form with social options (same profile as `LoginPage`) |
|
|
78
71
|
| `ForgotPasswordPage1` | Application | Four-step password recovery wizard |
|
|
79
72
|
| `Verify1` | Application | Two-factor verification (OTP or backup code) |
|
|
80
73
|
|
|
81
|
-
Interactions are exposed as callbacks (e.g. `onAddToCart`, `onBuyNow`, `onSelectColor`). `StorefrontHero`
|
|
74
|
+
Interactions are exposed as callbacks (e.g. `onAddToCart`, `onBuyNow`, `onSelectColor`). `StorefrontHero` and `LoginPage` render optional affordances only when their handlers are supplied; other blocks may keep elements in the DOM with no-op behavior when a callback is absent.
|
|
75
|
+
|
|
76
|
+
### Category listing
|
|
77
|
+
|
|
78
|
+
`CategoryListing` is the single controlled listing controller. Products, pagination, the selected query values, active-filter chips and the status are props; the block emits `on*Change`/`onClearFilters`/`onRepair`/`onRetry` and never fetches, parses the URL, or keeps a private query. `presentation` changes only the chrome: `sidebar` keeps a persistent desktop panel plus a mobile sheet, `toolbar` adds a sticky mobile toolbar with filter/sort drawers. Results render through the canonical `ProductCard`, so the listing never owns card markup. Facets are option-only groups (`{ id, label, options: [{ value, label, count? }] }`) supplied by the host; a group renders only when real metadata provides it, a real `0` count is displayed, and an absent/invalid count is omitted rather than invented.
|
|
79
|
+
|
|
80
|
+
### Footer sections
|
|
81
|
+
|
|
82
|
+
`FooterSection` is the canonical page-level footer. Its required `presentation`
|
|
83
|
+
is `brand-columns`, `newsletter-grid`, or `contact`; provide serializable text,
|
|
84
|
+
links, and social records as props. Social records use `{ label, href?, icon }`
|
|
85
|
+
with an optional `ariaLabel`; callbacks and visible fallback text keep using the
|
|
86
|
+
raw `label`. Bind icon IDs to `LucideIcon` components through `socialIcons`, and
|
|
87
|
+
keep that map, `logo` nodes, and callbacks outside JSON fixtures.
|
|
88
|
+
|
|
89
|
+
Column/legal and social links have separate `linkBehavior` and
|
|
90
|
+
`socialLinkBehavior` props, both defaulting to `host`. Host mode requires the
|
|
91
|
+
matching callback and renders callback-less destinations as text; `native`
|
|
92
|
+
preserves browser navigation. Contact address, phone, and email hrefs are native.
|
|
93
|
+
Newsletter controls stay visible but disabled without `onNewsletterSubmit`;
|
|
94
|
+
the callback receives the entered email and the block performs no network request.
|
|
95
|
+
|
|
96
|
+
The numbered `FooterSection1`, `FooterSection3`, and `FooterSection4` exports,
|
|
97
|
+
deep module paths, and legacy prop contracts have been removed. Migrate to
|
|
98
|
+
`FooterSection` by supplying an explicit presentation and canonical serializable
|
|
99
|
+
content props. Select `linkBehavior="native"` or
|
|
100
|
+
`socialLinkBehavior="native"` when browser navigation is intended; both default
|
|
101
|
+
to host-controlled behavior. Canonical link types are exported as
|
|
102
|
+
`CanonicalFooterLink` and `CanonicalFooterLinkColumn` to avoid collisions with
|
|
103
|
+
other package contracts.
|
|
104
|
+
|
|
105
|
+
### Login page
|
|
106
|
+
|
|
107
|
+
`LoginPage` is a sign-in composition with one shared credential form and a
|
|
108
|
+
required `presentation` of `card` or `social`. Both presentations submit the
|
|
109
|
+
same `{ email, password, remember }` payload. `remember` starts as `false` and
|
|
110
|
+
is reported to the host; the block does not decide session persistence.
|
|
111
|
+
|
|
112
|
+
The block renders into the host page and does not own it: no viewport height, no
|
|
113
|
+
page landmark, no page backdrop, no vertical centring. `card` caps its width at
|
|
114
|
+
`max-w-md` and `social` at the same cap, and the mounting page supplies the
|
|
115
|
+
canvas — a route shell, a modal, a split screen, or a template the host owns.
|
|
116
|
+
Before the shell was removed, each of these blocks painted a full-viewport
|
|
117
|
+
surface; a host that relied on that canvas must now provide it.
|
|
118
|
+
|
|
119
|
+
Provider data stays serializable. Pass provider IDs and labels separately from
|
|
120
|
+
the icon components, and provide a selection callback so provider buttons are
|
|
121
|
+
only rendered when they can act. IDs must be unique within the list; an
|
|
122
|
+
unmapped icon falls back to the visible provider label:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import { AtSign } from 'lucide-react'
|
|
126
|
+
import { LoginPage } from '@thorprovider/blocks'
|
|
127
|
+
|
|
128
|
+
<LoginPage
|
|
129
|
+
presentation='social'
|
|
130
|
+
providers={[{ id: 'email', label: 'Email' }]}
|
|
131
|
+
providerIcons={{ email: AtSign }}
|
|
132
|
+
onProviderSelect={providerId => startProviderLogin(providerId)}
|
|
133
|
+
onSubmit={({ email, password, remember }) => signIn({ email, password, remember })}
|
|
134
|
+
/>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The numbered `LoginPage1` and `LoginPage2` exports, deep module paths, and legacy
|
|
138
|
+
prop contracts have been removed. Migrate to `LoginPage` and select `card` or
|
|
139
|
+
`social` explicitly; both presentations use the canonical
|
|
140
|
+
`{ email, password, remember }` host callback contract.
|
|
141
|
+
|
|
142
|
+
## Testimonials
|
|
143
|
+
|
|
144
|
+
`TestimonialSection` renders a collection of testimonials in one of two explicit
|
|
145
|
+
presentations: `carousel` or `bento`. Each item requires a stable, collection-unique
|
|
146
|
+
`id`, `name`, and `content`. `role`, `avatar`, and `rating` are optional and are
|
|
147
|
+
rendered only when supplied; the block never invents attribution or ratings and
|
|
148
|
+
does not emit an empty image source.
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import { TestimonialSection } from '@thorprovider/blocks'
|
|
152
|
+
|
|
153
|
+
const testimonials = [
|
|
154
|
+
{ id: 't-1', name: 'Rosa Díaz', role: 'Propietaria', content: 'Excelente servicio.' },
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
<TestimonialSection
|
|
158
|
+
presentation="carousel"
|
|
159
|
+
items={testimonials}
|
|
160
|
+
title="Lo que dicen nuestros clientes"
|
|
161
|
+
carouselLabels={{
|
|
162
|
+
previous: 'Testimonio anterior',
|
|
163
|
+
next: 'Testimonio siguiente',
|
|
164
|
+
slide: 'Ir al testimonio {n}',
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
For `carousel`, `carouselLabels` is required and supplies localized previous,
|
|
170
|
+
next, and slide-button labels; `{n}` is replaced with the one-based item number.
|
|
171
|
+
The native buttons support keyboard activation, and the selected item is exposed
|
|
172
|
+
with `aria-current`. For `bento`, omit `carouselLabels`; one-to-three items render
|
|
173
|
+
without empty cards, and collections larger than four repeat the four-tile visual
|
|
174
|
+
rhythm without dropping entries. An empty `items` array renders no section or
|
|
175
|
+
controls. `title`, `description`, and `className` are optional for either
|
|
176
|
+
presentation.
|
|
177
|
+
|
|
178
|
+
The numbered `Testimonial1` and `Testimonial2` exports, deep module paths, and
|
|
179
|
+
legacy prop contracts have been removed. Migrate to `TestimonialSection` with
|
|
180
|
+
shared testimonial items and select `carousel` or `bento` explicitly.
|
|
82
181
|
|
|
83
182
|
## Optional brand props
|
|
84
183
|
|
|
85
184
|
Three existing blocks expose an optional brand seam. Omitting the prop renders
|
|
86
185
|
the block's current output unchanged; no demo content is added.
|
|
87
186
|
|
|
88
|
-
- `
|
|
187
|
+
- `StorefrontNavbar` — `logoAccent?: { text: string; className?: string }` accents a
|
|
89
188
|
trailing segment of `logoText`. With `logoText="ElectroCuba"` and
|
|
90
189
|
`logoAccent={{ text: 'Cuba', className: 'text-brand-promo' }}`, the block
|
|
91
190
|
renders `Electro` plus an accented `Cuba` span. If `text` is not a trailing
|
|
92
191
|
segment of `logoText`, the wordmark renders unchanged.
|
|
93
|
-
- `
|
|
192
|
+
- `FooterSection` — `brandAccent?: { text: string; className?: string }` applies
|
|
94
193
|
the same trailing-segment accent to `brandName`.
|
|
95
194
|
- `StorefrontHero` — `heroImage?: { src: string; alt: string }` renders a native
|
|
96
195
|
`<img>` when `src` is a non-empty string, and nothing when the prop is absent
|
|
@@ -101,13 +200,13 @@ the library hardcodes no brand color.
|
|
|
101
200
|
|
|
102
201
|
## Custom surface `className`
|
|
103
202
|
|
|
104
|
-
`Navbar4` (outer `<header>`) and `
|
|
203
|
+
`Navbar4` (outer `<header>`) and `FooterSection` (outer `<footer>`) accept an
|
|
105
204
|
optional `className`, merged with the existing `cn` helper. It is purely
|
|
106
205
|
presentational: no layout, structure, or behavior prop, and no hardcoded color.
|
|
107
206
|
|
|
108
207
|
- **Omitted `className`** — the block renders its default surface unchanged.
|
|
109
208
|
`Navbar4` keeps `bg-background/95` **and** its
|
|
110
|
-
`supports-backdrop-filter:bg-background/80` variant; `
|
|
209
|
+
`supports-backdrop-filter:bg-background/80` variant; `FooterSection` keeps
|
|
111
210
|
`bg-secondary`.
|
|
112
211
|
- **Supplied `className`** — the consumer owns the surface. The default
|
|
113
212
|
background **and its backdrop-filter variant** are not applied, so a semantic
|
|
@@ -116,13 +215,13 @@ presentational: no layout, structure, or behavior prop, and no hardcoded color.
|
|
|
116
215
|
it). Layout, border, sticky, and `backdrop-blur-md` classes stay internal.
|
|
117
216
|
|
|
118
217
|
The consumer is responsible for the text, transparency, and context tokens of a
|
|
119
|
-
supplied surface. `
|
|
218
|
+
supplied surface. `StorefrontNavbar` adapts its primary nav links: they inherit the
|
|
120
219
|
surface text color and add `hover:text-foreground` so `hover:bg-muted` never
|
|
121
220
|
paints white-on-light. No magic colors are introduced by the block.
|
|
122
221
|
|
|
123
|
-
##
|
|
222
|
+
## StorefrontNavbar account affordance
|
|
124
223
|
|
|
125
|
-
`
|
|
224
|
+
`StorefrontNavbar` renders a **session-driven** account affordance exclusively from
|
|
126
225
|
consumer-supplied props — it never resolves a session or authenticates itself.
|
|
127
226
|
Given a `user` (`StoreUser`; `avatar` is optional) and `accountMenuItems`, it
|
|
128
227
|
renders an authenticated account menu with the identity, the configured items,
|
|
@@ -140,10 +239,10 @@ authentication.
|
|
|
140
239
|
|
|
141
240
|
## Footer newsletter
|
|
142
241
|
|
|
143
|
-
`
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
242
|
+
`FooterSection` renders the newsletter form disabled when
|
|
243
|
+
`onNewsletterSubmit` is absent. When supplied, the callback receives the entered
|
|
244
|
+
email; the block performs no network request. Omit `newsletter` to hide the
|
|
245
|
+
newsletter block.
|
|
147
246
|
|
|
148
247
|
## Cart presentation (controlled)
|
|
149
248
|
|
|
@@ -175,10 +274,10 @@ either is visible in the other.
|
|
|
175
274
|
suggestion list (name, optional image, optional `Money` price) rendered only
|
|
176
275
|
when supplied.
|
|
177
276
|
|
|
178
|
-
`
|
|
277
|
+
`StorefrontNavbar` owns the lateral. Pass `cartOpen` + `onCartOpenChange` to control the
|
|
179
278
|
sheet's visibility from host state (omit both to keep the legacy uncontrolled
|
|
180
279
|
sheet); the block keeps `aria-expanded`/`data-state` in sync. Its cart sheet
|
|
181
|
-
(`
|
|
280
|
+
(`StorefrontNavbarCartSheet`) takes an optional `canonical` mode where each line shows the
|
|
182
281
|
canonical `lineTotal` (never the placeholder numeric price), plus
|
|
183
282
|
`checkoutUnavailable`/`viewCartUnavailable` to hide actions the store cannot
|
|
184
283
|
serve, and a `state` (`'idle' | 'pending' | 'error'`) with retry for read/mutation
|
|
@@ -210,18 +309,35 @@ affordance only when supplied. Prices render through the canonical
|
|
|
210
309
|
`formatMoney` path (no hardcoded currency), and every label is overridable
|
|
211
310
|
with Spanish defaults (`Agregar al carrito`, `Precio no disponible`, …).
|
|
212
311
|
|
|
213
|
-
## Checkout
|
|
312
|
+
## Checkout block (page-level, controlled)
|
|
313
|
+
|
|
314
|
+
`Checkout` is the single public checkout surface; it replaces the numbered `CheckoutForm1`, `CheckoutForm2`, and `CheckoutForm3` blocks. It is a **controlled component**: field values arrive through `values` and every edit is reported through `onChange`, so the host owns all commercial state. The block owns only the ephemeral step index of the `stepper` presentation.
|
|
315
|
+
|
|
316
|
+
- `presentation: 'single-page' | 'stepper'` (required) — `single-page` renders every section visible at once (summary first in DOM order on mobile); `stepper` shows one step at a time with an accessible progress indicator. Both accept the same props and emit the same callbacks.
|
|
317
|
+
- `showReviewStep?: boolean` — appends a confirmation step (`stepper`) or section (`single-page`) before submission.
|
|
318
|
+
- `values: CheckoutValues` — `email`, `firstName`, `lastName`, `phone`, `address`, `city`, `state`, `zipCode`, `country`, `paymentMethod`, `saveInfo`, `newsletter`, `promoCode`. There are no card fields.
|
|
319
|
+
- `onChange: (field, value) => void` — reported per edit. The block holds no private copy of the form data and applies no masking.
|
|
320
|
+
- `orderSummary?: { items, totals? }` — line identity, name, quantity, optional variant/image, optional canonical `Money` `lineTotal` and `unitPrice`, plus backend `totals` (`subtotal`, optional `discount`/`tax`/`shipping`, `total`). Amounts render verbatim; required totals that are absent or malformed render as unavailable with the retry action, never as zero. No local arithmetic, tax rate, shipping cost, threshold, or hardcoded currency.
|
|
321
|
+
- `totalsState?: 'idle' | 'pending' | 'error'` and `onRetryTotals?` — controlled reads; pending and failed states render status/alert copy without inventing amounts.
|
|
322
|
+
- `countries?: { value, label }[]`, `shippingOptions?`, `selectedShippingOptionId?`, `onSelectShippingOption?` — host-owned options and selection. With no country list the block renders a text input; with no shipping options it renders no method section; nothing is pre-selected.
|
|
323
|
+
- `paymentMethods?: PaymentMethod[]` — payment methods as data (canonical `@thorprovider/types`). One option per method, the selected method's description, an empty state for an empty list, and a non-selectable rendering for `isEnabled === false`. The block renders **no raw card fields**: card collection belongs to the host payment-provider surface.
|
|
324
|
+
- `labels?: Partial<CheckoutLabels>` and `formatMoney?: (money: Money) => string` — every user-facing string is overridable (neutral English defaults) and amounts format through canonical `Money` with no hardcoded symbol.
|
|
325
|
+
- `fieldErrors?` / `fieldDescriptions?` — per-field messages and helper text, rendered with `role="alert"` and `aria-invalid`/`aria-describedby` wiring.
|
|
326
|
+
- `validateStep?: (step: CheckoutStepId) => boolean` and `onStepChange?` — the block consults `validateStep` before advancing and for every applicable step before submitting; `onStepChange` is a notification only, never a controlled value. Without the callback the block advances and leaves submit protection to the host.
|
|
327
|
+
- `onSubmit?`, `submitting?`, `submitError?`, `onRetrySubmit?` — submission is reported, with a pending guard and a recoverable error state; the block never creates an order, clears a cart, or claims success.
|
|
328
|
+
|
|
329
|
+
Validation itself is **not** implemented in the block. Declare checks in your store spec's validation configuration (built-in `required`/`email`/`pattern`/`minLength`/`maxLength`/cross-field `requiredIf`/`equalTo`/`lessThan`/`greaterThan`, plus catalog-registered custom functions) and evaluate them with the renderer; the messages live in the spec so they are per-store and per-language.
|
|
330
|
+
|
|
331
|
+
The order summary is an internal projection of `Checkout`, not a separate block. Order receipt and fulfilment progress remain `CheckoutConfirmation`.
|
|
214
332
|
|
|
215
|
-
|
|
333
|
+
### Migrating from the numbered checkout blocks
|
|
216
334
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
- `validateStep?: (step: number) => boolean` (wizard only) — the block calls it on "Next"; advance only happens when it returns `true`, and the final submit only fires when every step validates.
|
|
335
|
+
| Retired | Replacement |
|
|
336
|
+
| --- | --- |
|
|
337
|
+
| `CheckoutForm1` | `<Checkout presentation="stepper" />` — same controlled values minus `cardNumber`/`expiryDate`/`cvv`/`cardName`/`sameAsBilling`; `validateStep` now receives a `CheckoutStepId` (`'contact' \| 'shipping' \| 'payment' \| 'review'`) instead of a number |
|
|
338
|
+
| `CheckoutForm2` | `<Checkout presentation="single-page" />` — the host supplies `Money` totals; the block no longer computes tax, shipping, or free-shipping thresholds |
|
|
339
|
+
| `CheckoutForm3` | `<Checkout presentation="stepper" showReviewStep />` — values become controlled; `apartment`, card fields, per-step icons, and internal `isStepComplete` state are gone |
|
|
223
340
|
|
|
224
|
-
Validation itself is **not** implemented in the blocks. Declare checks in your store spec's validation configuration (built-in `required`/`email`/`pattern`/`minLength`/`maxLength`/cross-field `requiredIf`/`equalTo`/`lessThan`/`greaterThan`, plus catalog-registered custom functions like `luhn`/`cvv`/`expiryDate`) and evaluate them with the renderer; the messages live in the spec so they are per-store and per-language. Without a validation provider the blocks render with no inline errors.
|
|
225
341
|
|
|
226
342
|
## Usage
|
|
227
343
|
|
|
@@ -295,4 +411,4 @@ callbacks (`useCallback`); a fresh object literal always notifies a reference-on
|
|
|
295
411
|
store guard; and every registered action completes its round-trip (handler → set +
|
|
296
412
|
emit → spec `on` → recompute → bind). See
|
|
297
413
|
[`docs/standards/interaction-contracts.md`](docs/standards/interaction-contracts.md)
|
|
298
|
-
for the rules and the review checklist.
|
|
414
|
+
for the rules and the review checklist.
|
package/package.json
CHANGED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { beforeAll, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { render, screen } from '@testing-library/react'
|
|
4
|
+
import userEvent from '@testing-library/user-event'
|
|
5
|
+
import { AtSign } from 'lucide-react'
|
|
6
|
+
import * as blockExports from '../../../index'
|
|
7
|
+
import { LoginPage, type LoginPageProps } from './login-page'
|
|
8
|
+
|
|
9
|
+
beforeAll(() => {
|
|
10
|
+
Object.assign(SVGElement.prototype, { getTotalLength: () => 15 })
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('LoginPage', () => {
|
|
14
|
+
it('is the only exported login page API', () => {
|
|
15
|
+
expect(blockExports.LoginPage).toBe(LoginPage)
|
|
16
|
+
expect('LoginPage1' in blockExports).toBe(false)
|
|
17
|
+
expect('LoginPage2' in blockExports).toBe(false)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it.each(['card', 'social'] as const)('submits the same structured credentials for the %s presentation', async presentation => {
|
|
21
|
+
const user = userEvent.setup()
|
|
22
|
+
const onSubmit = vi.fn()
|
|
23
|
+
|
|
24
|
+
render(
|
|
25
|
+
<LoginPage
|
|
26
|
+
presentation={presentation}
|
|
27
|
+
emailLabel='Email'
|
|
28
|
+
passwordLabel='Password'
|
|
29
|
+
rememberMeLabel='Remember me'
|
|
30
|
+
signInLabel='Sign in'
|
|
31
|
+
onSubmit={onSubmit}
|
|
32
|
+
/>,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
await user.type(screen.getByLabelText('Email'), 'shopper@example.com')
|
|
36
|
+
await user.type(screen.getByLabelText('Password'), 'correct-horse')
|
|
37
|
+
await user.click(screen.getByRole('checkbox', { name: 'Remember me' }))
|
|
38
|
+
await user.click(screen.getByRole('button', { name: 'Sign in' }))
|
|
39
|
+
|
|
40
|
+
expect(onSubmit).toHaveBeenCalledWith({
|
|
41
|
+
email: 'shopper@example.com',
|
|
42
|
+
password: 'correct-horse',
|
|
43
|
+
remember: true,
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('rejects a missing presentation instead of silently choosing a layout', () => {
|
|
48
|
+
expect(() => render(<LoginPage {...({} as LoginPageProps)} />)).toThrow(/presentation/i)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('round-trips serializable provider data and reports the selected provider id', async () => {
|
|
52
|
+
const user = userEvent.setup()
|
|
53
|
+
const onProviderSelect = vi.fn()
|
|
54
|
+
const providers = JSON.parse(JSON.stringify([{ id: 'github', label: 'GitHub' }])) as LoginPageProps['providers']
|
|
55
|
+
|
|
56
|
+
render(
|
|
57
|
+
<LoginPage
|
|
58
|
+
presentation='social'
|
|
59
|
+
providers={providers}
|
|
60
|
+
providerIcons={{ github: AtSign }}
|
|
61
|
+
onProviderSelect={onProviderSelect}
|
|
62
|
+
/>,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
await user.click(screen.getByRole('button', { name: 'GitHub' }))
|
|
66
|
+
|
|
67
|
+
expect(onProviderSelect).toHaveBeenCalledWith('github')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('renders provider labels when an icon is not mapped', () => {
|
|
71
|
+
render(
|
|
72
|
+
<LoginPage
|
|
73
|
+
presentation='card'
|
|
74
|
+
providers={[{ id: 'custom', label: 'Custom provider' }]}
|
|
75
|
+
onProviderSelect={vi.fn()}
|
|
76
|
+
/>,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
expect(screen.getByRole('button', { name: 'Custom provider' }).textContent).toBe('Custom provider')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('omits the provider group when there are no providers', () => {
|
|
83
|
+
render(<LoginPage presentation='card' onProviderSelect={vi.fn()} />)
|
|
84
|
+
|
|
85
|
+
expect(screen.queryByRole('group', { name: 'O continúa con' })).toBeNull()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('rejects duplicate provider IDs so provider React keys remain unique', () => {
|
|
89
|
+
expect(() =>
|
|
90
|
+
render(
|
|
91
|
+
<LoginPage
|
|
92
|
+
presentation='card'
|
|
93
|
+
providers={[
|
|
94
|
+
{ id: 'duplicate', label: 'First' },
|
|
95
|
+
{ id: 'duplicate', label: 'Second' },
|
|
96
|
+
]}
|
|
97
|
+
onProviderSelect={vi.fn()}
|
|
98
|
+
/>,
|
|
99
|
+
),
|
|
100
|
+
).toThrow(/provider ids must be unique/i)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('omits optional actions and providers when their handlers are absent', () => {
|
|
104
|
+
render(
|
|
105
|
+
<LoginPage
|
|
106
|
+
presentation='card'
|
|
107
|
+
forgotPasswordLabel='Forgot password'
|
|
108
|
+
createAccountLabel='Create account'
|
|
109
|
+
providers={[{ id: 'github', label: 'GitHub' }]}
|
|
110
|
+
/>,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
expect(screen.queryByRole('button', { name: 'Forgot password' })).toBeNull()
|
|
114
|
+
expect(screen.queryByRole('button', { name: 'Create account' })).toBeNull()
|
|
115
|
+
expect(screen.queryByRole('group', { name: 'O continúa con' })).toBeNull()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('invokes optional action handlers', async () => {
|
|
119
|
+
const user = userEvent.setup()
|
|
120
|
+
const onForgotPassword = vi.fn()
|
|
121
|
+
const onCreateAccount = vi.fn()
|
|
122
|
+
|
|
123
|
+
render(
|
|
124
|
+
<LoginPage
|
|
125
|
+
presentation='card'
|
|
126
|
+
forgotPasswordLabel='Forgot password'
|
|
127
|
+
createAccountLabel='Create account'
|
|
128
|
+
onForgotPassword={onForgotPassword}
|
|
129
|
+
onCreateAccount={onCreateAccount}
|
|
130
|
+
/>,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
await user.click(screen.getByRole('button', { name: 'Forgot password' }))
|
|
134
|
+
await user.click(screen.getByRole('button', { name: 'Create account' }))
|
|
135
|
+
|
|
136
|
+
expect(onForgotPassword).toHaveBeenCalledOnce()
|
|
137
|
+
expect(onCreateAccount).toHaveBeenCalledOnce()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('updates the password toggle accessible name with its state', async () => {
|
|
141
|
+
const user = userEvent.setup()
|
|
142
|
+
|
|
143
|
+
render(
|
|
144
|
+
<LoginPage
|
|
145
|
+
presentation='card'
|
|
146
|
+
passwordLabel='Password'
|
|
147
|
+
showPasswordAriaLabel='Show password'
|
|
148
|
+
hidePasswordAriaLabel='Hide password'
|
|
149
|
+
/>,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
expect(screen.getByRole('button', { name: 'Show password' })).toBeTruthy()
|
|
153
|
+
await user.click(screen.getByRole('button', { name: 'Show password' }))
|
|
154
|
+
expect(screen.getByRole('button', { name: 'Hide password' })).toBeTruthy()
|
|
155
|
+
expect(screen.getByLabelText('Password').getAttribute('type')).toBe('text')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('keeps fields and secondary actions reachable in keyboard order', async () => {
|
|
159
|
+
const user = userEvent.setup()
|
|
160
|
+
|
|
161
|
+
render(
|
|
162
|
+
<LoginPage
|
|
163
|
+
presentation='card'
|
|
164
|
+
emailLabel='Email'
|
|
165
|
+
passwordLabel='Password'
|
|
166
|
+
rememberMeLabel='Remember me'
|
|
167
|
+
forgotPasswordLabel='Forgot password'
|
|
168
|
+
signInLabel='Sign in'
|
|
169
|
+
createAccountLabel='Create account'
|
|
170
|
+
onForgotPassword={vi.fn()}
|
|
171
|
+
onCreateAccount={vi.fn()}
|
|
172
|
+
/>,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
const expectedFocusOrder = [
|
|
176
|
+
screen.getByLabelText('Email'),
|
|
177
|
+
screen.getByLabelText('Password'),
|
|
178
|
+
screen.getByRole('button', { name: 'Mostrar contraseña' }),
|
|
179
|
+
screen.getByRole('checkbox', { name: 'Remember me' }),
|
|
180
|
+
screen.getByRole('button', { name: 'Forgot password' }),
|
|
181
|
+
screen.getByRole('button', { name: 'Sign in' }),
|
|
182
|
+
screen.getByRole('button', { name: 'Create account' }),
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
for (const element of expectedFocusOrder) {
|
|
186
|
+
await user.tab()
|
|
187
|
+
expect(document.activeElement).toBe(element)
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
})
|