Skip to content

RF-1224: Use deployment dates instead of update dates in gear popup#1550

Open
jeslefcourt wants to merge 6 commits into
developfrom
RF-1224
Open

RF-1224: Use deployment dates instead of update dates in gear popup#1550
jeslefcourt wants to merge 6 commits into
developfrom
RF-1224

Conversation

@jeslefcourt
Copy link
Copy Markdown
Contributor

@jeslefcourt jeslefcourt commented Apr 23, 2026

Summary

  • Gear popup header date now uses the most recent last_deployed across all devices, falling back to gear.last_updated
  • Device list sorted ascending by last_deployed (undated devices last, stable sort for ties)
  • Gear popup width increased 25% (16.25rem)
  • Clicking the gear name in the sidebar list now jumps to location (same as the jump icon)

Fixes: RF-1224

Test plan

  • Open a gear popup on the map and verify the header date reflects the most recent device deployment date
  • Verify devices with no last_deployed appear after dated devices in the popup list
  • Verify clicking a gear name in the sidebar jumps the map to that gear
  • Run npx jest src/GearPopup/index.test.js — all 11 tests pass

🤖 Generated with Claude Code

Gear popup header and device list now show last_deployed timestamps.
The header falls back to gear.last_updated when no device has a deployment date.
Devices are sorted ascending by last_deployed, with undated devices last.
Popup width increased 25% and name is now clickable (jump to location).
Adds test coverage for device sort order and popup rendering branches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the Gear popup and sidebar interactions to use deployment dates (instead of update dates) and improve navigation to gear locations.

Changes:

  • Gear popup header now uses the most recent device.last_deployed (fallback to gear.last_updated) and device rows show last_deployed.
  • Devices in the popup are sorted ascending by last_deployed (undated devices last; ties preserve order).
  • Gear popup styling updated to widen the Mapbox popup content, and gear names in the sidebar are now clickable to jump to location.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/SideBar/GearTab/styles.module.scss Adds styling for a “clickable” gear display id in the sidebar list.
src/SideBar/GearTab/index.js Makes the gear name clickable/keyboard-focusable to jump to the gear’s location.
src/PopupLayer/index.js Applies a GearPopup-specific popup class for width styling via popup template attrs.
src/GearPopup/styles.module.scss Adds a .popup wrapper to increase Mapbox popup content width for gear popups.
src/GearPopup/index.js Switches popup header/device dates to last_deployed logic and sorts devices by deployment date.
src/GearPopup/index.test.js Adds unit tests for GearPopup rendering and device sort order.

Comment thread src/SideBar/GearTab/index.js
Comment thread src/SideBar/GearTab/styles.module.scss Outdated
Comment thread src/GearPopup/index.js
Comment thread src/GearPopup/index.test.js
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…oyed across devices

Agent-Logs-Url: https://github.com/PADAS/das-web-react/sessions/5f2a8b61-505a-4b8d-bebe-cedff1eba9cd

Co-authored-by: jeslefcourt <32883811+jeslefcourt@users.noreply.github.com>
jeslefcourt and others added 3 commits April 23, 2026 15:31
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@chrisj-er
Copy link
Copy Markdown
Contributor

@claude review

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — small frontend change with good test coverage.

Extended reasoning...

Overview

This PR updates the gear popup to surface the most recent device last_deployed date (with fallback to gear.last_updated), sorts devices ascending by deployment date, widens the popup, and makes the gear name in the sidebar list clickable to jump to location. Touches 7 files, all frontend (React + SCSS), with a new 141-line test file covering the popup behavior and sort ordering edge cases.

Security risks

None. No auth, crypto, network calls, user input parsing, or permission logic. Date strings are compared lexicographically (safe for ISO 8601), and the new offset option to useJumpToLocation is conditionally spread only when truthy.

Level of scrutiny

