Skip to content
Draft
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
3 changes: 1 addition & 2 deletions frontend/src/js/entity-history/ContentControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import {
faInfo,
} from "@fortawesome/free-solid-svg-icons";
import { memo, useMemo, useState } from "react";
import { ToggleButtonGroup } from "react-aria-components";
import { useTranslation } from "react-i18next";
import { Icon } from "../ui-components/Icon";
import { ToggleButton } from "../ui-components/ToggleButton";
import { ToggleButtonGroup } from "../ui-components/ToggleButtonGroup";
import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip";

export type ContentType =
Expand Down Expand Up @@ -56,7 +56,6 @@ const ContentControl = ({ value, onChange }: Props) => {

return (
<ToggleButtonGroup
className="flex flex-col items-center"
orientation="vertical"
selectionMode="multiple"
selectedKeys={options.filter((o) => value[o.key]).map((o) => o.key)}
Expand Down
58 changes: 26 additions & 32 deletions frontend/src/js/entity-history/DetailControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,19 @@ import {
faCircleDot,
} from "@fortawesome/free-solid-svg-icons";
import { type Dispatch, memo, type SetStateAction, useMemo } from "react";
import { type Key, ToggleButtonGroup } from "react-aria-components";
import type { Key } from "react-aria-components";
import { useTranslation } from "react-i18next";
import { tv } from "tailwind-variants";
import { Icon } from "../ui-components/Icon";
import { ToggleButton } from "../ui-components/ToggleButton";
import { ToggleButtonGroup } from "../ui-components/ToggleButtonGroup";
import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip";

const root = tv({ base: "flex flex-col items-center" });

const detailLevels: DetailLevel[] = ["summary", "detail", "full"];
const isDetailLevel = (key: Key): key is DetailLevel =>
detailLevels.some((level) => level === key);
export type DetailLevel = "summary" | "detail" | "full";

interface Props {
className?: string;
detailLevel: DetailLevel;
setDetailLevel: Dispatch<SetStateAction<DetailLevel>>;
}
Expand Down Expand Up @@ -53,30 +50,27 @@ const useButtonConfig = () => {
);
};

export const DetailControl = memo(
({ className, detailLevel, setDetailLevel }: Props) => {
const navOptions = useButtonConfig();
return (
<ToggleButtonGroup
className={root({ className })}
orientation="vertical"
selectionMode="single"
disallowEmptySelection
selectedKeys={[detailLevel]}
onSelectionChange={(keys) => {
const [key] = keys;
if (key !== undefined && isDetailLevel(key)) setDetailLevel(key);
}}
>
{navOptions.map(({ value, icon, tooltip }) => (
<TooltipTrigger key={value}>
<ToggleButton id={value} aria-label={tooltip}>
<Icon icon={icon} />
</ToggleButton>
<Tooltip placement="right">{tooltip}</Tooltip>
</TooltipTrigger>
))}
</ToggleButtonGroup>
);
},
);
export const DetailControl = memo(({ detailLevel, setDetailLevel }: Props) => {
const navOptions = useButtonConfig();
return (
<ToggleButtonGroup
orientation="vertical"
selectionMode="single"
disallowEmptySelection
selectedKeys={[detailLevel]}
onSelectionChange={(keys) => {
const [key] = keys;
if (key !== undefined && isDetailLevel(key)) setDetailLevel(key);
}}
>
{navOptions.map(({ value, icon, tooltip }) => (
<TooltipTrigger key={value}>
<ToggleButton id={value} aria-label={tooltip}>
<Icon icon={icon} />
</ToggleButton>
<Tooltip placement="right">{tooltip}</Tooltip>
</TooltipTrigger>
))}
</ToggleButtonGroup>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {
import DropzoneWithFileInput, {
type DragItemFile,
} from "../../ui-components/DropzoneWithFileInput";
import ToggleButtonGroup from "../../ui-components/ToggleButtonGroup";
import { ToggleButton } from "../../ui-components/ToggleButton";
import { ToggleButtonGroup } from "../../ui-components/ToggleButtonGroup";
import UploadConceptListModal from "../../upload-concept-list-modal/UploadConceptListModal";
import type { ConceptListDefaults as ConceptListDefaultsType } from "../config-types";
import { Description } from "../form-components/Description";
Expand Down Expand Up @@ -363,19 +364,22 @@ const FormConceptGroup = (props: Props) => {
{t("externalForms.common.connectedWith")}:
</Description>
<ToggleButtonGroup
value={props.value[i].connector}
onChange={(val) => {
segmented
size="sm"
selectionMode="single"
disallowEmptySelection
selectedKeys={[props.value[i].connector]}
onSelectionChange={(keys) => {
const [connector] = keys;
if (typeof connector !== "string") return;
props.onChange(
setValueProperties(props.value, i, {
connector: val,
}),
setValueProperties(props.value, i, { connector }),
);
}}
options={[
{ value: "OR", label: t("common.or") },
{ value: "AND", label: t("common.and") },
]}
/>
>
<ToggleButton id="OR">{t("common.or")}</ToggleButton>
<ToggleButton id="AND">{t("common.and")}</ToggleButton>
</ToggleButtonGroup>
</div>
)}
<DynamicInputGroup
Expand Down
60 changes: 36 additions & 24 deletions frontend/src/js/external-forms/form/fields/ConceptListField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { exists } from "../../../common/helpers/exists";
import { nodeIsInvalid } from "../../../model/node";
import ToggleButtonGroup from "../../../ui-components/ToggleButtonGroup";
import { ToggleButton } from "../../../ui-components/ToggleButton";
import { ToggleButtonGroup } from "../../../ui-components/ToggleButtonGroup";
import type { ConceptListField as ConceptListFieldT } from "../../config-types";
import FormConceptGroup from "../../form-concept-group/FormConceptGroup";
import type { FormConceptGroupT } from "../../form-concept-group/formConceptGroupState";
Expand Down Expand Up @@ -77,30 +78,41 @@ export const ConceptListField = ({
renderRowPrefix={
exists(field.rowPrefixField)
? ({ value: fieldValue, onChange, row, i }) => (
<ToggleButtonGroup
className="mb-[5px]"
options={field.rowPrefixField!.options.map((option) => ({
label: option.label[locale] || "",
value: option.value,
}))}
value={
/* Because we're essentially adding an extra dynamic field to FormConceptGroupT
<div className="mb-[5px]">
<ToggleButtonGroup
segmented
size="sm"
selectionMode="single"
disallowEmptySelection
selectedKeys={
/* Because we're essentially adding an extra dynamic field to FormConceptGroupT
with the key `field.rowPrefixField.name` */
(row as unknown as Record<string, string>)[
field.rowPrefixField!.name
]
}
onChange={(value) =>
onChange([
...fieldValue.slice(0, i),
{
...fieldValue[i],
[field.rowPrefixField!.name]: value,
},
...fieldValue.slice(i + 1),
])
}
/>
[
(row as unknown as Record<string, string>)[
field.rowPrefixField!.name
],
]
}
onSelectionChange={(keys) => {
const [key] = keys;
if (typeof key !== "string") return;
onChange([
...fieldValue.slice(0, i),
{
...fieldValue[i],
[field.rowPrefixField!.name]: key,
},
...fieldValue.slice(i + 1),
]);
}}
>
{field.rowPrefixField!.options.map((option) => (
<ToggleButton key={option.value} id={option.value}>
{option.label[locale] || ""}
</ToggleButton>
))}
</ToggleButtonGroup>
</div>
)
: undefined
}
Expand Down
27 changes: 21 additions & 6 deletions frontend/src/js/standard-query-editor/SecondaryIdSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { exists } from "../common/helpers/exists";
import { nodeIsConceptQueryNode } from "../model/node";
import { Icon } from "../ui-components/Icon";
import InfoTooltip from "../ui-components/InfoTooltip";
import ToggleButtonGroup from "../ui-components/ToggleButtonGroup";
import { ToggleButton } from "../ui-components/ToggleButton";
import { ToggleButtonGroup } from "../ui-components/ToggleButtonGroup";
import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip";

import { setSelectedSecondaryId } from "./actions";
import type { StandardQueryStateT } from "./queryReducer";
Expand Down Expand Up @@ -150,7 +152,7 @@ const SecondaryIdSelectorUI = memo(
value,
onChange,
}: {
options: { label: string; value: string }[];
options: { label: string; value: string; description?: string }[];
value: string | null;
onChange: (value: string) => void;
}) => {
Expand All @@ -167,10 +169,23 @@ const SecondaryIdSelectorUI = memo(
<InfoTooltip text={t("queryEditor.secondaryIdTooltip")} />
</h3>
<ToggleButtonGroup
value={value || "standard"}
onChange={onChange}
options={options}
/>
segmented
size="sm"
selectionMode="single"
disallowEmptySelection
selectedKeys={[value || "standard"]}
onSelectionChange={(keys) => {
const [key] = keys;
if (typeof key === "string") onChange(key);
}}
>
{options.map(({ value: id, label, description }) => (
<TooltipTrigger key={id}>
<ToggleButton id={id}>{label}</ToggleButton>
<Tooltip>{description}</Tooltip>
</TooltipTrigger>
))}
</ToggleButtonGroup>
</div>
);
},
Expand Down
23 changes: 15 additions & 8 deletions frontend/src/js/ui-components/InputRange.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import type { CurrencyConfigT } from "../api/types";
import { exists } from "../common/helpers/exists";
import InputPlain from "./InputPlain/InputPlain";
import InputRangeHeader from "./InputRangeHeader";
import ToggleButtonGroup from "./ToggleButtonGroup";
import { ToggleButton } from "./ToggleButton";
import { ToggleButtonGroup } from "./ToggleButtonGroup";

const container = tv({ base: ["flex flex-row", "w-full", "-mt-[3px]"] });

Expand Down Expand Up @@ -118,13 +119,19 @@ const InputRange = ({
tooltip={tooltip}
/>
<ToggleButtonGroup
value={mode || "range"}
onChange={(mode) => onSwitchMode(mode as ModeT)}
options={[
{ value: "range", label: t("inputRange.range") },
{ value: "exact", label: t("inputRange.exact") },
]}
/>
segmented
size="sm"
selectionMode="single"
disallowEmptySelection
selectedKeys={[mode || "range"]}
onSelectionChange={(keys) => {
const [key] = keys;
if (key === "range" || key === "exact") onSwitchMode(key);
}}
>
<ToggleButton id="range">{t("inputRange.range")}</ToggleButton>
<ToggleButton id="exact">{t("inputRange.exact")}</ToggleButton>
</ToggleButtonGroup>
<div className={container()}>
{isRangeMode ? (
<>
Expand Down
61 changes: 46 additions & 15 deletions frontend/src/js/ui-components/ToggleButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { tv } from "tailwind-variants";

import { buttonStyle } from "./Button";
import { Icon } from "./Icon";
import { useToggleButtonGroup } from "./ToggleButtonGroup";

// while selected, the button is bold and shows its highlight color
const toggleStyle = tv({
Expand All @@ -17,7 +18,31 @@ const toggleStyle = tv({
primary: "data-selected:text-primary-500",
danger: "data-selected:text-red",
},
// a segment of a segmented control: bordered, recessed while off, white
// while on, joined to its neighbors along the group's orientation
segmented: {
true: [
"rounded-none",
"border-gray-500",
"bg-gray-50 text-gray-500",
"hover:bg-white",
"data-selected:bg-white",
],
},
orientation: { horizontal: "", vertical: "" },
},
compoundVariants: [
{
segmented: true,
orientation: "horizontal",
class: "first:rounded-l last:rounded-r not-first:-ml-px",
},
{
segmented: true,
orientation: "vertical",
class: "first:rounded-t last:rounded-b not-first:-mt-px",
},
],
});

interface CommonProps
Expand Down Expand Up @@ -45,8 +70,9 @@ const isIconOnly = (children: ReactNode) => {
/**
* A button whose look reflects a state that is on or off, in Button's look.
* react-aria's ToggleButton underneath: `isSelected` / `onChange`, and it
* works inside a ToggleButtonGroup and as a tooltip trigger. Pressing may
* flip the state or open an editor for it.
* works as a tooltip trigger. Inside a ToggleButtonGroup it is keyed by `id`,
* takes the group's size unless it has its own, and joins a segmented group
* as one of its segments. Pressing may flip the state or open an editor for it.
*
* <ToggleButton isSelected={pinned} onChange={setPinned} aria-label="Pin">
* <Icon icon={faThumbtack} />
Expand All @@ -58,16 +84,21 @@ export const ToggleButton = ({
highlight = "primary",
children,
...props
}: ToggleButtonProps) => (
<RacToggleButton
className={toggleStyle({
intent,
size,
highlight,
iconOnly: isIconOnly(children),
})}
{...props}
>
{children}
</RacToggleButton>
);
}: ToggleButtonProps) => {
const group = useToggleButtonGroup();
return (
<RacToggleButton
className={toggleStyle({
intent,
size: size ?? group?.size,
highlight,
iconOnly: isIconOnly(children),
segmented: group?.segmented,
orientation: group?.orientation,
})}
{...props}
>
{children}
</RacToggleButton>
);
};
Loading
Loading