Skip to content

feat(a11y/ui): main landmark, price-refresh state, no-script fallback, handle norm (#306/#305/#299/#298)#310

Merged
Chucks1093 merged 1 commit into
accesslayerorg:mainfrom
chigozirim007:feat/a11y-handle-norm-marketplace-landmarks
May 28, 2026
Merged

feat(a11y/ui): main landmark, price-refresh state, no-script fallback, handle norm (#306/#305/#299/#298)#310
Chucks1093 merged 1 commit into
accesslayerorg:mainfrom
chigozirim007:feat/a11y-handle-norm-marketplace-landmarks

Conversation

@chigozirim007
Copy link
Copy Markdown
Contributor

@chigozirim007 chigozirim007 commented May 28, 2026

This PR closes #306, #305, #299, #298 — all four issues live in the same area of the marketplace UI (creator card / profile header / landing layout), so batching them in one PR is the natural shape.

closes #306
closes #305
closes #299
closes #298

What's in this PR

#306 — Main landmark for the creator marketplace

The previous structure had the entire LandingPage wrapped in <main>, including the brand banner. That makes landmark navigation land on the page chrome instead of the content.

  • Demoted the outer wrapper from <main> to a decorative <div>.
  • The brand banner stays a top-level <header> (<MarketplaceSection as="header">) — now a sibling of <main>, not a child of it.
  • Added a real <main id="creator-marketplace-main" aria-label="Creator marketplace"> that scopes to the search-and-filter bar, creator grid, profile pattern, and transaction timeline.
  • Exactly one <main> per page; verified by grep -rn "<main".

#305 — Loading state for the creator card price badge

  • New isPriceRefreshing?: boolean prop on CreatorCard.
  • When true, the price badge keeps its current value but applies a muted style, renders a small spinner, sets aria-busy="true", and includes an sr-only "Refreshing price" hint.
  • The value is wrapped in a min-w-[6.5rem] container with tabular-nums, so the badge does not collapse or shift width during a refresh.
  • LandingPage drives the flag with a 30-second simulator (setInterval + setTimeout) gated on prefers-reduced-motion — so we don't surface a non-essential animation to users who opted out. Real pipeline (WebSocket / polling hook) lands as a follow-up; this PR is the UI half of the handshake.

#299 — No-script fallback for key UI actions

The marketplace is a Vite SPA — without JavaScript the #root div stays empty and every interactive surface is unavailable.

  • Added a styled <noscript> block to index.html. Inline CSS so it renders even when external CSS is blocked; tone matches the rest of the app (gradient bg, amber eyebrow, jakarta-feel sans-serif fallback).
  • Body explains what requires JavaScript (browse, wallet, buy/sell, prices, tx status) — one block covers every interactive area, which is the right shape for an SPA where React never mounts without JS anyway.
  • role="alert" so assistive tech announces the fallback if scripting later fails to load.

#298 — Helper for creator handle display casing

  • New src/utils/handleDisplay.utils.ts exports formatCreatorHandle(raw):
    • Trim, strip a single leading @, lowercase, prepend one @.
    • Returns "" for empty / whitespace / nullish / lone-@ input so callers can choose a placeholder.
    • Idempotent and side-effect-free; the raw stored value is not modified.
  • Applied in:
    • CreatorCard.tsx — instructorId display (@creator fallback), socialHandle display row, and the bottom "Handle" CardMetaRow (value + title).
    • CreatorProfileHeader.tsx — subtitle below name, and the Web Share API title (with the raw handle as a safe fallback if the helper returns "").
  • 7 vitest cases in src/utils/__tests__/handleDisplay.utils.test.ts: mixed-case lowercasing, stripping leading @, double-@ preservation, whitespace trim, empty/whitespace/nullish, lone @, idempotence, and the no-mutation invariant.

Verification

$ pnpm install        # ok
$ pnpm test           # 99 passed | 1 failed (pre-existing, see disclosures)
$ pnpm lint           # clean
$ pnpm build          # clean (vite build, 5.57s)

Disclosures (pre-existing on main, unrelated to this PR)

  1. src/components/common/__tests__/CreatorInitialsAvatar.test.tsx fails on main. The test queries for getByLabelText('Alex Rivers initials avatar'), but the component renders the initials with aria-hidden="true" instead of an aria-label on the wrapping element. Reproducible on the unmodified upstream main (commits e92b472 / 0213bcd touched the component and its test independently). I did not touch this file or its test.
  2. No CreatorCard component test for the refresh indicator. The card depends on a chain of wagmi + telemetry hooks (useAccount, useNetworkMismatch, useTransactionTelemetry) with no existing test scaffolding in the repo. Adding a component test would require mocking all of those, which is more scaffolding than the visual change warrants. The utility (formatCreatorHandle) is covered directly; the refresh-indicator rendering is verified by build + manual review.

…handle normalisation

Closes accesslayerorg#306, accesslayerorg#305, accesslayerorg#299, accesslayerorg#298.

- accesslayerorg#306: Demoted the LandingPage's outer <main> to a decorative <div> and
  scoped a real <main aria-label="Creator marketplace"> to the
  marketplace-and-below content. The page header (logo, hero copy) is now
  a sibling <header> rather than living inside <main>, so screen-reader
  landmark navigation jumps directly to the primary content.
- accesslayerorg#305: Added isPriceRefreshing prop to CreatorCard. When true the price
  badge keeps its visible value but applies a muted style, renders a
  small spinner, sets aria-busy, and includes an sr-only "Refreshing
  price" hint. min-width + tabular-nums on the value preserve layout so
  the badge does not collapse or shift. LandingPage drives the flag from
  a simple 30-second simulator gated on prefers-reduced-motion until the
  real price-refresh pipeline lands.
- accesslayerorg#299: Added a styled <noscript> block in index.html with inline CSS so
  it renders even with external CSS blocked. The Vite SPA leaves #root
  empty without JS, so this is the one place a no-script user can see
  context; one block covers every interactive surface (browse, search,
  wallet, buy/sell, prices, tx status).
- accesslayerorg#298: New src/utils/handleDisplay.utils.ts exports formatCreatorHandle
  — trim, strip any leading @, lowercase, prepend one @. Applied in
  CreatorCard (instructorId + socialHandle, both display and CardMetaRow)
  and CreatorProfileHeader (subtitle + share title). Raw props are
  preserved for any equality / URL construction the caller does. 7
  vitest cases cover casing, leading @, whitespace, nullish, lone @, and
  idempotence.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@drips-wave
Copy link
Copy Markdown

drips-wave Bot commented May 28, 2026

@chigozirim007 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Chucks1093 Chucks1093 merged commit dd7f968 into accesslayerorg:main May 28, 2026
1 check passed
Chucks1093 pushed a commit that referenced this pull request May 28, 2026
…with drag-to-dismiss

Closes #315, #314.

- #315 CreatorBio: gains collapsible + collapsedMaxLines +
  collapseThresholdChars props. When enabled on the profile variant
  and the bio is long enough (default >200 chars), the paragraph
  renders clamped with a focusable Show more / Show less toggle that
  carries aria-expanded + aria-controls so screen readers know the
  bio's collapsed state. Short bios are unaffected (no toggle, no
  clamp). The card variant ignores collapsible since it already
  clamps via maxLines. Wired into CreatorProfileHeader where the
  profile bio actually renders.

- #314 BottomSheet: new mobile-first primitive built on Radix Dialog
  for the focus trap / role / Escape handling. Adds drag-to-dismiss
  via native pointer events:
    * The visual handle (BottomSheetHandle) registers itself with the
      sheet's content surface so a gesture that starts on the handle
      is always treated as 'grabbing the sheet'.
    * Otherwise the gesture is captured only when no inner scroller
      is engaged (walks up from the target, bails if any ancestor
      has scrollTop>0) — so a downward swipe on scrollable content
      scrolls instead of dismissing.
    * Dragging past dismissThresholdPx (default 96) dismisses by
      dispatching Escape so Radix's onOpenChange(false) pipeline runs.
    * Short / upward drags snap back via a brief transform reset.
    * The default close button always works as an alternative; pass
      enableDrag=false to make it the only path.

Tests (12 new, all passing):
- src/components/common/__tests__/CreatorBio.test.tsx (6 new cases):
  no toggle for short bio, no engagement on card variant, clamps +
  Show more wiring, toggles to Show less + removes clamp, custom
  collapseThresholdChars, custom collapsedMaxLines.
- src/components/ui/bottom-sheet.test.tsx (6 cases): render +
  handle + close button, close-button dismissal, drag past threshold
  dismisses, short drag does not, upward drag clamps + never
  dismisses, enableDrag=false leaves close button as only path.

Repo verification:
- pnpm test    129/130 (1 pre-existing CreatorInitialsAvatar failure,
                same one disclosed in #310 / #311 / #324).
- pnpm lint    clean
- pnpm build   clean

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants