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:
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user