Skip to content
Open
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
37 changes: 37 additions & 0 deletions src/pages/OutcomeChip.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@

import * as React from "react";
import type { MechanicHelpContent } from "@/components/patterns/MechanicHelp";
import { MechanicHelp } from "@/components/patterns/MechanicHelp";

/**
* OutcomeChip
Expand Down Expand Up @@ -36,6 +38,12 @@ export interface OutcomeChipProps {
onSelect?: () => void;
/** Optional additional className for layout composition */
className?: string;
/**
* Optional contextual help content. When provided, a help icon + popover
* is rendered next to the chip label, helping first-time users understand
* the outcome metric/field.
*/
helpContent?: MechanicHelpContent;
}

const variantTokenMap: Record<OutcomeChipVariant, { bg: string; bgSelected: string; fg: string; border: string }> = {
Expand Down Expand Up @@ -67,6 +75,7 @@ export const OutcomeChip: React.FC<OutcomeChipProps> = ({
disabled = false,
onSelect,
className = "",
helpContent,
}) => {
const tokens = variantTokenMap[variant];

Expand Down Expand Up @@ -111,6 +120,11 @@ export const OutcomeChip: React.FC<OutcomeChipProps> = ({
>
<span className="outcome-chip__label">{label}</span>
{badge ? <span className="outcome-chip__badge">{badge}</span> : null}
{helpContent ? (
<span className="outcome-chip__help" onClick={(e) => e.stopPropagation()}>
<MechanicHelp content={helpContent} />
</span>
) : null}

<style>{`
.outcome-chip {
Expand Down Expand Up @@ -173,6 +187,29 @@ export const OutcomeChip: React.FC<OutcomeChipProps> = ({
background-color: rgba(0, 0, 0, 0.06);
}

/* Contextual help icon: keep it compact and clickable without
expanding the chip's focus ring or tap target. stopPropagation
in JSX prevents the chip's onSelect from firing when the help
icon itself is activated. */
.outcome-chip__help {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 2px;
color: var(--outcome-help-fg, rgba(0, 0, 0, 0.55));
}

.outcome-chip__help :global(button) {
min-height: 28px;
min-width: 28px;
}

@media (prefers-contrast: more) {
.outcome-chip__help {
color: var(--outcome-help-fg, #000000);
}
}

/* <=375px: audited breakpoint from issue #636.
Stack label/badge and give the chip full-width tap area so
nothing truncates and the tap target stays comfortably >=44px. */
Expand Down
36 changes: 36 additions & 0 deletions src/pages/__tests__/OutcomeChip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@ import * as React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom";
import { OutcomeChip } from "../OutcomeChip";
import type { MechanicHelpContent } from "@/components/patterns/MechanicHelp";

const mockHelpContent: MechanicHelpContent = {
title: "Outcome odds",
tooltip: "The probability of each outcome based on market activity.",
summary: "Outcome odds reflect the market's current estimate of each outcome's likelihood.",
sections: [
{
title: "How odds work",
body: "Higher odds mean higher perceived probability. Odds shift as participants trade.",
},
],
};

describe("OutcomeChip", () => {
it("renders the label", () => {
Expand Down Expand Up @@ -65,4 +78,27 @@ describe("OutcomeChip", () => {
fireEvent.click(chip); // jsdom doesn't auto-trigger click on Enter for <button>
expect(onSelect).toHaveBeenCalled();
});

it("renders a contextual help button when helpContent is provided", () => {
render(<OutcomeChip label="Yes" variant="yes" helpContent={mockHelpContent} />);
// The help button is rendered with the "Quick help" aria-label
expect(
screen.getByRole("button", { name: /show quick help for outcome odds/i })
).toBeInTheDocument();
});

it("does not render a help button when helpContent is omitted", () => {
render(<OutcomeChip label="Yes" variant="yes" />);
expect(
screen.queryByRole("button", { name: /show quick help for/i })
).not.toBeInTheDocument();
});

it("does not trigger the chip onSelect when the help button is clicked", () => {
const onSelect = jest.fn();
render(<OutcomeChip label="Yes" variant="yes" onSelect={onSelect} helpContent={mockHelpContent} />);
const helpButton = screen.getByRole("button", { name: /show quick help for outcome odds/i });
fireEvent.click(helpButton);
expect(onSelect).not.toHaveBeenCalled();
});
});