Skip to content

Improve SearchBox UX: debounced search, clear button, and accessibility (a11y) fixes #4

Description

@Malek-Ghorbel

Summary

Make the search experience faster, more accessible, and more polished. The current SearchBox is functional but has several UX and a11y gaps: no associated label, vague placeholder, no way to clear the query without manually deleting text, and every keystroke triggers an immediate filter with no debouncing.


Current Behavior

  • No label / poor a11y: src/components/SearchBox.js:6-11 renders a bare <input type=\"search\" placeholder=\"robot\"> with no <label>, no aria-label, and no aria-describedby. Screen readers announce it poorly. The placeholder \"robot\" is also vague (singular, no hint that it filters by name).
  • Uncontrolled input: The input is uncontrolled — App.js:38 passes onSearchChange but never passes value={searchfield}. Clearing the field programmatically (e.g. a "Clear" button) would not reset the visible input without this.
  • No debouncing: src/containers/App.js:24-26 calls setState on every keystroke and render() re-filters the full list immediately. Fine for 10 users from jsonplaceholder, but the pattern does not scale and causes unnecessary renders.
  • No clear affordance: Once a user types a query there is no × / "Clear" button. They must select-all + delete. On mobile this is especially tedious.
  • Scroll container is rigid: src/components/Scroll.js:5 hardcodes height: '470px' with an inline style and a harsh 1px solid black border. It does not adapt to viewport height and looks cramped on small screens.
  • Card image alt is generic: src/components/Card.js:6 uses alt=\"photo\" — not descriptive for assistive tech.

Steps to reproduce

  1. npm start → open http://localhost:3000.
  2. Inspect the search input with DevTools → no <label> associated.
  3. Run Lighthouse Accessibility audit → flags "Form elements do not have associated labels".
  4. Type quickly in the search box → every keystroke triggers a render (visible in React DevTools Profiler).
  5. Type Leanne → no way to clear except manually deleting; shrink viewport to 375px wide → scroll area overflows / feels cramped.

Expected Behavior

  1. Accessible search input — Input has an associated label (visually hidden if needed) and proper ARIA attributes. Placeholder is descriptive. Lighthouse a11y for the control passes.
  2. Controlled input — Input value is driven by App.state.searchfield.
  3. Debounced filtering — Filtering is debounced (~250–300ms) so fast typing does not thrash renders. Immediate feedback still feels snappy.
  4. Clear button — An × button appears when searchfield is non-empty; clicking it clears the query, refocuses the input, and restores the full list.
  5. Responsive scroll area — Scroll container adapts to viewport (e.g. max-height: 60vh or flex layout) instead of a fixed 470px, with a softer border consistent with Tachyons styling.
  6. Descriptive alt text — Card images use alt=\"Robot avatar for {name}\" or similar.

Proposal

1. Make SearchBox controlled + accessible (src/components/SearchBox.js)

const SearchBox = ({ value, searchChange, onClear }) => {
  return (
    <div className=\"pa2 relative\">
      <label htmlFor=\"search-robots\" className=\"clip\">Search robots</label>
      <input
        id=\"search-robots\"
        className=\"pa3 ba b--green bg-lightest-blue\"
        type=\"search\"
        placeholder=\"Search robots by name\"
        aria-label=\"Search robots by name\"
        value={value}
        onChange={searchChange}
      />
      {value && (
        <button
          aria-label=\"Clear search\"
          className=\"ml2 pa2 ba b--green bg-white pointer\"
          onClick={onClear}
        >
          ×
        </button>
      )}
    </div>
  );
};
  • clip is a Tachyons helper for visually-hidden but screen-reader-visible text (already available via tachyons).
  • Keep styling consistent with existing Tachyons classes.

2. Wire up controlled value + clear + debounce in src/containers/App.js

// state
this.state = { robots: [], searchfield: '', /* existing */ }

// make input controlled
<SearchBox
  value={this.state.searchfield}
  searchChange={this.onSearchChange}
  onClear={() => this.setState({ searchfield: '' })}
/>

Debounce option A (simple, no new dep):
Add a debounced copy of the field:

// in constructor
this.state = { robots: [], searchfield: '', debouncedSearch: '' };
this.debouncedSetSearch = debounce((val) => this.setState({ debouncedSearch: val }), 300);

// onSearchChange
onSearchChange = (e) => {
  const val = e.target.value;
  this.setState({ searchfield: val });
  this.debouncedSetSearch(val);
};
// filter against debouncedSearch, not searchfield

Provide a tiny debounce util inline (no new dependency) or install lodash.debounce — either is acceptable, document the choice.

Debounce option B: Use useDeferredValue / useTransition — only if App is first migrated to a function component (out of scope for this ticket, but call it out if the assignee prefers).

3. Responsive Scroll (src/components/Scroll.js)

<div style={{ overflowY: 'auto', border: '1px solid #e0e0e0', maxHeight: '60vh' }}>
  {props.children}
</div>

Or move to a CSS class in src/containers/app.css. Avoid inline height: 470px.

4. Card alt text (src/components/Card.js:6)

<img alt={`Robot avatar for ${name}`} src={`https://robohash.org/${id}?50x50`} />

Note: also fix the robohash URL param — current is ?50*50 (typo), should be ?size=200x200 or ?50x50.


Acceptance Criteria

  • Search input has an associated <label> (or aria-label) and Lighthouse "Form elements do not have associated labels" no longer flags it.
  • Placeholder reads "Search robots by name" (or equally descriptive).
  • Input is controlled via value={searchfield} — typing and programmatic clear both update the visible field.
  • A clear (×) button is visible only when searchfield is non-empty; clicking it clears the field, refocuses the input, and restores all robots.
  • Filtering is debounced (~250–300ms) — rapid typing does not trigger a filter on every keystroke (verify with React DevTools Profiler or console log).
  • Scroll container is responsive (no fixed 470px height) and border is softened to match the Tachyons palette.
  • Card alt text is descriptive and includes the robot name.
  • Existing case-insensitive filtering (toLowerCase() in App.js:29-31) still works.
  • No new console warnings/errors; no visual regression on desktop + mobile (375px, 768px, 1280px).

Files to touch

  • src/components/SearchBox.js — main change
  • src/containers/App.js — wire value/onClear/debounce
  • src/components/Scroll.js — responsive height/border
  • src/components/Card.js — alt text + robohash URL fix (one-liner)

Out of Scope

Labels

enhancement, good first issue, a11y, UX

Priority

Medium

Effort

Small (~2–3 hours)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions