feat: booker atom confirm and back button styling (#20162)

* feat: submitButtonDisabled Booker atom prop

* feat: confirmStep.confirmButton & backButton booker styles

* refactor: rename submitButtonDisabled to confirmButtonDisabled

* feat: implement this feature for skip confirm step

* refactor: naming
This commit is contained in:
Lauris Skraucis
2025-03-19 11:30:48 +00:00
committed by GitHub
parent a64e7f9c2a
commit 91d4f1c219
9 changed files with 150 additions and 22 deletions
+95 -19
View File
@@ -122,24 +122,100 @@ Below is a list of props that can be passed to the booker atom.
| bannerUrl | No | Adds custom banner to the booker atom |
| onBookerStateChange | No | Callback function that is triggered when the state of the booker atom changes. |
| allowUpdatingUrlParams | No | Boolean indicating if the URL parameters should be updated, defaults to false. |
| confirmButtonDisabled | No | Boolean indicating if the submit button should be disabled, defaults to false. |
Along with the props, booker atom accepts custom styles via the **customClassNames** prop. Below is a list of props that fall under this **customClassNames** prop.
<p></p>
## Styling
| Name | Description |
|:-----------------------------|:----------------------------------------------------------------------|
| bookerContainer | Adds styling to the whole of the booker atom |
| eventMetaContainer | Styles the event meta component containing details about an event |
| eventMetaTitle | Adds styles to the event meta title |
| eventMetaTimezoneSelect | Adds styles to the event meta timezone selector |
| datePickerContainer | Adds styling to the date picker |
| datePickerTitle | Styles the date picker title |
| datePickerDays | Adds styling to all days in the date picker |
| datePickerDate | Adds styling to all dates in the date picker |
| datePickerDatesActive | Styles only the dates with available slots |
| datePickerToggle | Styles the left and right toggle buttons |
| availableTimeSlotsContainer | Adds styling to the available time slots component |
| availableTimeSlotsHeaderContainer | Styles only the header container |
| availableTimeSlotsTitle | Adds styles to the title |
| availableTimeSlotsTimeFormatToggle | Adds styles to the format toggle buttons |
| availableTimes | Styles all the available times container |
Booker atom accepts custom styles via the `customClassNames` prop. This prop is an object that contains root level styles and nested objects for styling different parts of the booker component. Each nested object groups related styles for a specific section of the booker (e.g., eventMeta, datePicker, availableTimeSlots, etc).
Here is an example booker with root level style `bookerContainer` and nested object `datePickerCustomClassNames` with `datePickerDatesActive` style:
```js
import { Booker } from "@calcom/atoms";
export default function Booker( props : BookerProps ) {
return (
<>
<Booker
...
customClassNames={{
bookerContainer: "!bg-[#F5F2FE] [&_button:!rounded-full] border-subtle border",
datePickerCustomClassNames: {
datePickerDatesActive: "!bg-[#D7CEF5]",
},
}}
/>
</>
)
}
```
Below is a list of **customClassNames** properties grouped by their parent objects:
### Root Level Styles
| Property | Description |
|:---------|:-------------|
| bookerContainer | Adds styling to the whole of the booker atom |
### Event Meta Styles (`eventMetaCustomClassNames`)
| Property | Description |
|:---------|:-------------|
| eventMetaContainer | Styles the event meta component containing details about an event |
| eventMetaTitle | Adds styles to the event meta title |
| eventMetaTimezoneSelect | Adds styles to the event meta timezone selector |
### Date Picker Styles (`datePickerCustomClassNames`)
| Property | Description |
|:---------|:-------------|
| datePickerContainer | Adds styling to the date picker |
| datePickerTitle | Styles the date picker title |
| datePickerDays | Adds styling to all days in the date picker |
| datePickerDate | Adds styling to all dates in the date picker |
| datePickerDatesActive | Styles only the dates with available slots |
| datePickerToggle | Styles the left and right toggle buttons |
### Available Time Slots Styles (`availableTimeSlotsCustomClassNames`)
| Property | Description |
|:---------|:-------------|
| availableTimeSlotsContainer | Adds styling to the available time slots component |
| availableTimeSlotsHeaderContainer | Styles only the header container |
| availableTimeSlotsTitle | Adds styles to the title |
| availableTimeSlotsTimeFormatToggle | Adds styles to the format toggle buttons |
| availableTimes | Styles all the available times container |
### Confirmation Step Styles (`confirmStep`)
| Property | Description |
|:---------|:-------------|
| confirmButton | Styles the confirm button in the booking form |
| backButton | Styles the back button in the booking form |
Here is an example with more custom styles:
```js
import { Booker } from "@calcom/atoms";
export default function Booker( props : BookerProps ) {
return (
<>
<Booker
...
customClassNames={{
bookerContainer: "!bg-[#F5F2FE] [&_button:!rounded-full] border-subtle border",
datePickerCustomClassNames: {
datePickerDatesActive: "!bg-[#D7CEF5]",
},
eventMetaCustomClassNames: {
eventMetaTitle: "text-[#7151DC]",
},
availableTimeSlotsCustomClassNames: {
availableTimeSlotsHeaderContainer: "!bg-[#F5F2FE]",
availableTimes: "!bg-[#D7CEF5]",
},
confirmStep: {
confirmButton: "!bg-purple-700",
backButton: "text-purple-700 hover:!bg-purple-700 hover:!text-white"
}
}}
/>
</>
)
}
```
@@ -82,6 +82,7 @@ const BookerComponent = ({
isBookingDryRun: isBookingDryRunProp,
renderCaptcha,
hashedLink,
confirmButtonDisabled,
}: BookerProps & WrappedBookerProps) => {
const searchParams = useCompatSearchParams();
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
@@ -239,6 +240,11 @@ const BookerComponent = ({
extraOptions={extraOptions}
rescheduleUid={rescheduleUid}
isVerificationCodeSending={isVerificationCodeSending}
confirmButtonDisabled={confirmButtonDisabled}
classNames={{
confirmButton: customClassNames?.confirmStep?.confirmButton,
backButton: customClassNames?.confirmStep?.backButton,
}}
isPlatform={isPlatform}>
<>
{!isPlatform && (
@@ -483,6 +489,8 @@ const BookerComponent = ({
skipConfirmStep={skipConfirmStep}
shouldRenderCaptcha={shouldRenderCaptcha}
watchedCfToken={watchedCfToken}
confirmButtonDisabled={confirmButtonDisabled}
confirmStepClassNames={customClassNames?.confirmStep}
/>
</BookerSection>
</AnimatePresence>
@@ -33,6 +33,9 @@ type AvailableTimeSlotsProps = {
availableTimeSlotsTimeFormatToggle?: string;
availableTimes?: string;
};
confirmStepClassNames?: {
confirmButton?: string;
};
loadingStates: IUseBookingLoadingStates;
isVerificationCodeSending: boolean;
renderConfirmNotVerifyEmailButtonCond: boolean;
@@ -44,6 +47,7 @@ type AvailableTimeSlotsProps = {
* This is the list of time slots that are unavailable to book
*/
unavailableTimeSlots: string[];
confirmButtonDisabled?: boolean;
};
/**
@@ -65,6 +69,8 @@ export const AvailableTimeSlots = ({
seatsPerTimeSlot,
onSubmit,
unavailableTimeSlots,
confirmButtonDisabled,
confirmStepClassNames,
...props
}: AvailableTimeSlotsProps) => {
const selectedDate = useBookerStore((state) => state.selectedDate);
@@ -224,6 +230,8 @@ export const AvailableTimeSlots = ({
skipConfirmStep={skipConfirmStep}
seatsPerTimeSlot={seatsPerTimeSlot}
handleSlotClick={handleSlotClick}
confirmButtonDisabled={confirmButtonDisabled}
confirmStepClassNames={confirmStepClassNames}
{...props}
/>
</div>
@@ -31,6 +31,11 @@ type BookEventFormProps = {
isVerificationCodeSending: boolean;
isTimeslotUnavailable: boolean;
shouldRenderCaptcha?: boolean;
confirmButtonDisabled?: boolean;
classNames?: {
confirmButton?: string;
backButton?: string;
};
};
export const BookEventForm = ({
@@ -49,6 +54,8 @@ export const BookEventForm = ({
isPlatform = false,
isTimeslotUnavailable,
shouldRenderCaptcha,
confirmButtonDisabled,
classNames,
}: Omit<BookEventFormProps, "event"> & {
eventQuery: {
isError: boolean;
@@ -195,7 +202,12 @@ export const BookEventForm = ({
) : (
<>
{!!onCancel && (
<Button color="minimal" type="button" onClick={onCancel} data-testid="back">
<Button
color="minimal"
type="button"
onClick={onCancel}
data-testid="back"
className={classNames?.backButton}>
{t("back")}
</Button>
)}
@@ -203,12 +215,15 @@ export const BookEventForm = ({
<Button
type="submit"
color="primary"
disabled={(!!shouldRenderCaptcha && !watchedCfToken) || isTimeslotUnavailable}
disabled={
(!!shouldRenderCaptcha && !watchedCfToken) || isTimeslotUnavailable || confirmButtonDisabled
}
loading={
loadingStates.creatingBooking ||
loadingStates.creatingRecurringBooking ||
isVerificationCodeSending
}
className={classNames?.confirmButton}
data-testid={
rescheduleUid && bookingData ? "confirm-reschedule-button" : "confirm-book-button"
}>
@@ -120,6 +120,7 @@ export type WrappedBookerPropsMain = {
customClassNames?: CustomClassNames;
isBookingDryRun?: boolean;
renderCaptcha?: boolean;
confirmButtonDisabled?: boolean;
};
export type WrappedBookerPropsForPlatform = WrappedBookerPropsMain & {
@@ -162,4 +163,8 @@ export type CustomClassNames = {
availableTimes?: string;
};
atomsWrapper?: string;
confirmStep?: {
confirmButton?: string;
backButton?: string;
};
};
@@ -62,6 +62,9 @@ type SlotItemProps = {
data?: Pick<BookerEvent, "length" | "bookingFields" | "price" | "currency" | "metadata"> | null;
};
customClassNames?: string;
confirmStepClassNames?: {
confirmButton?: string;
};
loadingStates?: IUseBookingLoadingStates;
isVerificationCodeSending?: boolean;
renderConfirmNotVerifyEmailButtonCond?: boolean;
@@ -69,6 +72,7 @@ type SlotItemProps = {
shouldRenderCaptcha?: boolean;
watchedCfToken?: string;
unavailableTimeSlots?: string[];
confirmButtonDisabled?: boolean;
handleSlotClick?: (slot: Slot, isOverlapping: boolean) => void;
};
@@ -89,6 +93,8 @@ const SlotItem = ({
handleSlotClick,
onTentativeTimeSelect,
unavailableTimeSlots = [],
confirmButtonDisabled,
confirmStepClassNames,
}: SlotItemProps) => {
const { t } = useLocale();
@@ -200,6 +206,7 @@ const SlotItem = ({
variant={layout === "column_view" ? "icon" : "button"}
StartIcon={layout === "column_view" ? "chevron-right" : undefined}
type="button"
className={confirmStepClassNames?.confirmButton}
onClick={() =>
onTimeSelect &&
onTimeSelect(slot.time, slot?.attendees || 0, seatsPerTimeSlot, slot.bookingUid)
@@ -211,7 +218,8 @@ const SlotItem = ({
loadingStates?.creatingBooking ||
loadingStates?.creatingRecurringBooking ||
isVerificationCodeSending ||
loadingStates?.creatingInstantBooking
loadingStates?.creatingInstantBooking ||
confirmButtonDisabled
}
color="primary"
loading={
@@ -58,6 +58,7 @@ export const BookerPlatformWrapper = (
preventEventTypeRedirect,
onBookerStateChange,
allowUpdatingUrlParams = false,
confirmButtonDisabled,
} = props;
const layout = BookerLayouts[view];
@@ -504,6 +505,7 @@ export const BookerPlatformWrapper = (
rescheduledBy={props.rescheduledBy ?? null}
bookingUid={props.bookingUid ?? null}
isRedirect={false}
confirmButtonDisabled={confirmButtonDisabled}
fromUserNameRedirected=""
hasSession={hasSession}
onGoBackInstantMeeting={function (): void {
+1
View File
@@ -73,6 +73,7 @@ export type BookerPlatformWrapperAtomProps = Omit<
hostsLimit?: number;
preventEventTypeRedirect?: boolean;
allowUpdatingUrlParams?: boolean;
confirmButtonDisabled?: boolean;
};
type VIEW_TYPE = keyof typeof BookerLayouts;
@@ -107,6 +107,7 @@ export default function Bookings(props: { calUsername: string; calEmail: string
}}
metadata={{ CustomKey: "CustomValue" }}
duration={eventTypeDuration}
confirmButtonDisabled={false}
customClassNames={{
bookerContainer: "!bg-[#F5F2FE] [&_button:!rounded-full] border-subtle border",
datePickerCustomClassNames: {
@@ -119,6 +120,10 @@ export default function Bookings(props: { calUsername: string; calEmail: string
availableTimeSlotsHeaderContainer: "!bg-[#F5F2FE]",
availableTimes: "!bg-[#D7CEF5]",
},
confirmStep: {
confirmButton: "!bg-purple-700",
backButton: "text-purple-700 hover:!bg-purple-700 hover:!text-white",
},
}}
{...(isTeamEvent
? { isTeamEvent: true, teamId: teams?.[0]?.id || 0 }