in frontend/src/components/MetricsList.tsx [31:277]
function MetricsList(props: Props) {
const caretUpRefs = props.metrics.map(() => createRef<HTMLButtonElement>());
const caretDownRefs = props.metrics.map(() => createRef<HTMLButtonElement>());
const { t } = useTranslation();
const onDelete = (metric: Metric) => {
if (props.onDelete) {
props.onDelete(metric);
}
};
const onEdit = (metric: Metric, position: number) => {
if (props.onEdit) {
props.onEdit(metric, position);
}
};
const onDrag = useCallback(
(index: number, newIndex: number) => {
if (props.onDrag) {
props.onDrag(index, newIndex);
}
},
[props]
);
const onDrop = () => {
if (props.onDrop) {
props.onDrop();
}
};
const onMoveDown = (index: number) => {
if (props.onMoveDown) {
setNextFocus(index, "Down");
props.onMoveDown(index);
}
};
const onMoveUp = (index: number) => {
if (props.onMoveUp) {
setNextFocus(index, "Up");
props.onMoveUp(index);
}
};
const setNextFocus = (index: number, direction: "Up" | "Down") => {
let ref;
/**
* When moving a metric up or down, the focus should follow the
* metric to its new position in the list. Determining the new
* position depends on whether the metric is moving up or down
* and if the metric has reached a boundary (i.e. beginning or
* end of the list), in which case the focus should swap to the
* opposite caret (careUp vs caretDown).
*
* We may not need this logic when we implement drag-n-drop
* because most of the existing libraries already handle browser
* focus when moving DOM objects.
*/
if (direction === "Up") {
ref = index === 1 ? caretDownRefs[1] : caretUpRefs[index];
} else {
const secondLast = props.metrics.length - 2;
ref =
index === secondLast ? caretUpRefs[secondLast] : caretDownRefs[index];
}
if (ref.current) {
ref.current.focus();
}
};
const moveMetric = useCallback(
(dragIndex: number, hoverIndex: number) => {
const dragItem = props.metrics[dragIndex];
if (dragItem) {
onDrag(dragIndex, hoverIndex);
}
},
[props.metrics, onDrag]
);
return (
<div className="display-block" role="group" aria-label={t("Metrics")}>
<label className="margin-bottom-0 margin-top-2 usa-label text-bold">
{t("Metrics")}
<span>*</span>
</label>
<p className="margin-top-2px usa-hint">{t("MetricsGuidance")}</p>
<div className="usa-checkbox margin-bottom-2">
<input
className="usa-checkbox__input"
id="oneMetricPerRow"
type="checkbox"
name="oneMetricPerRow"
ref={props.register && props.register()}
defaultChecked={props.defaultChecked}
/>
<label className="usa-checkbox__label" htmlFor="oneMetricPerRow">
{t("MetricsOnePerRow")}
</label>
</div>
{props.metrics && props.metrics.length ? (
<div role="list">
<DndProvider
backend={window.innerWidth < 1024 ? TouchBackend : HTML5Backend}
options={{ enableMouseEvents: true }}
>
{props.metrics.map((metric, index) => {
return (
<ContentItem
className="grid-row margin-y-1"
key={metric.title + metric.value}
index={index}
id={index}
moveItem={moveMetric}
onDrop={onDrop}
itemType="metric"
>
<div className="grid-row grid-col flex-2 padding-1">
<div className="text-base-darker grid-col flex-3 text-center display-flex flex-align-center flex-justify-center">
<FontAwesomeIcon icon={faGripLinesVertical} size="1x" />
</div>
<div className="grid-col flex-5 text-center display-flex flex-align-center flex-justify-center font-sans-md">
{index + 1}
</div>
<div className="grid-col flex-4 grid-row flex-column text-center">
<div className="grid-col flex-6">
{index > 0 && (
<Button
variant="unstyled"
type="button"
className="margin-top-0-important text-base-darker hover:text-base-darkest active:text-base-darkest"
ariaLabel={`Move ${metric.title} up`}
onClick={() => onMoveUp(index)}
ref={caretUpRefs[index]}
>
<FontAwesomeIcon
id={`${metric.title}-move-up`}
size="xs"
icon={faArrowUp}
/>
</Button>
)}
</div>
<div className="grid-col flex-6">
{index < props.metrics.length - 1 && (
<Button
variant="unstyled"
type="button"
className="margin-top-0-important text-base-darker hover:text-base-darkest active:text-base-darkest"
ariaLabel={`Move ${metric.title} down`}
onClick={() => onMoveDown(index)}
ref={caretDownRefs[index]}
>
<FontAwesomeIcon
id={`${metric.title}-move-down`}
size="xs"
icon={faArrowDown}
/>
</Button>
)}
</div>
</div>
</div>
<div className="border-base-lighter border-left"></div>
<div className="grid-col flex-10 grid-row padding-1 margin-y-1">
<div
className="grid-col flex-8 font-important usa-tooltip text-bold"
data-position="bottom"
title={metric.title}
>
<div
style={{ marginTop: "2px" }}
className="margin-left-1 text-no-wrap overflow-hidden text-overflow-ellipsis"
>
{metric.title}
</div>
</div>
<div className="grid-col grid-row flex-4">
<div className="grid-col flex-6 text-right margin-right-1">
<Button
variant="unstyled"
type="button"
className="margin-left-1 margin-top-0-important text-base-dark hover:text-base-darker active:text-base-darkest"
onClick={() => onEdit(metric, index)}
ariaLabel={`Edit ${metric.title}`}
>
{t("Edit")}
</Button>
</div>
<div className="grid-col flex-6">
<Button
variant="unstyled"
type="button"
className="margin-right-2 margin-top-0-important text-base-dark hover:text-base-darker active:text-base-darkest"
onClick={() => onDelete(metric)}
ariaLabel={`Delete ${metric.title}`}
>
{t("Delete")}
</Button>
</div>
</div>
</div>
</ContentItem>
);
})}
</DndProvider>
{props.allowAddMetric && (
<div className="text-center margin-top-2">
<Button
variant="outline"
type="button"
className="margin-top-0-important"
onClick={() => {
if (props.onClick) {
props.onClick();
}
}}
>
{t("MetricsAdd")}
</Button>
</div>
)}
</div>
) : (
<div className="text-center radius-lg padding-3 margin-y-1 border-base border-dashed bg-base-lightest border">
<p>{t("MetricsZero")}</p>
<div className="text-center">
<Button
variant="outline"
type="button"
onClick={() => {
if (props.onClick) {
props.onClick();
}
}}
>
{t("MetricsAdd")}
</Button>
</div>
</div>
)}
</div>
);
}