Low. This is presentation-layer logic with clear intent, deterministic behavior, and direct test coverage. The most-recent-date reduce, the sort comparator, and the conditional offset spread are all small and easy to verify by inspection. The test file specifically covers the most-recent-date selection (added in 00bfe94 in response to Copilot feedback), the fallback path, undated-devices-last ordering, and stable sort for ties.

Other factors

  • Prior Copilot review surfaced 4 items; 3 were addressed (focus-visible styling now present in SCSS, missing test added, PR description corrected).
  • The remaining nit (Space key on the role=button span doesn't preventDefault, so Space both activates and scrolls the page) is flagged as an inline comment by the bug-hunting system. It's a real a11y nit but minor and isolated — does not block approval.
  • No CODEOWNER-sensitive areas touched. The useJumpToLocation hook change is backward-compatible (new optional second-arg property).

Comment on lines +65 to +69
className={`${styles.displayId}${coordinates ? ` ${styles.displayIdClickable}` : ''}`}
onClick={coordinates ? onJumpClick : undefined}
role={coordinates ? 'button' : undefined}
tabIndex={coordinates ? 0 : undefined}
onKeyDown={coordinates ? (e) => { if (e.key === 'Enter' || e.key === ' ') onJumpClick(); } : undefined}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Pressing Space on the focused gear title (a span with role=button and tabIndex=0) both jumps to location AND scrolls the page because the onKeyDown handler at src/SideBar/GearTab/index.js:65-69 handles Space without calling e.preventDefault(). Fix by either rendering a real <button type="button"> (preferred) or calling e.preventDefault() when e.key === ' '. Copilot raised this as inline thread 3134201681; the thread was marked resolved but no code change was applied.

Extended reasoning...

The bug

The clickable gear title is rendered as a <span> (not a native <button>) with role="button" and tabIndex={0}, plus a custom onKeyDown handler:

onKeyDown={coordinates ? (e) => { if (e.key === 'Enter' || e.key === ' ') onJumpClick(); } : undefined}

For real <button> elements, browsers automatically suppress the default Space-scroll behavior. For a focused, tabbable <span>, they do not — Space triggers the page's default scroll-down behavior unless the handler explicitly calls e.preventDefault(). This handler does not.

Why existing code does not prevent it

There is no preventDefault() anywhere on this path, and no parent handler intercepts Space on this element. Enter does not have a default scroll behavior on focused elements, so Enter still works as intended — the bug is specific to Space.

Impact

A keyboard user who tabs to the gear name and presses Space will see the map jump to the gear's location AND the page scroll down by one viewport. This is confusing because the user pressed an activation key on what looks like a button, but got an unrelated scroll as well. It also breaks expected button semantics (Space should activate-on-keyup with no default side effects), which is a minor accessibility regression on a brand-new clickable affordance.

Step-by-step proof

  1. Open the sidebar Gear tab so the gear list is rendered.
  2. Tab through the page until focus lands on the gear-name span (it has tabIndex=0 when coordinates is truthy).
  3. Press the Space key.
  4. The onKeyDown handler runs and calls onJumpClick() → the map eases to the gear coordinates and the popup is shown.
  5. Because e.preventDefault() was never called, the browser also performs its default Space-on-focused-element behavior: scrolling the document by ~one viewport.
  6. Net result: both the jump-and-popup AND a full-page scroll happen from a single Space press.

For comparison, pressing Enter at step 3 only triggers the jump — Enter has no default scroll behavior on a focused span.

Fix

Either of the following:

  • Preferred: render a real <button type="button"> (matches Copilot's suggestion in inline thread 3134201681). This eliminates the need for role/tabIndex/onKeyDown entirely and gets correct keyboard semantics for free.
  • Minimal: keep the span but call e.preventDefault() when e.key === ' ' (and ideally activate on keyup to match native button behavior):
onKeyDown={coordinates ? (e) => {
  if (e.key === 'Enter') onJumpClick();
  else if (e.key === ' ') e.preventDefault();
} : undefined}
onKeyUp={coordinates ? (e) => { if (e.key === ' ') onJumpClick(); } : undefined}

History note

Copilot raised this exact issue in inline review 3134201681 with a code suggestion to switch to a native <button>. The thread is marked resolved in the PR timeline, but the modified-files content shows the handler is unchanged — no preventDefault was added and the element is still a <span>, so the fix was never actually applied.

🔬 also observed by 3134201681

Copy link
Copy Markdown
Contributor

@luixlive luixlive left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost done, some small suggestions on improvements for code readability and HTML semantics 👍

Comment thread src/GearPopup/index.js
: primaryLabel;
const { coordinates } = data;

const mostRecentDate = (gear.devices || []).reduce((best, device) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to memoize this so we don't run a loop on every render:

const lastDeployedDeviceDate = useMemo(() => (gear.devices || []).reduce((best, device) => {
  if (!device.last_deployed) {
    return best;
  }
  return !best || device.last_deployed > best ? device.last_deployed : best;
}, null), [gear.devices]);

const mostRecentUpdateDate = lastDeployedDeviceDate || gear.last_updated;

Just consider that in that case we need to put the useMemo hook over the line if (!gear) return null;.

Comment thread src/GearPopup/index.js
<dd>{gear.type}</dd>

{(gear.devices || []).map((device) => <React.Fragment key={device.device_id}>
{[...(gear.devices || [])].sort((a, b) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little bit harsh to read all these conditionals in the middle of the JSX. We can create a helper at the top of the file:

const sortByLastDeployed = (a, b) => {
  if (!a.last_deployed && !b.last_deployed) return 0;
  if (!a.last_deployed) return 1;
  if (!b.last_deployed) return -1;
  if (a.last_deployed === b.last_deployed) return 0;
  return a.last_deployed > b.last_deployed ? 1 : -1;
};

And here just call it:

{(gear.devices ?? []).sort(sortByLastDeployed).map((device) => <React.Fragment key={device.device_id}>


.popup {
[class*=mapboxgl-popup-content] {
width: 16.25rem;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not an issue here, just wanted to know if there's a good reason to override the default popup width. Will this new width look good in different device sizes? Mobile ones?

map.fitBounds(mapBoundaries, { linear: true, speed: 200, padding });
} else {
map.easeTo({ center: isArrayCoords ? coords[0] : coords, zoom, padding, speed: 200 });
map.easeTo({ center: isArrayCoords ? coords[0] : coords, zoom, padding, speed: 200, ...(offset ? { offset } : {}) });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way of receiving the third (options) parameter and spreading it feels a bit weird. What do you think of something like this:

return (coords, zoom = 15, options = {}) => {
  const isArrayCoords = Array.isArray(coords[0]);

  const padding = calcPadding(currentTab, isArrayCoords, itemId, isMediumLayoutOrLarger);

  if (isArrayCoords && coords.length > 1) {
    const mapBoundaries = coords.reduce(buildLocationJumpBounds, new LngLatBounds());
    map.fitBounds(mapBoundaries, { linear: true, speed: 200, padding, ...options });
  } else {
    map.easeTo({ center: isArrayCoords ? coords[0] : coords, zoom, padding, speed: 200, ...options });
  }
};

The options parameter is still optional and you can pass the offset there, which would be spreaded to both fitBounds and easeTo.

return <>
<p className={mapLayersStyles.itemTitle} data-testid="gear-item-name">
<span className={styles.displayId}>{rowTitle}</span>
<span
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see agents are causing noise around this change, and for a good reason. This is not the most semantic HTML nor the accessible way of hearing a click. It would be more appropriate to render a button instead of a span, and add stiles to make it look just like the span. Something like:

{coordinates ? (
  <button
    className={`${styles.displayId} ${styles.displayIdClickable}`}
    onClick={onJumpClick}
    type="button"
  >
    {rowTitle}
  </button>
) : (
  <span className={styles.displayId}>
    {rowTitle}
  </span>
)}

Notice the

is gone since intractable elements shouldn't live inside a

.

word-break: break-word;
}

.displayIdClickable {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These styles would be updated if you switch to a button as suggested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants