diff --git a/package-lock.json b/package-lock.json index 0ce8390db..445a2afaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -101,7 +101,7 @@ "rollup-plugin-typescript2": "^0.36.0", "storybook": "^8.2.8", "style-loader": "^3.3.1", - "styled-components": "^6.1.19", + "styled-components": "^6.4.1", "typescript": "^4.8.4" }, "peerDependencies": { diff --git a/package.json b/package.json index 82709bfb9..5d57205ec 100644 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "rollup-plugin-typescript2": "^0.36.0", "storybook": "^8.2.8", "style-loader": "^3.3.1", - "styled-components": "^6.1.19", + "styled-components": "^6.4.1", "typescript": "^4.8.4" }, "lint-staged": { diff --git a/src/__tests__/components/frontend-engine/custom-component.spec.tsx b/src/__tests__/components/frontend-engine/custom-component.spec.tsx index ba8279678..10a0f60af 100644 --- a/src/__tests__/components/frontend-engine/custom-component.spec.tsx +++ b/src/__tests__/components/frontend-engine/custom-component.spec.tsx @@ -40,10 +40,10 @@ const MyCustomComponent: TCustomComponent = (props: TCustomCompo const { error, id, + onBlur, onChange, schema: { displayTitle, validation }, value, - ...otherProps } = props; const { @@ -81,10 +81,10 @@ const MyCustomComponent: TCustomComponent = (props: TCustomCompo label={displayTitle} id={id} errorMessage={error?.message} + onBlur={onBlur} onChange={handleDispatchEvent} onClick={handleRemoveEvent} value={value || ""} - {...otherProps} /> ); }; diff --git a/src/components/custom/filter/filter-checkbox/filter-checkbox.tsx b/src/components/custom/filter/filter-checkbox/filter-checkbox.tsx index ea2827330..8ea642ba9 100644 --- a/src/components/custom/filter/filter-checkbox/filter-checkbox.tsx +++ b/src/components/custom/filter/filter-checkbox/filter-checkbox.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import { useFormContext } from "react-hook-form"; import useDeepCompareEffect from "use-deep-compare-effect"; import * as Yup from "yup"; -import { TestHelper } from "../../../../utils"; +import { TestHelper, filterSchemaProps } from "../../../../utils"; import { useValidationConfig } from "../../../../utils/hooks"; import { Sanitize } from "../../../shared"; import { IGenericCustomFieldProps } from "../../types"; @@ -14,12 +14,11 @@ export const FilterCheckbox = (props: IGenericCustomFieldProps(); // Current selected value state @@ -79,7 +78,7 @@ export const FilterCheckbox = (props: IGenericCustomFieldProps) => { // ========================================================================= // CONST, STATE, REF // ========================================================================= + const { error, id, schema, value } = props; const { - error, - id, - schema: { "data-testid": testId, src, validationTimeout = 2000, ...otherSchema }, - value, - } = props; + customSchema: { "data-testid": testId, src, validationTimeout = 2000, ...iframeProps }, + } = filterSchemaProps(schema); const formContext = useFormContext(); const iframeRef = useRef(null); const deferredRef = useRef<{ @@ -175,7 +174,7 @@ export const Iframe = (props: IGenericCustomFieldProps) => { // ========================================================================= return ( ) => { // RENDER FUNCTIONS // ========================================================================= const renderAccordion = (schema: IReviewSchemaAccordion) => { - const { button, bottomSection, expanded = true, label, topSection, ...otherSchema } = schema; + const { + commonSchema: { label }, + customSchema: { button, bottomSection, expanded = true, topSection, ...accordionProps }, + } = filterSchemaProps(schema); return ( ) => { ) } expanded={expanded} - {...otherSchema} + {...accordionProps} > ) => { }; const renderBox = (schema: IReviewSchemaBox) => { - const { label, description, topSection, bottomSection, ...otherSchema } = schema; + const { + commonSchema: { label }, + customSchema: { description, topSection, bottomSection, ...boxProps }, + } = filterSchemaProps(schema); return ( ) => { // CONST, STATE, REF // ============================================================================= const { schema, id } = props; - const { children, button, title, disableContentInset, ...otherSchema } = schema; + const { + customSchema: { button, children, title, disableContentInset, ...accordionProps }, + } = filterSchemaProps(schema); const { dispatchFieldEvent } = useFieldEvent(); @@ -22,7 +25,7 @@ export const Accordion = (props: IGenericElementProps) => { {title}} - {...otherSchema} + {...accordionProps} callToActionComponent={ button ? ( ) => { // ============================================================================= // CONST, STATE, REF // ============================================================================= + const { id, schema } = props; const { - id, - schema: { verticalMargin, ...otherSchema }, - } = props; + customSchema: { verticalMargin, ...dividerProps }, + } = filterSchemaProps(schema); // ============================================================================= // RENDER FUNCTIONS @@ -21,7 +21,7 @@ export const Divider = (props: IGenericElementProps) => { id={id} data-testid={TestHelper.generateId(id, "divider")} $verticalMargin={verticalMargin} - {...otherSchema} + {...dividerProps} /> ); }; diff --git a/src/components/elements/popover/popover.tsx b/src/components/elements/popover/popover.tsx index 847f20e80..8d051ec7b 100644 --- a/src/components/elements/popover/popover.tsx +++ b/src/components/elements/popover/popover.tsx @@ -1,6 +1,6 @@ import { PopoverInline } from "@lifesg/react-design-system/popover-v2"; import * as Icons from "@lifesg/react-icons"; -import { TestHelper } from "../../../utils"; +import { TestHelper, filterSchemaProps } from "../../../utils"; import { Sanitize } from "../../shared"; import { IGenericElementProps } from "../types"; import { Wrapper } from "../wrapper"; @@ -11,16 +11,16 @@ export const Popover = (props: IGenericElementProps) => { // ============================================================================= // CONST, STATE, REF // ============================================================================= + const { id, schema } = props; const { - id, - schema: { + customSchema: { children, className, icon, hint: { content: hintContent, ...hintProps }, - ...otherSchema + ...popoverProps }, - } = props; + } = filterSchemaProps(schema); // ============================================================================= // RENDER FUNCTIONS @@ -56,7 +56,7 @@ export const Popover = (props: IGenericElementProps) => { content={children && {children}} popoverContent={renderPopoverContent()} {...hintProps} - {...otherSchema} + {...popoverProps} /> ); }; diff --git a/src/components/elements/tab/tab.tsx b/src/components/elements/tab/tab.tsx index 5a956c8cb..d05a2aec7 100644 --- a/src/components/elements/tab/tab.tsx +++ b/src/components/elements/tab/tab.tsx @@ -15,10 +15,8 @@ export const Tab = (props: IGenericElementProps) => { // ========================================================================= // CONST, STATE, REF // ========================================================================= - const { - id, - schema: { currentActiveTabId, children, ...otherTabSchema }, - } = props; + const { id, schema } = props; + const { children, currentActiveTabId, fullWidthIndicatorLine } = schema; const [currentTabIndex, setCurrentTabIndex] = useState(getCurrentTabIndex()); const { removeFieldValidationConfig } = useValidationConfig(); const { unregister } = useFormContext(); @@ -100,21 +98,22 @@ export const Tab = (props: IGenericElementProps) => { // ========================================================================= return ( {Object.entries(children).map(([childId, childSchema]) => { - const { title, children, ...otherTabItemSchema } = childSchema; + const { children, title, width } = childSchema; + return ( {children} diff --git a/src/components/fields/button/button.tsx b/src/components/fields/button/button.tsx index 084a7d429..26333e77d 100644 --- a/src/components/fields/button/button.tsx +++ b/src/components/fields/button/button.tsx @@ -1,29 +1,21 @@ import { Button } from "@lifesg/react-design-system/button"; +import { Spacing } from "@lifesg/react-design-system/theme"; import * as Icons from "@lifesg/react-icons"; import styled from "styled-components"; import { IGenericFieldProps } from ".."; -import { IButtonSchema } from "./types"; import { useFieldEvent } from "../../../utils/hooks"; -import { Spacing } from "@lifesg/react-design-system/theme"; +import { filterSchemaProps } from "../../../utils/prop-helper"; +import { IButtonSchema } from "./types"; export const ButtonField = (props: IGenericFieldProps) => { // ============================================================================= // CONST, STATE, REF // ============================================================================= + const { id, schema } = props; const { - schema: { - label, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - uiType, - startIcon, - endIcon, - href, - target, - ...otherSchema - }, - id, - ...otherProps - } = props; + commonSchema: { label }, + customSchema: { endIcon, href, startIcon, target, ...buttonProps }, + } = filterSchemaProps(schema); const { dispatchFieldEvent } = useFieldEvent(); // ============================================================================= @@ -56,7 +48,7 @@ export const ButtonField = (props: IGenericFieldProps) => { }; return ( - + {renderIcon(startIcon)} {label} {renderIcon(endIcon)} diff --git a/src/components/fields/checkbox-group/checkbox-group.tsx b/src/components/fields/checkbox-group/checkbox-group.tsx index 54737f08f..336e110f3 100644 --- a/src/components/fields/checkbox-group/checkbox-group.tsx +++ b/src/components/fields/checkbox-group/checkbox-group.tsx @@ -5,7 +5,7 @@ import { useFormContext } from "react-hook-form"; import useDeepCompareEffect from "use-deep-compare-effect"; import * as Yup from "yup"; import { IGenericFieldProps } from ".."; -import { TestHelper, generateRandomId } from "../../../utils"; +import { TestHelper, filterSchemaProps, generateRandomId } from "../../../utils"; import { useValidationConfig } from "../../../utils/hooks"; import { Wrapper } from "../../elements/wrapper"; import { ERROR_MESSAGES, Sanitize, Warning } from "../../shared"; @@ -16,15 +16,11 @@ export const CheckboxGroup = (props: IGenericFieldProps) = // ============================================================================= // CONST, STATE, REFS // ============================================================================= + const { formattedLabel, error, id, onChange, schema, value, warning } = props; const { - formattedLabel, - error, - id, - onChange, - schema: { className, customOptions, disabled, label: _label, options, validation, ...otherSchema }, - value, - warning, - } = props; + commonSchema: { customOptions, validation }, + customSchema: { className, disabled, options, ...checkboxProps }, + } = filterSchemaProps(schema); const { setValue } = useFormContext(); const [stateValue, setStateValue] = useState(value || []); @@ -115,7 +111,7 @@ export const CheckboxGroup = (props: IGenericFieldProps) = className={className ? `${className}-checkbox-container` : undefined} > ) => { // ============================================================================= // CONST, STATE, REFS // ============================================================================= + const { error, formattedLabel, id, onChange, schema, value, warning } = props; const { - error, - formattedLabel, - id, - onChange, - schema: { disabled, label: _label, options, textarea, validation, ...otherSchema }, - value, - warning, - ...otherProps - } = props; + commonSchema: { validation }, + customSchema: { disabled, options, textarea, ...chipProps }, + } = filterSchemaProps(schema); const [stateValue, setStateValue] = useState(value || []); const [showTextarea, setShowTextarea] = useState(false); @@ -143,7 +138,7 @@ export const Chips = (props: IGenericFieldProps) => { const renderChips = (): JSX.Element[] => { return options.map((option, index) => ( handleChange(option.value)} isActive={isChipSelected(option.value)} @@ -162,7 +157,7 @@ export const Chips = (props: IGenericFieldProps) => { const textareaLabel = getTextareaLabel(); return ( handleTextareaChipClick(textareaLabel)} isActive={isChipSelected(textareaLabel)} > @@ -177,17 +172,17 @@ export const Chips = (props: IGenericFieldProps) => { } const textareaId = getTextareaId(); - const schema: ITextareaSchema = { + const textareaSchema: ITextareaSchema = { uiType: "textarea", - className: otherSchema.className ? `${otherSchema.className}-textarea` : undefined, + className: schema.className ? `${schema.className}-textarea` : undefined, ...textarea, }; - return showTextarea ? : <>; + return showTextarea ? : <>; }; return ( <> - + {renderChips()} {renderTextareaChip()} diff --git a/src/components/fields/contact-field/contact-field.tsx b/src/components/fields/contact-field/contact-field.tsx index 559ecb092..26b3abb7c 100644 --- a/src/components/fields/contact-field/contact-field.tsx +++ b/src/components/fields/contact-field/contact-field.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from "react"; import { useFormContext } from "react-hook-form"; import * as Yup from "yup"; import { IGenericFieldProps } from ".."; -import { TestHelper } from "../../../utils"; +import { TestHelper, filterSchemaProps } from "../../../utils"; import { usePrevious, useValidationConfig } from "../../../utils/hooks"; import { Warning } from "../../shared"; import { ERROR_MESSAGES } from "../../shared/error-messages"; @@ -16,18 +16,11 @@ export const ContactField = (props: IGenericFieldProps) => // ============================================================================= // CONST, STATE, REF // ============================================================================= + const { isDirty, formattedLabel, error, id, onBlur, onChange, schema, value, warning } = props; const { - isDirty, - formattedLabel, - error, - id, - name, - onChange, - schema: { defaultCountry, disabled, enableSearch, label: _label, placeholder, validation, ...otherSchema }, - value, - warning, - ...otherProps - } = props; + commonSchema: { validation }, + customSchema: { defaultCountry, disabled, enableSearch, placeholder, ...inputProps }, + } = filterSchemaProps(schema); const { resetField } = useFormContext(); const [stateValue, setStateValue] = useState(value || ""); @@ -190,8 +183,7 @@ export const ContactField = (props: IGenericFieldProps) => return ( <> ) => fixedCountry={fixedCountry} id={id} label={formattedLabel} - name={name} placeholder={getPlaceholderText()} value={formatDisplayValue()} + onBlur={onBlur} onChange={handleChange} /> diff --git a/src/components/fields/date-field/date-field.tsx b/src/components/fields/date-field/date-field.tsx index 793b7ecc9..a2b10786f 100644 --- a/src/components/fields/date-field/date-field.tsx +++ b/src/components/fields/date-field/date-field.tsx @@ -5,7 +5,7 @@ import { Form } from "@lifesg/react-design-system/form"; import { useEffect, useState } from "react"; import { useFormContext } from "react-hook-form"; import * as Yup from "yup"; -import { DateTimeHelper, TestHelper } from "../../../utils"; +import { DateTimeHelper, TestHelper, filterSchemaProps } from "../../../utils"; import { useValidationConfig } from "../../../utils/hooks"; import { ERROR_MESSAGES, Warning } from "../../shared"; import { IGenericFieldProps } from "../types"; @@ -20,17 +20,11 @@ export const DateField = (props: IGenericFieldProps) => { // ============================================================================= // CONST, STATE, REF // ============================================================================= + const { error, formattedLabel, id, isDirty, onBlur, onChange, schema, value, warning } = props; const { - error, - formattedLabel, - id, - isDirty, - onChange, - schema: { label: _label, useCurrentDate, dateFormat = DEFAULT_DATE_FORMAT, validation, ...otherSchema }, - value, - warning, - ...otherProps - } = props; + commonSchema: { validation }, + customSchema: { useCurrentDate, dateFormat = DEFAULT_DATE_FORMAT, ...inputProps }, + } = filterSchemaProps(schema); const { setValue } = useFormContext(); const [stateValue, setStateValue] = useState(value || ""); // always uuuu-MM-dd because it is passed to Form.DateInput const [derivedProps, setDerivedProps] = useState>(); @@ -243,13 +237,13 @@ export const DateField = (props: IGenericFieldProps) => { return ( <> diff --git a/src/components/fields/date-range-field/date-range-field.tsx b/src/components/fields/date-range-field/date-range-field.tsx index e02018aa7..c94def0c7 100644 --- a/src/components/fields/date-range-field/date-range-field.tsx +++ b/src/components/fields/date-range-field/date-range-field.tsx @@ -7,7 +7,7 @@ import { useEffect, useState } from "react"; import useDeepCompareEffect from "use-deep-compare-effect"; import * as Yup from "yup"; import { IGenericFieldProps } from ".."; -import { DateTimeHelper, TestHelper } from "../../../utils"; +import { DateTimeHelper, TestHelper, filterSchemaProps } from "../../../utils"; import { useValidationConfig } from "../../../utils/hooks"; import { ERROR_MESSAGES, Warning } from "../../shared"; import { IDateRangeFieldValidationRule, TDateRangeFieldSchema, TDateRangeInputType } from "./types"; @@ -26,12 +26,16 @@ export const DateRangeField = (props: IGenericFieldProps) formattedLabel, id, isDirty, + onBlur, onChange, - schema: { dateFormat = DEFAULT_DATE_FORMAT, label: _label, validation, variant, ...otherSchema }, + schema, value = { from: undefined, to: undefined }, warning, - ...otherProps } = props; + const { + commonSchema: { validation }, + customSchema: { dateFormat = DEFAULT_DATE_FORMAT, variant, ...inputProps }, + } = filterSchemaProps(schema); const [stateValue, setStateValue] = useState(value.from || ""); // always uuuu-MM-dd because it is passed to Form.DateInput const [stateValueEnd, setStateValueEnd] = useState(value.to || ""); // always uuuu-MM-dd because it is passed to Form.DateInput const [derivedProps, setDerivedProps] = useState(); @@ -367,13 +371,13 @@ export const DateRangeField = (props: IGenericFieldProps) return ( <> { const previousValue = usePrevious(value); const { setValue } = useFormContext(); const { dispatchFieldEvent } = useFieldEvent(); + const isMounted = useRef(false); const sessionId = useRef(); + const setFilesIfMounted: typeof setFiles = (value) => { + if (isMounted.current) { + setFiles(value); + } + }; // ============================================================================= // EFFECTS // ============================================================================= useEffect(() => { + isMounted.current = true; sessionId.current = generateRandomId(); + + return () => { + isMounted.current = false; + }; }, []); useEffect( @@ -123,7 +134,7 @@ const FileUploadManager = (props: IProps) => { // ============================================================================= const handleGenericError = (index: number) => { - setFiles((prev) => { + setFilesIfMounted((prev) => { const updatedFiles = [...prev]; const file = prev[index]; updatedFiles[index] = { @@ -233,7 +244,7 @@ const FileUploadManager = (props: IProps) => { // FILE STATUS HANDLERS // ============================================================================= const injectFile = async (fileToInject: IFile, index: number) => { - setFiles((prev) => { + setFilesIfMounted((prev) => { const updatedFiles = [...prev]; updatedFiles[index] = { ...prev[index], @@ -273,7 +284,7 @@ const FileUploadManager = (props: IProps) => { const thumbnailImageDataUrl = rawFile ? await generateThumbnail(fileToInject, fileType?.mime) : undefined; - setFiles((prev) => { + setFilesIfMounted((prev) => { const updatedFiles = [...prev]; updatedFiles[index] = { ...fileToInject, @@ -302,7 +313,7 @@ const FileUploadManager = (props: IProps) => { const dataURL = await FileHelper.fileToDataUrl(compressedFile.rawFile); const { errorMessage, fileType, status } = await readFile({ dataURL, ...compressedFile }); - setFiles((prev) => { + setFilesIfMounted((prev) => { const updatedFiles = [...prev]; updatedFiles[index] = { ...compressedFile, @@ -326,7 +337,7 @@ const FileUploadManager = (props: IProps) => { }; const uploadFile = async (fileToUpload: IFile, index: number) => { - setFiles((prev) => { + setFilesIfMounted((prev) => { const updatedFiles = [...prev]; updatedFiles[index] = { ...prev[index], @@ -353,7 +364,7 @@ const FileUploadManager = (props: IProps) => { }, onUploadProgress: (progressEvent) => { const { loaded, total } = progressEvent; - setFiles((prev) => { + setFilesIfMounted((prev) => { if (!prev[index]) return prev; const updatedFiles = [...prev]; updatedFiles[index] = { @@ -370,7 +381,7 @@ const FileUploadManager = (props: IProps) => { }); const thumbnailImageDataUrl = await generateThumbnail(fileToUpload); - setFiles((prev) => { + setFilesIfMounted((prev) => { if (!prev[index]) return prev; const updatedFiles = [...prev]; updatedFiles[index] = { @@ -397,7 +408,7 @@ const FileUploadManager = (props: IProps) => { }; const deleteFile = (index: number) => { - setFiles((prev) => prev.filter((_file, i) => i !== index)); + setFilesIfMounted((prev) => prev.filter((_file, i) => i !== index)); }; const compressImageFile = async (fileToCompress: IFile) => { diff --git a/src/components/fields/histogram-slider/histogram-slider.tsx b/src/components/fields/histogram-slider/histogram-slider.tsx index 570008f28..9768aa613 100644 --- a/src/components/fields/histogram-slider/histogram-slider.tsx +++ b/src/components/fields/histogram-slider/histogram-slider.tsx @@ -1,13 +1,13 @@ import { Form } from "@lifesg/react-design-system/form"; import isNil from "lodash/isNil"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useFormContext } from "react-hook-form"; import * as Yup from "yup"; import { IGenericFieldProps } from ".."; import { TestHelper } from "../../../utils"; import { useValidationConfig } from "../../../utils/hooks"; import { ERROR_MESSAGES, Warning } from "../../shared"; -import { IHistogramSliderSchema } from "./types"; +import { IHistogramSliderSchema, IHistogramSliderValue } from "./types"; export const HistogramSlider = (props: IGenericFieldProps) => { // ============================================================================= @@ -25,6 +25,13 @@ export const HistogramSlider = (props: IGenericFieldProps(undefined); const { setFieldValidationConfig } = useValidationConfig(); + const rangeMin = useMemo(() => Math.min(...bins.map((bin) => bin.minValue)), [bins]); + const rangeMax = useMemo(() => Math.max(...bins.map((bin) => bin.minValue)) + interval, [bins, interval]); + + const getDisplayValue = (value: IHistogramSliderValue | undefined): [number, number] => [ + typeof value?.from === "number" ? value.from : rangeMin, + typeof value?.to === "number" ? value.to : rangeMax, + ]; // ============================================================================= // EFFECTS @@ -32,13 +39,10 @@ export const HistogramSlider = (props: IGenericFieldProps { // prepopulate with full range selected if range is not selected if (!value) { - const min = Math.min(...bins.map((bin) => bin.minValue)); - const max = Math.max(...bins.map((bin) => bin.minValue)) + interval; - - resetField(id, { defaultValue: { from: min, to: max }, keepDirty: true }); - setStateValue([min, max]); + resetField(id, { defaultValue: { from: rangeMin, to: rangeMax }, keepDirty: true }); + setStateValue([rangeMin, rangeMax]); } else { - setStateValue([value.from, value.to]); + setStateValue(getDisplayValue(value)); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [value]); diff --git a/src/components/fields/image-upload/image-input/file-item/file-item.styles.ts b/src/components/fields/image-upload/image-input/file-item/file-item.styles.ts index 165cde4ac..f346202b1 100644 --- a/src/components/fields/image-upload/image-input/file-item/file-item.styles.ts +++ b/src/components/fields/image-upload/image-input/file-item/file-item.styles.ts @@ -3,13 +3,13 @@ import { Typography } from "@lifesg/react-design-system/typography"; import styled, { css } from "styled-components"; import { Border, Colour, Font, MediaQuery, Radius, Spacing } from "@lifesg/react-design-system/theme"; -export const Wrapper = styled.div<{ isError?: boolean; isCustomMuted?: boolean }>` +export const Wrapper = styled.div<{ $isError?: boolean; $isCustomMuted?: boolean }>` display: flex; - flex-wrap: ${(props) => (props.isCustomMuted ? "nowrap" : "wrap")}; + flex-wrap: ${(props) => (props.$isCustomMuted ? "nowrap" : "wrap")}; align-items: center; gap: ${Spacing["spacing-8"]}; border: ${(props) => - props.isError + props.$isError ? css` ${Border["width-010"]} ${Border.solid} ${Colour["border-error"]} ` @@ -19,7 +19,7 @@ export const Wrapper = styled.div<{ isError?: boolean; isCustomMuted?: boolean } border-radius: ${Radius.sm}; border-radius: ${Radius.sm}; background-color: ${(props) => - props.isError ? `${Colour["bg-error"](props)}` : `${Colour["bg-primary-subtlest"](props)}`}; + props.$isError ? `${Colour["bg-error"](props)}` : `${Colour["bg-primary-subtlest"](props)}`}; min-height: 3.5rem; margin-bottom: ${Spacing["spacing-16"]}; padding: ${Spacing["spacing-16"]} ${Spacing["spacing-32"]}; @@ -59,11 +59,11 @@ export const CellDeleteButton = styled.div` width: 19.15%; `; -export const Thumbnail = styled.div<{ src: string }>` +export const Thumbnail = styled.div<{ $src: string }>` margin-right: ${Spacing["spacing-32"]}; width: 6rem; height: 6rem; - background: url(${(props) => props.src}) no-repeat center / cover; + background: url(${(props) => props.$src}) no-repeat center / cover; overflow: hidden; border-radius: ${Radius.sm}; ${Font["body-sm-bold"]} diff --git a/src/components/fields/image-upload/image-input/file-item/file-item.tsx b/src/components/fields/image-upload/image-input/file-item/file-item.tsx index 95f5bf410..097525b2f 100644 --- a/src/components/fields/image-upload/image-input/file-item/file-item.tsx +++ b/src/components/fields/image-upload/image-input/file-item/file-item.tsx @@ -169,7 +169,7 @@ export const FileItem = ({ id = "file-item", index, fileItem, maxSizeInKb, accep <> @@ -195,7 +195,7 @@ export const FileItem = ({ id = "file-item", index, fileItem, maxSizeInKb, accep <> {status === EImageStatus.UPLOADED && !isError && ( @@ -215,8 +215,8 @@ export const FileItem = ({ id = "file-item", index, fileItem, maxSizeInKb, accep return ( diff --git a/src/components/fields/image-upload/image-review/image-editor/image-editor.styles.ts b/src/components/fields/image-upload/image-review/image-editor/image-editor.styles.ts index f7c17ce62..99bed2e0a 100644 --- a/src/components/fields/image-upload/image-review/image-editor/image-editor.styles.ts +++ b/src/components/fields/image-upload/image-review/image-editor/image-editor.styles.ts @@ -7,6 +7,6 @@ export const Wrapper = styled.div` touch-action: none; `; -export const Canvas = styled.canvas<{ canDraw: boolean }>` - ${({ canDraw }) => canDraw && "cursor: crosshair;"}; +export const Canvas = styled.canvas<{ $canDraw: boolean }>` + ${({ $canDraw }) => $canDraw && "cursor: crosshair;"}; `; diff --git a/src/components/fields/image-upload/image-review/image-editor/image-editor.tsx b/src/components/fields/image-upload/image-review/image-editor/image-editor.tsx index d4761325c..49cc34a5a 100644 --- a/src/components/fields/image-upload/image-review/image-editor/image-editor.tsx +++ b/src/components/fields/image-upload/image-review/image-editor/image-editor.tsx @@ -406,7 +406,7 @@ export const ImageEditor = forwardRef((props: IImageEditorProps, ref: ForwardedR return ( - + ); }); diff --git a/src/components/fields/image-upload/image-review/image-thumbnails/image-thumbnails.styles.ts b/src/components/fields/image-upload/image-review/image-thumbnails/image-thumbnails.styles.ts index 5fdabc5ff..344afa007 100644 --- a/src/components/fields/image-upload/image-review/image-thumbnails/image-thumbnails.styles.ts +++ b/src/components/fields/image-upload/image-review/image-thumbnails/image-thumbnails.styles.ts @@ -11,7 +11,7 @@ export const ThumbnailsWrapper = styled.div` max-height: 5rem; `; -export const ThumbnailItem = styled.button<{ src?: string; error?: boolean }>` +export const ThumbnailItem = styled.button<{ $src?: string; $error?: boolean }>` position: relative; cursor: pointer; width: 3rem; @@ -19,8 +19,8 @@ export const ThumbnailItem = styled.button<{ src?: string; error?: boolean }>` padding: 0; border: none; border-radius: ${Radius.xs}; - ${({ src }) => `background-image: url(${src});`} - background-color: ${({ error }) => error && "#eee"}; + ${({ $src }) => `background-image: url(${$src});`} + background-color: ${({ $error }) => $error && "#eee"}; background-position: center; background-size: cover; `; @@ -83,9 +83,9 @@ export const LoadingBox = styled.div` } `; -export const BorderOverlay = styled.div<{ isSelected: boolean }>` +export const BorderOverlay = styled.div<{ $isSelected: boolean }>` border: ${(props) => - props.isSelected + props.$isSelected ? css` ${Border.solid} ${Border["width-020"]} ` diff --git a/src/components/fields/image-upload/image-review/image-thumbnails/image-thumbnails.tsx b/src/components/fields/image-upload/image-review/image-thumbnails/image-thumbnails.tsx index ac1a9d57a..2be1fbff5 100644 --- a/src/components/fields/image-upload/image-review/image-thumbnails/image-thumbnails.tsx +++ b/src/components/fields/image-upload/image-review/image-thumbnails/image-thumbnails.tsx @@ -75,12 +75,12 @@ export const ImageThumbnails = (props: IProps) => { key={index} id={TestHelper.generateId(id, `item-${index + 1}`)} data-testid={TestHelper.generateId(id, `item-${index + 1}`)} - src={image.thumbnailDataURL || image.dataURL || ADD_PLACEHOLDER_ICON} + $src={image.thumbnailDataURL || image.dataURL || ADD_PLACEHOLDER_ICON} type="button" aria-label={`thumbnail of ${image.name}`} onClick={() => onClickThumbnail(index)} > - + ); } else if (image.addedFrom === "reviewModal" || image.status < EImageStatus.NONE) { @@ -92,9 +92,9 @@ export const ImageThumbnails = (props: IProps) => { type="button" aria-label={`error with ${image.name}`} onClick={() => onClickThumbnail(index)} - error + $error > - + ); diff --git a/src/components/fields/image-upload/image-upload.tsx b/src/components/fields/image-upload/image-upload.tsx index c0d401d10..915f1ff1c 100644 --- a/src/components/fields/image-upload/image-upload.tsx +++ b/src/components/fields/image-upload/image-upload.tsx @@ -43,11 +43,11 @@ export const ImageUploadInner = (props: IGenericFieldProps) tooltip, }, id, + error, isDirty, isTouched, value, warning, - ...otherProps } = props; const { images, setImages, currentFileIds } = useContext(ImageContext); const previousImages = usePrevious(images); @@ -288,7 +288,7 @@ export const ImageUploadInner = (props: IGenericFieldProps) maxFiles={maxFiles} maxSizeInKb={maxFileSize} dimensions={dimensions} - errorMessage={otherProps.error?.message} + errorMessage={error?.message} validation={validation} multiple={multiple} warning={warning} diff --git a/src/components/fields/location-field/location-modal/location-modal.tsx b/src/components/fields/location-field/location-modal/location-modal.tsx index 8f37f0207..b4e7f8a22 100644 --- a/src/components/fields/location-field/location-modal/location-modal.tsx +++ b/src/components/fields/location-field/location-modal/location-modal.tsx @@ -82,6 +82,7 @@ const LocationModal = ({ const [mapPickedLatLng, setMapPickedLatLng] = useState(); const [currentLocation, setCurrentLocation] = useState(); + const isMounted = useRef(true); const shouldCallGetSelectablePins = useRef(true); const theme = useContext(ThemeContext); @@ -131,6 +132,8 @@ const LocationModal = ({ }; const restoreFormvalues = useCallback(() => { + if (!isMounted.current) return; + // Retain current form values setSelectedAddressInfo(formValues || {}); }, [formValues]); @@ -150,6 +153,8 @@ const LocationModal = ({ }; const handleApiErrors = (error?: any) => { + if (!isMounted.current) return; + const handleError = (errorType: TErrorType["errorType"], defaultHandle: () => void) => { const shouldPreventDefault = !dispatchFieldEvent("error", id, { payload: { @@ -225,6 +230,12 @@ const LocationModal = ({ // ============================================================================= // EFFECTS // ============================================================================= + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + useEffect(() => { const handleError = (e: TLocationFieldEvents["error-end"]) => { const errorType = e.detail?.payload?.errorType; diff --git a/src/components/fields/location-field/location-modal/location-search/location-search.styles.ts b/src/components/fields/location-field/location-modal/location-search/location-search.styles.ts index 302edac3b..b50dc5e87 100644 --- a/src/components/fields/location-field/location-modal/location-search/location-search.styles.ts +++ b/src/components/fields/location-field/location-modal/location-search/location-search.styles.ts @@ -8,7 +8,7 @@ import { Typography } from "@lifesg/react-design-system/typography"; import { Button } from "@lifesg/react-design-system/button"; interface ISinglePanelStyle { - panelInputMode: TPanelInputMode; + $panelInputMode: TPanelInputMode; } export const SearchWrapper = styled.div` @@ -19,23 +19,23 @@ export const SearchWrapper = styled.div` ${MediaQuery.MaxWidth.lg}, (orientation: landscape) and (max-height: ${Breakpoint["sm-max"]}px) { flex: unset; - height: ${({ panelInputMode }) => (panelInputMode === "search" ? `100%` : `auto`)}; + height: ${({ $panelInputMode }) => ($panelInputMode === "search" ? `100%` : `auto`)}; padding: ${Spacing["spacing-24"]} ${Spacing["spacing-20"]} 0; } `; -export const SearchBarContainer = styled.div<{ hasScrolled?: boolean }>` +export const SearchBarContainer = styled.div<{ $hasScrolled?: boolean }>` position: relative; display: flex; gap: ${Spacing["spacing-8"]}; padding-bottom: ${Spacing["spacing-8"]}; - alight-items: center; + align-items: center; justify-content: space-between; border-bottom: ${Border["width-010"]} ${Border.solid} ${Colour.border}; clip-path: inset(0 0 -0.3rem 0); transition: box-shadow ${Motion["duration-250"]} ${Motion["ease-default"]}; - ${({ hasScrolled }) => (hasScrolled ? `box-shadow: 0 0.06rem 0.4rem rgba(0,0,0,.12);` : "")} + ${({ $hasScrolled }) => ($hasScrolled ? `box-shadow: 0 0.06rem 0.4rem rgba(0,0,0,.12);` : "")} &:focus-within { border-bottom: ${Border["width-010"]} ${Border.solid} ${Colour["border-focus"]}; @@ -109,7 +109,7 @@ export const ResultWrapper = styled.div` border-bottom: ${Border["width-010"]} ${Border.solid} ${Colour.border}; ${MediaQuery.MaxWidth.lg}, (orientation: landscape) and (max-height: ${Breakpoint["sm-max"]}px) { - display: ${({ panelInputMode }) => (panelInputMode !== "map" ? `block` : `none`)}; + display: ${({ $panelInputMode }) => ($panelInputMode !== "map" ? `block` : `none`)}; border-bottom: 0; } `; @@ -127,7 +127,7 @@ export const NoResultTitle = styled(Typography.BodyMD)` overflow-y: scroll; `; -export const ResultItem = styled.div<{ active?: boolean }>` +export const ResultItem = styled.div<{ $active?: boolean }>` display: flex; align-items: center; gap: ${Spacing["spacing-16"]}; @@ -135,7 +135,7 @@ export const ResultItem = styled.div<{ active?: boolean }>` border-bottom: ${Border["width-010"]} ${Border.solid} ${Colour.border}; text-transform: uppercase; cursor: pointer; - background-color: ${({ active }) => (active ? Colour["bg-selected"] : `transparent`)}; + background-color: ${({ $active }) => ($active ? Colour["bg-selected"] : `transparent`)}; .keyword { font-weight: ${Font.Spec["weight-semibold"]}; @@ -155,7 +155,7 @@ export const ButtonWrapper = styled.div` padding-top: ${Spacing["spacing-16"]}; ${MediaQuery.MaxWidth.lg}, (orientation: landscape) and (max-height: ${Breakpoint["sm-max"]}px) { - display: ${({ panelInputMode }) => (panelInputMode === "map" ? `block` : `none`)}; + display: ${({ $panelInputMode }) => ($panelInputMode === "map" ? `block` : `none`)}; position: absolute; left: 0; bottom: 0; @@ -164,12 +164,12 @@ export const ButtonWrapper = styled.div` } `; -export const ButtonItem = styled(Button.Default)<{ buttonType: "cancel" | "confirm" }>` +export const ButtonItem = styled(Button.Default)<{ $buttonType: "cancel" | "confirm" }>` width: 9.5rem; ${MediaQuery.MaxWidth.lg}, (orientation: landscape) and (max-height: ${Breakpoint["sm-max"]}px) { - ${({ buttonType }) => buttonType === "cancel" && `display: none`} - ${({ buttonType }) => buttonType === "confirm" && `width: 100%`} + ${({ $buttonType }) => $buttonType === "cancel" && `display: none`} + ${({ $buttonType }) => $buttonType === "confirm" && `width: 100%`} } `; diff --git a/src/components/fields/location-field/location-modal/location-search/location-search.tsx b/src/components/fields/location-field/location-modal/location-search/location-search.tsx index 35285e1f4..7b2e278a3 100644 --- a/src/components/fields/location-field/location-modal/location-search/location-search.tsx +++ b/src/components/fields/location-field/location-modal/location-search/location-search.tsx @@ -90,6 +90,7 @@ export const LocationSearch = ({ const inputRef = useRef(null); const resultRef = useRef(null); + const isMounted = useRef(true); const reverseGeocodeAborter = useRef(null); const [hasScrolled, setHasScrolled] = useState(false); @@ -128,6 +129,13 @@ export const LocationSearch = ({ // ============================================================================= // EFFECTS // ============================================================================= + useEffect(() => { + return () => { + isMounted.current = false; + reverseGeocodeAborter.current?.abort(); + }; + }, []); + // check if any of the services is working useEffect(() => { if (!showLocationModal || !isRecaptchaReady) return; @@ -323,6 +331,8 @@ export const LocationSearch = ({ } }, (error) => { + if (!isMounted.current) return; + if (error instanceof SyntaxError || error instanceof TypeError) { populateDisplayList({ results: [], queryString }); } else { @@ -440,6 +450,8 @@ export const LocationSearch = ({ }; const resetResultsList = () => { + if (!isMounted.current) return; + setSelectedIndex(-1); setCurrentPaginationPageNum(1); setTotalNumPages(0); @@ -494,6 +506,8 @@ export const LocationSearch = ({ setAPIPageNum(res.apiPageNum); }, (error) => { + if (!isMounted.current) return; + resetResultsList(); handleApiErrors(new OneMapError(error)); }, @@ -547,6 +561,8 @@ export const LocationSearch = ({ return; } + if (!isMounted.current) return; + if (resultListItem.length === 0) { setQueryString(""); const shouldPanToCurrentLocation = @@ -596,6 +612,8 @@ export const LocationSearch = ({ recaptchaToken, mapApiHeaders ); + if (!isMounted.current) return; + locationFieldValue.x = X; locationFieldValue.y = Y; } @@ -608,6 +626,8 @@ export const LocationSearch = ({ * Stores proper state */ const populateDisplayList = (params: IDisplayResultListParams) => { + if (!isMounted.current) return; + const { results, boldResults, apiPageNum, totalNumPages, queryString } = params; let displayResults = results; @@ -642,7 +662,7 @@ export const LocationSearch = ({ handleClickResult(item, index)} - active={selectedIndex === index} + $active={selectedIndex === index} id={TestHelper.generateId(`location-search-modal-search-result-${index + 0}`)} data-testid={TestHelper.generateId( `location-search-modal-search-result-${index + 0}`, @@ -687,7 +707,7 @@ export const LocationSearch = ({ id={TestHelper.generateId(id, "location-search")} data-testid={TestHelper.generateId(id, "location-search")} className={`${className}-location-search`} - panelInputMode={panelInputMode} + $panelInputMode={panelInputMode} > - + @@ -754,15 +774,15 @@ export const LocationSearch = ({ - + Cancel diff --git a/src/components/fields/masked-field/masked-field.tsx b/src/components/fields/masked-field/masked-field.tsx index 4d325c734..9ecff063a 100644 --- a/src/components/fields/masked-field/masked-field.tsx +++ b/src/components/fields/masked-field/masked-field.tsx @@ -17,11 +17,11 @@ export const MaskedField = (props: IGenericFieldProps) => { error, formattedLabel, id, + onBlur, onChange, value, schema: { label: _label, uiType, validation, maskRegex, iconMask, iconUnmask, ...otherSchema }, warning, - ...otherProps } = props; const [stateValue, setStateValue] = useState(value || ""); @@ -87,11 +87,11 @@ export const MaskedField = (props: IGenericFieldProps) => { <> ) => { // ============================================================================= // CONST, STATE, REFS // ============================================================================= + const { error, formattedLabel, id, onBlur, onChange, schema, value, warning } = props; const { - error, - formattedLabel, - id, - onChange, - schema: { label: _label, options = [], validation, ...otherSchema }, - value, - warning, - ...otherProps - } = props; + commonSchema: { validation }, + customSchema: { options = [], ...selectProps }, + } = filterSchemaProps(schema); const { setValue } = useFormContext(); const [stateValue, setStateValue] = useState(value || []); @@ -81,12 +76,12 @@ export const MultiSelect = (props: IGenericFieldProps) => { return ( <> item.value} diff --git a/src/components/fields/nested-multi-select/nested-multi-select.tsx b/src/components/fields/nested-multi-select/nested-multi-select.tsx index 24286aea4..4c65292a7 100644 --- a/src/components/fields/nested-multi-select/nested-multi-select.tsx +++ b/src/components/fields/nested-multi-select/nested-multi-select.tsx @@ -23,7 +23,6 @@ export const NestedMultiSelect = (props: IGenericFieldProps (value || ""); @@ -93,7 +89,7 @@ export const RadioButtonGroup = (props: IGenericFieldProps ) => { error, formattedLabel, id, + onBlur, onChange, - schema: { label: _label, options, validation, ...otherSchema }, + schema, value = { from: undefined, to: undefined }, warning, - ...otherProps } = props; + const { + commonSchema: { validation }, + customSchema: { options, ...rangeSelectProps }, + } = filterSchemaProps(schema); const { setValue } = useFormContext(); const [toStateValue, setToStateValue] = useState(value.from || ""); @@ -113,10 +117,10 @@ export const RangeSelect = (props: IGenericFieldProps) => { return ( <> ) => { // ============================================================================= // CONST, STATE, REF // ============================================================================= + const { id, schema } = props; const { - id, - schema: { disabled, ignoreDefaultValues, label, ...otherSchema }, - ...otherProps - } = props; + commonSchema: { label }, + customSchema: { disabled, ignoreDefaultValues, ...buttonProps }, + } = filterSchemaProps(schema); const { reset, getValues } = useFormContext(); const { resetFields } = useFormValues(); @@ -51,15 +52,7 @@ export const ResetButton = (props: IGenericFieldProps) => { // RENDER FUNCTIONS // ============================================================================= return ( - + {label} ); diff --git a/src/components/fields/select-histogram/select-histogram.tsx b/src/components/fields/select-histogram/select-histogram.tsx index 43a11507a..c522bf25a 100644 --- a/src/components/fields/select-histogram/select-histogram.tsx +++ b/src/components/fields/select-histogram/select-histogram.tsx @@ -17,11 +17,11 @@ export const SelectHistogram = (props: IGenericFieldProps ) => { // ============================================================================= // CONST, STATE, REFS // ============================================================================= + const { error, formattedLabel, id, onBlur, onChange, schema, value, warning } = props; const { - error, - formattedLabel, - id, - onChange, - schema: { label: _label, options, validation, ...otherSchema }, - value, - warning, - ...otherProps - } = props; + commonSchema: { validation }, + customSchema: { options, ...selectProps }, + } = filterSchemaProps(schema); const { setValue } = useFormContext(); const [stateValue, setStateValue] = useState(value || ""); @@ -64,13 +59,13 @@ export const Select = (props: IGenericFieldProps) => { return ( <> item.label} diff --git a/src/components/fields/submit-button/submit-button.tsx b/src/components/fields/submit-button/submit-button.tsx index 647a9f011..fd16469dc 100644 --- a/src/components/fields/submit-button/submit-button.tsx +++ b/src/components/fields/submit-button/submit-button.tsx @@ -5,17 +5,18 @@ import useDeepCompareEffect from "use-deep-compare-effect"; import * as Yup from "yup"; import { IGenericFieldProps } from ".."; import { useFrontendEngineForm, useValidationConfig, useValidationSchema } from "../../../utils/hooks"; +import { filterSchemaProps } from "../../../utils/prop-helper"; import { ISubmitButtonSchema } from "./types"; export const SubmitButton = (props: IGenericFieldProps) => { // ============================================================================= // CONST, STATE, REF // ============================================================================= + const { id, schema } = props; const { - id, - schema: { disabled, label, ...otherSchema }, - ...otherProps - } = props; + commonSchema: { label }, + customSchema: { disabled, ...buttonProps }, + } = filterSchemaProps(schema); const { submitHandler, wrapInForm } = useFrontendEngineForm(); const { setFieldValidationConfig } = useValidationConfig(); const { hardValidationSchema } = useValidationSchema(); @@ -63,8 +64,7 @@ export const SubmitButton = (props: IGenericFieldProps) => // ============================================================================= return ( ) => { // ============================================================================= // CONST, STATE, REFS // ============================================================================= + const { error, formattedLabel, id, onChange, schema, value, warning } = props; const { - error, - formattedLabel, - id, - onChange, - schema: { className, customOptions, disabled, label, validation, ...otherSchema }, - value, - warning, - } = props; + commonSchema: { customOptions, label, validation }, + customSchema: { className, disabled, ...toggleProps }, + } = filterSchemaProps(schema); const [stateValue, setStateValue] = useState(value || undefined); const { setFieldValidationConfig } = useValidationConfig(); @@ -69,7 +65,7 @@ export const Switch = (props: IGenericFieldProps) => { aria-label={typeof label === "string" ? label : sanitize(label.mainLabel, { allowedTags: [] })} > ) => { Yes (value || ""); + const [stateValue, setStateValue] = useState(value ?? ""); const [derivedAttributes, setDerivedAttributes] = useState({}); const { setFieldValidationConfig } = useValidationConfig(); @@ -86,7 +89,7 @@ export const TextField = (props: IGenericFieldProps { if (value !== stateValue) { - setStateValue(value || ""); + setStateValue(value ?? ""); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [value]); @@ -118,7 +121,7 @@ export const TextField = (props: IGenericFieldProps (customOptions?.preventDragAndDrop ? e.preventDefault() : null)} inputMode={formatInputMode()} diff --git a/src/components/fields/textarea/textarea.styles.tsx b/src/components/fields/textarea/textarea.styles.tsx index 77d642191..a6c50c8fe 100644 --- a/src/components/fields/textarea/textarea.styles.tsx +++ b/src/components/fields/textarea/textarea.styles.tsx @@ -3,15 +3,15 @@ import { Form } from "@lifesg/react-design-system/form"; import styled, { css } from "styled-components"; interface ITextareaProps extends React.TextareaHTMLAttributes { - resizable?: boolean | undefined; + $resizable?: boolean | undefined; } // ============================================================================= // STYLING // ============================================================================= -export const Wrapper = styled.div<{ chipPosition?: "top" | "bottom" | undefined }>` +export const Wrapper = styled.div<{ $chipPosition?: "top" | "bottom" | undefined }>` display: flex; - flex-direction: ${({ chipPosition }) => (chipPosition !== "bottom" ? "column" : "column-reverse")}; + flex-direction: ${({ $chipPosition }) => ($chipPosition !== "bottom" ? "column" : "column-reverse")}; `; export const ChipContainer = styled.div<{ $chipPosition?: "top" | "bottom" | undefined }>` @@ -33,7 +33,7 @@ export const StyledTextarea = styled(Form.Textarea)` width: auto; ${(props) => - !props.resizable + !props.$resizable ? css` resize: none; ` diff --git a/src/components/fields/textarea/textarea.tsx b/src/components/fields/textarea/textarea.tsx index 63a81067d..856d2e41a 100644 --- a/src/components/fields/textarea/textarea.tsx +++ b/src/components/fields/textarea/textarea.tsx @@ -3,7 +3,7 @@ import { kebabCase } from "lodash"; import React, { useEffect, useRef, useState } from "react"; import { useFormContext } from "react-hook-form"; import * as Yup from "yup"; -import { TestHelper } from "../../../utils"; +import { TestHelper, filterSchemaProps } from "../../../utils"; import { useValidationConfig } from "../../../utils/hooks"; import { Chip, Warning } from "../../shared"; import { IGenericFieldProps } from "../types"; @@ -14,18 +14,11 @@ export const Textarea = (props: IGenericFieldProps) => { // ============================================================================= // CONST, STATE, REF // ============================================================================= + const { error, formattedLabel, id, onChange, schema, value, warning, onBlur } = props; const { - error, - formattedLabel, - id, - name, - onChange, - schema: { className, chipTexts, chipPosition, rows = 1, resizable, label: _label, validation, ...otherSchema }, - value, - warning, - onBlur, - ...otherProps - } = props; + commonSchema: { validation }, + customSchema: { className, chipTexts, chipPosition, rows = 1, resizable, ...textareaProps }, + } = filterSchemaProps(schema); const { setValue } = useFormContext(); const [stateValue, setStateValue] = useState(value || ""); const [maxLength, setMaxLength] = useState(); @@ -113,19 +106,17 @@ export const Textarea = (props: IGenericFieldProps) => { return ( <> - + {renderChips()} ) => { // ============================================================================= // CONST, STATE, REFS // ============================================================================= + const { error, formattedLabel, id, onBlur, onChange, schema, value, warning } = props; const { - error, - formattedLabel, - id, - onChange, - schema: { is24HourFormat, label: _label, placeholder, useCurrentTime, validation, ...otherSchema }, - value, - warning, - ...otherProps - } = props; + commonSchema: { validation }, + customSchema: { is24HourFormat, placeholder, useCurrentTime, ...timepickerProps }, + } = filterSchemaProps(schema); const [stateValue, setStateValue] = useState(value || ""); const { setFieldValidationConfig } = useValidationConfig(); @@ -68,8 +63,7 @@ export const TimeField = (props: IGenericFieldProps) => { return ( <> ) => { value={stateValue} placeholder={placeholder} format={is24HourFormat ? "24hr" : "12hr"} + onBlur={onBlur} onChange={handleChange} /> diff --git a/src/components/fields/unit-number-field/unit-number-field.tsx b/src/components/fields/unit-number-field/unit-number-field.tsx index 489c1a356..18bc31306 100644 --- a/src/components/fields/unit-number-field/unit-number-field.tsx +++ b/src/components/fields/unit-number-field/unit-number-field.tsx @@ -2,7 +2,7 @@ import { Form } from "@lifesg/react-design-system/form"; import { useEffect, useState } from "react"; import * as Yup from "yup"; import { IGenericFieldProps } from ".."; -import { TestHelper } from "../../../utils"; +import { TestHelper, filterSchemaProps } from "../../../utils"; import { useValidationConfig } from "../../../utils/hooks"; import { ERROR_MESSAGES, Warning } from "../../shared"; import { IUnitNumberFieldSchema } from "./types"; @@ -11,16 +11,11 @@ export const UnitNumberField = (props: IGenericFieldProps(value || ""); const { setFieldValidationConfig } = useValidationConfig(); @@ -58,12 +53,12 @@ export const UnitNumberField = (props: IGenericFieldProps diff --git a/src/components/shared/chip/chip.styles.ts b/src/components/shared/chip/chip.styles.ts index 7bfd7c4b0..40d87c771 100644 --- a/src/components/shared/chip/chip.styles.ts +++ b/src/components/shared/chip/chip.styles.ts @@ -1,12 +1,11 @@ import { Border, Colour } from "@lifesg/react-design-system/theme"; import { Typography } from "@lifesg/react-design-system/typography"; import styled, { css } from "styled-components"; -import { IChipButtonProps } from "./types"; // ============================================================================= // STYLING // ============================================================================= -export const ChipButton = styled.button` +export const ChipButton = styled.button<{ $isActive?: boolean }>` background-color: ${Colour.bg}; border: ${Border["width-010"]} ${Border.solid} ${Colour.border}; border-radius: 1rem; @@ -25,7 +24,7 @@ export const ChipButton = styled.button` } ${(props) => { - if (props.isActive) { + if (props.$isActive) { return css` background-color: ${Colour["bg-inverse-subtlest"](props)}; diff --git a/src/components/shared/chip/chip.tsx b/src/components/shared/chip/chip.tsx index c97df654d..e01f0468e 100644 --- a/src/components/shared/chip/chip.tsx +++ b/src/components/shared/chip/chip.tsx @@ -5,8 +5,8 @@ import { IChipButtonProps } from "./types"; interface IProps extends React.ButtonHTMLAttributes, IChipButtonProps {} -export const Chip = ({ children, ...otherProps }: IProps) => ( - +export const Chip = ({ children, isActive, ...otherProps }: IProps) => ( + {children} ); diff --git a/src/utils/index.ts b/src/utils/index.ts index 5cf449ad1..89bd1b231 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -8,3 +8,4 @@ export * from "./math-helper"; export * from "./object-helper"; export * from "./test-helper"; export * from "./types"; +export * from "./prop-helper"; diff --git a/src/utils/prop-helper.ts b/src/utils/prop-helper.ts new file mode 100644 index 000000000..6b06e5ef2 --- /dev/null +++ b/src/utils/prop-helper.ts @@ -0,0 +1,26 @@ +import omit from "lodash/omit"; + +const COMMON_SCHEMA_PROP_KEYS = [ + "columns", + "customOptions", + "label", + "referenceKey", + "showIf", + "uiType", + "validation", +] as const; + +type CommonSchemaPropKey = (typeof COMMON_SCHEMA_PROP_KEYS)[number]; +type ExistingCommonSchemaPropKey = Extract; + +export const filterSchemaProps = (schema: T) => { + const commonKeys = COMMON_SCHEMA_PROP_KEYS.filter((key) => key in schema) as ExistingCommonSchemaPropKey[]; + + return { + commonSchema: Object.fromEntries(commonKeys.map((key) => [key, schema[key]])) as Pick< + T, + ExistingCommonSchemaPropKey + >, + customSchema: omit(schema, commonKeys) as Omit>, + }; +};