diff --git a/package-lock.json b/package-lock.json index 883697c66..40ecac463 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@lifesg/web-frontend-engine", - "version": "2.2.1", + "version": "2.2.2", "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -96,7 +96,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": { @@ -2186,27 +2186,18 @@ } }, "node_modules/@emotion/is-prop-valid": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", - "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", - "dev": true, + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", "license": "MIT", "dependencies": { - "@emotion/memoize": "^0.8.1" + "@emotion/memoize": "^0.9.0" } }, "node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@emotion/unitless": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", - "dev": true, + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", "license": "MIT" }, "node_modules/@erase2d/fabric": { @@ -5147,18 +5138,6 @@ "node": ">=12" } }, - "node_modules/@testing-library/react/node_modules/@types/react": { - "version": "17.0.88", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.88.tgz", - "integrity": "sha512-HEOvpzcFWkEcHq4EsTChnpimRc3Lz1/qzYRDFtobFp4obVa6QVjCDMjWmkgxgaTYttNvyjnldY8MUflGp5YiUw==", - "dev": true, - "peer": true, - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "^0.16", - "csstype": "^3.0.2" - } - }, "node_modules/@testing-library/react/node_modules/@types/react-dom": { "version": "17.0.26", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.26.tgz", @@ -5501,13 +5480,6 @@ "htmlparser2": "^8.0.0" } }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "dev": true, - "peer": true - }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -5522,13 +5494,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/stylis": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", - "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.9", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", @@ -7933,9 +7898,9 @@ "license": "MIT" }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "dev": true, "license": "MIT" }, @@ -15321,6 +15286,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", @@ -16345,13 +16311,6 @@ "node": ">= 0.4" } }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", - "dev": true, - "license": "MIT" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -16960,21 +16919,16 @@ } }, "node_modules/styled-components": { - "version": "6.1.19", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz", - "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.4.1.tgz", + "integrity": "sha512-ADu2dF53esUzzM4I0ewxhxFtsDd6v4V6dNkg3vG0iFKhnt06sJneTZnRvujAosZwW0XD58IKgGMQoqri4wHRqg==", "dev": true, "license": "MIT", "dependencies": { - "@emotion/is-prop-valid": "1.2.2", - "@emotion/unitless": "0.8.1", - "@types/stylis": "4.2.5", + "@emotion/is-prop-valid": "1.4.0", "css-to-react-native": "3.2.0", - "csstype": "3.1.3", - "postcss": "8.4.49", - "shallowequal": "1.1.0", - "stylis": "4.3.2", - "tslib": "2.6.2" + "csstype": "3.2.3", + "stylis": "4.3.6" }, "engines": { "node": ">= 16" @@ -16984,46 +16938,23 @@ "url": "https://opencollective.com/styled-components" }, "peerDependencies": { + "css-to-react-native": ">= 3.2.0", "react": ">= 16.8.0", - "react-dom": ">= 16.8.0" - } - }, - "node_modules/styled-components/node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "react-dom": ">= 16.8.0", + "react-native": ">= 0.68.0" + }, + "peerDependenciesMeta": { + "css-to-react-native": { + "optional": true }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" + "react-dom": { + "optional": true }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" + "react-native": { + "optional": true } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" } }, - "node_modules/styled-components/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true, - "license": "0BSD" - }, "node_modules/stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -17056,9 +16987,9 @@ } }, "node_modules/stylis": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", - "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index c77e7d8b5..2f420ef89 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,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/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/elements/wrapper/field-wrapper.tsx b/src/components/elements/wrapper/field-wrapper.tsx index 5c397ce87..9e8be7c91 100644 --- a/src/components/elements/wrapper/field-wrapper.tsx +++ b/src/components/elements/wrapper/field-wrapper.tsx @@ -27,6 +27,17 @@ interface IProps { warning?: string | undefined; } +type TForwardedFieldStateProp = keyof Pick; + +const FIELD_STATE_PROPS_BY_UI_TYPE: Partial> = + { + "contact-field": ["isDirty"], + "date-field": ["isDirty"], + "date-range-field": ["isDirty"], + "file-upload": ["isDirty", "isTouched"], + "image-upload": ["isDirty", "isTouched"], + }; + export const FieldWrapper = ({ Field, id, schema, warning }: IProps) => { // ========================================================================= // CONST, STATE, REFS @@ -129,12 +140,21 @@ export const FieldWrapper = ({ Field, id, schema, warning }: IProps) => { value: getField(id), warning, }; - return ; + return ; }; return ; }; +const getFieldStateProps = (schema: TFrontendEngineFieldSchema, fieldState: ControllerFieldState) => { + const forwardedProps = FIELD_STATE_PROPS_BY_UI_TYPE[schema.uiType] ?? []; + + return { + error: fieldState.error, + ...Object.fromEntries(forwardedProps.map((prop) => [prop, fieldState[prop]])), + }; +}; + const StyledSublabel = styled(Sanitize)` &.sub-label { display: block; 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, ...otherProps } = 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,12 +172,12 @@ 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 ( diff --git a/src/components/fields/contact-field/contact-field.tsx b/src/components/fields/contact-field/contact-field.tsx index 559ecb092..618b385fb 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, name, onChange, schema, value, warning, ...otherProps } = 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,7 +183,7 @@ export const ContactField = (props: IGenericFieldProps) => return ( <> ) => { // ============================================================================= // CONST, STATE, REF // ============================================================================= + const { error, formattedLabel, id, isDirty, onChange, schema, value, warning, ...otherProps } = 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,7 +237,7 @@ export const DateField = (props: IGenericFieldProps) => { return ( <> ) id, isDirty, 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,7 +371,7 @@ export const DateRangeField = (props: IGenericFieldProps) return ( <> ) => { // ============================================================================= @@ -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 6327ddabb..b45cbc3d2 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 @@ -379,7 +379,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/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 d4519c00c..9863f9544 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) return; @@ -321,6 +329,8 @@ export const LocationSearch = ({ } }, (error) => { + if (!isMounted.current) return; + if (error instanceof SyntaxError || error instanceof TypeError) { populateDisplayList({ results: [], queryString }); } else { @@ -438,6 +448,8 @@ export const LocationSearch = ({ }; const resetResultsList = () => { + if (!isMounted.current) return; + setSelectedIndex(-1); setCurrentPaginationPageNum(1); setTotalNumPages(0); @@ -488,6 +500,8 @@ export const LocationSearch = ({ setAPIPageNum(res.apiPageNum); }, (error) => { + if (!isMounted.current) return; + resetResultsList(); handleApiErrors(new OneMapError(error)); }, @@ -541,6 +555,8 @@ export const LocationSearch = ({ return; } + if (!isMounted.current) return; + if (resultListItem.length === 0) { setQueryString(""); const shouldPanToCurrentLocation = @@ -590,6 +606,8 @@ export const LocationSearch = ({ recaptchaToken, mapApiHeaders ); + if (!isMounted.current) return; + locationFieldValue.x = X; locationFieldValue.y = Y; } @@ -602,6 +620,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; @@ -636,7 +656,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}`, @@ -681,7 +701,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} > - + @@ -748,15 +768,15 @@ export const LocationSearch = ({ - + Cancel diff --git a/src/components/fields/multi-select/multi-select.tsx b/src/components/fields/multi-select/multi-select.tsx index 70e8cadcf..1f3bbbd88 100644 --- a/src/components/fields/multi-select/multi-select.tsx +++ b/src/components/fields/multi-select/multi-select.tsx @@ -4,7 +4,7 @@ import { useFormContext } from "react-hook-form"; import { useDeepCompareEffectNoCheck } from "use-deep-compare-effect"; 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 { ISelectOption } from "../select/types"; @@ -14,16 +14,11 @@ export const MultiSelect = (props: IGenericFieldProps) => { // ============================================================================= // CONST, STATE, REFS // ============================================================================= + const { error, formattedLabel, id, onChange, schema, value, warning, ...otherProps } = 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,7 +76,7 @@ export const MultiSelect = (props: IGenericFieldProps) => { return ( <> (value || ""); @@ -93,7 +89,7 @@ export const RadioButtonGroup = (props: IGenericFieldProps ) => { formattedLabel, id, 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,7 +117,7 @@ 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/select.tsx b/src/components/fields/select/select.tsx index 249238e93..7e5a0f68e 100644 --- a/src/components/fields/select/select.tsx +++ b/src/components/fields/select/select.tsx @@ -4,7 +4,7 @@ import { useFormContext } from "react-hook-form"; import useDeepCompareEffect from "use-deep-compare-effect"; import * as Yup from "yup"; import { IGenericFieldProps } from ".."; -import { TestHelper } from "../../../utils"; +import { TestHelper, filterSchemaProps } from "../../../utils"; import { useValidationConfig } from "../../../utils/hooks"; import { Warning } from "../../shared"; import { ISelectOption, ISelectSchema } from "./types"; @@ -13,16 +13,11 @@ export const Select = (props: IGenericFieldProps) => { // ============================================================================= // CONST, STATE, REFS // ============================================================================= + const { error, formattedLabel, id, onChange, schema, value, warning, ...otherProps } = 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,7 +59,7 @@ export const Select = (props: IGenericFieldProps) => { return ( <> ) => { // ============================================================================= // 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..acce2f7b2 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, name, onChange, schema, value, warning, onBlur, ...otherProps } = 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,11 +106,11 @@ export const Textarea = (props: IGenericFieldProps) => { return ( <> - + {renderChips()} ) => { name={name} maxLength={maxLength} rows={rows} - resizable={resizable} + $resizable={resizable} onChange={handleChange} value={stateValue} errorMessage={error?.message} diff --git a/src/components/fields/time-field/time-field.tsx b/src/components/fields/time-field/time-field.tsx index e5f5e1acc..df28ffcbd 100644 --- a/src/components/fields/time-field/time-field.tsx +++ b/src/components/fields/time-field/time-field.tsx @@ -3,7 +3,7 @@ import { Form } from "@lifesg/react-design-system/form"; import { useEffect, useState } from "react"; 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 { Warning } from "../../shared"; import { ITimeFieldSchema } from "./types"; @@ -12,16 +12,11 @@ export const TimeField = (props: IGenericFieldProps) => { // ============================================================================= // CONST, STATE, REFS // ============================================================================= + const { error, formattedLabel, id, onChange, schema, value, warning, ...otherProps } = 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,7 +63,7 @@ export const TimeField = (props: IGenericFieldProps) => { return ( <> (value || ""); const { setFieldValidationConfig } = useValidationConfig(); @@ -58,7 +53,7 @@ export const UnitNumberField = (props: IGenericFieldProps ` +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>, + }; +};