From 49cf8e6461a3a78b876f71faf39276f24578ecc4 Mon Sep 17 00:00:00 2001 From: Quek Ruo Ling Date: Mon, 15 May 2023 15:09:18 +0800 Subject: [PATCH 01/14] [MISC][RL] Extract time picker dropdown --- .../timepicker-dropdown.styles.tsx | 12 ++++++------ .../timepicker-dropdown.tsx | 7 +++---- src/timepicker/timepicker.tsx | 4 ++-- .../helper.ts => util/time-helper.ts} | 18 ++++++------------ 4 files changed, 17 insertions(+), 24 deletions(-) rename src/{timepicker => shared/timepicker-dropdown}/timepicker-dropdown.styles.tsx (94%) rename src/{timepicker => shared/timepicker-dropdown}/timepicker-dropdown.tsx (98%) rename src/{timepicker/helper.ts => util/time-helper.ts} (94%) diff --git a/src/timepicker/timepicker-dropdown.styles.tsx b/src/shared/timepicker-dropdown/timepicker-dropdown.styles.tsx similarity index 94% rename from src/timepicker/timepicker-dropdown.styles.tsx rename to src/shared/timepicker-dropdown/timepicker-dropdown.styles.tsx index b99b8d41a..9e714524a 100644 --- a/src/timepicker/timepicker-dropdown.styles.tsx +++ b/src/shared/timepicker-dropdown/timepicker-dropdown.styles.tsx @@ -1,11 +1,11 @@ import { animated } from "react-spring"; import styled from "styled-components"; -import { Button } from "../button"; -import { Color } from "../color"; -import { IconButton } from "../icon-button"; -import { MediaQuery } from "../media"; -import { Text, TextStyleHelper } from "../text"; -import { Toggle } from "../toggle"; +import { Button } from "../../button"; +import { Color } from "../../color"; +import { IconButton } from "../../icon-button"; +import { MediaQuery } from "../../media"; +import { Text, TextStyleHelper } from "../../text"; +import { Toggle } from "../../toggle"; // ============================================================================= // STYLING diff --git a/src/timepicker/timepicker-dropdown.tsx b/src/shared/timepicker-dropdown/timepicker-dropdown.tsx similarity index 98% rename from src/timepicker/timepicker-dropdown.tsx rename to src/shared/timepicker-dropdown/timepicker-dropdown.tsx index 9af01bb4a..3e6192e23 100644 --- a/src/timepicker/timepicker-dropdown.tsx +++ b/src/shared/timepicker-dropdown/timepicker-dropdown.tsx @@ -3,8 +3,8 @@ import { ChevronUpIcon } from "@lifesg/react-icons/chevron-up"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { useResizeDetector } from "react-resize-detector"; import { useSpring } from "react-spring"; -import { StringHelper } from "../util/string-helper"; -import { Period, TimepickerHelper } from "./helper"; +import { StringHelper } from "../../util/string-helper"; +import { Period, TimeFormat, TimepickerHelper } from "../../util/time-helper"; import { AnimatedDiv, Container, @@ -19,7 +19,6 @@ import { TimePeriodSection, TimePeriodToggle, } from "./timepicker-dropdown.styles"; -import { TimepickerFormat } from "./types"; enum EInputButtonName { HOUR_UP = "hour-up", @@ -42,7 +41,7 @@ interface IProps { id?: string; value: string; show: boolean; - format: TimepickerFormat; + format: TimeFormat; onChange: (value: string) => void; onCancel: () => void; } diff --git a/src/timepicker/timepicker.tsx b/src/timepicker/timepicker.tsx index f90668f17..bbf9ab683 100644 --- a/src/timepicker/timepicker.tsx +++ b/src/timepicker/timepicker.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { TimepickerHelper } from "./helper"; -import { TimepickerDropdown } from "./timepicker-dropdown"; +import { TimepickerHelper } from "../util/time-helper"; +import { TimepickerDropdown } from "../shared/timepicker-dropdown/timepicker-dropdown"; import { InputSelectorElement, Wrapper } from "./timepicker.styles"; import { TimepickerProps } from "./types"; diff --git a/src/timepicker/helper.ts b/src/util/time-helper.ts similarity index 94% rename from src/timepicker/helper.ts rename to src/util/time-helper.ts index 8fcfdb90b..376dd422a 100644 --- a/src/timepicker/helper.ts +++ b/src/util/time-helper.ts @@ -1,10 +1,10 @@ -import { StringHelper } from "../util/string-helper"; -import { TimepickerFormat } from "./types"; +import { StringHelper } from "./string-helper"; // ============================================================================= // INTERFACES // ============================================================================= export type Period = "am" | "pm"; +export type TimeFormat = "12hr" | "24hr"; export interface TimeValues { hour: string; @@ -24,7 +24,7 @@ interface TimeValuesPlain { // ============================================================================= export namespace TimepickerHelper { export const getTimeValues = ( - format: TimepickerFormat, + format: TimeFormat, value?: string ): TimeValues => { // Default value @@ -154,10 +154,7 @@ export namespace TimepickerHelper { return StringHelper.padValue(hourString); }; - export const formatValue = ( - value: string, - format: TimepickerFormat - ): string => { + export const formatValue = (value: string, format: TimeFormat): string => { try { const plain = convertToPlain(value, format); @@ -182,7 +179,7 @@ export namespace TimepickerHelper { // ============================================================================= // NON-EXPORTABLES // ============================================================================= -const isValidHour = (hourString: string, format: TimepickerFormat): boolean => { +const isValidHour = (hourString: string, format: TimeFormat): boolean => { const numValue = parseInt(hourString); return format === "24hr" ? numValue >= 0 && numValue <= 23 @@ -201,10 +198,7 @@ const isValidTimePeriod = (timePeriodString: string): boolean => { ); }; -const convertToPlain = ( - value: string, - format: TimepickerFormat -): TimeValuesPlain => { +const convertToPlain = (value: string, format: TimeFormat): TimeValuesPlain => { const timeArr = value.split(":"); const error = new Error("Invalid format"); From bcc89e8ff06a061c921bf049eb734157fd136018 Mon Sep 17 00:00:00 2001 From: Quek Ruo Ling Date: Mon, 15 May 2023 17:10:57 +0800 Subject: [PATCH 02/14] [MISC][RL] Align styles --- .../timepicker-dropdown.styles.tsx | 59 ++++++++----------- 1 file changed, 24 insertions(+), 35 deletions(-) diff --git a/src/shared/timepicker-dropdown/timepicker-dropdown.styles.tsx b/src/shared/timepicker-dropdown/timepicker-dropdown.styles.tsx index 9e714524a..c9f1a3456 100644 --- a/src/shared/timepicker-dropdown/timepicker-dropdown.styles.tsx +++ b/src/shared/timepicker-dropdown/timepicker-dropdown.styles.tsx @@ -20,16 +20,11 @@ export const AnimatedDiv = styled(animated.div)` position: absolute; top: 3.5rem; left: 0; - width: 27rem; box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.2); background: ${Color.Neutral[8]}; border-radius: ${BORDER_RADIUS}; overflow: hidden; z-index: 1; - - ${MediaQuery.MaxWidth.tablet} { - width: 100%; - } `; export const Container = styled.div` @@ -54,11 +49,11 @@ export const ControlSection = styled.div` display: flex; justify-content: flex-end; margin-top: 1rem; + gap: 0.5rem 1rem; ${MediaQuery.MaxWidth.mobileS} { - border-top: 1px solid ${Color.Neutral[5]}; + flex-direction: column-reverse; // FIXME: this breaks tab focus margin-top: 2rem; - padding-top: 1.5rem; } `; @@ -70,13 +65,14 @@ export const HourMinuteSection = styled.div` align-items: center; margin-right: 2rem; - ${MediaQuery.MaxWidth.mobileM} { + ${MediaQuery.MaxWidth.mobileS} { margin-right: 0; } `; export const TimePeriodSection = styled.div` display: flex; + gap: 0.5rem; ${MediaQuery.MaxWidth.tablet} { flex-direction: column; @@ -97,18 +93,29 @@ export const InputContainer = styled.div` export const SwitchButton = styled(IconButton)` width: 5rem; padding: 1rem 0; - color: ${Color.Primary}; + color: ${Color.Neutral[3]}; + + svg { + height: 1rem; + width: 1rem; + } + + &:hover { + color: ${Color.Primary}; + } `; export const DividerLabel = styled(Text.Body)` - margin: 0 0.75rem; + width: 1.5rem; + margin: 0 0.25rem; + text-align: center; ${MediaQuery.MaxWidth.tablet} { - margin: 0 0.5rem; + margin: 0; } ${MediaQuery.MaxWidth.mobileS} { - margin: 0 0.75rem; + margin: 0 0.25rem; } `; @@ -153,25 +160,7 @@ export const TimeInput = styled.input` export const TimePeriodToggle = styled(Toggle)` min-width: 5rem; - - :not(:last-of-type) { - margin-right: 0.5rem; - } - - ${MediaQuery.MaxWidth.tablet} { - :not(:last-of-type) { - margin-right: 0; - margin-bottom: 0.5rem; - } - } - - ${MediaQuery.MaxWidth.mobileS} { - width: 50%; - :not(:last-of-type) { - margin-right: 0.5rem; - margin-bottom: 0; - } - } + flex: 1; `; // ----------------------------------------------------------------------------- @@ -180,11 +169,11 @@ export const TimePeriodToggle = styled(Toggle)` export const ControlButton = styled(Button.Small)` width: 7rem; - :not(:last-of-type) { - margin-right: 0.5rem; + ${MediaQuery.MaxWidth.mobileL} { + flex: 1; } - ${MediaQuery.MaxWidth.tablet} { - width: 50%; + ${MediaQuery.MaxWidth.mobileS} { + width: 100%; } `; From e9b6b79bf52ce117981da3af5d383edb293a218b Mon Sep 17 00:00:00 2001 From: Quek Ruo Ling Date: Mon, 15 May 2023 22:31:04 +0800 Subject: [PATCH 03/14] [MISC][RL] Use shared timepicker dropdown in range input --- .../timepicker-dropdown.tsx | 28 +- src/time-range-picker/helper.ts | 260 ---------- .../time-range-picker-dropdown.styles.tsx | 206 -------- .../time-range-picker-dropdown.tsx | 445 ------------------ src/time-range-picker/time-range-picker.tsx | 19 +- src/timepicker/timepicker.tsx | 4 +- src/util/time-helper.ts | 2 +- 7 files changed, 21 insertions(+), 943 deletions(-) delete mode 100644 src/time-range-picker/helper.ts delete mode 100644 src/time-range-picker/time-range-picker-dropdown.styles.tsx delete mode 100644 src/time-range-picker/time-range-picker-dropdown.tsx diff --git a/src/shared/timepicker-dropdown/timepicker-dropdown.tsx b/src/shared/timepicker-dropdown/timepicker-dropdown.tsx index 3e6192e23..45d35274f 100644 --- a/src/shared/timepicker-dropdown/timepicker-dropdown.tsx +++ b/src/shared/timepicker-dropdown/timepicker-dropdown.tsx @@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; import { useResizeDetector } from "react-resize-detector"; import { useSpring } from "react-spring"; import { StringHelper } from "../../util/string-helper"; -import { Period, TimeFormat, TimepickerHelper } from "../../util/time-helper"; +import { Period, TimeFormat, TimeHelper } from "../../util/time-helper"; import { AnimatedDiv, Container, @@ -37,7 +37,7 @@ enum ETimePeriodToggleName { PM = "pm", } -interface IProps { +interface TimepickerDropdownProps { id?: string; value: string; show: boolean; @@ -53,11 +53,11 @@ export const TimepickerDropdown = ({ format, onChange, onCancel, -}: IProps) => { +}: TimepickerDropdownProps) => { // ============================================================================= // CONST, STATE, REF // ============================================================================= - const timeValues = TimepickerHelper.getTimeValues(format, value); + const timeValues = TimeHelper.getTimeValues(format, value); const [hourValue, setHourValue] = useState(timeValues.hour); const [minuteValue, setMinuteValue] = useState(timeValues.minute); @@ -78,7 +78,7 @@ export const TimepickerDropdown = ({ if (show) { // reset time values especially when a Cancel or blur event happened - const { hour, minute, period } = TimepickerHelper.getTimeValues( + const { hour, minute, period } = TimeHelper.getTimeValues( format, value ); @@ -161,23 +161,19 @@ export const TimepickerDropdown = ({ switch (event.currentTarget.name) { case EInputButtonName.MINUTE_UP: setMinuteValue( - TimepickerHelper.updateMinutes(minuteValue, "add") + TimeHelper.updateMinutes(minuteValue, "add") ); break; case EInputButtonName.MINUTE_DOWN: setMinuteValue( - TimepickerHelper.updateMinutes(minuteValue, "minus") + TimeHelper.updateMinutes(minuteValue, "minus") ); break; case EInputButtonName.HOUR_UP: - setHourValue( - TimepickerHelper.updateHours(hourValue, "add") - ); + setHourValue(TimeHelper.updateHours(hourValue, "add")); break; case EInputButtonName.HOUR_DOWN: - setHourValue( - TimepickerHelper.updateHours(hourValue, "minus") - ); + setHourValue(TimeHelper.updateHours(hourValue, "minus")); break; default: break; @@ -215,7 +211,7 @@ export const TimepickerDropdown = ({ const valueToSet = value > 23 || value < 0 ? timeValues.hour - : TimepickerHelper.convertHourTo12HourFormat( + : TimeHelper.convertHourTo12HourFormat( event.target.value ); @@ -255,7 +251,7 @@ export const TimepickerDropdown = ({ let formattedValue: string; if (format === "24hr") { - formattedValue = TimepickerHelper.convertTo24HourFormat({ + formattedValue = TimeHelper.convertTo24HourFormat({ hour: hourValue, minute: minuteValue, period: timePeriod, @@ -424,7 +420,7 @@ export const TimepickerDropdown = ({ disabled={hourValue === "" || minuteValue === ""} data-testid={getTestId("confirm-button")} > - Confirm + Done diff --git a/src/time-range-picker/helper.ts b/src/time-range-picker/helper.ts deleted file mode 100644 index 12bbe24ca..000000000 --- a/src/time-range-picker/helper.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { StringHelper } from "../util/string-helper"; -import { TimeRangePickerFormat } from "./types"; - -// ============================================================================= -// INTERFACES -// ============================================================================= -export type Period = "am" | "pm"; - -export interface TimeValues { - hour: string; - minute: string; - period: Period; -} - -// unexportable -interface TimeValuesPlain { - hour: string; - minute: string; - period?: string; -} - -// ============================================================================= -// HELPER FUNCTIONS -// ============================================================================= -export namespace TimeRangePickerHelper { - export const getTimeValues = ( - format: TimeRangePickerFormat, - value?: string - ): TimeValues => { - // Default value - const timeValues: TimeValues = { - hour: "", - minute: "", - period: "am", - }; - - if (!value) return timeValues; - - try { - // TODO: hold release 24hr format - // if (format === "24hr") { - // const plain = convertToPlain(value, format); - // // Add minute - // timeValues.minute = StringHelper.padValue(plain.minute); - - // // Determine period, and convert to 12 hour value - // const hour = parseInt(plain.hour); - - // if (Math.floor(hour / 12) === 0) { - // timeValues.period = "am"; - // timeValues.hour = - // hour === 0 - // ? "12" - // : StringHelper.padValue(hour.toString()); - // } else { - // timeValues.period = "pm"; - // timeValues.hour = - // hour === 12 - // ? hour.toString() - // : StringHelper.padValue((hour - 12).toString()); - // } - // } else { - // const plain = convertToPlain(value, format); - - // timeValues.hour = StringHelper.padValue(plain.hour); - // timeValues.minute = StringHelper.padValue(plain.minute); - // timeValues.period = - // plain.period.toLowerCase() === "am" ? "am" : "pm"; - // } - - const plain = convertToPlain(value, format); - - timeValues.hour = StringHelper.padValue(plain.hour); - timeValues.minute = StringHelper.padValue(plain.minute); - timeValues.period = - plain.period.toLowerCase() === "am" ? "am" : "pm"; - - return timeValues; - } catch (error) { - return timeValues; - } - }; - - export const updateMinutes = ( - valueString: string, - direction: "add" | "minus" - ): string => { - const currentValue = parseInt(valueString); - - if (isNaN(currentValue)) { - return direction === "add" ? StringHelper.padValue("0") : "55"; - } - - /** - * We would increment or decrement by steps of 5 - * Hence from 0 to 55, there are 12 steps - */ - const steps = 12; - - /** - * Look for last nearest step - * E.g. value 4 would be step 0 = 0, value 6 would be step 1 = 5 - */ - const lastNearestStep = Math.floor(currentValue / 5); - - /** - * Getting the next step value would have to incorporate a cycle of the upper - * and lower limits. In this case upper = 12, lower = 0. - * If one is to increment from step 12, it would then go back to 0. - * If one is to decrement from step 0, it would then go back to 12. - * Reference: https://dev.to/turneremma21/circular-access-of-array-in-javascript-j52 - */ - const nextStep = - direction === "add" - ? lastNearestStep + 1 - : currentValue % 5 === 0 - ? lastNearestStep - 1 // if it is a value on the step, decrement one more - : lastNearestStep; // else the last nearest step would suffice - const newStep = ((nextStep % steps) + steps) % steps; - - return StringHelper.padValue((newStep * 5).toString()); - }; - - export const updateHours = ( - valueString: string, - direction: "add" | "minus" - ): string => { - const currentValue = parseInt(valueString); - - if (isNaN(currentValue)) { - return direction === "add" ? StringHelper.padValue("1") : "12"; - } - - const nextValue = - direction === "add" ? currentValue + 1 : currentValue - 1; - - return nextValue <= 12 && nextValue > 0 - ? StringHelper.padValue(nextValue.toString()) - : nextValue === 13 - ? StringHelper.padValue("1") - : "12"; - }; - - export const convertTo24HourFormat = (values: TimeValues): string => { - const hour = parseInt(values.hour); - let hourString: string; - - if (values.period === "pm") { - hourString = hour === 12 ? hour.toString() : (hour + 12).toString(); - } else { - hourString = hour === 12 ? "00" : values.hour; - } - - return `${hourString}:${values.minute}`; - }; - - export const convertHourTo12HourFormat = (hourValue: string): string => { - const hour = parseInt(hourValue); - const hourString = - hour % 12 === 0 ? (12).toString() : (hour % 12).toString(); - - return StringHelper.padValue(hourString); - }; - - export const formatValue = ( - value: string, - format: TimeRangePickerFormat - ): string => { - try { - const plain = convertToPlain(value, format); - - const paddedHour = StringHelper.padValue(plain.hour); - const paddedMinute = StringHelper.padValue(plain.minute); - - let formatted = `${paddedHour}:${paddedMinute}`; - - if (format === "12hr") { - formatted += plain.period.toLowerCase(); - - return formatted; - } - - return formatted; - } catch (error) { - return ""; - } - }; -} - -// ============================================================================= -// NON-EXPORTABLES -// ============================================================================= -const isValidHour = ( - hourString: string, - format: TimeRangePickerFormat -): boolean => { - const numValue = parseInt(hourString); - // hold release 24hr format - // return format === "24hr" - // ? numValue >= 0 && numValue <= 23 - // : numValue >= 1 && numValue <= 12; - - return numValue >= 1 && numValue <= 12; -}; - -const isValidMinutes = (hourString: string): boolean => { - const numValue = parseInt(hourString); - return numValue >= 0 && numValue <= 59; -}; - -const isValidTimePeriod = (timePeriodString: string): boolean => { - return ( - timePeriodString.toLowerCase() === "am" || - timePeriodString.toLowerCase() === "pm" - ); -}; - -const convertToPlain = ( - value: string, - format: TimeRangePickerFormat -): TimeValuesPlain => { - const timeArr = value.split(":"); - const error = new Error("Invalid format"); - - if (format === "12hr") { - // Check format - if (timeArr.length !== 2 || timeArr[1].length !== 4) throw error; - - const minute = timeArr[1].substring(0, 2); - const period = timeArr[1].substring(2); - - // Validate hour, minute and time period values - if ( - !isValidHour(timeArr[0], format) || - !isValidMinutes(minute) || - !isValidTimePeriod(period) - ) { - throw error; - } - - return { - hour: timeArr[0], - minute, - period: timeArr[1].substring(2), - }; - } else { - // Check format - if (timeArr.length !== 2) throw error; - - // Validate hour and minute values - if (!isValidHour(timeArr[0], format) || !isValidMinutes(timeArr[1])) { - throw error; - } - - return { - hour: timeArr[0], - minute: timeArr[1], - }; - } -}; diff --git a/src/time-range-picker/time-range-picker-dropdown.styles.tsx b/src/time-range-picker/time-range-picker-dropdown.styles.tsx deleted file mode 100644 index c38083411..000000000 --- a/src/time-range-picker/time-range-picker-dropdown.styles.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import { animated } from "react-spring"; -import styled from "styled-components"; -import { Button } from "../button"; -import { Color } from "../color"; -import { IconButton } from "../icon-button"; -import { MediaQuery } from "../media"; -import { Text, TextStyleHelper } from "../text"; -import { Toggle } from "../toggle"; - -// ============================================================================= -// STYLING -// ============================================================================= - -const BORDER_RADIUS = "4px"; - -// ----------------------------------------------------------------------------- -// MAIN WRAPPER -// ----------------------------------------------------------------------------- -export const AnimatedDiv = styled(animated.div)` - position: absolute; - top: 3.5rem; - left: 0; - width: 27rem; - box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.2); - box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.2); - background: ${Color.Neutral[8]}; - border-radius: ${BORDER_RADIUS}; - z-index: 1; - overflow: hidden; - - ${MediaQuery.MaxWidth.tablet} { - width: 100%; - } - - ${MediaQuery.MaxWidth.mobileL} { - width: 20rem; - } - - ${MediaQuery.MaxWidth.mobileM} { - width: 19rem; - } - - ${MediaQuery.MaxWidth.mobileS} { - width: 15rem; - } -`; - -export const Container = styled.div` - position: relative; - width: 100%; - padding: 0.5rem 1.25rem 1.5rem 1.25rem; - display: flex; - flex-direction: column; -`; - -export const InputSection = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - - ${MediaQuery.MaxWidth.mobileS} { - flex-direction: column; - } -`; - -export const ControlSection = styled.div` - display: flex; - justify-content: flex-end; - margin-top: 1rem; - - ${MediaQuery.MaxWidth.mobileS} { - flex-direction: column; - margin-top: 2rem; - padding-top: 1.5rem; - } -`; - -// ----------------------------------------------------------------------------- -// INPUT COMPONENTS -// ----------------------------------------------------------------------------- -export const HourMinuteSection = styled.div` - display: flex; - align-items: center; - margin-right: 2rem; - - ${MediaQuery.MaxWidth.mobileM} { - margin-right: 0; - } -`; - -export const TimePeriodSection = styled.div` - display: flex; - - ${MediaQuery.MaxWidth.tablet} { - flex-direction: column; - } - - ${MediaQuery.MaxWidth.mobileS} { - flex-direction: row; - width: 100%; - } -`; - -export const InputContainer = styled.div` - display: flex; - flex-direction: column; - align-items: center; -`; - -export const SwitchButton = styled(IconButton)` - width: 5rem; - padding: 1rem 0; - color: ${Color.Primary}; -`; - -export const DividerLabel = styled(Text.Body)` - margin: 0 0.75rem; - - ${MediaQuery.MaxWidth.tablet} { - margin: 0 0.5rem; - } - - ${MediaQuery.MaxWidth.mobileS} { - margin: 0 0.75rem; - } -`; - -export const TimeInput = styled.input` - ${TextStyleHelper.getTextStyle("Body", "regular")} - border-radius: ${BORDER_RADIUS}; - width: 80px; - height: 48px; - text-align: center; - border: 1px solid ${Color.Neutral[5]}; - background: ${Color.Neutral[8]}; - color: ${Color.Neutral[1]}; - - :focus, - :active { - outline: none; - border: 1px solid ${Color.Accent.Light[1]}; - box-shadow: inset 0 0 5px 1px rgba(87, 169, 255, 0.5); - } - - :focus::placeholder { - color: transparent; - } - - // Chrome, Safari, Edge, Opera - ::-webkit-outer-spin-button, - ::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; - } - - // Safari (remove top shadow) - --webkit-appearance: none; - - // Firefox - --moz-appearance: textfield; - - ${MediaQuery.MaxWidth.mobileS} { - width: 6rem; - } -`; - -export const TimePeriodToggle = styled(Toggle)` - min-width: 5rem; - :not(:last-of-type) { - margin-right: 0.5rem; - } - - ${MediaQuery.MaxWidth.tablet} { - :not(:last-of-type) { - margin-right: 0; - margin-bottom: 0.5rem; - } - } - - ${MediaQuery.MaxWidth.mobileS} { - width: 50%; - :not(:last-of-type) { - margin-right: 0.5rem; - margin-bottom: 0; - } - } -`; - -// ----------------------------------------------------------------------------- -// CONTROL COMPONENTS -// ----------------------------------------------------------------------------- -export const ControlButton = styled(Button.Small)` - width: 7rem; - - :not(:last-of-type) { - margin-right: 0.5rem; - } - - ${MediaQuery.MaxWidth.tablet} { - width: 50%; - } - ${MediaQuery.MaxWidth.mobileS} { - width: 100%; - margin-bottom: 1rem; - } -`; diff --git a/src/time-range-picker/time-range-picker-dropdown.tsx b/src/time-range-picker/time-range-picker-dropdown.tsx deleted file mode 100644 index 00ccfee11..000000000 --- a/src/time-range-picker/time-range-picker-dropdown.tsx +++ /dev/null @@ -1,445 +0,0 @@ -import { ChevronDownIcon } from "@lifesg/react-icons/chevron-down"; -import { ChevronUpIcon } from "@lifesg/react-icons/chevron-up"; -import React, { useCallback, useEffect, useRef, useState } from "react"; -import { useResizeDetector } from "react-resize-detector"; -import { useSpring } from "react-spring"; -import { StringHelper } from "../util/string-helper"; -import { Period, TimeRangePickerHelper } from "./helper"; -import { - AnimatedDiv, - Container, - ControlButton, - ControlSection, - DividerLabel, - HourMinuteSection, - InputContainer, - InputSection, - SwitchButton, - TimeInput, - TimePeriodSection, - TimePeriodToggle, -} from "./time-range-picker-dropdown.styles"; -import { TimeRangePickerFormat } from "./types"; - -enum EInputButtonName { - HOUR_UP = "hour-up", - HOUR_DOWN = "hour-down", - MINUTE_UP = "minute-up", - MINUTE_DOWN = "minute-down", -} - -enum EInputName { - HOUR = "hour", - MINUTE = "minute", -} - -enum ETimePeriodToggleName { - AM = "am", - PM = "pm", -} - -interface IProps { - id?: string; - value: string; - show: boolean; - format: TimeRangePickerFormat; - onChange: (value: string) => void; - onCancel: () => void; -} - -export const TimeRangePickerDropdown = ({ - id, - value, - show, - format, - onChange, - onCancel, -}: IProps) => { - // ============================================================================= - // CONST, STATE, REF - // ============================================================================= - const timeValues = TimeRangePickerHelper.getTimeValues(format, value); - - const [hourValue, setHourValue] = useState(timeValues.hour); - const [minuteValue, setMinuteValue] = useState(timeValues.minute); - const [timePeriod, setTimePeriod] = useState(timeValues.period); - - const hourInputRef = useRef(); - const minuteInputRef = useRef(); - const resizeDetector = useResizeDetector(); - - // ============================================================================= - // EFFECTS - // ============================================================================= - useEffect(() => { - // Focus hour input on display of dropdown - if (show && hourInputRef.current) { - hourInputRef.current.focus(); - } - - if (show) { - // reset time values especially when a Cancel or blur event happened - const { hour, minute, period } = - TimeRangePickerHelper.getTimeValues(format, value); - setHourValue(hour); - setMinuteValue(minute); - setTimePeriod(period); - } - }, [show, value, format]); - - useEffect(() => { - const hourInput = hourInputRef.current; - const minuteInput = minuteInputRef.current; - - if (hourInput) hourInput.addEventListener("keydown", handleKeyDown); - if (minuteInput) minuteInput.addEventListener("keydown", handleKeyDown); - - return () => { - if (hourInput) - hourInput.removeEventListener("keydown", handleKeyDown); - if (minuteInput) - minuteInput.removeEventListener("keydown", handleKeyDown); - }; - }, []); - - // ============================================================================= - // EVENT HANDLERS - // ============================================================================= - const handleKeyDown = (event: KeyboardEvent) => { - /** - * NOTE: This is the most deterministic way in handling - * incorrect characters from being entered. The pattern - * added in the input still allows some special characters - * to slip through. - */ - - const permittableEventCodes = [ - "Digit0", - "Digit1", - "Digit2", - "Digit3", - "Digit4", - "Digit5", - "Digit6", - "Digit7", - "Digit8", - "Digit9", - "Tab", - "Backspace", - "Delete", - "ArrowLeft", - "ArrowRight", - "ArrowUp", - "ArrowDown", - ]; - - const permittableEventKeys = [ - "Backspace", - "0", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - ]; - - if ( - !permittableEventCodes.includes(event.code) && - !permittableEventKeys.includes(event.key) // mobile devices - ) { - event.preventDefault(); - } - }; - - const handleInputButtonClick = useCallback( - (event: React.MouseEvent) => { - switch (event.currentTarget.name) { - case EInputButtonName.MINUTE_UP: - setMinuteValue( - TimeRangePickerHelper.updateMinutes(minuteValue, "add") - ); - break; - case EInputButtonName.MINUTE_DOWN: - setMinuteValue( - TimeRangePickerHelper.updateMinutes( - minuteValue, - "minus" - ) - ); - break; - case EInputButtonName.HOUR_UP: - setHourValue( - TimeRangePickerHelper.updateHours(hourValue, "add") - ); - break; - case EInputButtonName.HOUR_DOWN: - setHourValue( - TimeRangePickerHelper.updateHours(hourValue, "minus") - ); - break; - default: - break; - } - }, - [hourValue, minuteValue] - ); - - const handleFocus = (event: React.FocusEvent) => { - event.target.select(); - }; - - const handleChange = (event: React.ChangeEvent) => { - const value = event.target.value; - - switch (event.target.name) { - case EInputName.HOUR: - if (value.length <= 2) setHourValue(value); - break; - case EInputName.MINUTE: - if (value.length <= 2) setMinuteValue(value); - break; - default: - break; - } - }; - - const handleBlur = (event: React.FocusEvent) => { - const value = parseInt(event.target.value); - - if (isNaN(value)) return; - - switch (event.target.name) { - case EInputName.HOUR: { - const valueToSet = - value > 23 || value < 0 - ? timeValues.hour - : TimeRangePickerHelper.convertHourTo12HourFormat( - event.target.value - ); - - setHourValue(valueToSet); - break; - } - case EInputName.MINUTE: { - const valueToSet = - value > 59 || value < 0 - ? timeValues.minute - : event.target.value; - - setMinuteValue(StringHelper.padValue(valueToSet)); - break; - } - default: - break; - } - }; - - const handleTimePeriodChange = ( - event: React.ChangeEvent - ) => { - switch (event.target.name) { - case ETimePeriodToggleName.AM: - setTimePeriod("am"); - break; - case ETimePeriodToggleName.PM: - setTimePeriod("pm"); - break; - default: - break; - } - }; - - const handleConfirm = () => { - const formattedValue = `${hourValue}:${minuteValue}${timePeriod}`; - - // TODO: hold release 24hr format - // if (format === "24hr") { - // formattedValue = TimeRangePickerHelper.convertTo24HourFormat({ - // hour: hourValue, - // minute: minuteValue, - // period: timePeriod, - // }); - // } else { - // formattedValue = `${hourValue}:${minuteValue}${timePeriod}`; - // } - - onChange(formattedValue); - }; - - // ============================================================================= - // HELPER FUNCTIONS - // ============================================================================= - const getTestId = (baseTestId: string): string => { - return id ? `${id}-${baseTestId}` : baseTestId; - }; - - // ============================================================================= - // RENDER FUNCTIONS - // ============================================================================= - const renderHourInput = () => ( - - - - - - - - - - ); - - const renderMinuteInput = () => ( - - - - - - - - - - ); - - const renderTimePeriodControl = () => { - // FIXME: this results in a flash when switching inputs - if (!show) return; - - return ( - - - AM - - - PM - - - ); - }; - - // React spring animation configuration - const styles = useSpring({ - height: show - ? resizeDetector.height + 32 // include vertical padding - : 0, - }); - - return ( - - - - - {renderHourInput()} - : - {renderMinuteInput()} - - {renderTimePeriodControl()} - - - - Cancel - - - Done - - - - - ); -}; diff --git a/src/time-range-picker/time-range-picker.tsx b/src/time-range-picker/time-range-picker.tsx index 1e616de7d..6c36f1a49 100644 --- a/src/time-range-picker/time-range-picker.tsx +++ b/src/time-range-picker/time-range-picker.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from "react"; -import { TimeRangePickerHelper } from "./helper"; -import { TimeRangePickerDropdown } from "./time-range-picker-dropdown"; +import { TimepickerDropdown } from "../shared/timepicker-dropdown/timepicker-dropdown"; +import { TimeHelper } from "../util/time-helper"; import { ArrowRangeContainer, ArrowRight, @@ -155,10 +155,7 @@ export const TimeRangePicker = ({ $focused={showStartTimeSelector} readOnly placeholder={"From"} - value={TimeRangePickerHelper.formatValue( - startTimeVal, - format - )} + value={TimeHelper.formatValue(startTimeVal, format)} disabled={disabled} $error={error} data-testid={ @@ -168,8 +165,7 @@ export const TimeRangePicker = ({ } /> {showStartTimeSelector && } - - - Date: Tue, 16 May 2023 08:33:17 +0800 Subject: [PATCH 04/14] [MISC][RL] Rename file to match convention --- .../{form-timerangepicker.tsx => form-time-range-picker.tsx} | 0 src/form/index.ts | 4 ++-- src/index.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/form/{form-timerangepicker.tsx => form-time-range-picker.tsx} (100%) diff --git a/src/form/form-timerangepicker.tsx b/src/form/form-time-range-picker.tsx similarity index 100% rename from src/form/form-timerangepicker.tsx rename to src/form/form-time-range-picker.tsx diff --git a/src/form/index.ts b/src/form/index.ts index 955b05897..1f9688c2a 100644 --- a/src/form/index.ts +++ b/src/form/index.ts @@ -4,12 +4,12 @@ import { FormInput } from "./form-input"; import { FormInputGroup } from "./form-input-group"; import { FormLabel } from "./form-label"; import { FormMultiSelect } from "./form-multi-select"; +import { FormPhoneNumberInput } from "./form-phone-number-input"; import { FormSelect } from "./form-select"; import { FormTextarea } from "./form-textarea"; +import { FormTimeRangePicker } from "./form-time-range-picker"; import { FormTimepicker } from "./form-timepicker"; -import { FormTimeRangePicker } from "./form-timerangepicker"; import { FormUnitNumberInput } from "./form-unit-number-input"; -import { FormPhoneNumberInput } from "./form-phone-number-input"; export const Form = { DateInput: FormDateInput, diff --git a/src/index.ts b/src/index.ts index 446b9ceb6..8b340f997 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,11 +38,11 @@ export * from "./smart-app-banner"; export * from "./text"; export * from "./text-list"; export * from "./theme"; +export * from "./time-range-picker"; export * from "./time-slot-bar"; export * from "./timeline"; export * from "./timepicker"; -export * from "./time-range-picker"; +export * from "./toast"; export * from "./toggle"; export * from "./tooltip"; export * from "./transition"; -export * from "./toast"; From 403f77844493ee26efd98609ae2b3201da88e768 Mon Sep 17 00:00:00 2001 From: Quek Ruo Ling Date: Tue, 16 May 2023 09:49:33 +0800 Subject: [PATCH 05/14] [MISC][RL] Align styles with figma and date range input --- .../time-range-picker.styles.tsx | 191 +++++------------- src/time-range-picker/time-range-picker.tsx | 36 ++-- 2 files changed, 63 insertions(+), 164 deletions(-) diff --git a/src/time-range-picker/time-range-picker.styles.tsx b/src/time-range-picker/time-range-picker.styles.tsx index 0396642d1..1ae4a7677 100644 --- a/src/time-range-picker/time-range-picker.styles.tsx +++ b/src/time-range-picker/time-range-picker.styles.tsx @@ -1,20 +1,12 @@ import styled, { css } from "styled-components"; import { Color } from "../color"; -import { BookingSGColorSet } from "../spec/color-spec/bookingsg-color-set"; import { TextStyleHelper } from "../text/helper"; -import { MediaQuery } from "../media"; import { ArrowRightIcon } from "@lifesg/react-icons/arrow-right"; import { DesignToken } from "src/design-token"; // ============================================================================= -// STYLE INTERFACe +// STYLE INTERFACE // ============================================================================= -interface StyleProps { - $focused?: boolean; - $disabled?: boolean; - $error?: boolean; - $readOnly?: boolean; -} interface ContainerStyleProps { $disabled?: boolean; @@ -22,6 +14,10 @@ interface ContainerStyleProps { $readOnly?: boolean; } +interface IndicatorStyleProps { + $position: "start" | "end" | "none"; +} + // ============================================================================= // STYLING // ============================================================================= @@ -32,17 +28,19 @@ export const Wrapper = styled.div` export const TimeContainer = styled.div` display: flex; align-items: center; - padding: 11px 16px; - gap: 8px; + gap: 0.5rem; width: 100%; - height: 48px; - border-radius: 4px; + height: 3rem; + border-radius: 0.25rem; border: 1px solid ${Color.Neutral[5]}; + padding: 11px 16px; + :focus, :focus-within { border: 1px solid ${Color.Accent.Light[1]}; box-shadow: ${DesignToken.InputBoxShadow}; } + ${(props) => { if (props.$readOnly) { return css` @@ -50,20 +48,19 @@ export const TimeContainer = styled.div` padding: 0; :focus, :focus-within { - border: 0px; + border: none; box-shadow: none; } `; } else if (props.$disabled) { return css` - background: ${Color.Neutral[6](props)} !important; + background: ${Color.Neutral[6]}; :hover { cursor: not-allowed; } :focus-within { - border: 0px; + border: none; box-shadow: none; - // border: 1px solid ${Color.Neutral[5](props)}; } `; } else if (props.$error) { @@ -76,154 +73,60 @@ export const TimeContainer = styled.div` `; } }} - - ${MediaQuery.MaxWidth.mobileS} { - width: 235px; - } -`; - -export const ArrowRangeContainer = styled.div` - position: absolute; - left: 50%; - transform: translateX(-50%); - padding: 0; `; export const ArrowRight = styled(ArrowRightIcon)` color: ${Color.Neutral[3]}; - cursor: pointer; width: 1.125rem; - height: 1rem; -`; - -export const BottomHighlightStartTime = styled.div` - position: absolute; - bottom: -0.1rem; - height: 2px; - left: 1rem; - right: 23rem; - background-color: ${Color.Primary}; - - ${MediaQuery.MaxWidth.tablet} { - width: 40%; - } - - ${MediaQuery.MaxWidth.mobileL} { - /* width: 335px; */ - width: 40%; - } - - ${MediaQuery.MaxWidth.mobileM} { - width: 40%; - } - - ${MediaQuery.MaxWidth.mobileS} { - width: 40%; - } + height: 1.125rem; + flex-shrink: 0; `; -export const BottomHighlightEndTime = styled.div` +export const Indicator = styled.div` position: absolute; - bottom: -0.1rem; - height: 2px; - left: 16rem; - right: 7rem; background-color: ${Color.Primary}; + height: 0.125rem; + width: calc(100% - 50% - 2rem); // paddingX is 2rem + transition: left 350ms ease-in-out, opacity 350ms ease-in-out; + left: 1rem; + bottom: 0; - ${MediaQuery.MaxWidth.tablet} { - width: 8rem; - left: 12rem; - } - - ${MediaQuery.MaxWidth.mobileL} { - /* width: 335px; */ - width: 8rem; - left: 10rem; - } - - ${MediaQuery.MaxWidth.mobileM} { - width: 6rem; - left: 10rem; - } - - ${MediaQuery.MaxWidth.mobileS} { - width: 5rem; - left: 8rem; - } -`; - -export const InputSelectorStartTimeElement = styled.input` - ${TextStyleHelper.getTextStyle("Body", "regular")} - - display: block; - width: 100%; - height: 26px; - background: ${BookingSGColorSet.Neutral[8]}; - color: ${BookingSGColorSet.Neutral[1]}; - border: 0px; - :focus, - :active { - outline: none; - } - :disabled { - background: ${Color.Neutral[6]} !important; - :hover { - cursor: not-allowed; - } - } ${(props) => { - if (props.$readOnly) { - return css` - border: none; - cursor: none; - `; - } - if (props.$disabled) { - return css` - background: ${Color.Neutral[6](props)} !important; - :hover { - cursor: not-allowed; - } - `; + switch (props.$position) { + case "start": + return css` + left: 1rem; + opacity: 1; + `; + case "end": + return css` + left: calc(50% + 1rem); + opacity: 1; + `; + case "none": + return css` + left: 1rem; + opacity: 0; + `; } }} `; -export const InputSelectorEndTimeElement = styled.input` +export const SelectorInput = styled.input` + /* reset default styles */ ${TextStyleHelper.getTextStyle("Body", "regular")} + color: ${Color.Neutral[1]}; + background-color: transparent; + border: none; + outline: none; - display: block; - width: 100%; - height: 26px; - margin-left: 1rem; - background: ${BookingSGColorSet.Neutral[8]}; - color: ${BookingSGColorSet.Neutral[1]}; - border: 0px; - :focus, - :active { - outline: none; - } :disabled { - background: ${Color.Neutral[6]} !important; :hover { cursor: not-allowed; } } - ${(props) => { - if (props.$readOnly) { - return css` - border: none; - cursor: none; - `; - } - if (props.$disabled) { - return css` - background: ${Color.Neutral[6](props)} !important; - :hover { - cursor: not-allowed; - } - `; - } - }} + display: block; + width: 100%; + flex: 1; `; diff --git a/src/time-range-picker/time-range-picker.tsx b/src/time-range-picker/time-range-picker.tsx index 6c36f1a49..42dc16f61 100644 --- a/src/time-range-picker/time-range-picker.tsx +++ b/src/time-range-picker/time-range-picker.tsx @@ -2,12 +2,9 @@ import { useEffect, useRef, useState } from "react"; import { TimepickerDropdown } from "../shared/timepicker-dropdown/timepicker-dropdown"; import { TimeHelper } from "../util/time-helper"; import { - ArrowRangeContainer, ArrowRight, - BottomHighlightEndTime, - BottomHighlightStartTime, - InputSelectorEndTimeElement, - InputSelectorStartTimeElement, + Indicator, + SelectorInput, TimeContainer, Wrapper, } from "./time-range-picker.styles"; @@ -150,21 +147,18 @@ export const TimeRangePicker = ({ $error={error} $readOnly={readOnly} > - - {showStartTimeSelector && } - - - - - {showEndTimeSelector && } - - + + ); From d049496585a04d9dc1b2311530e3caedc19ca897 Mon Sep 17 00:00:00 2001 From: Quek Ruo Ling Date: Tue, 16 May 2023 13:42:59 +0800 Subject: [PATCH 06/14] [MISC][RL] Use shared input wrapper --- src/input-group/input-group.tsx | 4 +- src/input/input.tsx | 2 +- src/shared/input-wrapper/input-wrapper.tsx | 16 ++--- .../time-range-picker.styles.tsx | 59 ++----------------- src/unit-number/unit-number-input.tsx | 2 +- 5 files changed, 16 insertions(+), 67 deletions(-) diff --git a/src/input-group/input-group.tsx b/src/input-group/input-group.tsx index 6cb827c03..b57062037 100644 --- a/src/input-group/input-group.tsx +++ b/src/input-group/input-group.tsx @@ -46,7 +46,7 @@ const Component = ( return ( ( if (labelAddon.value) { return ( ` +export const InputWrapper = styled.div` display: flex; align-items: center; position: relative; @@ -46,22 +46,22 @@ export const InputWrapper = styled.div` box-shadow: none; } `; - } else if (props.disabled) { + } else if (props.$disabled) { return css` - background: ${Color.Neutral[6](props)}; + background: ${Color.Neutral[6]}; cursor: not-allowed; :focus-within { - border: 1px solid ${Color.Neutral[5](props)}; + border: 1px solid ${Color.Neutral[5]}; box-shadow: none; } `; } else if (props.$error) { return css` - border: 1px solid ${Color.Validation.Red.Border(props)}; + border: 1px solid ${Color.Validation.Red.Border}; :focus-within { - border: 1px solid ${Color.Validation.Red.Border(props)}; + border: 1px solid ${Color.Validation.Red.Border}; box-shadow: ${DesignToken.InputErrorBoxShadow}; } `; diff --git a/src/time-range-picker/time-range-picker.styles.tsx b/src/time-range-picker/time-range-picker.styles.tsx index 1ae4a7677..8973fe873 100644 --- a/src/time-range-picker/time-range-picker.styles.tsx +++ b/src/time-range-picker/time-range-picker.styles.tsx @@ -1,19 +1,13 @@ +import { ArrowRightIcon } from "@lifesg/react-icons/arrow-right"; +import { InputWrapper } from "src/shared/input-wrapper/input-wrapper"; import styled, { css } from "styled-components"; import { Color } from "../color"; import { TextStyleHelper } from "../text/helper"; -import { ArrowRightIcon } from "@lifesg/react-icons/arrow-right"; -import { DesignToken } from "src/design-token"; // ============================================================================= // STYLE INTERFACE // ============================================================================= -interface ContainerStyleProps { - $disabled?: boolean; - $error?: boolean; - $readOnly?: boolean; -} - interface IndicatorStyleProps { $position: "start" | "end" | "none"; } @@ -25,54 +19,9 @@ export const Wrapper = styled.div` position: relative; `; -export const TimeContainer = styled.div` - display: flex; - align-items: center; - gap: 0.5rem; - width: 100%; +export const TimeContainer = styled(InputWrapper)` height: 3rem; - border-radius: 0.25rem; - border: 1px solid ${Color.Neutral[5]}; - padding: 11px 16px; - - :focus, - :focus-within { - border: 1px solid ${Color.Accent.Light[1]}; - box-shadow: ${DesignToken.InputBoxShadow}; - } - - ${(props) => { - if (props.$readOnly) { - return css` - border: 0; - padding: 0; - :focus, - :focus-within { - border: none; - box-shadow: none; - } - `; - } else if (props.$disabled) { - return css` - background: ${Color.Neutral[6]}; - :hover { - cursor: not-allowed; - } - :focus-within { - border: none; - box-shadow: none; - } - `; - } else if (props.$error) { - return css` - border: 1px solid ${Color.Validation.Red.Border(props)}; - :focus-within { - border: 1px solid ${Color.Validation.Red.Border(props)}; - box-shadow: ${DesignToken.InputErrorBoxShadow}; - } - `; - } - }} + gap: 0.5rem; `; export const ArrowRight = styled(ArrowRightIcon)` diff --git a/src/unit-number/unit-number-input.tsx b/src/unit-number/unit-number-input.tsx index 84ec73f2d..fb691b260 100644 --- a/src/unit-number/unit-number-input.tsx +++ b/src/unit-number/unit-number-input.tsx @@ -338,7 +338,7 @@ export const UnitNumberInput = ({ Date: Tue, 16 May 2023 15:40:30 +0800 Subject: [PATCH 07/14] [MISC][RL] Extract shared standalone native input --- src/date-input/stand-alone-input.style.tsx | 34 ++------------ .../dropdown-list/dropdown-search.styles.tsx | 17 +------ src/shared/input-wrapper/input-wrapper.tsx | 43 +++++++++++++++++- .../timepicker-dropdown.styles.tsx | 21 ++------- .../time-range-picker.styles.tsx | 21 +++------ src/timepicker/timepicker.styles.tsx | 44 ++----------------- src/timepicker/timepicker.tsx | 14 ++++-- 7 files changed, 68 insertions(+), 126 deletions(-) diff --git a/src/date-input/stand-alone-input.style.tsx b/src/date-input/stand-alone-input.style.tsx index ae6ebe602..4bba5a108 100644 --- a/src/date-input/stand-alone-input.style.tsx +++ b/src/date-input/stand-alone-input.style.tsx @@ -1,6 +1,7 @@ import styled, { css } from "styled-components"; -import { MediaQuery } from "../media"; import { Color } from "../color"; +import { MediaQuery } from "../media"; +import { BasicInput } from "../shared/input-wrapper/input-wrapper"; import { TextStyleHelper } from "../text/helper"; import { Text } from "../text/text"; import { DateInputVariant } from "./types"; @@ -112,39 +113,10 @@ export const InputContainer = styled.div` }} `; -const BaseInput = styled.input` - ${TextStyleHelper.getTextStyle("Body", "regular")} +const BaseInput = styled(BasicInput)` background: transparent; height: 100%; - border: none; text-align: center; - padding: 0; - - // Chrome, Safari, Edge, Opera - ::-webkit-outer-spin-button, - ::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; - } - - // Safari (remove top shadow) - --webkit-appearance: none; - - // Firefox - --moz-appearance: textfield; - - :focus, - :active { - outline: none; - } - - ${(props) => { - if (props.disabled) { - return css` - cursor: not-allowed; - `; - } - }} `; export const DayInput = styled(BaseInput)` diff --git a/src/shared/dropdown-list/dropdown-search.styles.tsx b/src/shared/dropdown-list/dropdown-search.styles.tsx index 86138deea..81fe6d903 100644 --- a/src/shared/dropdown-list/dropdown-search.styles.tsx +++ b/src/shared/dropdown-list/dropdown-search.styles.tsx @@ -3,7 +3,7 @@ import { MagnifierIcon } from "@lifesg/react-icons/magnifier"; import styled from "styled-components"; import { Color } from "../../color"; import { IconButton } from "../../icon-button"; -import { TextStyleHelper } from "../../text"; +import { BasicInput } from "../input-wrapper/input-wrapper"; export const Container = styled.li` background: ${Color.Neutral[7]}; @@ -12,24 +12,11 @@ export const Container = styled.li` align-items: center; `; -export const SearchInput = styled.input` - ${TextStyleHelper.getTextStyle("Body", "regular")} +export const SearchInput = styled(BasicInput)` height: 3rem; - border: none; - background: transparent; flex: 1; padding: 0 0.5rem 0 0; width: 100%; - - :focus, - :active { - outline: none; - } - - ::placeholder, - ::-webkit-input-placeholder { - color: ${Color.Neutral[3]}; - } `; export const SearchIcon = styled(MagnifierIcon)` diff --git a/src/shared/input-wrapper/input-wrapper.tsx b/src/shared/input-wrapper/input-wrapper.tsx index dcfde60bf..036def65c 100644 --- a/src/shared/input-wrapper/input-wrapper.tsx +++ b/src/shared/input-wrapper/input-wrapper.tsx @@ -1,6 +1,7 @@ +import styled, { css } from "styled-components"; import { Color } from "../../color"; import { DesignToken } from "../../design-token"; -import styled, { css } from "styled-components"; +import { TextStyleHelper } from "../../text"; // ============================================================================= // STYLE INTERFACE, transient props are denoted with $ @@ -68,3 +69,43 @@ export const InputWrapper = styled.div` } }} `; + +/** + * standalone native input with stripped-down styles, intended to be used in + * combination with `InputWrapper` or other wrappers to build composite widgets + */ +export const BasicInput = styled.input` + ${TextStyleHelper.getTextStyle("Body", "regular")} + color: ${Color.Neutral[1]}; + display: block; + background: transparent; + border: none; + outline: none; + box-shadow: none; + padding: 0; + margin: 0; + + :disabled { + :hover { + cursor: not-allowed; + } + } + + ::placeholder, + ::-webkit-input-placeholder { + color: ${Color.Neutral[3]}; + } + + // Chrome, Safari, Edge, Opera + ::-webkit-outer-spin-button, + ::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + // Safari (remove top shadow) + --webkit-appearance: none; + + // Firefox + --moz-appearance: textfield; +`; diff --git a/src/shared/timepicker-dropdown/timepicker-dropdown.styles.tsx b/src/shared/timepicker-dropdown/timepicker-dropdown.styles.tsx index c9f1a3456..be8950ebf 100644 --- a/src/shared/timepicker-dropdown/timepicker-dropdown.styles.tsx +++ b/src/shared/timepicker-dropdown/timepicker-dropdown.styles.tsx @@ -4,8 +4,9 @@ import { Button } from "../../button"; import { Color } from "../../color"; import { IconButton } from "../../icon-button"; import { MediaQuery } from "../../media"; -import { Text, TextStyleHelper } from "../../text"; +import { Text } from "../../text"; import { Toggle } from "../../toggle"; +import { BasicInput } from "../input-wrapper/input-wrapper"; // ============================================================================= // STYLING @@ -119,19 +120,16 @@ export const DividerLabel = styled(Text.Body)` } `; -export const TimeInput = styled.input` - ${TextStyleHelper.getTextStyle("Body", "regular")} +export const TimeInput = styled(BasicInput)` border-radius: ${BORDER_RADIUS}; height: 3rem; width: 5rem; text-align: center; border: 1px solid ${Color.Neutral[5]}; background: ${Color.Neutral[8]}; - color: ${Color.Neutral[1]}; :focus, :active { - outline: none; border: 1px solid ${Color.Accent.Light[1]}; box-shadow: inset 0 0 5px 1px ${Color.Shadow.Accent}; } @@ -140,19 +138,6 @@ export const TimeInput = styled.input` color: transparent; } - // Chrome, Safari, Edge, Opera - ::-webkit-outer-spin-button, - ::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; - } - - // Safari (remove top shadow) - --webkit-appearance: none; - - // Firefox - --moz-appearance: textfield; - ${MediaQuery.MaxWidth.mobileS} { width: 6rem; } diff --git a/src/time-range-picker/time-range-picker.styles.tsx b/src/time-range-picker/time-range-picker.styles.tsx index 8973fe873..bf835022f 100644 --- a/src/time-range-picker/time-range-picker.styles.tsx +++ b/src/time-range-picker/time-range-picker.styles.tsx @@ -1,8 +1,10 @@ import { ArrowRightIcon } from "@lifesg/react-icons/arrow-right"; -import { InputWrapper } from "src/shared/input-wrapper/input-wrapper"; import styled, { css } from "styled-components"; import { Color } from "../color"; -import { TextStyleHelper } from "../text/helper"; +import { + BasicInput, + InputWrapper, +} from "../shared/input-wrapper/input-wrapper"; // ============================================================================= // STYLE INTERFACE @@ -61,20 +63,7 @@ export const Indicator = styled.div` }} `; -export const SelectorInput = styled.input` - /* reset default styles */ - ${TextStyleHelper.getTextStyle("Body", "regular")} - color: ${Color.Neutral[1]}; - background-color: transparent; - border: none; - outline: none; - - :disabled { - :hover { - cursor: not-allowed; - } - } - +export const SelectorInput = styled(BasicInput)` display: block; width: 100%; flex: 1; diff --git a/src/timepicker/timepicker.styles.tsx b/src/timepicker/timepicker.styles.tsx index aa5442412..cfe44b808 100644 --- a/src/timepicker/timepicker.styles.tsx +++ b/src/timepicker/timepicker.styles.tsx @@ -1,7 +1,5 @@ -import styled, { css } from "styled-components"; -import { Color } from "../color"; -import { DesignToken } from "../design-token"; -import { TextStyleHelper } from "../text/helper"; +import styled from "styled-components"; +import { BasicInput } from "../shared/input-wrapper/input-wrapper"; // ============================================================================= // STYLE INTERFACe @@ -19,42 +17,6 @@ export const Wrapper = styled.div` position: relative; `; -export const InputSelectorElement = styled.input` - ${TextStyleHelper.getTextStyle("Body", "regular")} - border: 1px solid ${Color.Neutral[5]}; - border-radius: 4px; - display: block; - padding: 0.2rem 1rem 0.3rem 1rem; // Somehow the input text appears lower +export const InputSelectorElement = styled(BasicInput)` height: 3rem; - width: 100%; - background: ${Color.Neutral[8]}; - color: ${Color.Neutral[1]}; - - :focus, - :active { - outline: none; - } - - ${(props) => { - if (props.disabled) { - return css` - background: ${Color.Neutral[6](props)}; - cursor: not-allowed; - `; - } else if (props.error && !props.focused) { - return css` - border: 1px solid ${Color.Validation.Red.Border(props)}; - `; - } else if (props.error && props.focused) { - return css` - border: 1px solid ${Color.Validation.Red.Border(props)}; - box-shadow: ${DesignToken.InputErrorBoxShadow}; - `; - } else if (props.focused) { - return css` - border: 1px solid ${Color.Accent.Light[1]}; - box-shadow: ${DesignToken.InputBoxShadow}; - `; - } - }} `; diff --git a/src/timepicker/timepicker.tsx b/src/timepicker/timepicker.tsx index 0b5f7393a..51d11b006 100644 --- a/src/timepicker/timepicker.tsx +++ b/src/timepicker/timepicker.tsx @@ -1,7 +1,8 @@ import { useCallback, useEffect, useRef, useState } from "react"; +import { InputWrapper } from "../shared/input-wrapper/input-wrapper"; import { TimepickerDropdown } from "../shared/timepicker-dropdown/timepicker-dropdown"; import { TimeHelper } from "../util/time-helper"; -import { InputSelectorElement, Wrapper } from "./timepicker.styles"; +import { InputSelectorElement } from "./timepicker.styles"; import { TimepickerProps } from "./types"; export const Timepicker = ({ @@ -108,7 +109,6 @@ export const Timepicker = ({ value={TimeHelper.formatValue(value, format)} defaultValue={defaultValue} disabled={disabled} - error={error} data-testid={ id ? `${id}-timepicker-selector` : "timepicker-selector" } @@ -116,7 +116,13 @@ export const Timepicker = ({ ); return ( - + {renderSelector()} - + ); }; From 636b370f043fcba02aa1220259ceb5c234ac0724 Mon Sep 17 00:00:00 2001 From: Quek Ruo Ling Date: Tue, 16 May 2023 14:05:26 +0800 Subject: [PATCH 08/14] [MISC][RL] Disable input interactivity when not visible --- .eslintrc | 3 ++- custom-types/react-augment.d.ts | 4 ++++ src/shared/timepicker-dropdown/timepicker-dropdown.tsx | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 84af66b7c..5170ff9fc 100644 --- a/.eslintrc +++ b/.eslintrc @@ -83,6 +83,7 @@ "accessibility": "explicit" } ], - "react/react-in-jsx-scope": "off" + "react/react-in-jsx-scope": "off", + "react/no-unknown-property": ["error", { "ignore": ["inert"] }] } } diff --git a/custom-types/react-augment.d.ts b/custom-types/react-augment.d.ts index 2682a663e..ef2c08c86 100644 --- a/custom-types/react-augment.d.ts +++ b/custom-types/react-augment.d.ts @@ -5,4 +5,8 @@ declare module "react" { function forwardRef( render: (props: P, ref: Ref) => ReactElement | null ): (props: P & RefAttributes) => ReactElement | null; + + interface HTMLAttributes extends HTMLAttributes { + inert?: string | undefined; + } } diff --git a/src/shared/timepicker-dropdown/timepicker-dropdown.tsx b/src/shared/timepicker-dropdown/timepicker-dropdown.tsx index 45d35274f..81fef2c34 100644 --- a/src/shared/timepicker-dropdown/timepicker-dropdown.tsx +++ b/src/shared/timepicker-dropdown/timepicker-dropdown.tsx @@ -394,6 +394,7 @@ export const TimepickerDropdown = ({ From ac5a0d8ef6d3fdb0648bc1c29d9a8553a6688854 Mon Sep 17 00:00:00 2001 From: Quek Ruo Ling Date: Tue, 16 May 2023 15:41:16 +0800 Subject: [PATCH 09/14] [MISC][RL] Add readonly state to timepicker --- src/timepicker/timepicker.tsx | 6 +++-- src/timepicker/types.ts | 1 + .../form-timepicker.stories.mdx | 26 ++++++++++++++++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/timepicker/timepicker.tsx b/src/timepicker/timepicker.tsx index 51d11b006..41c25f29d 100644 --- a/src/timepicker/timepicker.tsx +++ b/src/timepicker/timepicker.tsx @@ -8,6 +8,7 @@ import { TimepickerProps } from "./types"; export const Timepicker = ({ id, disabled = false, + readOnly = false, error, value, defaultValue, @@ -42,13 +43,13 @@ export const Timepicker = ({ // EVENT HANDLERS // ============================================================================= const handleInputFocus = useCallback(() => { - if (!disabled && !showSelector) { + if (!disabled && !readOnly && !showSelector) { setShowSelector(true); } }, [showSelector]); const handleMouseDownEvent = (event: MouseEvent) => { - if (!disabled) { + if (!disabled && !readOnly) { runOutsideFocusHandler(event); } }; @@ -119,6 +120,7 @@ export const Timepicker = ({ void) | undefined; onBlur?: (() => void) | undefined; diff --git a/stories/form/form-timepicker/form-timepicker.stories.mdx b/stories/form/form-timepicker/form-timepicker.stories.mdx index 06ef078bd..c25a12e53 100644 --- a/stories/form/form-timepicker/form-timepicker.stories.mdx +++ b/stories/form/form-timepicker/form-timepicker.stories.mdx @@ -30,7 +30,6 @@ import { Form } from "@lifesg/react-design-system/form"; const [time1, setTime1] = useState(""); const [time2, setTime2] = useState(""); const [time3, setTime3] = useState(""); - const [time4, setTime4] = useState(""); return ( @@ -43,17 +42,38 @@ import { Form } from "@lifesg/react-design-system/form"; label="This is the disabled state" disabled /> + setTime3(value)} errorMessage="Time input required" /> + + + ); + }} + + + +12hr format + + + + {() => { + const [time, setTime] = useState(""); + return ( + + setTime4(value)} format="12hr" + value={time} + onChange={(value) => setTime(value)} /> From fc24aa8438d322443f195c3c9562b2dcba13d9a6 Mon Sep 17 00:00:00 2001 From: Quek Ruo Ling Date: Tue, 16 May 2023 16:40:28 +0800 Subject: [PATCH 10/14] [MISC][RL] Set color for indicator with error --- src/date-input/date-input.style.tsx | 6 ++++-- src/date-input/date-input.tsx | 7 ++++++- src/time-range-picker/time-range-picker.styles.tsx | 4 +++- src/time-range-picker/time-range-picker.tsx | 1 + 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/date-input/date-input.style.tsx b/src/date-input/date-input.style.tsx index 88db1e033..f470c7f94 100644 --- a/src/date-input/date-input.style.tsx +++ b/src/date-input/date-input.style.tsx @@ -19,6 +19,7 @@ interface ContainerStyleProps { interface IndicateBarStyleProps { $position: "start" | "end" | "none"; + $error: boolean; } // ============================================================================= @@ -131,12 +132,13 @@ export const ArrowRight = styled(ArrowRightIcon)` export const IndicateBar = styled.div` position: absolute; - background-color: ${Color.Primary}; + background-color: ${(props) => + props.$error ? Color.Validation.Red.Border : Color.Primary}; height: 0.125rem; width: calc(100% - 50% - 2rem); // paddingX is 2rem, transition: left 350ms ease-in-out, opacity 350ms ease-in-out; left: 1rem; - bottom: -0.1rem; + bottom: 0; ${(props) => { switch (props.$position) { diff --git a/src/date-input/date-input.tsx b/src/date-input/date-input.tsx index 10a4f1557..e5f83fb09 100644 --- a/src/date-input/date-input.tsx +++ b/src/date-input/date-input.tsx @@ -480,7 +480,12 @@ export const DateInput = ({ const renderIndicateBar = () => { if (variant === "single" || disabled || readOnly) return; - return ; + return ( + + ); }; const renderRangeInput = () => { diff --git a/src/time-range-picker/time-range-picker.styles.tsx b/src/time-range-picker/time-range-picker.styles.tsx index bf835022f..8c3a65048 100644 --- a/src/time-range-picker/time-range-picker.styles.tsx +++ b/src/time-range-picker/time-range-picker.styles.tsx @@ -12,6 +12,7 @@ import { interface IndicatorStyleProps { $position: "start" | "end" | "none"; + $error: boolean; } // ============================================================================= @@ -35,7 +36,8 @@ export const ArrowRight = styled(ArrowRightIcon)` export const Indicator = styled.div` position: absolute; - background-color: ${Color.Primary}; + background-color: ${(props) => + props.$error ? Color.Validation.Red.Border : Color.Primary}; height: 0.125rem; width: calc(100% - 50% - 2rem); // paddingX is 2rem transition: left 350ms ease-in-out, opacity 350ms ease-in-out; diff --git a/src/time-range-picker/time-range-picker.tsx b/src/time-range-picker/time-range-picker.tsx index 42dc16f61..4025e666a 100644 --- a/src/time-range-picker/time-range-picker.tsx +++ b/src/time-range-picker/time-range-picker.tsx @@ -196,6 +196,7 @@ export const TimeRangePicker = ({ ? "end" : "none" } + $error={error} /> From 834dd16a925055e76d874524d11def703105327c Mon Sep 17 00:00:00 2001 From: Quek Ruo Ling Date: Wed, 17 May 2023 11:44:20 +0800 Subject: [PATCH 11/14] [MISC][RL] Allow inputs to grow to desired width on mobile --- .../timepicker-dropdown.styles.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/shared/timepicker-dropdown/timepicker-dropdown.styles.tsx b/src/shared/timepicker-dropdown/timepicker-dropdown.styles.tsx index be8950ebf..93538073e 100644 --- a/src/shared/timepicker-dropdown/timepicker-dropdown.styles.tsx +++ b/src/shared/timepicker-dropdown/timepicker-dropdown.styles.tsx @@ -26,6 +26,10 @@ export const AnimatedDiv = styled(animated.div)` border-radius: ${BORDER_RADIUS}; overflow: hidden; z-index: 1; + + ${MediaQuery.MaxWidth.mobileS} { + max-width: 100%; + } `; export const Container = styled.div` @@ -43,6 +47,7 @@ export const InputSection = styled.div` ${MediaQuery.MaxWidth.mobileS} { flex-direction: column; + width: 100%; } `; @@ -68,6 +73,7 @@ export const HourMinuteSection = styled.div` ${MediaQuery.MaxWidth.mobileS} { margin-right: 0; + width: 100%; } `; @@ -89,6 +95,10 @@ export const InputContainer = styled.div` display: flex; flex-direction: column; align-items: center; + + ${MediaQuery.MaxWidth.mobileS} { + width: 6rem; + } `; export const SwitchButton = styled(IconButton)` @@ -139,7 +149,7 @@ export const TimeInput = styled(BasicInput)` } ${MediaQuery.MaxWidth.mobileS} { - width: 6rem; + width: 100%; } `; From 35dca8c93b21144d85447c1bbdb63272342e87da Mon Sep 17 00:00:00 2001 From: Quek Ruo Ling Date: Thu, 18 May 2023 14:43:28 +0800 Subject: [PATCH 12/14] [MISC][RL] Restore 24hr input --- src/time-range-picker/time-range-picker.tsx | 2 +- src/time-range-picker/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/time-range-picker/time-range-picker.tsx b/src/time-range-picker/time-range-picker.tsx index 4025e666a..e46380281 100644 --- a/src/time-range-picker/time-range-picker.tsx +++ b/src/time-range-picker/time-range-picker.tsx @@ -15,7 +15,7 @@ export const TimeRangePicker = ({ disabled = false, error, value, - format = "12hr", + format = "24hr", readOnly, onChange, onBlur, diff --git a/src/time-range-picker/types.ts b/src/time-range-picker/types.ts index bd1cdb9ac..3e983ae58 100644 --- a/src/time-range-picker/types.ts +++ b/src/time-range-picker/types.ts @@ -1,4 +1,4 @@ -export type TimeRangePickerFormat = "12hr"; +export type TimeRangePickerFormat = "12hr" | "24hr"; export interface TimeRangeInputValue { startTime: string; From 17533927e0a42b7a196518eb54b828f72e137122 Mon Sep 17 00:00:00 2001 From: Quek Ruo Ling Date: Thu, 18 May 2023 16:01:19 +0800 Subject: [PATCH 13/14] [MISC][RL] Update props documentation --- src/time-range-picker/time-range-picker.tsx | 24 +++++----- src/time-range-picker/types.ts | 30 +++++++++---- src/timepicker/timepicker.tsx | 6 +-- src/timepicker/types.ts | 21 ++++++--- .../form-time-range-picker.stories.mdx | 37 ++++++++++++--- .../form-time-range-picker/props-table.tsx | 45 ++++++++++--------- stories/form/form-timepicker/props-table.tsx | 21 --------- 7 files changed, 107 insertions(+), 77 deletions(-) diff --git a/src/time-range-picker/time-range-picker.tsx b/src/time-range-picker/time-range-picker.tsx index e46380281..9af01fe6d 100644 --- a/src/time-range-picker/time-range-picker.tsx +++ b/src/time-range-picker/time-range-picker.tsx @@ -8,7 +8,7 @@ import { TimeContainer, Wrapper, } from "./time-range-picker.styles"; -import { TimeRangeInputValue, TimeRangePickerProps } from "./types"; +import { TimeRangePickerProps, TimeRangePickerValue } from "./types"; export const TimeRangePicker = ({ id, @@ -28,21 +28,21 @@ export const TimeRangePicker = ({ useState(false); const [showEndTimeSelector, setShowEndTimeSelector] = useState(false); - const [startTimeVal, setStartTimeVal] = useState(""); - const [endTimeVal, setEndTimeVal] = useState(""); + const [startTimeVal, setStartTimeVal] = useState(""); + const [endTimeVal, setEndTimeVal] = useState(""); const nodeRef = useRef(); // ============================================================================= // EFFECTS // ============================================================================= - useEffect(() => { if (value) { - setStartTimeVal(value.startTime); - setEndTimeVal(value.endTime); + setStartTimeVal(value.start); + setEndTimeVal(value.end); } }, []); + useEffect(() => { document.addEventListener("mousedown", handleMouseDownEvent); document.addEventListener("keyup", handleKeyUpEvent); @@ -96,9 +96,9 @@ export const TimeRangePicker = ({ setShowEndTimeSelector(true); setStartTimeVal(value); - const timeValue: TimeRangeInputValue = { - startTime: value, - endTime: endTimeVal, + const timeValue: TimeRangePickerValue = { + start: value, + end: endTimeVal, }; onChange && onChange(timeValue); @@ -112,9 +112,9 @@ export const TimeRangePicker = ({ setShowStartTimeSelector(true); } - const timeValue: TimeRangeInputValue = { - startTime: startTimeVal, - endTime: value, + const timeValue: TimeRangePickerValue = { + start: startTimeVal, + end: value, }; onChange && onChange(timeValue); diff --git a/src/time-range-picker/types.ts b/src/time-range-picker/types.ts index 3e983ae58..ef5f52429 100644 --- a/src/time-range-picker/types.ts +++ b/src/time-range-picker/types.ts @@ -1,8 +1,8 @@ export type TimeRangePickerFormat = "12hr" | "24hr"; -export interface TimeRangeInputValue { - startTime: string; - endTime: string; +export interface TimeRangePickerValue { + start: string; + end: string; } export interface TimeRangePickerProps { @@ -10,15 +10,29 @@ export interface TimeRangePickerProps { className?: string | undefined; id?: string | undefined; style?: React.CSSProperties | undefined; - readOnly?: boolean | undefined; - "data-testid"?: string | undefined; // Input-specific attributes - - value?: TimeRangeInputValue | undefined; + "data-testid"?: string | undefined; + /** + * An object with `start` and `end` values. Can be an empty string or a + * string based format. 24 hour uses "hh:mm", while 12 hour uses "hh:mma" + */ + value?: TimeRangePickerValue | undefined; + /** + * The time input format in `12hr` or `24hr`. Defaults to `24hr` + */ format?: TimeRangePickerFormat | undefined; disabled?: boolean | undefined; + readOnly?: boolean | undefined; error?: boolean | undefined; - onChange?: ((value: TimeRangeInputValue) => void) | undefined; + /** + * Called when a selection is made. Returns an object with `start` and + * `end` values. Can be an empty string or a string based format. + * 24 hour uses "hh:mm", while 12 hour uses "hh:mma" + */ + onChange?: ((value: TimeRangePickerValue) => void) | undefined; + /** + * Called when a defocus is made on the field + */ onBlur?: (() => void) | undefined; } diff --git a/src/timepicker/timepicker.tsx b/src/timepicker/timepicker.tsx index 41c25f29d..77be18ab9 100644 --- a/src/timepicker/timepicker.tsx +++ b/src/timepicker/timepicker.tsx @@ -11,7 +11,6 @@ export const Timepicker = ({ readOnly = false, error, value, - defaultValue, placeholder, format = "24hr", onChange, @@ -105,10 +104,9 @@ export const Timepicker = ({ void) | undefined; + /** + * Called when a defocus is made on the field + */ onBlur?: (() => void) | undefined; + /** + * Called when the "Cancel" button is clicked + */ onSelectionCancel?: (() => void) | undefined; } diff --git a/stories/form/form-time-range-picker/form-time-range-picker.stories.mdx b/stories/form/form-time-range-picker/form-time-range-picker.stories.mdx index 48d457911..80cae2f0c 100644 --- a/stories/form/form-time-range-picker/form-time-range-picker.stories.mdx +++ b/stories/form/form-time-range-picker/form-time-range-picker.stories.mdx @@ -28,16 +28,16 @@ import { Form } from "@lifesg/react-design-system/form"; {() => { const [time1, setTime1] = useState({ - startTime: "", - endTime: "", + start: "", + end: "", }); const [time2, setTime2] = useState({ - startTime: "", - endTime: "", + start: "", + end: "", }); const [time3] = useState({ - startTime: "12:00am", - endTime: "12:30am", + start: "12:00am", + end: "12:30am", }); return ( @@ -69,6 +69,31 @@ import { Form } from "@lifesg/react-design-system/form"; +12 hour format + + + + {() => { + const [time, setTime] = useState({ + start: "12:00am", + end: "", + }); + return ( + + + setTime(value)} + /> + + + ); + }} + + + Using the field as a standalone In the case that you require the timepicker field as a standalone, you can do this. diff --git a/stories/form/form-time-range-picker/props-table.tsx b/stories/form/form-time-range-picker/props-table.tsx index 178dd7cce..ccce0bba3 100644 --- a/stories/form/form-time-range-picker/props-table.tsx +++ b/stories/form/form-time-range-picker/props-table.tsx @@ -9,20 +9,14 @@ const DATA: ApiTableSectionProps[] = [ attributes: [ { name: "value", - description: ( - <> - The value of the time in string based format. 24 hour - will be hh:mm, while 12 hour will be{" "} - hh:mma - - ), - propTypes: ["string"], + description: "The time range values in the format specified", + propTypes: ["TimeRangePickerValue"], }, { name: "format", description: "The time input format", - propTypes: [`"12hr"`], - defaultValue: `"12hr"`, + propTypes: [`"12hr"`, `"24hr"`], + defaultValue: `"24hr"`, }, { name: "disabled", @@ -69,30 +63,39 @@ const DATA: ApiTableSectionProps[] = [ { name: "onChange", description: - "Called when the user clicks on the 'Done' button in the time selection box. Returns the time value in the format specified", - propTypes: ["(value: TimeRangeInputValue) => void"], + "Called when the user clicks on the 'Done' button in the time selection box. Returns the time range values in the format specified", + propTypes: ["(value: TimeRangePickerValue) => void"], }, { name: "onBlur", - description: - "Called when a defocus happens. Any changes in the time selection box will not be applied", + description: "Called when a defocus happens", propTypes: ["() => void"], }, ], }, { - name: "TimeRangeInputValue", + name: "TimeRangePickerValue", attributes: [ { - name: "startTime", - description: - "The selected start time value in the format specified", + name: "start", + description: ( + <> + The selected start time value as an empty string or a + string-based format. 24 hour uses hh:mm, + while 12 hour uses hh:mma + + ), propTypes: ["string"], }, { - name: "endTime", - description: - "The selected end time value in the format specified", + name: "end", + description: ( + <> + The selected start time value as an empty string or a + string-based format. 24 hour uses hh:mm, + while 12 hour uses hh:mma + + ), propTypes: ["string"], }, ], diff --git a/stories/form/form-timepicker/props-table.tsx b/stories/form/form-timepicker/props-table.tsx index b8d6b283c..f36051bc1 100644 --- a/stories/form/form-timepicker/props-table.tsx +++ b/stories/form/form-timepicker/props-table.tsx @@ -18,17 +18,6 @@ const DATA: ApiTableSectionProps[] = [ ), propTypes: ["string"], }, - { - name: "defaultValue", - description: ( - <> - The default value of the time in string based format. 24 - hour will be hh:mm, while 12 hour will be{" "} - hh:mma - - ), - propTypes: ["string"], - }, { name: "placeholder", description: ( @@ -71,21 +60,11 @@ const DATA: ApiTableSectionProps[] = [ description: "The unique identifier of the component", propTypes: ["string"], }, - { - name: "name", - description: "The name of the component", - propTypes: ["string"], - }, { name: "style", description: "Allows for inline styling of the component", propTypes: ["React.CSSProperties"], }, - { - name: "tabIndex", - description: "Specifies the tab order of the component", - propTypes: ["number"], - }, { name: "data-testid", description: "The test identifier of the component", From ffd81f1b1d11bc246864270eddf4d7aa66f2a061 Mon Sep 17 00:00:00 2001 From: Quek Ruo Ling Date: Thu, 18 May 2023 16:07:17 +0800 Subject: [PATCH 14/14] [MISC][RL] Replace onSelectionCancel with onBlur --- src/time-range-picker/time-range-picker.tsx | 28 ++++++------------ src/timepicker/timepicker.tsx | 30 +++++++------------- src/timepicker/types.ts | 4 --- stories/form/form-timepicker/props-table.tsx | 6 ---- 4 files changed, 20 insertions(+), 48 deletions(-) diff --git a/src/time-range-picker/time-range-picker.tsx b/src/time-range-picker/time-range-picker.tsx index 9af01fe6d..2234deed9 100644 --- a/src/time-range-picker/time-range-picker.tsx +++ b/src/time-range-picker/time-range-picker.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { TimepickerDropdown } from "../shared/timepicker-dropdown/timepicker-dropdown"; import { TimeHelper } from "../util/time-helper"; +import { useEventListener } from "../util/use-event-listener"; import { ArrowRight, Indicator, @@ -43,15 +44,8 @@ export const TimeRangePicker = ({ } }, []); - useEffect(() => { - document.addEventListener("mousedown", handleMouseDownEvent); - document.addEventListener("keyup", handleKeyUpEvent); - - return () => { - document.removeEventListener("mousedown", handleMouseDownEvent); - document.removeEventListener("keyup", handleKeyUpEvent); - }; - }, [showEndTimeSelector]); + useEventListener("mousedown", handleMouseDownEvent, document); + useEventListener("keyup", handleKeyUpEvent, document); // ============================================================================= // EVENT HANDLERS @@ -70,13 +64,13 @@ export const TimeRangePicker = ({ } }; - const handleMouseDownEvent = (event: MouseEvent) => { + function handleMouseDownEvent(event: MouseEvent) { if (!disabled) { runOutsideFocusHandler(event); } - }; + } - const handleKeyUpEvent = (event: KeyboardEvent) => { + function handleKeyUpEvent(event: KeyboardEvent) { switch (event.code) { case "Tab": runOutsideFocusHandler(event); @@ -84,11 +78,10 @@ export const TimeRangePicker = ({ default: break; } - }; + } const handleSelectionDropdownCancel = () => { - setShowEndTimeSelector(false); - setShowStartTimeSelector(false); + runOnBlurHandler(); }; const handleStartTime = (value: string) => { @@ -131,10 +124,7 @@ export const TimeRangePicker = ({ const runOutsideFocusHandler = (event: MouseEvent | KeyboardEvent) => { if (nodeRef && !nodeRef.current.contains(event.target as any)) { - if (!showEndTimeSelector) { - runOnBlurHandler(); - } - if (!showStartTimeSelector) { + if (showEndTimeSelector || showStartTimeSelector) { runOnBlurHandler(); } } diff --git a/src/timepicker/timepicker.tsx b/src/timepicker/timepicker.tsx index 77be18ab9..fdec5f70c 100644 --- a/src/timepicker/timepicker.tsx +++ b/src/timepicker/timepicker.tsx @@ -1,7 +1,8 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useRef, useState } from "react"; import { InputWrapper } from "../shared/input-wrapper/input-wrapper"; import { TimepickerDropdown } from "../shared/timepicker-dropdown/timepicker-dropdown"; import { TimeHelper } from "../util/time-helper"; +import { useEventListener } from "../util/use-event-listener"; import { InputSelectorElement } from "./timepicker.styles"; import { TimepickerProps } from "./types"; @@ -15,7 +16,6 @@ export const Timepicker = ({ format = "24hr", onChange, onBlur, - onSelectionCancel, ...otherProps }: TimepickerProps) => { // ============================================================================= @@ -28,32 +28,25 @@ export const Timepicker = ({ // ============================================================================= // EFFECTS // ============================================================================= - useEffect(() => { - document.addEventListener("mousedown", handleMouseDownEvent); - document.addEventListener("keyup", handleKeyUpEvent); - - return () => { - document.removeEventListener("mousedown", handleMouseDownEvent); - document.removeEventListener("keyup", handleKeyUpEvent); - }; - }, [showSelector]); + useEventListener("mousedown", handleMouseDownEvent, document); + useEventListener("keyup", handleKeyUpEvent, document); // ============================================================================= // EVENT HANDLERS // ============================================================================= - const handleInputFocus = useCallback(() => { + const handleInputFocus = () => { if (!disabled && !readOnly && !showSelector) { setShowSelector(true); } - }, [showSelector]); + }; - const handleMouseDownEvent = (event: MouseEvent) => { + function handleMouseDownEvent(event: MouseEvent) { if (!disabled && !readOnly) { runOutsideFocusHandler(event); } - }; + } - const handleKeyUpEvent = (event: KeyboardEvent) => { + function handleKeyUpEvent(event: KeyboardEvent) { switch (event.code) { case "Tab": runOutsideFocusHandler(event); @@ -61,11 +54,10 @@ export const Timepicker = ({ default: break; } - }; + } const handleSelectionDropdownCancel = () => { - setShowSelector(false); - onSelectionCancel && onSelectionCancel(); + runOnBlurHandler(); }; const handleChange = (value: string) => { diff --git a/src/timepicker/types.ts b/src/timepicker/types.ts index 48f8f531e..cfca87f3c 100644 --- a/src/timepicker/types.ts +++ b/src/timepicker/types.ts @@ -27,8 +27,4 @@ export interface TimepickerProps { * Called when a defocus is made on the field */ onBlur?: (() => void) | undefined; - /** - * Called when the "Cancel" button is clicked - */ - onSelectionCancel?: (() => void) | undefined; } diff --git a/stories/form/form-timepicker/props-table.tsx b/stories/form/form-timepicker/props-table.tsx index f36051bc1..366dd8bdd 100644 --- a/stories/form/form-timepicker/props-table.tsx +++ b/stories/form/form-timepicker/props-table.tsx @@ -82,12 +82,6 @@ const DATA: ApiTableSectionProps[] = [ "Called when a defocus happens. Any changes in the time selection box will not be applied", propTypes: ["() => void"], }, - { - name: "onSelectionCancel", - description: - "Called when the user clicks on the 'Cancel' button in the time selection box. Any changes will not be applied", - propTypes: ["() => void"], - }, ], }, ...SHARED_FORM_PROPS_DATA,