fix: ui and ux improvements in booking questions (#7734)

This commit is contained in:
Udit Takkar
2023-04-06 08:17:53 +00:00
committed by GitHub
parent 7b5e2c8bab
commit a363b1e903
16 changed files with 183 additions and 100 deletions
@@ -11,7 +11,6 @@ import { z } from "zod";
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
import { getEventTypeAppData } from "@calcom/app-store/utils";
import type { LocationObject } from "@calcom/core/location";
@@ -29,6 +28,7 @@ import {
import getBookingResponsesSchema, {
getBookingResponsesPartialSchema,
} from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilder";
import classNames from "@calcom/lib/classNames";
import { APP_NAME } from "@calcom/lib/constants";
@@ -143,26 +143,14 @@ const BookingFields = ({
}
const optionsInputs = field.optionsInputs;
const options = locations.map((location) => {
const eventLocation = getEventLocationType(location.type);
const locationString = locationKeyToString(location);
if (typeof locationString !== "string" || !eventLocation) {
// It's possible that location app got uninstalled
return null;
}
const type = eventLocation.type;
const optionInput = optionsInputs[type as keyof typeof optionsInputs];
// TODO: Instead of `getLocationOptionsForSelect` options should be retrieved from dataStore[field.getOptionsAt]. It would make it agnostic of the `name` of the field.
const options = getLocationOptionsForSelect(locations, t);
options.forEach((option) => {
const optionInput = optionsInputs[option.value as keyof typeof optionsInputs];
if (optionInput) {
optionInput.placeholder = t(eventLocation?.attendeeInputPlaceholder || "");
optionInput.placeholder = option.inputPlaceholder;
}
return {
label: t(locationString),
value: type,
};
});
field.options = options.filter(
(location): location is NonNullable<(typeof options)[number]> => !!location
);
@@ -8,6 +8,7 @@ import { v5 as uuidv5 } from "uuid";
import type { EventNameObjectType } from "@calcom/core/event";
import { getEventName } from "@calcom/core/event";
import getLocationsOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import { classNames } from "@calcom/lib";
@@ -148,6 +149,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
description={t("booking_questions_description")}
addFieldLabel={t("add_a_booking_question")}
formProp="bookingFields"
dataStore={{
options: {
locations: getLocationsOptionsForSelect(eventType?.locations ?? [], t),
},
}}
/>
<hr className="border-subtle" />
<RequiresConfirmationController
@@ -1,4 +1,4 @@
import type { Page, PlaywrightTestArgs } from "@playwright/test";
import type { Locator, Page, PlaywrightTestArgs } from "@playwright/test";
import { expect } from "@playwright/test";
import { WebhookTriggerEvents } from "@prisma/client";
import type { createUsersFixture } from "playwright/fixtures/users";
@@ -9,6 +9,10 @@ import prisma from "@calcom/prisma";
import { test } from "./lib/fixtures";
import { createHttpServer, waitFor, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
async function getLabelText(field: Locator) {
return await field.locator("label").first().locator("span").first().innerText();
}
test.describe("Manage Booking Questions", () => {
test.afterEach(async ({ users }) => {
await users.deleteAll();
@@ -91,7 +95,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
await expect(userFieldLocator.locator('[name="how_are_you"]')).toBeVisible();
// There are 2 labels right now. Will be one in future. The second one is hidden
expect(await userFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
expect(await getLabelText(userFieldLocator)).toBe("How are you?");
await expect(userFieldLocator.locator("input[type=text]")).toBeVisible();
});
});
@@ -140,7 +144,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
expect(
await formBuilderFieldLocator.locator('[name="how_are_you"]').getAttribute("placeholder")
).toBe("I'm fine, thanks");
expect(await formBuilderFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
expect(await getLabelText(formBuilderFieldLocator)).toBe("How are you?");
await formBuilderFieldLocator.locator('[name="how_are_you"]').fill("I am great!");
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
@@ -1625,8 +1625,7 @@
"not_enough_seats": "Not enough seats",
"form_builder_field_already_exists": "A field with this name already exists",
"form_builder_field_add_subtitle": "Customize the questions asked on the booking page",
"form_builder_system_field_cant_delete": "This system field can't be removed.",
"form_builder_system_field_cant_toggle": "This system field can't be toggled.",
"show_on_booking_page":"Show on booking page",
"get_started_zapier_templates": "Get started with Zapier templates",
"team_is_unpublished": "{{team}} is unpublished",
"team_is_unpublished_description": "This team link is currently not available. Please contact the team owner or ask them publish it.",
@@ -1614,8 +1614,6 @@
"booking_confirmation_failed": "Échec de la confirmation de la réservation",
"form_builder_field_already_exists": "Un champ avec ce nom existe déjà",
"form_builder_field_add_subtitle": "Personnalisez les questions posées sur la page de réservation.",
"form_builder_system_field_cant_delete": "Ce champ système ne peut pas être supprimé.",
"form_builder_system_field_cant_toggle": "Ce champ système ne peut pas être activé.",
"get_started_zapier_templates": "Démarrer avec les modèles Zapier",
"team_member": "Membre d'équipe",
"a_routing_form": "Un formulaire de routage",
@@ -1614,8 +1614,6 @@
"booking_confirmation_failed": "אישור ההזמנה נכשל",
"form_builder_field_already_exists": "שדה עם שם זה כבר קיים",
"form_builder_field_add_subtitle": "התאמת השאלות שנשאלות בדף התזמון",
"form_builder_system_field_cant_delete": "שדה מערכת זה לא ניתן להסרה.",
"form_builder_system_field_cant_toggle": "שדה מערכת זה לא ניתן לשינוי.",
"get_started_zapier_templates": "התחל עם תבניות Zapier",
"team_is_unpublished": "צוות {{team}} אינו מפורסם",
"team_is_unpublished_description": "קישור הצוות הזה אינו זמין כעת. אנא צור קשר עם הבעלים של הצוות או בקש מהם לפרסם אותו.",
@@ -1614,8 +1614,6 @@
"booking_confirmation_failed": "Не удалось подтвердить бронирование",
"form_builder_field_already_exists": "Поле с таким именем уже существует",
"form_builder_field_add_subtitle": "Настроить вопросы, задаваемые на странице бронирования",
"form_builder_system_field_cant_delete": "Это системное поле нельзя удалить.",
"form_builder_system_field_cant_toggle": "Это системное поле нельзя переключить.",
"get_started_zapier_templates": "Начать работу с шаблонами Zapier",
"team_member": "Участник команды",
"a_routing_form": "Форма маршрутизации",
@@ -191,9 +191,9 @@ export const ensureBookingInputsHaveSystemFields = ({
type: "radioInput",
name: "location",
editable: "system",
hideWhenJustOneOption: true,
required: false,
// Populated on the fly from locations. I don't want to duplicate storing locations and instead would like to be able to refer to locations in eventType.
// options: `eventType.locations`
getOptionsAt: "locations",
optionsInputs: {
attendeeInPerson: {
type: "address",
@@ -121,12 +121,13 @@ function preprocess<T extends z.ZodType>({
const views = bookingField.views;
const isFieldApplicableToCurrentView =
currentView === "ALL_VIEWS" ? true : views ? views.find((view) => view.id === currentView) : true;
let hidden = bookingField.hidden;
const numOptions = bookingField.options?.length ?? 0;
if (bookingField.hideWhenJustOneOption) {
hidden = hidden || numOptions <= 1;
}
// If the field is hidden, then it can never be required
const isRequired = bookingField.hidden
? false
: isFieldApplicableToCurrentView
? bookingField.required
: false;
const isRequired = hidden ? false : isFieldApplicableToCurrentView ? bookingField.required : false;
if ((isPartialSchema || !isRequired) && value === undefined) {
return;
@@ -0,0 +1,29 @@
import type { LocationObject } from "@calcom/app-store/locations";
import { locationKeyToString } from "@calcom/app-store/locations";
import { getEventLocationType } from "@calcom/app-store/locations";
import type { useLocale } from "@calcom/lib/hooks/useLocale";
import notEmpty from "@calcom/lib/notEmpty";
export default function getLocationsOptionsForSelect(
locations: LocationObject[],
t: ReturnType<typeof useLocale>["t"]
) {
return locations
.map((location) => {
const eventLocation = getEventLocationType(location.type);
const locationString = locationKeyToString(location);
if (typeof locationString !== "string" || !eventLocation) {
// It's possible that location app got uninstalled
return null;
}
const type = eventLocation.type;
return {
label: t(locationString),
value: type,
inputPlaceholder: t(eventLocation?.attendeeInputPlaceholder || ""),
};
})
.filter(notEmpty);
}
+11 -4
View File
@@ -48,6 +48,7 @@ type Component =
value: { value: string; optionValue: string };
} & {
name?: string;
required?: boolean;
}
>(
props: TProps
@@ -323,8 +324,14 @@ export const Components: Record<BookingFieldType, Component> = {
})
) : (
// Show option itself as label because there is just one option
// TODO: Support asterisk for required fields
<Label>{options[0].label}</Label>
<>
<Label>
{options[0].label}
{!readOnly && optionsInputs[options[0].value]?.required ? (
<span className="ml-1 mb-1 text-sm font-medium leading-none dark:text-white">*</span>
) : null}
</Label>
</>
)}
</div>
</div>
@@ -342,10 +349,10 @@ export const Components: Record<BookingFieldType, Component> = {
name: "optionField",
}}
value={value?.optionValue}
setValue={(val: string) => {
setValue={(val: string | undefined) => {
setValue({
value: value?.value,
optionValue: val,
optionValue: val || "",
});
}}
/>
+82 -48
View File
@@ -44,11 +44,18 @@ export const FormBuilder = function FormBuilder({
description,
addFieldLabel,
formProp,
dataStore,
}: {
formProp: string;
title: string;
description: string;
addFieldLabel: string;
/**
* A readonly dataStore that is used to lookup the options for the fields. It works in conjunction with the field.getOptionAt property which acts as the key in options
*/
dataStore: {
options: Record<string, { label: string; value: string; inputPlaceholder?: string }[]>;
};
}) {
const FieldTypesMap: Record<
string,
@@ -112,6 +119,8 @@ export const FormBuilder = function FormBuilder({
value: "radioInput",
isTextType: false,
systemOnly: true,
// This is false currently because we don't want to show the options for Location field right now. It is the only field with type radioInput.
// needsOptions: true,
},
checkbox: {
label: "Checkbox Group",
@@ -148,13 +157,14 @@ export const FormBuilder = function FormBuilder({
function OptionsField({
label = "Options",
value,
onChange,
// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = () => {},
className = "",
readOnly = false,
}: {
label?: string;
value: { label: string; value: string }[];
onChange: (value: { label: string; value: string }[]) => void;
onChange?: (value: { label: string; value: string }[]) => void;
className?: string;
readOnly?: boolean;
}) {
@@ -257,6 +267,7 @@ export const FormBuilder = function FormBuilder({
const fieldType = FieldTypesMap[fieldForm.watch("type") || "text"];
const isFieldEditMode = fieldDialog.fieldIndex !== -1;
return (
<div>
<div>
@@ -264,9 +275,20 @@ export const FormBuilder = function FormBuilder({
<p className="text-subtle max-w-[280px] break-words py-1 text-sm sm:max-w-[500px]">{description}</p>
<ul className="border-default divide-subtle mt-2 divide-y-2 rounded-md border ">
{fields.map((field, index) => {
const options = field.options
? field.options
: field.getOptionsAt
? dataStore.options[field.getOptionsAt as keyof typeof dataStore]
: [];
const numOptions = options?.length ?? 0;
if (field.hideWhenJustOneOption && numOptions <= 1) {
return null;
}
const fieldType = FieldTypesMap[field.type];
const isRequired = field.required;
const isFieldEditableSystemButOptional = field.editable === "system-but-optional";
const isFieldEditableSystem = field.editable === "system";
const isUserField = !isFieldEditableSystem && !isFieldEditableSystemButOptional;
if (!fieldType) {
throw new Error(`Invalid field type - ${field.type}`);
@@ -286,7 +308,7 @@ export const FormBuilder = function FormBuilder({
<li
key={field.name}
data-testid={`field-${field.name}`}
className="group relative flex items-center justify-between p-4">
className="hover:bg-muted group relative flex items-center justify-between p-4 ">
{index >= 1 && (
<button
type="button"
@@ -305,15 +327,15 @@ export const FormBuilder = function FormBuilder({
)}
<div>
<div className="flex flex-col lg:flex-row lg:items-center">
<div className="text-default text-sm font-semibold ltr:mr-1 rtl:ml-1">
<div className="text-default text-sm font-semibold ltr:mr-2 rtl:ml-2">
{field.label || t(field.defaultLabel || "")}
</div>
<div className="flex items-center space-x-2">
{field.hidden ? (
// Hidden field can't be required, so we don't need to show the Optional badge
<Badge variant="gray">{t("hidden")}</Badge>
<Badge variant="grayWithoutHover">{t("hidden")}</Badge>
) : (
<Badge variant="gray">{isRequired ? t("required") : t("optional")}</Badge>
<Badge variant="grayWithoutHover">{isRequired ? t("required") : t("optional")}</Badge>
)}
{Object.entries(groupedBySourceLabel).map(([sourceLabel, sources], key) => (
// We don't know how to pluralize `sourceLabel` because it can be anything
@@ -323,43 +345,43 @@ export const FormBuilder = function FormBuilder({
))}
</div>
</div>
<p className="text-subtle max-w-[280px] break-words py-1 text-sm sm:max-w-[500px]">
<p className="text-subtle max-w-[280px] break-words pt-1 text-sm sm:max-w-[500px]">
{fieldType.label}
</p>
</div>
{field.editable !== "user-readonly" && (
<div className="flex items-center space-x-2">
<Switch
data-testid="toggle-field"
disabled={field.editable === "system"}
tooltip={field.editable === "system" ? t("form_builder_system_field_cant_toggle") : ""}
checked={!field.hidden}
onCheckedChange={(checked) => {
update(index, { ...field, hidden: !checked });
}}
/>
{!isFieldEditableSystem && (
<Switch
data-testid="toggle-field"
disabled={isFieldEditableSystem}
checked={!field.hidden}
onCheckedChange={(checked) => {
update(index, { ...field, hidden: !checked });
}}
classNames={{ container: "p-2 hover:bg-gray-100 rounded" }}
tooltip={t("show_on_booking_page")}
/>
)}
{isUserField && (
<Button
color="destructive"
disabled={!isUserField}
variant="icon"
onClick={() => {
removeField(index);
}}
StartIcon={FiTrash2}
/>
)}
<Button
data-testid="edit-field-action"
color="secondary"
onClick={() => {
editField(index, field);
}}>
Edit
{t("edit")}
</Button>
<Button
color="secondary"
tooltip={
field.editable === "system" || field.editable === "system-but-optional"
? t("form_builder_system_field_cant_delete")
: ""
}
disabled={field.editable === "system" || field.editable === "system-but-optional"}
variant="icon"
onClick={() => {
removeField(index);
}}
StartIcon={FiTrash2}
/>
</div>
)}
</li>
@@ -434,7 +456,7 @@ export const FormBuilder = function FormBuilder({
}}
value={FieldTypesMap[fieldForm.getValues("type")]}
options={FieldTypes.filter((f) => !f.systemOnly)}
label="Input Type"
label={t("input_type")}
/>
<InputField
required
@@ -444,7 +466,7 @@ export const FormBuilder = function FormBuilder({
fieldForm.getValues("editable") === "system" ||
fieldForm.getValues("editable") === "system-but-optional"
}
label="Name"
label="Identifier"
/>
<InputField
{...fieldForm.register("label")}
@@ -462,8 +484,7 @@ export const FormBuilder = function FormBuilder({
placeholder={t(fieldForm.getValues("defaultPlaceholder") || "")}
/>
) : null}
{fieldType?.needsOptions ? (
{fieldType?.needsOptions && !fieldForm.getValues("getOptionsAt") ? (
<Controller
name="options"
render={({ field: { value, onChange } }) => {
@@ -471,6 +492,14 @@ export const FormBuilder = function FormBuilder({
}}
/>
) : null}
{/* TODO: Maybe we should show location options in readOnly mode in Booking Questions. Right now options are not shown in Manage Booking Questions UI for location Booking Question */}
{/* {fieldForm.getValues("getOptionsAt") ? (
<OptionsField
readOnly={true}
value={dataStore.options[fieldForm.getValues("getOptionsAt") as keyof typeof dataStore]}
className="mt-6"
/>
) : null} */}
<Controller
name="required"
control={fieldForm.control}
@@ -489,7 +518,7 @@ export const FormBuilder = function FormBuilder({
}}
/>
<DialogFooter>
<DialogClose color="secondary">Cancel</DialogClose>
<DialogClose color="secondary">{t("cancel")}</DialogClose>
<Button data-testid="field-add-save" type="submit">
{isFieldEditMode ? t("save") : t("add")}
</Button>
@@ -520,10 +549,12 @@ const WithLabel = ({
{/* Component itself managing it's label should remove these checks */}
{field.type !== "boolean" && field.type !== "multiemail" && field.label && (
<div className="mb-2 flex items-center">
<Label className="!mb-0 flex items-center">{field.label}</Label>
<span className="dark:text-inverted ml-1 -mb-1 text-sm font-medium leading-none">
{!readOnly && field.required ? "*" : ""}
</span>
<Label className="!mb-0">
<span>{field.label}</span>
<span className="dark:text-inverted ml-1 -mb-1 text-sm font-medium leading-none">
{!readOnly && field.required ? "*" : ""}
</span>
</Label>
</div>
)}
{children}
@@ -587,7 +618,6 @@ export const ComponentForField = ({
`Value ${value} is not valid for type ${componentConfig.propsType} for field ${field.name}`
);
}
if (componentConfig.propsType === "text") {
return (
<WithLabel field={field} readOnly={readOnly}>
@@ -683,6 +713,7 @@ export const ComponentForField = ({
setValue={setValue as (arg: typeof value) => void}
optionsInputs={field.optionsInputs}
options={field.options}
required={field.required}
/>
</WithLabel>
) : null;
@@ -708,7 +739,7 @@ export const FormBuilderField = ({
control={control}
// Make it a variable
name={`responses.${field.name}`}
render={({ field: { value, onChange } }) => {
render={({ field: { value, onChange }, fieldState: { error } }) => {
return (
<div>
<ComponentForField
@@ -722,10 +753,13 @@ export const FormBuilderField = ({
<ErrorMessage
name="responses"
errors={formState.errors}
render={({ message }) => {
const name = message?.replace(/\{([^}]+)\}.*/, "$1");
// Use the message targeted for it.
if (name !== field.name) {
render={({ message }: { message: string | undefined }) => {
message = message || "";
// If the error comes due to parsing the `responses` object(which can have error for any field), we need to identify the field that has the error from the message
const name = message.replace(/\{([^}]+)\}.*/, "$1");
const isResponsesErrorForThisField = name === field.name;
// If the error comes for the specific property of responses(Possible for system fields), then also we would go ahead and show the error
if (!isResponsesErrorForThisField && !error) {
return null;
}
@@ -738,7 +772,7 @@ export const FormBuilderField = ({
data-testid={`error-message-${field.name}`}
className="mt-2 flex items-center text-sm text-red-700 ">
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p>{t(message)}</p>
<p>{t(message || "invalid_input")}</p>
</div>
);
}}
@@ -44,7 +44,14 @@ const fieldSchema = z.object({
.array()
.optional(),
type: fieldTypeEnum,
options: z.array(z.object({ label: z.string(), value: z.string() })).optional(),
/**
* This is an alternate way to specify options when the options are stored elsewhere. Form Builder expects options to be present at `dataStore[getOptionsAt]`
* This allows keeping a single source of truth in DB.
*/
getOptionsAt: z.string().optional(),
optionsInputs: z
.record(
z.object({
@@ -56,6 +63,10 @@ const fieldSchema = z.object({
})
)
.optional(),
/**
* It is used to hide fields such as location when there are less than two options
*/
hideWhenJustOneOption: z.boolean().default(false).optional(),
required: z.boolean().default(false).optional(),
hidden: z.boolean().optional(),
+1
View File
@@ -18,6 +18,7 @@ const badgeStyles = cva("font-medium inline-flex items-center justify-center rou
blue: "bg-info text-info",
red: "bg-error text-error",
error: "bg-error text-error",
grayWithoutHover: "bg-gray-100 text-gray-800 dark:bg-darkgray-200 dark:text-darkgray-800",
},
size: {
sm: "px-1 py-0.5 text-xs leading-3",
+16 -10
View File
@@ -3,7 +3,7 @@ import * as Label from "@radix-ui/react-label";
import * as PrimitiveSwitch from "@radix-ui/react-switch";
import React from "react";
import classNames from "@calcom/lib/classNames";
import cx from "@calcom/lib/classNames";
import { Tooltip } from "../../tooltip";
@@ -16,21 +16,27 @@ const Wrapper = ({ children, tooltip }: { tooltip?: string; children: React.Reac
const Switch = (
props: React.ComponentProps<typeof PrimitiveSwitch.Root> & {
label?: string;
thumbProps?: {
className?: string;
};
fitToHeight?: boolean;
tooltip?: string;
classNames?: {
container?: string;
thumb?: string;
};
}
) => {
const { label, fitToHeight, ...primitiveProps } = props;
const { label, fitToHeight, classNames, ...primitiveProps } = props;
const id = useId();
return (
<Wrapper tooltip={props.tooltip}>
<div className={classNames("flex h-auto w-auto flex-row items-center", fitToHeight && "h-fit")}>
<div
className={cx(
"flex h-auto w-auto flex-row items-center",
fitToHeight && "h-fit",
classNames?.container
)}>
<PrimitiveSwitch.Root
className={classNames(
className={cx(
props.checked ? "bg-inverted" : "bg-emphasis",
primitiveProps.disabled && "cursor-not-allowed",
"focus:ring-brand-default h-5 w-[34px] rounded-full shadow-none",
@@ -39,17 +45,17 @@ const Switch = (
{...primitiveProps}>
<PrimitiveSwitch.Thumb
id={id}
className={classNames(
className={cx(
"block h-[14px] w-[14px] rounded-full transition will-change-transform ltr:translate-x-[4px] rtl:-translate-x-[4px] ltr:[&[data-state='checked']]:translate-x-[17px] rtl:[&[data-state='checked']]:-translate-x-[17px]",
props.checked ? "bg-default shadow-inner" : "bg-inverted",
props.thumbProps?.className
classNames?.thumb
)}
/>
</PrimitiveSwitch.Root>
{label && (
<Label.Root
htmlFor={id}
className={classNames(
className={cx(
"text-emphasis align-text-top text-sm font-medium ltr:ml-2 rtl:mr-2",
primitiveProps.disabled ? "cursor-not-allowed opacity-25" : "cursor-pointer "
)}>
@@ -32,8 +32,10 @@ export const BooleanToggleGroup = function BooleanToggleGroup({
onValueChange(defaultValue);
return null;
}
const commonClass =
"w-full inline-flex items-center justify-center rounded py-[10px] px-4 text-sm font-medium leading-4";
const commonClass = classNames(
"w-full inline-flex items-center justify-center rounded py-[10px] px-4 text-sm font-medium leading-4",
disabled && "cursor-not-allowed"
);
const selectedClass = classNames(commonClass, "bg-emphasis text-emphasis");
const unselectedClass = classNames(commonClass, "text-default hover:bg-subtle hover:text-emphasis");
return (
@@ -53,6 +55,7 @@ export const BooleanToggleGroup = function BooleanToggleGroup({
value="yes">
Yes
</ToggleGroupItemPrimitive>
<ToggleGroupItemPrimitive
disabled={disabled}
className={classNames(!boolean(yesNoValue) ? selectedClass : unselectedClass)}