Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BOOKINGSG-6090-timetable][JZ|FY] Added TimeTable component and enhanced popover #581

Merged
merged 18 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export * from "./time-slot-bar-week";
export * from "./time-slot-week-view";
export * from "./timeline";
export * from "./timepicker";
export * from "./timetable";
export * from "./toast";
export * from "./toggle";
export * from "./tooltip";
Expand Down
8 changes: 6 additions & 2 deletions src/popover-v2/popover-trigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const PopoverTrigger = ({
placement: position,
whileElementsMounted: autoUpdate,
middleware: [
offset(16),
offset(otherProps.offset ?? 16),
xyukianesa marked this conversation as resolved.
Show resolved Hide resolved
flip(),
shift({
limiter: limitShift(),
Expand All @@ -71,7 +71,10 @@ export const PopoverTrigger = ({
const hover = useHover(context, {
enabled: trigger === "hover",
// short window to enter the floating element without it closing
delay: { close: 500 },
delay: {
open: otherProps?.delay?.open ?? 0,
close: otherProps?.delay?.close ?? 500,
},
});

const { getReferenceProps, getFloatingProps } = useInteractions([
Expand Down Expand Up @@ -136,6 +139,7 @@ export const PopoverTrigger = ({
}}
style={{
...floatingStyles,
outline: "none",
zIndex: zIndex ?? parentZIndex,
}}
{...getFloatingProps()}
Expand Down
3 changes: 3 additions & 0 deletions src/popover-v2/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export interface PopoverV2TriggerProps {
* the popover may not be visible. Specify the parent element here instead
*/
rootNode?: RefObject<HTMLElement> | undefined;
offset?: number | undefined;
// in milliseconds
delay?: { open: number; close: number } | undefined;
xyukianesa marked this conversation as resolved.
Show resolved Hide resolved
onPopoverAppear?: (() => void) | undefined;
onPopoverDismiss?: (() => void) | undefined;
}
Expand Down
2 changes: 2 additions & 0 deletions src/timetable/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./timetable";
export * from "./types";
40 changes: 40 additions & 0 deletions src/timetable/timetable-navigator/timetable-navigator.style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import styled, { css } from "styled-components";
import { Color } from "../../color";
import { IconButton } from "../../icon-button";
import { Text } from "../../text";

export const StyledRefreshButton = styled(IconButton)<{ $isLoading: boolean }>`
color: ${Color.Neutral[3]};
@keyframes spin {
xyukianesa marked this conversation as resolved.
Show resolved Hide resolved
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
svg {
${(props) => {
if (props.$isLoading) {
return css`
-webkit-animation: spin 4s linear infinite;
-moz-animation: spin 4s linear infinite;
`;
}
}}
}
`;

export const NavigationHeaderWrapper = styled.div`
width: 252px;
padding-bottom: 1rem;
`;

export const NavigationHeaderSubtitleWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
gap: 0.625rem;
`;

export const StyledResultText = styled(Text.H6)`
color: ${Color.Neutral[3]};
`;
107 changes: 107 additions & 0 deletions src/timetable/timetable-navigator/timetable-navigator.tsx
xyukianesa marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { RefreshIcon } from "@lifesg/react-icons";
import { MutableRefObject } from "react";
import { DateNavigator } from "../../date-navigator/date-navigator";
import {
NavigationHeaderSubtitleWrapper,
NavigationHeaderWrapper,
StyledRefreshButton,
StyledResultText,
} from "./timetable-navigator.style";

interface TimeTableNavigatorProps {
selectedDate: string;
isLoading: boolean;
tableContainerRef: MutableRefObject<HTMLDivElement>;
minDate?: string | undefined;
maxDate?: string | undefined;
totalRecords?: number | undefined;
onLeftArrowClick?: (currentDate: string) => void | undefined;
onRightArrowClick?: (currentDate: string) => void | undefined;
onRefresh?: (() => void) | undefined;
}

export const TimeTableNavigator = ({
selectedDate,
isLoading,
tableContainerRef,
...optionalProps
xyukianesa marked this conversation as resolved.
Show resolved Hide resolved
}: TimeTableNavigatorProps) => {
// =============================================================================
// EVENT HANDLERS
// =============================================================================

const scrollToTop = () => {
if (tableContainerRef.current) {
tableContainerRef.current.scrollTop = 0;
}
};

const onRefresh = () => {
xyukianesa marked this conversation as resolved.
Show resolved Hide resolved
if (!optionalProps.onRefresh) return;
scrollToTop();
optionalProps.onRefresh();
};

const handleRightArrowClick = (date: string) => {
scrollToTop();
optionalProps.onRightArrowClick(date);
};

const handleLeftArrowClick = (date: string) => {
scrollToTop();
optionalProps.onLeftArrowClick(date);
};
// =============================================================================
// RENDER FUNCTIONS
// =============================================================================
const renderRecordsSection = () => {
if (optionalProps.totalRecords === undefined) return <></>;
return (
<NavigationHeaderSubtitleWrapper id="timetable-records-wrapper-id">
<StyledResultText
id="timetable-records-results-id"
xyukianesa marked this conversation as resolved.
Show resolved Hide resolved
data-testid="timetable-records-results"
weight={"semibold"}
>
{optionalProps.totalRecords} results found
</StyledResultText>
{optionalProps.onRefresh && (
<StyledRefreshButton
id="timetable-records-refresh-btn-id"
data-testid="timetable-records-refresh-btn"
styleType="light"
sizeType="small"
disabled={isLoading}
onClick={onRefresh}
$isLoading={isLoading}
>
<RefreshIcon />
</StyledRefreshButton>
)}
</NavigationHeaderSubtitleWrapper>
);
};

return (
<NavigationHeaderWrapper id="timetable-navigation-header-wrapper-id">
{
<DateNavigator
selectedDate={selectedDate}
isLoading={isLoading}
{...optionalProps}
onRightArrowClick={
optionalProps.onRightArrowClick
? handleRightArrowClick
: undefined
}
onLeftArrowClick={
optionalProps.onLeftArrowClick
? handleLeftArrowClick
: undefined
}
/>
}
{renderRecordsSection()}
</NavigationHeaderWrapper>
);
};
6 changes: 6 additions & 0 deletions src/timetable/timetable-row/row-bar.style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import styled from "styled-components";

export const RowCellContainer = styled.div`
display: flex;
width: 100%;
`;
122 changes: 122 additions & 0 deletions src/timetable/timetable-row/row-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import dayjs from "dayjs";
import { MutableRefObject } from "react";
import {
CustomPopoverProps,
RowBarColors,
RowCellData,
RowData,
} from "../types";
import { RowCellContainer } from "./row-bar.style";
import { RowCell } from "./row-cell";

interface RowBarProps extends RowData {
timetableMinTime: string;
timetableMaxTime: string;
intervalWidth: number;
containerRef: MutableRefObject<HTMLDivElement>;
rowBarColor: RowBarColors;
outsideOpHoursCellCustomPopover?: CustomPopoverProps | undefined;
"data-testid"?: string | undefined;
onCellClick?: (data: RowCellData, e: React.MouseEvent) => void;
}

export const RowBar = ({
id,
timetableMinTime,
timetableMaxTime,
rowMinTime = timetableMinTime,
rowMaxTime = timetableMaxTime,
rowCells,
rowBarColor,
intervalWidth,
containerRef,
...optionalProps
}: RowBarProps) => {
// =============================================================================
// CONST, STATE, REF
// =============================================================================
const rowCellArray: RowCellData[] = [];

// ===========================================================================
// HELPER FUNCTIONS
// ===========================================================================

const isOnTheSameHour = (
time1: dayjs.Dayjs,
time2: dayjs.Dayjs
): boolean => {
return time1.get("hour") == time2.get("hour");
};

// Handle non-op before hours
if (dayjs(timetableMinTime, "HH:mm").isBefore(dayjs(rowMinTime, "HH:mm"))) {
rowCellArray.push({
id,
startTime: timetableMinTime,
endTime: rowMinTime,
status: "blocked",
customPopover: optionalProps.outsideOpHoursCellCustomPopover,
});
}

rowCells.forEach((cell, index) => {
const { endTime } = cell;
rowCellArray.push(cell);

const nextSlotStartTime = dayjs(
rowCells[index + 1]?.startTime,
"HH:mm"
);
const parsedEndTime = dayjs(endTime, "HH:mm");

let curr = parsedEndTime;
while (curr.isBefore(nextSlotStartTime)) {
if (!isOnTheSameHour(curr, nextSlotStartTime)) {
const nextHour = curr.add(1, "hour").startOf("hour"); // Round to the next hour
rowCellArray.push({
id,
startTime: curr.format("HH:mm").toString(),
endTime: nextHour.format("HH:mm").toString(),
status: undefined,
});
curr = nextHour;
} else {
rowCellArray.push({
id,
startTime: curr.format("HH:mm").toString(),
endTime: nextSlotStartTime.format("HH:mm").toString(),
status: undefined,
});
curr = nextSlotStartTime;
}
}
});

// Handle non-op after hours
if (dayjs(timetableMaxTime, "HH:mm").isAfter(dayjs(rowMaxTime, "HH:mm"))) {
rowCellArray.push({
id,
startTime: rowMaxTime,
endTime: timetableMaxTime,
status: "blocked",
customPopover: optionalProps.outsideOpHoursCellCustomPopover,
});
}

return (
<RowCellContainer data-testid={optionalProps["data-testid"]}>
{rowCellArray.map((cell, index) => {
return (
<RowCell
key={`${index}-row-cell-key`}
{...cell}
intervalWidth={intervalWidth}
rowBarColor={rowBarColor}
containerRef={containerRef}
onCellClick={optionalProps.onCellClick}
/>
);
})}
</RowCellContainer>
);
};
Loading
Loading