Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions docs/MethodChip-responsive-fwc26.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# MethodChip — Responsive & Accessibility Fixes (GrantFox FWC26)

## Overview

This document describes the changes made to `src/components/MethodChip.tsx` and
`src/components/MethodChip.css` as part of the GrantFox FWC26 UI/UX campaign.

All fixes are backwards-compatible: the public API (`method` prop) and rendered
HTML structure are unchanged except for the addition of a `<span class="method-chip-label">`
wrapper around the method text (see [Structural change](#structural-change-method-chip-label-span)
below).

---

## Bugs fixed

### 1. CSS custom-property name mismatch

**File:** `MethodChip.tsx` — `METHOD_COLORS` map

The `fg` key in `METHOD_COLORS` referenced tokens named `--method-<verb>-fg`, but
`src/index.css` defines these tokens under the suffix `-color`
(e.g. `--method-get-color`). The mismatch caused foreground colours to fall back
to the browser default, making chips render with the wrong text colour in both
light and dark themes.

| Before | After |
|---|---|
| `fg: 'var(--method-get-fg)'` | `color: 'var(--method-get-color)'` |
| `style={{ color: colors.fg }}` | `style={{ color: colors.color }}` |

Affected verbs: GET, POST, PUT, DELETE, PATCH.

---

### 2. Icon-wrapper CSS class mismatch

**Files:** `MethodChip.tsx`, `MethodChip.css`

The JSX used `className="method-chip-icon"` but the CSS defined `.method-icon`.
Because the selector did not match, the icon's right-margin was never applied and
it sat flush against the method text.

| Before (CSS) | After (CSS) |
|---|---|
| `.method-icon { margin-right: 4px; }` | `.method-chip-icon { margin-right: var(--mkt-space-sm, 4px); }` |

---

### 3. Tap target too small (WCAG 2.1 AA §2.5.5)

**File:** `MethodChip.css`

The chip's `padding: 2px 8px` produced a visual height of ~18 px — well below the
44 × 44 px minimum recommended by WCAG 2.1 AA for pointer targets.

The fix uses a `::before` pseudo-element to expand the interactive hit area to at
least 44 × 44 px without changing the visual size of the chip:

```css
.method-chip::before {
content: '';
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: max(100%, 44px);
height: max(100%, 44px);
pointer-events: none;
}
```

The chip container must remain `position: relative` (already the case) for this
to work correctly.

---

### 4. No ≤375 px breakpoint

**File:** `MethodChip.css`

The chip's `min-width: 60px` was fixed at all viewport widths. On 320–375 px
screens (iPhone SE, older Android phones) a row of chips inside an endpoint card
could overflow the container.

```css
@media (max-width: 375px) {
.method-chip {
padding: var(--mkt-space-xs, 2px) 6px;
min-width: 52px;
font-size: 0.7rem;
}

.method-chip-icon {
margin-right: var(--mkt-space-xs, 2px);
}
}
```

The icon is still shown at ≤375 px (it is only 14 px wide); only spacing is
tightened.

---

### 5. No text-overflow protection

**File:** `MethodChip.css`

Long labels (e.g. `DELETE`) could overflow their container on narrow screens. The
chip now clips overflowing text with an ellipsis:

```css
.method-chip {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
```

---

## Structural change — `.method-chip-label` span

A new `<span class="method-chip-label">` wraps the method text inside the chip.
This is required so that `text-overflow: ellipsis` applies only to the label, not
the icon, preventing the icon from being truncated before the text.

```tsx
// Before
<span className="method-chip" ...>
<span className="method-chip-icon">…</span>
{upper}
</span>

// After
<span className="method-chip" ...>
<span className="method-chip-icon">…</span>
<span className="method-chip-label">{upper}</span>
</span>
```

Consumers who use CSS selectors targeting direct text nodes of `.method-chip`
should update their selectors to `.method-chip-label`.

---

## API

`MethodChip` accepts a single prop and its signature is unchanged:

```ts
type MethodChipProps = {
/**
* The HTTP verb to display (case-insensitive).
* Recognised values: GET | POST | PUT | DELETE | PATCH.
* Unrecognised values fall back to a neutral grey chip.
*/
method: string;
};
```

---

## Accessibility

| Criterion | Implementation |
|---|---|
| WCAG 2.1 AA §1.1.1 Non-text content | `role="img"` + `aria-label="<VERB> request"` |
| WCAG 2.1 AA §1.3.1 Info & Relationships | Icon wrapper carries `aria-hidden="true"` |
| WCAG 2.1 AA §2.1.1 Keyboard | `tabIndex={0}`; tooltip shown on `focus`, hidden on `blur` |
| WCAG 2.1 AA §2.5.5 Pointer Target Size | `::before` pseudo-element ensures ≥44 × 44 px hit area |
| WCAG 2.1 AA §4.1.3 Status Messages | Tooltip uses `role="tooltip"` |

---

## Tests

36 Vitest tests live in `src/components/MethodChip.test.tsx`:

- All 5 recognised HTTP verbs rendered correctly
- Case-insensitive input (`delete`, `patch`, …)
- Fallback chip for unrecognised verbs
- `aria-label` / `role="img"` contract
- Tooltip appears on `mouseenter` / `focus` and disappears on `mouseleave` / `blur`
- CSS class contracts (`method-chip`, `method-chip-icon`, `method-chip-label`)
- Colour-token contract: inline `style` references `--method-<verb>-bg` and
`--method-<verb>-color` (not the deprecated `-fg` suffix)
- Tap-target structural contract (no inline width/height blocks `::before`)
- No inline overflow styles that would break the high-contrast token cascade

Run with:

```bash
npx vitest run src/components/MethodChip.test.tsx
```

---

## Design tokens used

| Token | Purpose |
|---|---|
| `--method-<verb>-bg` | Chip background (both themes) |
| `--method-<verb>-color` | Chip foreground text (both themes) |
| `--method-default-bg` | Fallback background for unknown verbs |
| `--method-default-color` | Fallback foreground for unknown verbs |
| `--mkt-space-xs` / `--mkt-space-sm` / `--mkt-space-md` | Spacing tokens |
| `--mkt-font-size-micro` | Label font size (0.75 rem) |
| `--accent` | `:focus-visible` outline colour |
| `--tooltip-bg` / `--tooltip-fg` | Tooltip colours |

All tokens are defined in `src/index.css` and `src/styles/tokens.css`.
134 changes: 117 additions & 17 deletions src/components/MethodChip.css
Original file line number Diff line number Diff line change
@@ -1,55 +1,155 @@
/* MethodChip.css */
/**
* MethodChip.css
*
* Styles for the <MethodChip> component.
*
* Responsive fixes (GrantFox FWC26):
* - Tap-target: a ::before pseudo-element expands the interactive hit area to
* ≥44×44px on all viewports without inflating the visual chip size.
* (WCAG 2.1 AA §2.5.5 — Pointer Target Spacing)
* - ≤375px breakpoint: min-width and padding are tightened so narrow endpoint
* rows (e.g. inside ApiDetailPage doc panels) don't overflow.
* - Text overflow: the label is clipped with an ellipsis rather than wrapping,
* keeping the chip to a single line at any width.
*
* Bug-fixes:
* - Icon wrapper class renamed from .method-icon → .method-chip-icon to match
* the className used in MethodChip.tsx.
*/

/* ── Base chip ────────────────────────────────────────────────────────────── */

.method-chip {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 8px;
margin: 2px;
min-width: 60px; /* uniform width */
/* Default spacing: comfortable on ≥376px viewports */
padding: var(--mkt-space-xs, 2px) var(--mkt-space-md, 8px);
margin: var(--mkt-space-xs, 2px);
min-width: 68px;
max-width: 100%;
border-radius: 4px;
font-size: 0.75rem;
font-size: var(--mkt-font-size-micro, 0.75rem); /* 12 px */
font-weight: 600;
/* Prevent text wrapping; overflow clipped with ellipsis */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: default;
user-select: none;
/* fallback colors if CSS vars are not defined */
/* Fallback colours when CSS custom props are unavailable */
background-color: var(--method-default-bg, #e0e0e0);
color: var(--method-default-fg, #333);
color: var(--method-default-color, #333);
/* Smooth theme transitions (honours .no-theme-transition escape hatch) */
transition: background-color 0.2s ease, color 0.2s ease;
}
/* Keyboard-only focus ring, matching the global @layer focus treatment.
Using :focus-visible (not :focus) means a mouse click shows no ring. */

/* ── WCAG 2.1 AA §2.5.5 — 44×44 px tap target via pseudo-element ─────────
The chip's visible size stays compact, but the clickable/tappable surface
is padded out to the minimum recommended 44px in both dimensions. The
pseudo-element is absolutely positioned so it does not affect layout. */
.method-chip::before {
content: '';
position: absolute;
/* Centre the expanded target over the chip */
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
min-width: 44px;
min-height: 44px;
/* Extend outward: (44px - chip-height) / 2 in each direction.
The negative inset ensures the hitbox is always ≥44px even when the
chip itself is taller than 44px (it simply clamps to 0). */
width: max(100%, 44px);
height: max(100%, 44px);
/* Transparent so only the chip background is visible */
background: transparent;
pointer-events: none; /* cosmetic only — the <span> handles events */
border-radius: inherit;
}

/* ── Keyboard-only focus ring ──────────────────────────────────────────────
Matches the global @layer focus treatment in src/styles/focus.css.
Using :focus-visible (not :focus) suppresses the ring on mouse clicks. */
.method-chip:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
.method-icon {
margin-right: 4px;

/* ── Icon wrapper ──────────────────────────────────────────────────────────
FIX: renamed from .method-icon → .method-chip-icon to align with the
className="method-chip-icon" in MethodChip.tsx. */
.method-chip-icon {
/* Reserve just enough room to breathe next to the label */
margin-right: var(--mkt-space-sm, 4px);
/* Prevent the icon from shrinking when space is tight */
flex-shrink: 0;
display: inline-flex;
align-items: center;
font-size: 0.85rem;
}

/* ── Label text ────────────────────────────────────────────────────────────
Isolated in its own <span> so text-overflow: ellipsis can be applied
precisely without clipping the icon. */
.method-chip-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
/* Allow the label to shrink when the chip runs out of horizontal space */
min-width: 0;
}

/* ── Tooltip ───────────────────────────────────────────────────────────────
Positioned below the chip; pointer-events: none so it never interrupts
hover/focus events on the chip itself. */
.method-tooltip {
position: absolute;
top: 120%;
/* Place just below the chip with a small gap */
top: calc(100% + var(--mkt-space-sm, 4px));
left: 50%;
transform: translateX(-50%);
background: var(--tooltip-bg, #333);
color: var(--tooltip-fg, #fff);
padding: 4px 8px;
padding: var(--mkt-space-sm, 4px) var(--mkt-space-md, 8px);
border-radius: 4px;
font-size: 0.7rem;
white-space: nowrap;
z-index: 10;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
pointer-events: none;
}
/* Arrow for tooltip */

/* Upward-pointing caret */
.method-tooltip::after {
content: '';
position: absolute;
top: -5px;
left: 50%;
transform: translateX(-50%);
border-width: 5px 5px 0 5px;
border-width: 0 5px 5px 5px;
border-style: solid;
border-color: var(--tooltip-bg, #333) transparent transparent transparent;
}
border-color: transparent transparent var(--tooltip-bg, #333) transparent;
}

/* ── ≤375 px breakpoint (GrantFox FWC26) ──────────────────────────────────
On the narrowest phone widths the chip needs to yield horizontal space.
We:
1. Tighten horizontal padding from 8px → 6px.
2. Reduce min-width from 68px → 52px so a row of chips doesn't overflow
an endpoint card.
3. Hide the icon on very narrow chips (DELETE already risks clipping).
The 44px tap target is still preserved via the ::before pseudo-element. */
@media (max-width: 375px) {
.method-chip {
padding: var(--mkt-space-xs, 2px) 6px;
min-width: 52px;
font-size: 0.7rem;
}

/* Reduce icon margin so the label has a few extra pixels */
.method-chip-icon {
margin-right: var(--mkt-space-xs, 2px);
}
}
Loading
Loading