@lifesg/react-design-system 4.0.0-alpha.7 → 4.0.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/accordion/accordion-item.js.map +1 -1
- package/accordion/accordion.js.map +1 -1
- package/alert/alert.js.map +1 -1
- package/button/button.js +1 -1
- package/button/button.js.map +1 -1
- package/button/button.styles.d.ts +1 -0
- package/button/button.styles.js +1 -1
- package/button/button.styles.js.map +1 -1
- package/button/button.styles_19i4dfq.css +21 -0
- package/cjs/accordion/accordion-item.js.map +1 -1
- package/cjs/accordion/accordion.js.map +1 -1
- package/cjs/alert/alert.js.map +1 -1
- package/cjs/button/button.js +1 -1
- package/cjs/button/button.js.map +1 -1
- package/cjs/button/button.styles.js +1 -1
- package/cjs/button/button.styles.js.map +1 -1
- package/cjs/button/button.styles_19i4dfq.css +21 -0
- package/cjs/countdown-timer/countdown-timer.js.map +1 -1
- package/cjs/data-table/data-table.js +1 -1
- package/cjs/data-table/data-table.js.map +1 -1
- package/cjs/data-table/data-table.styles.js +1 -1
- package/cjs/data-table/data-table.styles.js.map +1 -1
- package/{data-table/data-table.styles_rxganh.css → cjs/data-table/data-table.styles_1tnlmhe.css} +7 -6
- package/cjs/date-range-input/date-range-input.js.map +1 -1
- package/cjs/e-signature/e-signature-canvas.js.map +1 -1
- package/cjs/error-display/error-display-data.js +1 -1
- package/cjs/error-display/error-display-data.js.map +1 -1
- package/cjs/error-display/inactivity-timer.js.map +1 -1
- package/cjs/external/dompurify/dist/purify.es.js +2 -2
- package/cjs/external/dompurify/dist/purify.es.js.map +1 -1
- package/cjs/file-download/file-list-card/file-list-card.js.map +1 -1
- package/cjs/file-upload/custom-sensors.js.map +1 -1
- package/cjs/file-upload/file-list/file-list.js.map +1 -1
- package/cjs/filter/addons/filter-item-checkbox-helpers.js.map +1 -1
- package/cjs/filter/addons/filter-item-checkbox.js.map +1 -1
- package/cjs/filter/addons/filter-item-checkbox.styles.js +1 -1
- package/cjs/filter/addons/filter-item-checkbox.styles.js.map +1 -1
- package/{filter/addons/filter-item-checkbox.styles_7degw7.css → cjs/filter/addons/filter-item-checkbox.styles_6nbpdl.css} +1 -1
- package/cjs/filter/filter-item.js.map +1 -1
- package/cjs/filter/filter-item.styles.js +1 -1
- package/cjs/filter/filter-item.styles.js.map +1 -1
- package/cjs/filter/{filter-item.styles_cphjnj.css → filter-item.styles_1yixp8h.css} +1 -1
- package/cjs/filter/filter-modal.styles.js +1 -1
- package/cjs/filter/filter-modal.styles.js.map +1 -1
- package/{filter/filter-modal.styles_qw3xlg.css → cjs/filter/filter-modal.styles_kuth21.css} +1 -1
- package/cjs/filter/filter-sidebar.styles.js +1 -1
- package/cjs/filter/filter-sidebar.styles.js.map +1 -1
- package/cjs/filter/{filter-sidebar.styles_1otwpjl.css → filter-sidebar.styles_rbfq4f.css} +1 -1
- package/cjs/filter/filter.js.map +1 -1
- package/cjs/form/form-textarea/form-textarea.js +1 -1
- package/cjs/form/form-textarea/form-textarea.js.map +1 -1
- package/cjs/fullscreen-image-carousel/stateful-image.js.map +1 -1
- package/cjs/histogram-slider/histogram-slider.js.map +1 -1
- package/cjs/input-range-slider/input-range-slider.js.map +1 -1
- package/cjs/input-slider/input-slider.js.map +1 -1
- package/cjs/input-textarea/textarea-counter.js.map +1 -1
- package/cjs/input-textarea/textarea.js +1 -1
- package/cjs/input-textarea/textarea.js.map +1 -1
- package/cjs/input-textarea/textarea.styles.js +1 -1
- package/cjs/input-textarea/textarea.styles.js.map +1 -1
- package/cjs/input-textarea/{textarea.styles_q77m5x.css → textarea.styles_6we7bs.css} +3 -1
- package/cjs/modal/index.js.map +1 -1
- package/cjs/modal-v2/modal-v2.js +1 -1
- package/cjs/modal-v2/modal-v2.js.map +1 -1
- package/cjs/modal-v2/slots/card.js +1 -1
- package/cjs/modal-v2/slots/card.js.map +1 -1
- package/cjs/modal-v2/slots/header.js +2 -0
- package/cjs/modal-v2/slots/header.js.map +1 -0
- package/cjs/modal-v2/slots/slot-styles.js +1 -1
- package/cjs/modal-v2/slots/slot-styles.js.map +1 -1
- package/cjs/modal-v2/slots/{slot-styles_13sd376.css → slot-styles_10ut1v7.css} +5 -1
- package/cjs/navbar/navbar-items.js +1 -1
- package/cjs/navbar/navbar-items.js.map +1 -1
- package/cjs/navbar/navbar-items.styles.js +1 -1
- package/cjs/navbar/navbar-items.styles.js.map +1 -1
- package/cjs/navbar/{navbar-items.styles_1i3ltot.css → navbar-items.styles_1b1zx.css} +9 -8
- package/cjs/navbar/navbar-logo-data.js +1 -1
- package/cjs/navbar/navbar-logo-data.js.map +1 -1
- package/cjs/navbar/navbar.js.map +1 -1
- package/cjs/otp-input/otp-input.js.map +1 -1
- package/cjs/overlay/overlay.js.map +1 -1
- package/cjs/pagination/pagination.js.map +1 -1
- package/cjs/phone-number-input/phone-number-input.js.map +1 -1
- package/cjs/predictive-text-input/predictive-text-input.js.map +1 -1
- package/cjs/shared/dropdown-list/dropdown-list.js.map +1 -1
- package/cjs/shared/internal-calendar/calendar-manager.js.map +1 -1
- package/cjs/shared/standalone-date-input/standalone-date-input.js.map +1 -1
- package/cjs/singpass-button/singpass-button.styles.js +1 -1
- package/cjs/singpass-button/singpass-button.styles.js.map +1 -1
- package/cjs/singpass-button/{singpass-button.styles_1miyex3.css → singpass-button.styles_1fj2lru.css} +1 -1
- package/cjs/smart-app-banner/smart-app-banner.styles.js +1 -1
- package/cjs/smart-app-banner/smart-app-banner.styles.js.map +1 -1
- package/cjs/smart-app-banner/{smart-app-banner.styles_15u6yv4.css → smart-app-banner.styles_r6yjq9.css} +1 -1
- package/cjs/theme/types.js +1 -1
- package/cjs/theme/types.js.map +1 -1
- package/cjs/time-slot-bar/time-slot-bar.js.map +1 -1
- package/cjs/time-slot-bar-week/time-slot-bar-week-days.js.map +1 -1
- package/cjs/timetable/timetable.js.map +1 -1
- package/cjs/toast/toast.styles.js +1 -1
- package/cjs/toast/toast.styles.js.map +1 -1
- package/{toast/toast.styles_1e0l60w.css → cjs/toast/toast.styles_57j3lf.css} +1 -1
- package/cjs/toggle/toggle-icon.js +1 -1
- package/cjs/toggle/toggle-icon.js.map +1 -1
- package/cjs/toggle/toggle-icon.styles.js +1 -1
- package/cjs/toggle/toggle-icon.styles.js.map +1 -1
- package/cjs/toggle/{toggle-icon.styles_ba71y.css → toggle-icon.styles_o6dxr3.css} +3 -2
- package/cjs/toggle/toggle.js +1 -1
- package/cjs/toggle/toggle.js.map +1 -1
- package/cjs/toggle/toggle.styles.js +1 -1
- package/cjs/toggle/toggle.styles.js.map +1 -1
- package/cjs/toggle/toggle.styles_11tmfsr.css +41 -0
- package/cjs/unit-number/unit-number-input.js.map +1 -1
- package/cjs/util/is-styled-components.js.map +1 -1
- package/cjs/util/string-helper.js +1 -1
- package/cjs/util/string-helper.js.map +1 -1
- package/cjs/util/use-event-listener.js.map +1 -1
- package/cjs/util/use-state-actions.js.map +1 -1
- package/countdown-timer/countdown-timer.js.map +1 -1
- package/data-table/data-table.d.ts +1 -1
- package/data-table/data-table.js +1 -1
- package/data-table/data-table.js.map +1 -1
- package/data-table/data-table.styles.d.ts +1 -0
- package/data-table/data-table.styles.js +1 -1
- package/data-table/data-table.styles.js.map +1 -1
- package/{cjs/data-table/data-table.styles_rxganh.css → data-table/data-table.styles_1tnlmhe.css} +7 -6
- package/data-table/types.d.ts +4 -2
- package/date-range-input/date-range-input.js.map +1 -1
- package/docs/component-catalog.json +1 -1
- package/e-signature/e-signature-canvas.js.map +1 -1
- package/error-display/error-display-data.js +1 -1
- package/error-display/error-display-data.js.map +1 -1
- package/error-display/inactivity-timer.js.map +1 -1
- package/external/dompurify/dist/purify.es.js +2 -2
- package/external/dompurify/dist/purify.es.js.map +1 -1
- package/file-download/file-list-card/file-list-card.js.map +1 -1
- package/file-upload/custom-sensors.d.ts +2 -2
- package/file-upload/custom-sensors.js.map +1 -1
- package/file-upload/file-list/file-list.js.map +1 -1
- package/filter/addons/filter-item-checkbox-helpers.js.map +1 -1
- package/filter/addons/filter-item-checkbox.js.map +1 -1
- package/filter/addons/filter-item-checkbox.styles.js +1 -1
- package/filter/addons/filter-item-checkbox.styles.js.map +1 -1
- package/{cjs/filter/addons/filter-item-checkbox.styles_7degw7.css → filter/addons/filter-item-checkbox.styles_6nbpdl.css} +1 -1
- package/filter/filter-item.js.map +1 -1
- package/filter/filter-item.styles.js +1 -1
- package/filter/filter-item.styles.js.map +1 -1
- package/filter/{filter-item.styles_cphjnj.css → filter-item.styles_1yixp8h.css} +1 -1
- package/filter/filter-modal.styles.js +1 -1
- package/filter/filter-modal.styles.js.map +1 -1
- package/{cjs/filter/filter-modal.styles_qw3xlg.css → filter/filter-modal.styles_kuth21.css} +1 -1
- package/filter/filter-sidebar.styles.js +1 -1
- package/filter/filter-sidebar.styles.js.map +1 -1
- package/filter/{filter-sidebar.styles_1otwpjl.css → filter-sidebar.styles_rbfq4f.css} +1 -1
- package/filter/filter.js.map +1 -1
- package/form/form-textarea/form-textarea.js +1 -1
- package/form/form-textarea/form-textarea.js.map +1 -1
- package/fullscreen-image-carousel/stateful-image.js.map +1 -1
- package/fullscreen-image-carousel/types.d.ts +2 -2
- package/histogram-slider/histogram-slider.js.map +1 -1
- package/input-range-slider/input-range-slider.js.map +1 -1
- package/input-slider/input-slider.js.map +1 -1
- package/input-textarea/textarea-counter.js.map +1 -1
- package/input-textarea/textarea.js +1 -1
- package/input-textarea/textarea.js.map +1 -1
- package/input-textarea/textarea.styles.d.ts +2 -0
- package/input-textarea/textarea.styles.js +1 -1
- package/input-textarea/textarea.styles.js.map +1 -1
- package/input-textarea/{textarea.styles_q77m5x.css → textarea.styles_6we7bs.css} +3 -1
- package/input-textarea/types.d.ts +7 -0
- package/modal/index.d.ts +4 -0
- package/modal/index.js.map +1 -1
- package/modal/types.d.ts +2 -0
- package/modal-v2/modal-v2.d.ts +6 -0
- package/modal-v2/modal-v2.js +1 -1
- package/modal-v2/modal-v2.js.map +1 -1
- package/modal-v2/slots/card.js +1 -1
- package/modal-v2/slots/card.js.map +1 -1
- package/modal-v2/slots/header.d.ts +5 -0
- package/modal-v2/slots/header.js +2 -0
- package/modal-v2/slots/header.js.map +1 -0
- package/modal-v2/slots/index.d.ts +1 -0
- package/modal-v2/slots/slot-styles.d.ts +4 -0
- package/modal-v2/slots/slot-styles.js +1 -1
- package/modal-v2/slots/slot-styles.js.map +1 -1
- package/modal-v2/slots/{slot-styles_13sd376.css → slot-styles_10ut1v7.css} +5 -1
- package/modal-v2/types.d.ts +21 -0
- package/navbar/navbar-items.js +1 -1
- package/navbar/navbar-items.js.map +1 -1
- package/navbar/navbar-items.styles.d.ts +2 -1
- package/navbar/navbar-items.styles.js +1 -1
- package/navbar/navbar-items.styles.js.map +1 -1
- package/navbar/{navbar-items.styles_1i3ltot.css → navbar-items.styles_1b1zx.css} +9 -8
- package/navbar/navbar-logo-data.js +1 -1
- package/navbar/navbar-logo-data.js.map +1 -1
- package/navbar/navbar.js.map +1 -1
- package/otp-input/otp-input.js.map +1 -1
- package/overlay/overlay.js.map +1 -1
- package/package.json +1 -1
- package/pagination/pagination.js.map +1 -1
- package/phone-number-input/phone-number-input.js.map +1 -1
- package/predictive-text-input/predictive-text-input.js.map +1 -1
- package/shared/dropdown-list/dropdown-list.js.map +1 -1
- package/shared/internal-calendar/calendar-manager.js.map +1 -1
- package/shared/standalone-date-input/standalone-date-input.js.map +1 -1
- package/singpass-button/singpass-button.styles.js +1 -1
- package/singpass-button/singpass-button.styles.js.map +1 -1
- package/singpass-button/{singpass-button.styles_1miyex3.css → singpass-button.styles_1fj2lru.css} +1 -1
- package/smart-app-banner/smart-app-banner.styles.js +1 -1
- package/smart-app-banner/smart-app-banner.styles.js.map +1 -1
- package/smart-app-banner/{smart-app-banner.styles_15u6yv4.css → smart-app-banner.styles_r6yjq9.css} +1 -1
- package/theme/styles/a11y-playground.css +358 -87
- package/theme/styles/bookingsg.css +391 -85
- package/theme/styles/careercompass.css +396 -90
- package/theme/styles/ccube.css +402 -45
- package/theme/styles/imda.css +348 -85
- package/theme/styles/lifesg.css +377 -87
- package/theme/styles/mylegacy.css +382 -85
- package/theme/styles/oneservice.css +349 -86
- package/theme/styles/pa.css +172 -104
- package/theme/styles/rbs.css +403 -46
- package/theme/styles/sgw-digital-lobby.css +352 -87
- package/theme/styles/smgs.css +401 -95
- package/theme/styles/spf.css +208 -99
- package/theme/styles/supportgowhere.css +387 -90
- package/theme/styles/tote-board.css +816 -0
- package/theme/types.d.ts +1 -1
- package/theme/types.js +1 -1
- package/theme/types.js.map +1 -1
- package/time-slot-bar/time-slot-bar.js.map +1 -1
- package/time-slot-bar-week/time-slot-bar-week-days.js.map +1 -1
- package/timetable/timetable.js.map +1 -1
- package/toast/toast.styles.js +1 -1
- package/toast/toast.styles.js.map +1 -1
- package/{cjs/toast/toast.styles_1e0l60w.css → toast/toast.styles_57j3lf.css} +1 -1
- package/toggle/toggle-icon.d.ts +2 -1
- package/toggle/toggle-icon.js +1 -1
- package/toggle/toggle-icon.js.map +1 -1
- package/toggle/toggle-icon.styles.d.ts +1 -0
- package/toggle/toggle-icon.styles.js +1 -1
- package/toggle/toggle-icon.styles.js.map +1 -1
- package/toggle/{toggle-icon.styles_ba71y.css → toggle-icon.styles_o6dxr3.css} +3 -2
- package/toggle/toggle.js +1 -1
- package/toggle/toggle.js.map +1 -1
- package/toggle/toggle.styles.d.ts +18 -15
- package/toggle/toggle.styles.js +1 -1
- package/toggle/toggle.styles.js.map +1 -1
- package/toggle/toggle.styles_11tmfsr.css +41 -0
- package/tools/storybook-argtypes/services/arg-types-row-builder.d.ts +6 -0
- package/tools/storybook-argtypes/types.d.ts +13 -1
- package/unit-number/unit-number-input.js.map +1 -1
- package/util/is-styled-components.js.map +1 -1
- package/util/string-helper.d.ts +2 -2
- package/util/string-helper.js +1 -1
- package/util/string-helper.js.map +1 -1
- package/util/use-event-listener.js.map +1 -1
- package/util/use-state-actions.js.map +1 -1
- package/button/button.styles_1pxy0co.css +0 -20
- package/cjs/button/button.styles_1pxy0co.css +0 -20
- package/cjs/form/form-textarea/form-textarea.styles.js +0 -2
- package/cjs/form/form-textarea/form-textarea.styles.js.map +0 -1
- package/cjs/form/form-textarea/form-textarea.styles_1kneyif.css +0 -3
- package/cjs/toggle/toggle.styles_1t5qrye.css +0 -38
- package/form/form-textarea/form-textarea.styles.d.ts +0 -3
- package/form/form-textarea/form-textarea.styles.js +0 -2
- package/form/form-textarea/form-textarea.styles.js.map +0 -1
- package/form/form-textarea/form-textarea.styles_1kneyif.css +0 -3
- package/theme/styles/default.css +0 -158
- package/toggle/toggle.styles_1t5qrye.css +0 -38
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otp-input.js","sources":["../../src/otp-input/otp-input.tsx"],"sourcesContent":["import clsx from \"clsx\";\nimport type React from \"react\";\nimport {\n forwardRef,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n} from \"react\";\n\nimport { Button } from \"../button\";\nimport { FormErrorMessage } from \"../form/form-label\";\nimport { Input } from \"../input\";\nimport { VisuallyHidden } from \"../shared/accessibility\";\nimport { Typography } from \"../typography\";\nimport { StringHelper, useId } from \"../util\";\nimport * as styles from \"./otp-input.styles\";\nimport type { OtpInputProps, OtpInputRef } from \"./types\";\nimport { stripOtpFromAutofill, validateUserInput } from \"./utils\";\n\nconst Component = (\n {\n id,\n value = [],\n \"data-testid\": dataTestId,\n className,\n cooldownDuration,\n actionButtonProps,\n errorMessage,\n numOfInput,\n prefix,\n onChange,\n onCooldownStart,\n onCooldownEnd,\n otpOnly,\n onCountdownChange,\n ...otherProps\n }: OtpInputProps,\n ref: React.Ref<OtpInputRef>\n) => {\n // =============================================================================\n // CONST, STATE, REF\n // =============================================================================\n const {\n disabled,\n onClick,\n styleType = \"secondary\",\n ...otherCtaProps\n } = actionButtonProps ?? {};\n\n const inputRefs = useRef<(HTMLInputElement | null)[]>([]);\n const onChangeRef = useRef(onChange);\n\n const [otpValues, setOtpValues] = useState<string[]>(\n new Array(numOfInput).fill(\"\")\n );\n const [countDown, setCountDown] = useState<number>(cooldownDuration);\n const [lastCtaTimestamp, setLastCtaTimestamp] = useState<Date>(new Date());\n const internalId = useId();\n\n const hasError = !!errorMessage;\n const errorId = `${internalId}-error`;\n const timerId = `${internalId}-timer`;\n\n // =============================================================================\n // EFFECTS\n // =============================================================================\n useEffect(() => {\n inputRefs?.current[0]?.focus();\n document.addEventListener(\"paste\", handlePaste);\n return () => document.removeEventListener(\"paste\", handlePaste);\n }, []);\n\n useEffect(() => {\n if (cooldownDuration !== 0) {\n if (onCooldownStart) onCooldownStart();\n const cleanup = beginCountdown();\n\n return () => cleanup();\n }\n }, [lastCtaTimestamp]);\n\n useImperativeHandle(ref, () => ({\n startCooldown: () => {\n setLastCtaTimestamp(new Date());\n setCountDown(cooldownDuration);\n },\n }));\n\n useEffect(() => {\n onChangeRef.current = onChange;\n }, [onChange]);\n\n useEffect(() => {\n if (value.length === numOfInput) {\n setOtpValues(value);\n }\n }, [value]);\n\n // =============================================================================\n // EVENT HANDLERS\n // =============================================================================\n const handleInputFullValue = (incomingValue: string) => {\n if (incomingValue && validateUserInput(incomingValue, numOfInput)) {\n const incomingValueArr = incomingValue.split(\"\");\n setOtpValues(incomingValueArr);\n if (onChangeRef.current) {\n onChangeRef.current(incomingValueArr);\n }\n\n return true;\n }\n\n return false;\n };\n\n const handleChange =\n (index: number) => (event: React.ChangeEvent<HTMLInputElement>) => {\n const eventValue = event.target.value;\n\n /* handle autofill */\n const fieldIsEmpty = otpValues.every((num) => num === \"\");\n const autofillOtp = stripOtpFromAutofill(\n eventValue,\n numOfInput,\n prefix\n );\n if (fieldIsEmpty && handleInputFullValue(autofillOtp)) {\n return;\n }\n\n /* handle normal input */\n const value = eventValue.replace(/[^0-9]/g, \"\");\n if (validateUserInput(value)) {\n const newOtpValues = [...otpValues];\n\n newOtpValues[index] = value.substring(value.length - 1);\n inputRefs.current[index + 1]?.focus();\n\n setOtpValues(newOtpValues);\n\n if (onChange) {\n onChange(newOtpValues);\n }\n }\n };\n\n const handleKeyDown =\n (index: number) => (event: React.KeyboardEvent<HTMLInputElement>) => {\n const { key, code } = event;\n\n if (key === \"Backspace\" || code === \"Backspace\") {\n const newOtpValues = [...otpValues];\n if (newOtpValues[index] !== \"\") {\n newOtpValues[index] = \"\";\n } else if (index !== 0) {\n newOtpValues[index - 1] = \"\";\n inputRefs.current[index - 1]?.focus();\n }\n\n setOtpValues(newOtpValues);\n\n if (onChange) {\n onChange(newOtpValues);\n }\n } else if (event.code === \"ArrowRight\") {\n inputRefs.current[index + 1]?.focus();\n } else if (event.code === \"ArrowLeft\") {\n inputRefs.current[index - 1]?.focus();\n }\n };\n\n const handlePaste = (event: ClipboardEvent): void => {\n if (!event.clipboardData) return;\n\n const pastedValue = event.clipboardData.getData(\"text\");\n const autofillOtp = stripOtpFromAutofill(\n pastedValue,\n numOfInput,\n prefix\n );\n if (!handleInputFullValue(autofillOtp)) {\n event.preventDefault();\n }\n };\n\n const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n setOtpValues(new Array(numOfInput).fill(\"\"));\n\n if (!isWithinCooldown()) {\n setLastCtaTimestamp(new Date());\n setCountDown(cooldownDuration);\n }\n\n if (onClick) {\n onClick(event);\n }\n };\n\n // =========================================================================\n // HELPER FUNCTIONS\n // =========================================================================\n const isWithinCooldown = (): boolean => {\n const currentTime = Date.now();\n const coolDownInMilliseconds = cooldownDuration * 1000;\n\n return (\n currentTime < lastCtaTimestamp.valueOf() + coolDownInMilliseconds\n );\n };\n\n const beginCountdown = () => {\n const interval = setInterval(() => {\n const currentTime = Date.now();\n const coolDownInMilliseconds = cooldownDuration * 1000;\n\n const timer = Math.ceil(\n (lastCtaTimestamp.valueOf() +\n coolDownInMilliseconds -\n currentTime) /\n 1000\n );\n setCountDown(timer);\n if (timer <= 0) {\n if (onCooldownEnd) onCooldownEnd();\n clearInterval(interval);\n } else if (onCountdownChange) {\n onCountdownChange(timer);\n }\n }, 1000);\n\n return () => {\n clearInterval(interval);\n };\n };\n\n const generateId = (index: number, name: string, prefix?: string) => {\n return prefix\n ? `${prefix}-${name}-${index + 1}`\n : `${name}-${index + 1}`;\n };\n\n const displaySeconds = (time: number) => {\n return `${time} second${time === 1 ? \"\" : \"s\"}`;\n };\n\n const getCTALabelProps = (): Partial<\n React.HTMLAttributes<HTMLButtonElement>\n > => {\n return otherCtaProps.children\n ? { children: otherCtaProps.children }\n : {\n children:\n countDown > 0\n ? `Resend OTP in ${displaySeconds(countDown)}`\n : \"Resend OTP\",\n // use a fixed label to avoid repeated screen reader updates when the button is focused\n \"aria-label\":\n countDown > 0\n ? `Resend OTP in ${displaySeconds(cooldownDuration)}`\n : \"Resend OTP\",\n // use a secondary element to reflect the latest countdown\n \"aria-describedby\": timerId,\n };\n };\n\n // =============================================================================\n // RENDER FUNCTIONS\n // =============================================================================\n return (\n <div\n id={id}\n data-testid={dataTestId}\n className={clsx(styles.wrapper, className)}\n >\n <div\n className={styles.inputContainer}\n role=\"group\"\n aria-label={`${numOfInput}-digit OTP input field`}\n >\n {prefix && (\n <Typography.BodyBL\n as=\"span\"\n className={styles.prefix}\n data-testid=\"otp-prefix\"\n weight=\"semibold\"\n >\n <VisuallyHidden>O T P prefix</VisuallyHidden>\n {`${prefix.value} ${prefix.separator}`}\n </Typography.BodyBL>\n )}\n {otpValues.map((data, index) => {\n return (\n <Input\n className={styles.inputField}\n id={generateId(index, \"otp-input\", id)}\n data-testid={generateId(\n index,\n \"otp-input\",\n dataTestId\n )}\n aria-label={`${StringHelper.formatOrdinal(\n index + 1\n )} digit`}\n aria-invalid={hasError}\n aria-describedby={hasError ? errorId : undefined}\n key={index}\n ref={(el) => (inputRefs.current[index] = el)}\n type=\"text\"\n inputMode=\"numeric\"\n value={data}\n error={hasError}\n onChange={handleChange(index)}\n onKeyDown={handleKeyDown(index)}\n autoComplete=\"off\"\n {...otherProps}\n />\n );\n })}\n </div>\n {errorMessage && (\n <FormErrorMessage id={errorId} data-testid=\"otp-error-message\">\n {errorMessage}\n </FormErrorMessage>\n )}\n {/* visually hidden elements need to be rendered before the button so that Voiceover/Chrome doesn't skip the button */}\n <VisuallyHidden role=\"timer\" id={timerId}>\n {displaySeconds(countDown)} remaining\n </VisuallyHidden>\n <VisuallyHidden aria-live=\"polite\">\n {countDown > 0 ? \"\" : \"Ready to resend OTP\"}\n </VisuallyHidden>\n {!otpOnly && (\n <Button\n className={styles.ctaButton}\n styleType={styleType}\n type=\"button\"\n sizeType=\"small\"\n {...otherCtaProps}\n onClick={handleClick}\n disabled={disabled || isWithinCooldown()}\n {...getCTALabelProps()}\n />\n )}\n </div>\n );\n};\n\n// @catalog\n/**\n * A field composed of individual inputs for a numeric one-time password\n * with an in-built countdown mechanism.\n *\n * Use `OtpInput` when users must enter the one-time password (OTP)\n * for multi-factored authentication methods.\n */\nexport const OtpInput = forwardRef(Component);\n"],"names":["OtpInput","forwardRef","_a","ref","id","value","dataTestId","className","cooldownDuration","actionButtonProps","errorMessage","numOfInput","prefix","onChange","onCooldownStart","onCooldownEnd","otpOnly","onCountdownChange","otherProps","__rest","_b","disabled","onClick","styleType","otherCtaProps","inputRefs","useRef","onChangeRef","otpValues","setOtpValues","useState","Array","fill","countDown","setCountDown","lastCtaTimestamp","setLastCtaTimestamp","Date","internalId","useId","hasError","errorId","timerId","useEffect","current","focus","document","addEventListener","handlePaste","removeEventListener","cleanup","beginCountdown","useImperativeHandle","startCooldown","length","handleInputFullValue","incomingValue","validateUserInput","incomingValueArr","split","handleChange","index","event","eventValue","target","fieldIsEmpty","every","num","autofillOtp","stripOtpFromAutofill","replace","newOtpValues","substring","handleKeyDown","key","code","_c","clipboardData","pastedValue","getData","preventDefault","isWithinCooldown","currentTime","now","coolDownInMilliseconds","valueOf","interval","setInterval","timer","Math","ceil","clearInterval","generateId","name","displaySeconds","time","_jsxs","clsx","styles.wrapper","children","styles.inputContainer","role","Typography","BodyBL","as","styles.prefix","weight","_jsx","VisuallyHidden","separator","map","data","Input","Object","assign","styles.inputField","StringHelper","formatOrdinal","undefined","el","type","inputMode","error","onKeyDown","autoComplete","FormErrorMessage","Button","styles.ctaButton","sizeType"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"otp-input.js","sources":["../../src/otp-input/otp-input.tsx"],"sourcesContent":["import clsx from \"clsx\";\nimport type React from \"react\";\nimport {\n forwardRef,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n} from \"react\";\n\nimport { Button } from \"../button\";\nimport { FormErrorMessage } from \"../form/form-label\";\nimport { Input } from \"../input\";\nimport { VisuallyHidden } from \"../shared/accessibility\";\nimport { Typography } from \"../typography\";\nimport { StringHelper, useId } from \"../util\";\nimport * as styles from \"./otp-input.styles\";\nimport type { OtpInputProps, OtpInputRef } from \"./types\";\nimport { stripOtpFromAutofill, validateUserInput } from \"./utils\";\n\nconst Component = (\n {\n id,\n value = [],\n \"data-testid\": dataTestId,\n className,\n cooldownDuration,\n actionButtonProps,\n errorMessage,\n numOfInput,\n prefix,\n onChange,\n onCooldownStart,\n onCooldownEnd,\n otpOnly,\n onCountdownChange,\n ...otherProps\n }: OtpInputProps,\n ref: React.Ref<OtpInputRef>\n) => {\n // =============================================================================\n // CONST, STATE, REF\n // =============================================================================\n const {\n disabled,\n onClick,\n styleType = \"secondary\",\n ...otherCtaProps\n } = actionButtonProps ?? {};\n\n const inputRefs = useRef<(HTMLInputElement | null)[]>([]);\n const onChangeRef = useRef(onChange);\n\n const [otpValues, setOtpValues] = useState<string[]>(\n new Array(numOfInput).fill(\"\")\n );\n const [countDown, setCountDown] = useState<number>(cooldownDuration);\n const [lastCtaTimestamp, setLastCtaTimestamp] = useState<Date>(new Date());\n const internalId = useId();\n\n const hasError = !!errorMessage;\n const errorId = `${internalId}-error`;\n const timerId = `${internalId}-timer`;\n\n // =============================================================================\n // EFFECTS\n // =============================================================================\n useEffect(() => {\n inputRefs?.current[0]?.focus();\n document.addEventListener(\"paste\", handlePaste);\n return () => document.removeEventListener(\"paste\", handlePaste);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (cooldownDuration !== 0) {\n if (onCooldownStart) onCooldownStart();\n const cleanup = beginCountdown();\n\n return () => cleanup();\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [lastCtaTimestamp]);\n\n useImperativeHandle(ref, () => ({\n startCooldown: () => {\n setLastCtaTimestamp(new Date());\n setCountDown(cooldownDuration);\n },\n }));\n\n useEffect(() => {\n onChangeRef.current = onChange;\n }, [onChange]);\n\n useEffect(() => {\n if (value.length === numOfInput) {\n setOtpValues(value);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [value]);\n\n // =============================================================================\n // EVENT HANDLERS\n // =============================================================================\n const handleInputFullValue = (incomingValue: string) => {\n if (incomingValue && validateUserInput(incomingValue, numOfInput)) {\n const incomingValueArr = incomingValue.split(\"\");\n setOtpValues(incomingValueArr);\n if (onChangeRef.current) {\n onChangeRef.current(incomingValueArr);\n }\n\n return true;\n }\n\n return false;\n };\n\n const handleChange =\n (index: number) => (event: React.ChangeEvent<HTMLInputElement>) => {\n const eventValue = event.target.value;\n\n /* handle autofill */\n const fieldIsEmpty = otpValues.every((num) => num === \"\");\n const autofillOtp = stripOtpFromAutofill(\n eventValue,\n numOfInput,\n prefix\n );\n if (fieldIsEmpty && handleInputFullValue(autofillOtp)) {\n return;\n }\n\n /* handle normal input */\n const value = eventValue.replace(/[^0-9]/g, \"\");\n if (validateUserInput(value)) {\n const newOtpValues = [...otpValues];\n\n newOtpValues[index] = value.substring(value.length - 1);\n inputRefs.current[index + 1]?.focus();\n\n setOtpValues(newOtpValues);\n\n if (onChange) {\n onChange(newOtpValues);\n }\n }\n };\n\n const handleKeyDown =\n (index: number) => (event: React.KeyboardEvent<HTMLInputElement>) => {\n const { key, code } = event;\n\n if (key === \"Backspace\" || code === \"Backspace\") {\n const newOtpValues = [...otpValues];\n if (newOtpValues[index] !== \"\") {\n newOtpValues[index] = \"\";\n } else if (index !== 0) {\n newOtpValues[index - 1] = \"\";\n inputRefs.current[index - 1]?.focus();\n }\n\n setOtpValues(newOtpValues);\n\n if (onChange) {\n onChange(newOtpValues);\n }\n } else if (event.code === \"ArrowRight\") {\n inputRefs.current[index + 1]?.focus();\n } else if (event.code === \"ArrowLeft\") {\n inputRefs.current[index - 1]?.focus();\n }\n };\n\n const handlePaste = (event: ClipboardEvent): void => {\n if (!event.clipboardData) return;\n\n const pastedValue = event.clipboardData.getData(\"text\");\n const autofillOtp = stripOtpFromAutofill(\n pastedValue,\n numOfInput,\n prefix\n );\n if (!handleInputFullValue(autofillOtp)) {\n event.preventDefault();\n }\n };\n\n const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n setOtpValues(new Array(numOfInput).fill(\"\"));\n\n if (!isWithinCooldown()) {\n setLastCtaTimestamp(new Date());\n setCountDown(cooldownDuration);\n }\n\n if (onClick) {\n onClick(event);\n }\n };\n\n // =========================================================================\n // HELPER FUNCTIONS\n // =========================================================================\n const isWithinCooldown = (): boolean => {\n const currentTime = Date.now();\n const coolDownInMilliseconds = cooldownDuration * 1000;\n\n return (\n currentTime < lastCtaTimestamp.valueOf() + coolDownInMilliseconds\n );\n };\n\n const beginCountdown = () => {\n const interval = setInterval(() => {\n const currentTime = Date.now();\n const coolDownInMilliseconds = cooldownDuration * 1000;\n\n const timer = Math.ceil(\n (lastCtaTimestamp.valueOf() +\n coolDownInMilliseconds -\n currentTime) /\n 1000\n );\n setCountDown(timer);\n if (timer <= 0) {\n if (onCooldownEnd) onCooldownEnd();\n clearInterval(interval);\n } else if (onCountdownChange) {\n onCountdownChange(timer);\n }\n }, 1000);\n\n return () => {\n clearInterval(interval);\n };\n };\n\n const generateId = (index: number, name: string, prefix?: string) => {\n return prefix\n ? `${prefix}-${name}-${index + 1}`\n : `${name}-${index + 1}`;\n };\n\n const displaySeconds = (time: number) => {\n return `${time} second${time === 1 ? \"\" : \"s\"}`;\n };\n\n const getCTALabelProps = (): Partial<\n React.HTMLAttributes<HTMLButtonElement>\n > => {\n return otherCtaProps.children\n ? { children: otherCtaProps.children }\n : {\n children:\n countDown > 0\n ? `Resend OTP in ${displaySeconds(countDown)}`\n : \"Resend OTP\",\n // use a fixed label to avoid repeated screen reader updates when the button is focused\n \"aria-label\":\n countDown > 0\n ? `Resend OTP in ${displaySeconds(cooldownDuration)}`\n : \"Resend OTP\",\n // use a secondary element to reflect the latest countdown\n \"aria-describedby\": timerId,\n };\n };\n\n // =============================================================================\n // RENDER FUNCTIONS\n // =============================================================================\n return (\n <div\n id={id}\n data-testid={dataTestId}\n className={clsx(styles.wrapper, className)}\n >\n <div\n className={styles.inputContainer}\n role=\"group\"\n aria-label={`${numOfInput}-digit OTP input field`}\n >\n {prefix && (\n <Typography.BodyBL\n as=\"span\"\n className={styles.prefix}\n data-testid=\"otp-prefix\"\n weight=\"semibold\"\n >\n <VisuallyHidden>O T P prefix</VisuallyHidden>\n {`${prefix.value} ${prefix.separator}`}\n </Typography.BodyBL>\n )}\n {otpValues.map((data, index) => {\n return (\n <Input\n className={styles.inputField}\n id={generateId(index, \"otp-input\", id)}\n data-testid={generateId(\n index,\n \"otp-input\",\n dataTestId\n )}\n aria-label={`${StringHelper.formatOrdinal(\n index + 1\n )} digit`}\n aria-invalid={hasError}\n aria-describedby={hasError ? errorId : undefined}\n key={index}\n ref={(el) => (inputRefs.current[index] = el)}\n type=\"text\"\n inputMode=\"numeric\"\n value={data}\n error={hasError}\n onChange={handleChange(index)}\n onKeyDown={handleKeyDown(index)}\n autoComplete=\"off\"\n {...otherProps}\n />\n );\n })}\n </div>\n {errorMessage && (\n <FormErrorMessage id={errorId} data-testid=\"otp-error-message\">\n {errorMessage}\n </FormErrorMessage>\n )}\n {/* visually hidden elements need to be rendered before the button so that Voiceover/Chrome doesn't skip the button */}\n <VisuallyHidden role=\"timer\" id={timerId}>\n {displaySeconds(countDown)} remaining\n </VisuallyHidden>\n <VisuallyHidden aria-live=\"polite\">\n {countDown > 0 ? \"\" : \"Ready to resend OTP\"}\n </VisuallyHidden>\n {!otpOnly && (\n <Button\n className={styles.ctaButton}\n styleType={styleType}\n type=\"button\"\n sizeType=\"small\"\n {...otherCtaProps}\n onClick={handleClick}\n disabled={disabled || isWithinCooldown()}\n {...getCTALabelProps()}\n />\n )}\n </div>\n );\n};\n\n// @catalog\n/**\n * A field composed of individual inputs for a numeric one-time password\n * with an in-built countdown mechanism.\n *\n * Use `OtpInput` when users must enter the one-time password (OTP)\n * for multi-factored authentication methods.\n */\nexport const OtpInput = forwardRef(Component);\n"],"names":["OtpInput","forwardRef","_a","ref","id","value","dataTestId","className","cooldownDuration","actionButtonProps","errorMessage","numOfInput","prefix","onChange","onCooldownStart","onCooldownEnd","otpOnly","onCountdownChange","otherProps","__rest","_b","disabled","onClick","styleType","otherCtaProps","inputRefs","useRef","onChangeRef","otpValues","setOtpValues","useState","Array","fill","countDown","setCountDown","lastCtaTimestamp","setLastCtaTimestamp","Date","internalId","useId","hasError","errorId","timerId","useEffect","current","focus","document","addEventListener","handlePaste","removeEventListener","cleanup","beginCountdown","useImperativeHandle","startCooldown","length","handleInputFullValue","incomingValue","validateUserInput","incomingValueArr","split","handleChange","index","event","eventValue","target","fieldIsEmpty","every","num","autofillOtp","stripOtpFromAutofill","replace","newOtpValues","substring","handleKeyDown","key","code","_c","clipboardData","pastedValue","getData","preventDefault","isWithinCooldown","currentTime","now","coolDownInMilliseconds","valueOf","interval","setInterval","timer","Math","ceil","clearInterval","generateId","name","displaySeconds","time","_jsxs","clsx","styles.wrapper","children","styles.inputContainer","role","Typography","BodyBL","as","styles.prefix","weight","_jsx","VisuallyHidden","separator","map","data","Input","Object","assign","styles.inputField","StringHelper","formatOrdinal","undefined","el","type","inputMode","error","onKeyDown","autoComplete","FormErrorMessage","Button","styles.ctaButton","sizeType"],"mappings":"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"}
|
package/overlay/overlay.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"overlay.js","sources":["../../src/overlay/overlay.tsx"],"sourcesContent":["import {\n FloatingNode,\n FloatingTree,\n useFloatingNodeId,\n} from \"@floating-ui/react\";\nimport clsx from \"clsx\";\nimport type React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport ReactDOM from \"react-dom\";\n\nimport { useApplyStyle, useInheritedThemeScope } from \"../theme\";\nimport { useId } from \"../util\";\nimport * as styles from \"./overlay.styles\";\nimport type { OverlayProps } from \"./types\";\nimport { useFloatingParent } from \"./use-floating-context\";\n\nconst OverlayComponent = ({\n show = false,\n rootId,\n onOverlayClick,\n children,\n backgroundBlur = true,\n disableTransition = false,\n enableOverlayClick = false,\n zIndex: customZIndex,\n id,\n containerRef,\n}: OverlayProps): JSX.Element | null => {\n // =============================================================================\n // CONST, STATE, REF\n // =============================================================================\n const [rootElement, setRootElement] = useState<HTMLElement | null>(null);\n const [isStacked, _setIsStacked] = useState<boolean>();\n const uid = useId();\n const nodeId = useFloatingNodeId();\n\n const stacked = useRef<boolean>();\n // Track where mousedown started to prevent closing drawer during text selection drag\n const mouseDownInsideModalRef = useRef(false);\n const rootRef = useRef<HTMLDivElement>(null);\n const wrapperRef = useRef<HTMLDivElement>(null);\n\n const overlayRootId = id\n ? `lifesg-ds-overlay-root-${id}`\n : \"lifesg-ds-overlay-root\";\n const zIndex = customZIndex ?? (isStacked ? 99999 : 99998);\n const { themeProps, themeStyle } = useInheritedThemeScope(show);\n\n useFloatingParent(zIndex);\n\n useApplyStyle(rootRef, { [styles.tokens.root.zIndex]: zIndex });\n\n // =============================================================================\n // EFFECTS\n // =============================================================================\n useEffect(() => {\n initialiseStylesForDocumentBody();\n setRootElement(getRootElement());\n\n return () => {\n removeOverlay();\n\n if (getOverlayOrder().length < 1) {\n if (isIOS()) {\n applyScrollLockClassForIOS(\"remove\");\n scrollToLastScrollPositionForIOS();\n }\n\n applyBodyStyleClass(\"remove\");\n }\n };\n }, []);\n\n useEffect(() => {\n if (show) {\n const isStacked = checkIfStacked();\n setStacked(isStacked);\n\n addOverlay();\n\n if (isIOS() && getOverlayOrder().length === 1) {\n saveScrollPosition();\n applyScrollLockClassForIOS(\"add\");\n }\n\n const timerId = setTimeout(() => {\n applyBodyStyleClass(\"add\");\n }, 200); // Allow overlay animations to complete\n\n return () => clearTimeout(timerId);\n } else {\n removeOverlay();\n\n if (isIOS() && getOverlayOrder().length < 1) {\n applyScrollLockClassForIOS(\"remove\");\n scrollToLastScrollPositionForIOS();\n }\n\n const timerId = setTimeout(() => {\n if (getOverlayOrder().length < 1) {\n applyBodyStyleClass(\"remove\");\n }\n }, 200); // Allow overlay animations to complete\n\n return () => clearTimeout(timerId);\n }\n }, [show]);\n\n // Track mousedown origin to distinguish clicks from text selection drags\n useEffect(() => {\n if (!show) return;\n\n document.addEventListener(\"mousedown\", handleDocumentMouseDown, true);\n return () => {\n document.removeEventListener(\n \"mousedown\",\n handleDocumentMouseDown,\n true\n );\n };\n }, [show]);\n\n // =============================================================================\n // REF FUNCTIONS\n // =============================================================================\n const setStacked = (data: boolean) => {\n stacked.current = data;\n _setIsStacked(data);\n };\n\n // =============================================================================\n // HELPER FUNCTIONS\n // =============================================================================\n const isIOS = () => /(iPad|iPhone|iPod)/g.test(navigator.userAgent);\n\n const getRootElement = (): HTMLElement | null => {\n if (document && rootId) {\n return document.getElementById(rootId);\n } else if (document) {\n // If rootId not specified, we'll use body\n return document.body;\n } else {\n return null;\n }\n };\n\n const checkIfStacked = () => {\n return getOverlayOrder().length > 0;\n };\n\n const initialiseStylesForDocumentBody = () => {\n const documentWidth = document.documentElement.clientWidth;\n const windowWidth = window.innerWidth;\n const scrollBarWidth = windowWidth - documentWidth;\n\n document.documentElement.style.setProperty(\n styles.tokens.global.scrollbarWidth,\n `${scrollBarWidth}px`\n );\n };\n\n /**\n * This sets the style of <body> to hide the scrollbar\n */\n const applyBodyStyleClass = (action: \"add\" | \"remove\") => {\n const isOverlayStyleClassApplied = document.body.classList.contains(\n styles.tokens.global.overlayOpenClass\n );\n\n if (action === \"add\" && !isOverlayStyleClassApplied) {\n document.body.classList.add(styles.tokens.global.overlayOpenClass);\n } else if (action === \"remove\" && isOverlayStyleClassApplied) {\n document.body.classList.remove(\n styles.tokens.global.overlayOpenClass\n );\n }\n };\n\n const getOverlayOrder = () => {\n const attr = document.body.dataset.lifesgDsOverlayOrder;\n return attr ? attr.split(\",\") : [];\n };\n\n const addOverlay = () => {\n const order = getOverlayOrder();\n document.body.dataset.lifesgDsOverlayOrder = [...order, uid].join(\",\");\n };\n\n const removeOverlay = () => {\n const order = getOverlayOrder();\n document.body.dataset.lifesgDsOverlayOrder = order\n .filter((o) => o !== uid)\n .join(\",\");\n };\n\n /**\n * This sets the style of <body> to further prevent scrolling on iOS\n *\n * ref: https://stackoverflow.com/a/57455820/3932279\n * `overflow: hidden` and `overscroll-behavior` is ignored by iOS, this is\n * an alternative solution\n *\n * as a side effect this causes the scroll position to reset, so additional\n * logic to restore the scroll on close is required\n */\n const applyScrollLockClassForIOS = (action: \"add\" | \"remove\") => {\n if (!isIOS()) {\n return;\n }\n\n const isClassApplied = document.body.classList.contains(\n styles.tokens.global.overlayScrollLockClass\n );\n\n if (action === \"add\" && !isClassApplied) {\n document.body.classList.add(\n styles.tokens.global.overlayScrollLockClass\n );\n } else if (action === \"remove\" && isClassApplied) {\n document.body.classList.remove(\n styles.tokens.global.overlayScrollLockClass\n );\n }\n };\n\n const saveScrollPosition = () => {\n const bodyStyle = document.body.style;\n const scrollY = bodyStyle.top ? bodyStyle.top : window.scrollY + \"px\";\n\n document.body.style.setProperty(styles.tokens.global.scrollY, scrollY);\n };\n\n const scrollToLastScrollPositionForIOS = () => {\n if (!isIOS()) {\n return;\n }\n\n const scrollY = document.body.style.getPropertyValue(\n styles.tokens.global.scrollY\n );\n requestAnimationFrame(() => {\n window.scrollTo({\n top: parseFloat(scrollY),\n });\n });\n };\n\n // =============================================================================\n // EVENT HANDLERS\n // =============================================================================\n const handleDocumentMouseDown = (e: MouseEvent) => {\n const container = containerRef?.current ?? wrapperRef.current;\n const wrapper = wrapperRef.current;\n if (!container) {\n mouseDownInsideModalRef.current = false;\n return;\n }\n\n const target = e.target as Node;\n const clickedOnWrapper = wrapper === target;\n const clickedOnContainer = container === target;\n const clickedInsideContainer = container.contains(target);\n\n // Track if mousedown started inside modal content\n mouseDownInsideModalRef.current =\n !clickedOnWrapper && (clickedOnContainer || clickedInsideContainer);\n };\n\n const handleWrapperClick = (event: React.MouseEvent<HTMLDivElement>) => {\n const container = containerRef?.current ?? wrapperRef.current;\n const wrapper = wrapperRef.current;\n if (!container) {\n mouseDownInsideModalRef.current = false;\n return;\n }\n\n const target = event.target as Node;\n const clickedOnWrapper = wrapper === target;\n const clickedOnContainer = container === target;\n const clickedInsideContainer = container.contains(target);\n const clickedInsideModal = clickedOnContainer || clickedInsideContainer;\n\n // Only close if both mousedown AND click happened outside modal\n if (\n (clickedOnWrapper || !clickedInsideModal) &&\n !mouseDownInsideModalRef.current &&\n onOverlayClick &&\n enableOverlayClick\n ) {\n event.preventDefault();\n onOverlayClick();\n }\n\n mouseDownInsideModalRef.current = false;\n };\n\n // =============================================================================\n // RENDER\n // =============================================================================\n const renderWrapper = () => (\n <FloatingNode id={nodeId}>\n <div\n ref={wrapperRef}\n data-testid=\"overlay-wrapper\"\n className={clsx(\n styles.wrapper,\n show ? styles.wrapperShow : styles.wrapperHide,\n isStacked && styles.wrapperStacked,\n backgroundBlur && styles.wrapperBackgroundBlur,\n disableTransition && styles.wrapperDisableTransition\n )}\n onClick={handleWrapperClick}\n >\n {children}\n </div>\n </FloatingNode>\n );\n\n const renderComponent = () => (\n <div\n ref={rootRef}\n id={overlayRootId}\n data-testid={overlayRootId}\n className={clsx(styles.root, show && styles.rootShow)}\n {...themeProps}\n style={themeStyle}\n >\n {children && renderWrapper()}\n </div>\n );\n\n return rootElement\n ? ReactDOM.createPortal(renderComponent(), rootElement)\n : null;\n};\n\n// @catalog\n/**\n * A full-screen dimmed overlay backdrop.\n *\n * Use `Overlay` as the backdrop for modal dialogs, drawers, and similar\n * blocking UI patterns. It manages body scroll-lock automatically and stacks\n * correctly when multiple overlays are open at once.\n */\nexport const Overlay = (props: OverlayProps) => {\n return (\n <FloatingTree>\n <OverlayComponent {...props} />\n </FloatingTree>\n );\n};\n"],"names":["OverlayComponent","show","rootId","onOverlayClick","children","backgroundBlur","disableTransition","enableOverlayClick","zIndex","customZIndex","id","containerRef","rootElement","setRootElement","useState","isStacked","_setIsStacked","uid","useId","nodeId","useFloatingNodeId","stacked","useRef","mouseDownInsideModalRef","rootRef","wrapperRef","overlayRootId","themeProps","themeStyle","useInheritedThemeScope","useFloatingParent","useApplyStyle","styles.tokens","root","useEffect","initialiseStylesForDocumentBody","getRootElement","removeOverlay","getOverlayOrder","length","isIOS","applyScrollLockClassForIOS","scrollToLastScrollPositionForIOS","applyBodyStyleClass","checkIfStacked","setStacked","addOverlay","saveScrollPosition","timerId","setTimeout","clearTimeout","document","addEventListener","handleDocumentMouseDown","removeEventListener","data","current","test","navigator","userAgent","getElementById","body","documentWidth","documentElement","clientWidth","scrollBarWidth","window","innerWidth","style","setProperty","global","scrollbarWidth","action","isOverlayStyleClassApplied","classList","contains","overlayOpenClass","remove","add","attr","dataset","lifesgDsOverlayOrder","split","order","join","filter","o","isClassApplied","overlayScrollLockClass","bodyStyle","scrollY","top","getPropertyValue","requestAnimationFrame","scrollTo","parseFloat","e","container","_a","wrapper","target","clickedOnWrapper","clickedOnContainer","clickedInsideContainer","handleWrapperClick","event","preventDefault","ReactDOM","createPortal","_jsx","Object","assign","ref","className","clsx","styles.root","styles.rootShow","FloatingNode","styles.wrapper","styles.wrapperShow","styles.wrapperHide","styles.wrapperStacked","styles.wrapperBackgroundBlur","styles.wrapperDisableTransition","onClick","Overlay","props","FloatingTree"],"mappings":"2hCAgBA,MAAMA,EAAmB,EACrBC,QAAO,EACPC,SACAC,iBACAC,WACAC,kBAAiB,EACjBC,qBAAoB,EACpBC,sBAAqB,EACrBC,OAAQC,EACRC,KACAC,mBAKA,MAAOC,EAAaC,GAAkBC,EAA6B,OAC5DC,EAAWC,GAAiBF,IAC7BG,EAAMC,IACNC,EAASC,IAETC,EAAUC,IAEVC,EAA0BD,GAAO,GACjCE,EAAUF,EAAuB,MACjCG,EAAaH,EAAuB,MAEpCI,EAAgBhB,EAChB,0BAA0BA,IAC1B,yBACAF,EAASC,QAAAA,EAAiBM,EAAY,MAAQ,OAC9CY,WAAEA,EAAUC,WAAEA,GAAeC,EAAuB5B,GAE1D6B,EAAkBtB,GAElBuB,EAAcP,EAAS,CAAE,CAACQ,EAAcC,KAAKzB,QAASA,IAKtD0B,GAAU,KACNC,IACAtB,EAAeuB,KAER,KACHC,IAEIC,IAAkBC,OAAS,IACvBC,MACAC,GAA2B,UAC3BC,MAGJC,EAAoB,UACxB,IAEL,IAEHT,GAAU,KACN,GAAIjC,EAAM,CACN,MAAMc,EAAY6B,IAClBC,EAAW9B,GAEX+B,IAEIN,KAAwC,IAA7BF,IAAkBC,SAC7BQ,KACAN,GAA2B,QAG/B,MAAMO,EAAUC,YAAW,KACvBN,EAAoB,MAAM,GAC3B,KAEH,MAAO,IAAMO,aAAaF,EAC9B,CAAO,CACHX,IAEIG,KAAWF,IAAkBC,OAAS,IACtCE,GAA2B,UAC3BC,MAGJ,MAAMM,EAAUC,YAAW,KACnBX,IAAkBC,OAAS,GAC3BI,EAAoB,SACxB,GACD,KAEH,MAAO,IAAMO,aAAaF,EAC9B,IACD,CAAC/C,IAGJiC,GAAU,KACN,GAAKjC,EAGL,OADAkD,SAASC,iBAAiB,YAAaC,IAAyB,GACzD,KACHF,SAASG,oBACL,YACAD,IACA,EACH,CACJ,GACF,CAACpD,IAKJ,MAAM4C,EAAcU,IAChBlC,EAAQmC,QAAUD,EAClBvC,EAAcuC,EAAK,EAMjBf,EAAQ,IAAM,sBAAsBiB,KAAKC,UAAUC,WAEnDvB,EAAiB,IACfe,UAAYjD,EACLiD,SAASS,eAAe1D,GACxBiD,SAEAA,SAASU,KAET,KAITjB,EAAiB,IACZN,IAAkBC,OAAS,EAGhCJ,EAAkC,KACpC,MAAM2B,EAAgBX,SAASY,gBAAgBC,YAEzCC,EADcC,OAAOC,WACUL,EAErCX,SAASY,gBAAgBK,MAAMC,YAC3BrC,EAAcsC,OAAOC,eACrB,GAAGN,MACN,EAMCtB,EAAuB6B,IACzB,MAAMC,EAA6BtB,SAASU,KAAKa,UAAUC,SACvD3C,EAAcsC,OAAOM,kBAGV,QAAXJ,GAAqBC,EAEH,WAAXD,GAAuBC,GAC9BtB,SAASU,KAAKa,UAAUG,OACpB7C,EAAcsC,OAAOM,kBAHzBzB,SAASU,KAAKa,UAAUI,IAAI9C,EAAcsC,OAAOM,iBAKrD,EAGEtC,EAAkB,KACpB,MAAMyC,EAAO5B,SAASU,KAAKmB,QAAQC,qBACnC,OAAOF,EAAOA,EAAKG,MAAM,KAAO,EAAE,EAGhCpC,EAAa,KACf,MAAMqC,EAAQ7C,IACda,SAASU,KAAKmB,QAAQC,qBAAuB,IAAIE,EAAOlE,GAAKmE,KAAK,IAAI,EAGpE/C,EAAgB,KAClB,MAAM8C,EAAQ7C,IACda,SAASU,KAAKmB,QAAQC,qBAAuBE,EACxCE,QAAQC,GAAMA,IAAMrE,IACpBmE,KAAK,IAAI,EAaZ3C,GAA8B+B,IAChC,IAAKhC,IACD,OAGJ,MAAM+C,EAAiBpC,SAASU,KAAKa,UAAUC,SAC3C3C,EAAcsC,OAAOkB,wBAGV,QAAXhB,GAAqBe,EAIH,WAAXf,GAAuBe,GAC9BpC,SAASU,KAAKa,UAAUG,OACpB7C,EAAcsC,OAAOkB,wBALzBrC,SAASU,KAAKa,UAAUI,IACpB9C,EAAcsC,OAAOkB,uBAM7B,EAGEzC,GAAqB,KACvB,MAAM0C,EAAYtC,SAASU,KAAKO,MAC1BsB,EAAUD,EAAUE,IAAMF,EAAUE,IAAMzB,OAAOwB,QAAU,KAEjEvC,SAASU,KAAKO,MAAMC,YAAYrC,EAAcsC,OAAOoB,QAASA,EAAQ,EAGpEhD,GAAmC,KACrC,IAAKF,IACD,OAGJ,MAAMkD,EAAUvC,SAASU,KAAKO,MAAMwB,iBAChC5D,EAAcsC,OAAOoB,SAEzBG,uBAAsB,KAClB3B,OAAO4B,SAAS,CACZH,IAAKI,WAAWL,IAClB,GACJ,EAMArC,GAA2B2C,UAC7B,MAAMC,EAAiC,QAArBC,EAAAvF,aAAY,EAAZA,EAAc6C,eAAO,IAAA0C,EAAAA,EAAIzE,EAAW+B,QAChD2C,EAAU1E,EAAW+B,QAC3B,IAAKyC,EAED,YADA1E,EAAwBiC,SAAU,GAItC,MAAM4C,EAASJ,EAAEI,OACXC,EAAmBF,IAAYC,EAC/BE,EAAqBL,IAAcG,EACnCG,EAAyBN,EAAUtB,SAASyB,GAGlD7E,EAAwBiC,SACnB6C,IAAqBC,GAAsBC,EAAuB,EAGrEC,GAAsBC,UACxB,MAAMR,EAAiC,QAArBC,EAAAvF,aAAY,EAAZA,EAAc6C,eAAO,IAAA0C,EAAAA,EAAIzE,EAAW+B,QAChD2C,EAAU1E,EAAW+B,QAC3B,IAAKyC,EAED,YADA1E,EAAwBiC,SAAU,GAItC,MAAM4C,EAASK,EAAML,OACfC,EAAmBF,IAAYC,EAC/BE,EAAqBL,IAAcG,EACnCG,EAAyBN,EAAUtB,SAASyB,IAK7CC,IAJsBC,GAAsBC,IAK5ChF,EAAwBiC,UACzBrD,IACAI,IAEAkG,EAAMC,iBACNvG,KAGJoB,EAAwBiC,SAAU,CAAK,EAsC3C,OAAO5C,EACD+F,EAASC,aAbXC,EAAA,MAAAC,OAAAC,OAAA,CACIC,IAAKxF,EACLd,GAAIgB,EAAa,cACJA,EACbuF,UAAWC,EAAKC,EAAalH,GAAQmH,IACjCzF,EAAU,CACdyC,MAAOxC,WAENxB,GA3BLyG,EAACQ,EAAY,CAAC3G,GAAIS,EAAMf,SACpByG,SACIG,IAAKvF,EAAU,cACH,kBACZwF,UAAWC,EACPI,EACArH,EAAOsH,EAAqBC,EAC5BzG,GAAa0G,EACbpH,GAAkBqH,EAClBpH,GAAqBqH,GAEzBC,QAASpB,GAAkBpG,SAE1BA,SAmBkCQ,GACzC,IAAI,EAWDiH,EAAWC,GAEhBjB,EAACkB,EAAY,CAAA3H,SACTyG,EAAC7G,EAAgB8G,OAAAC,OAAA,CAAA,EAAKe"}
|
|
1
|
+
{"version":3,"file":"overlay.js","sources":["../../src/overlay/overlay.tsx"],"sourcesContent":["import {\n FloatingNode,\n FloatingTree,\n useFloatingNodeId,\n} from \"@floating-ui/react\";\nimport clsx from \"clsx\";\nimport type React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport ReactDOM from \"react-dom\";\n\nimport { useApplyStyle, useInheritedThemeScope } from \"../theme\";\nimport { useId } from \"../util\";\nimport * as styles from \"./overlay.styles\";\nimport type { OverlayProps } from \"./types\";\nimport { useFloatingParent } from \"./use-floating-context\";\n\nconst OverlayComponent = ({\n show = false,\n rootId,\n onOverlayClick,\n children,\n backgroundBlur = true,\n disableTransition = false,\n enableOverlayClick = false,\n zIndex: customZIndex,\n id,\n containerRef,\n}: OverlayProps): JSX.Element | null => {\n // =============================================================================\n // CONST, STATE, REF\n // =============================================================================\n const [rootElement, setRootElement] = useState<HTMLElement | null>(null);\n const [isStacked, _setIsStacked] = useState<boolean>();\n const uid = useId();\n const nodeId = useFloatingNodeId();\n\n const stacked = useRef<boolean>();\n // Track where mousedown started to prevent closing drawer during text selection drag\n const mouseDownInsideModalRef = useRef(false);\n const rootRef = useRef<HTMLDivElement>(null);\n const wrapperRef = useRef<HTMLDivElement>(null);\n\n const overlayRootId = id\n ? `lifesg-ds-overlay-root-${id}`\n : \"lifesg-ds-overlay-root\";\n const zIndex = customZIndex ?? (isStacked ? 99999 : 99998);\n const { themeProps, themeStyle } = useInheritedThemeScope(show);\n\n useFloatingParent(zIndex);\n\n useApplyStyle(rootRef, { [styles.tokens.root.zIndex]: zIndex });\n\n // =============================================================================\n // EFFECTS\n // =============================================================================\n useEffect(() => {\n initialiseStylesForDocumentBody();\n setRootElement(getRootElement());\n\n return () => {\n removeOverlay();\n\n if (getOverlayOrder().length < 1) {\n if (isIOS()) {\n applyScrollLockClassForIOS(\"remove\");\n scrollToLastScrollPositionForIOS();\n }\n\n applyBodyStyleClass(\"remove\");\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (show) {\n const isStacked = checkIfStacked();\n setStacked(isStacked);\n\n addOverlay();\n\n if (isIOS() && getOverlayOrder().length === 1) {\n saveScrollPosition();\n applyScrollLockClassForIOS(\"add\");\n }\n\n const timerId = setTimeout(() => {\n applyBodyStyleClass(\"add\");\n }, 200); // Allow overlay animations to complete\n\n return () => clearTimeout(timerId);\n } else {\n removeOverlay();\n\n if (isIOS() && getOverlayOrder().length < 1) {\n applyScrollLockClassForIOS(\"remove\");\n scrollToLastScrollPositionForIOS();\n }\n\n const timerId = setTimeout(() => {\n if (getOverlayOrder().length < 1) {\n applyBodyStyleClass(\"remove\");\n }\n }, 200); // Allow overlay animations to complete\n\n return () => clearTimeout(timerId);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [show]);\n\n // Track mousedown origin to distinguish clicks from text selection drags\n useEffect(() => {\n if (!show) return;\n\n document.addEventListener(\"mousedown\", handleDocumentMouseDown, true);\n return () => {\n document.removeEventListener(\n \"mousedown\",\n handleDocumentMouseDown,\n true\n );\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [show]);\n\n // =============================================================================\n // REF FUNCTIONS\n // =============================================================================\n const setStacked = (data: boolean) => {\n stacked.current = data;\n _setIsStacked(data);\n };\n\n // =============================================================================\n // HELPER FUNCTIONS\n // =============================================================================\n const isIOS = () => /(iPad|iPhone|iPod)/g.test(navigator.userAgent);\n\n const getRootElement = (): HTMLElement | null => {\n if (document && rootId) {\n return document.getElementById(rootId);\n } else if (document) {\n // If rootId not specified, we'll use body\n return document.body;\n } else {\n return null;\n }\n };\n\n const checkIfStacked = () => {\n return getOverlayOrder().length > 0;\n };\n\n const initialiseStylesForDocumentBody = () => {\n const documentWidth = document.documentElement.clientWidth;\n const windowWidth = window.innerWidth;\n const scrollBarWidth = windowWidth - documentWidth;\n\n document.documentElement.style.setProperty(\n styles.tokens.global.scrollbarWidth,\n `${scrollBarWidth}px`\n );\n };\n\n /**\n * This sets the style of <body> to hide the scrollbar\n */\n const applyBodyStyleClass = (action: \"add\" | \"remove\") => {\n const isOverlayStyleClassApplied = document.body.classList.contains(\n styles.tokens.global.overlayOpenClass\n );\n\n if (action === \"add\" && !isOverlayStyleClassApplied) {\n document.body.classList.add(styles.tokens.global.overlayOpenClass);\n } else if (action === \"remove\" && isOverlayStyleClassApplied) {\n document.body.classList.remove(\n styles.tokens.global.overlayOpenClass\n );\n }\n };\n\n const getOverlayOrder = () => {\n const attr = document.body.dataset.lifesgDsOverlayOrder;\n return attr ? attr.split(\",\") : [];\n };\n\n const addOverlay = () => {\n const order = getOverlayOrder();\n document.body.dataset.lifesgDsOverlayOrder = [...order, uid].join(\",\");\n };\n\n const removeOverlay = () => {\n const order = getOverlayOrder();\n document.body.dataset.lifesgDsOverlayOrder = order\n .filter((o) => o !== uid)\n .join(\",\");\n };\n\n /**\n * This sets the style of <body> to further prevent scrolling on iOS\n *\n * ref: https://stackoverflow.com/a/57455820/3932279\n * `overflow: hidden` and `overscroll-behavior` is ignored by iOS, this is\n * an alternative solution\n *\n * as a side effect this causes the scroll position to reset, so additional\n * logic to restore the scroll on close is required\n */\n const applyScrollLockClassForIOS = (action: \"add\" | \"remove\") => {\n if (!isIOS()) {\n return;\n }\n\n const isClassApplied = document.body.classList.contains(\n styles.tokens.global.overlayScrollLockClass\n );\n\n if (action === \"add\" && !isClassApplied) {\n document.body.classList.add(\n styles.tokens.global.overlayScrollLockClass\n );\n } else if (action === \"remove\" && isClassApplied) {\n document.body.classList.remove(\n styles.tokens.global.overlayScrollLockClass\n );\n }\n };\n\n const saveScrollPosition = () => {\n const bodyStyle = document.body.style;\n const scrollY = bodyStyle.top ? bodyStyle.top : window.scrollY + \"px\";\n\n document.body.style.setProperty(styles.tokens.global.scrollY, scrollY);\n };\n\n const scrollToLastScrollPositionForIOS = () => {\n if (!isIOS()) {\n return;\n }\n\n const scrollY = document.body.style.getPropertyValue(\n styles.tokens.global.scrollY\n );\n requestAnimationFrame(() => {\n window.scrollTo({\n top: parseFloat(scrollY),\n });\n });\n };\n\n // =============================================================================\n // EVENT HANDLERS\n // =============================================================================\n const handleDocumentMouseDown = (e: MouseEvent) => {\n const container = containerRef?.current ?? wrapperRef.current;\n const wrapper = wrapperRef.current;\n if (!container) {\n mouseDownInsideModalRef.current = false;\n return;\n }\n\n const target = e.target as Node;\n const clickedOnWrapper = wrapper === target;\n const clickedOnContainer = container === target;\n const clickedInsideContainer = container.contains(target);\n\n // Track if mousedown started inside modal content\n mouseDownInsideModalRef.current =\n !clickedOnWrapper && (clickedOnContainer || clickedInsideContainer);\n };\n\n const handleWrapperClick = (event: React.MouseEvent<HTMLDivElement>) => {\n const container = containerRef?.current ?? wrapperRef.current;\n const wrapper = wrapperRef.current;\n if (!container) {\n mouseDownInsideModalRef.current = false;\n return;\n }\n\n const target = event.target as Node;\n const clickedOnWrapper = wrapper === target;\n const clickedOnContainer = container === target;\n const clickedInsideContainer = container.contains(target);\n const clickedInsideModal = clickedOnContainer || clickedInsideContainer;\n\n // Only close if both mousedown AND click happened outside modal\n if (\n (clickedOnWrapper || !clickedInsideModal) &&\n !mouseDownInsideModalRef.current &&\n onOverlayClick &&\n enableOverlayClick\n ) {\n event.preventDefault();\n onOverlayClick();\n }\n\n mouseDownInsideModalRef.current = false;\n };\n\n // =============================================================================\n // RENDER\n // =============================================================================\n const renderWrapper = () => (\n <FloatingNode id={nodeId}>\n <div\n ref={wrapperRef}\n data-testid=\"overlay-wrapper\"\n className={clsx(\n styles.wrapper,\n show ? styles.wrapperShow : styles.wrapperHide,\n isStacked && styles.wrapperStacked,\n backgroundBlur && styles.wrapperBackgroundBlur,\n disableTransition && styles.wrapperDisableTransition\n )}\n onClick={handleWrapperClick}\n >\n {children}\n </div>\n </FloatingNode>\n );\n\n const renderComponent = () => (\n <div\n ref={rootRef}\n id={overlayRootId}\n data-testid={overlayRootId}\n className={clsx(styles.root, show && styles.rootShow)}\n {...themeProps}\n style={themeStyle}\n >\n {children && renderWrapper()}\n </div>\n );\n\n return rootElement\n ? ReactDOM.createPortal(renderComponent(), rootElement)\n : null;\n};\n\n// @catalog\n/**\n * A full-screen dimmed overlay backdrop.\n *\n * Use `Overlay` as the backdrop for modal dialogs, drawers, and similar\n * blocking UI patterns. It manages body scroll-lock automatically and stacks\n * correctly when multiple overlays are open at once.\n */\nexport const Overlay = (props: OverlayProps) => {\n return (\n <FloatingTree>\n <OverlayComponent {...props} />\n </FloatingTree>\n );\n};\n"],"names":["OverlayComponent","show","rootId","onOverlayClick","children","backgroundBlur","disableTransition","enableOverlayClick","zIndex","customZIndex","id","containerRef","rootElement","setRootElement","useState","isStacked","_setIsStacked","uid","useId","nodeId","useFloatingNodeId","stacked","useRef","mouseDownInsideModalRef","rootRef","wrapperRef","overlayRootId","themeProps","themeStyle","useInheritedThemeScope","useFloatingParent","useApplyStyle","styles.tokens","root","useEffect","initialiseStylesForDocumentBody","getRootElement","removeOverlay","getOverlayOrder","length","isIOS","applyScrollLockClassForIOS","scrollToLastScrollPositionForIOS","applyBodyStyleClass","checkIfStacked","setStacked","addOverlay","saveScrollPosition","timerId","setTimeout","clearTimeout","document","addEventListener","handleDocumentMouseDown","removeEventListener","data","current","test","navigator","userAgent","getElementById","body","documentWidth","documentElement","clientWidth","scrollBarWidth","window","innerWidth","style","setProperty","global","scrollbarWidth","action","isOverlayStyleClassApplied","classList","contains","overlayOpenClass","remove","add","attr","dataset","lifesgDsOverlayOrder","split","order","join","filter","o","isClassApplied","overlayScrollLockClass","bodyStyle","scrollY","top","getPropertyValue","requestAnimationFrame","scrollTo","parseFloat","e","container","_a","wrapper","target","clickedOnWrapper","clickedOnContainer","clickedInsideContainer","handleWrapperClick","event","preventDefault","ReactDOM","createPortal","_jsx","Object","assign","ref","className","clsx","styles.root","styles.rootShow","FloatingNode","styles.wrapper","styles.wrapperShow","styles.wrapperHide","styles.wrapperStacked","styles.wrapperBackgroundBlur","styles.wrapperDisableTransition","onClick","Overlay","props","FloatingTree"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pagination.js","sources":["../../src/pagination/pagination.tsx"],"sourcesContent":["import { Chevron2LeftIcon } from \"@lifesg/react-icons/chevron-2-left\";\nimport { Chevron2RightIcon } from \"@lifesg/react-icons/chevron-2-right\";\nimport { ChevronLeftIcon } from \"@lifesg/react-icons/chevron-left\";\nimport { ChevronLineLeftIcon } from \"@lifesg/react-icons/chevron-line-left\";\nimport { ChevronLineRightIcon } from \"@lifesg/react-icons/chevron-line-right\";\nimport { ChevronRightIcon } from \"@lifesg/react-icons/chevron-right\";\nimport { EllipsisHorizontalIcon } from \"@lifesg/react-icons/ellipsis-horizontal\";\nimport clsx from \"clsx\";\nimport React, { useEffect, useState } from \"react\";\n\nimport { Input } from \"../input\";\nimport { InputSelect } from \"../input-select\";\nimport { VisuallyHidden } from \"../shared/accessibility\";\nimport { ClickableIcon } from \"../shared/clickable-icon\";\nimport { useMaxWidthMediaQuery } from \"../theme\";\nimport { Typography } from \"../typography\";\nimport { useId, useIsMounted } from \"../util\";\nimport * as styles from \"./pagination.styles\";\nimport type { PageSizeItemProps, PaginationProps } from \"./types\";\n\nconst Component = (\n {\n id,\n \"data-testid\": dataTestId,\n className,\n pageSize = 10,\n totalItems,\n activePage,\n pageSizeOptions = DEFAULT_OPTIONS,\n showFirstAndLastNav,\n showPageSizeChanger = false,\n onPageChange,\n onPageSizeChange,\n }: PaginationProps,\n ref: React.Ref<HTMLDivElement>\n) => {\n // =============================================================================\n // CONST, STATE, REF\n // =============================================================================\n\n const isMounted = useIsMounted();\n const isMobile = useMaxWidthMediaQuery(\"sm\");\n const [hoverRightButton, setHoverRightButton] = useState(false);\n const [hoverLeftButton, setHoverLeftButton] = useState(false);\n const [inputText, setInputText] = useState<string>(\"\");\n\n const [selectedOption, setSelectedOption] = useState<\n PageSizeItemProps | undefined\n >(pageSizeOptions[0]);\n const [pageSizeLocal, setPageSize] = useState<number>(\n !isMobile && showPageSizeChanger\n ? selectedOption\n ? selectedOption.value\n : pageSize\n : pageSize\n );\n\n const boundaryRange = 1;\n const siblingRange = 1;\n\n const internalId = useId();\n const paginationId = `${internalId}-pagination`;\n\n const totalPages = Math.ceil(totalItems / pageSizeLocal);\n const isFirstPage = activePage === 1;\n const isLastPage = activePage === totalPages;\n\n const firstPaginationItem =\n activePage > 1 ? () => handlePaginationItemClick(1) : undefined;\n const lastPaginationItem =\n activePage < totalPages\n ? () => handlePaginationItemClick(totalPages)\n : undefined;\n const prevPaginationItem =\n activePage > 1\n ? () => handlePaginationItemClick(activePage - 1)\n : undefined;\n const nextPaginationItem =\n activePage < totalPages\n ? () =>\n handlePaginationItemClick(parseInt(activePage.toString()) + 1)\n : undefined;\n\n // =============================================================================\n // EFFECTS\n // =============================================================================\n useEffect(() => {\n if (activePage) {\n setInputValue(activePage);\n }\n }, [activePage]);\n\n useEffect(() => {\n setPageSize(pageSize);\n setSelectedOption(\n pageSizeOptions.find((option) => option.value === pageSize)\n );\n }, [pageSize]);\n\n // =============================================================================\n // HELPER FUNCTIONS\n // =============================================================================\n const setInputValue = (value: number) => {\n setInputText(value.toString());\n };\n\n const closeAllTooltips = () => {\n setHoverRightButton(false);\n setHoverLeftButton(false);\n };\n\n // =============================================================================\n // EVENT HANDLERS\n // =============================================================================\n const handlePaginationItemClick = (pageIndex: number) => {\n if (onPageChange) {\n onPageChange(pageIndex);\n setInputValue(pageIndex);\n }\n };\n\n const handleFastForwardClick = () => {\n const currentIndex = Math.min(totalPages, activePage + 5);\n handlePaginationItemClick(currentIndex);\n setInputValue(currentIndex);\n setHoverRightButton(true);\n setHoverLeftButton(false);\n };\n\n const handleFastBackwardClick = () => {\n const currentIndex = Math.max(1, activePage - 5);\n handlePaginationItemClick(currentIndex);\n setInputValue(currentIndex);\n setHoverRightButton(false);\n setHoverLeftButton(true);\n };\n\n const handleInput = (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = event.target.value;\n const numericRegex = /^[0-9]+$/;\n if (value === undefined || value.length === 0) {\n setInputText(\"\");\n } else if (!numericRegex.test(value)) {\n setInputText(value.replace(/[^0-9]/g, \"\"));\n } else {\n const newPage = parseInt(value.replace(/[^0-9]/g, \"\"));\n setInputValue(Math.max(1, Math.min(totalPages, newPage)));\n }\n };\n\n const handleInputSubmit = (event: React.FormEvent) => {\n event.preventDefault();\n if (inputText) {\n onPageChange?.(parseInt(inputText));\n }\n };\n\n const onHoverRightButton = () => {\n setHoverRightButton(true);\n };\n\n const onBlurRightButton = () => {\n setHoverRightButton(false);\n };\n\n const onHoverLeftButton = () => {\n setHoverLeftButton(true);\n };\n\n const onBlurLeftButton = () => {\n setHoverLeftButton(false);\n };\n\n const handleListItemClick = (item: PageSizeItemProps) => {\n setSelectedOption(item);\n const pagesize = item.value;\n const totalPage = Math.ceil(totalItems / pagesize);\n\n setPageSize(pagesize);\n\n const page = activePage >= totalPage ? totalPage : activePage;\n\n if (onPageSizeChange) {\n onPageSizeChange(page, pagesize);\n }\n };\n\n // =============================================================================\n // RENDER FUNCTIONS\n // =============================================================================\n const renderPaginationItems = () => {\n return [...Array(totalPages)].map((e, i) => {\n const pageIndex = i + 1;\n const startRange = boundaryRange + 2 + siblingRange * 2;\n const endRange = totalPages - startRange;\n const totalRange = (boundaryRange + siblingRange) * 2 + 3;\n const active = activePage === pageIndex;\n if (totalPages <= totalRange) {\n return (\n <button\n key={pageIndex}\n onClick={() => handlePaginationItemClick(pageIndex)}\n className={clsx(\n styles.pageItem,\n active && styles.pageItemSelected\n )}\n aria-label={`page ${pageIndex} of ${totalPages}`}\n aria-current={active ? \"page\" : false}\n onMouseOver={closeAllTooltips}\n onFocus={closeAllTooltips}\n >\n {pageIndex}\n </button>\n );\n }\n\n const ellipsisStart =\n activePage + siblingRange > startRange &&\n pageIndex === boundaryRange + 1;\n if (ellipsisStart) {\n return renderStartEllipsis(pageIndex);\n }\n\n const ellipsisEnd =\n activePage - siblingRange <= endRange &&\n pageIndex === totalPages - boundaryRange - 1;\n if (ellipsisEnd) {\n return renderEndEllipsis(pageIndex);\n }\n\n const paginationStart =\n (pageIndex <= startRange &&\n activePage + siblingRange <= startRange) ||\n pageIndex <= boundaryRange;\n const paginationMiddle =\n pageIndex === activePage ||\n (pageIndex <= activePage + siblingRange &&\n pageIndex >= activePage - siblingRange - 1);\n const paginationEnd =\n (pageIndex > endRange &&\n activePage - siblingRange > endRange) ||\n pageIndex > totalPages - boundaryRange;\n if (paginationStart || paginationMiddle || paginationEnd) {\n return (\n <button\n key={pageIndex}\n onClick={() => handlePaginationItemClick(pageIndex)}\n className={clsx(\n styles.pageItem,\n active && styles.pageItemSelected\n )}\n aria-label={`page ${pageIndex} of ${totalPages}`}\n aria-current={active ? \"page\" : false}\n onMouseOver={closeAllTooltips}\n onFocus={closeAllTooltips}\n >\n {pageIndex}\n </button>\n );\n }\n\n return null;\n });\n };\n\n const renderStartEllipsis = (pageIndex: number) => (\n <div key={pageIndex} className={styles.ellipsisContainer}>\n <ClickableIcon\n className={styles.iconButton}\n focusHighlight={false}\n focusOutline=\"browser\"\n aria-label={\"Previous 5 pages\"}\n onMouseEnter={onHoverLeftButton}\n onMouseLeave={onBlurLeftButton}\n onFocus={onHoverLeftButton}\n onBlur={onBlurLeftButton}\n onClick={handleFastBackwardClick}\n >\n {hoverLeftButton ? (\n <Chevron2LeftIcon aria-hidden />\n ) : (\n <EllipsisHorizontalIcon aria-hidden />\n )}\n </ClickableIcon>\n {hoverLeftButton && (\n <div className={styles.hover}>Previous 5 pages</div>\n )}\n </div>\n );\n\n const renderEndEllipsis = (pageIndex: number) => (\n <div key={pageIndex} className={styles.ellipsisContainer}>\n <ClickableIcon\n className={styles.iconButton}\n focusHighlight={false}\n focusOutline=\"browser\"\n aria-label={\"Next 5 pages\"}\n onMouseEnter={onHoverRightButton}\n onMouseLeave={onBlurRightButton}\n onFocus={onHoverRightButton}\n onBlur={onBlurRightButton}\n onClick={handleFastForwardClick}\n >\n {hoverRightButton ? (\n <Chevron2RightIcon aria-hidden />\n ) : (\n <EllipsisHorizontalIcon aria-hidden />\n )}\n </ClickableIcon>\n {hoverRightButton && (\n <div className={styles.hover}>Next 5 pages</div>\n )}\n </div>\n );\n\n const renderMobile = () => (\n <div className={styles.paginationMobileInput}>\n <form onSubmit={handleInputSubmit}>\n <Input\n className={styles.inputView}\n value={inputText}\n onChange={handleInput}\n autoComplete=\"off\"\n type=\"numeric\"\n id={(id || \"pagination\") + \"-input\"}\n data-testid={(dataTestId || \"pagination\") + \"-input\"}\n aria-label={`Page ${activePage} of ${totalPages}`}\n />\n </form>\n <Typography.BodyBL className={styles.labelDivider}>\n /\n </Typography.BodyBL>\n <Typography.BodyBL className={styles.label}>\n {totalPages}\n </Typography.BodyBL>\n </div>\n );\n\n return (\n <nav\n className={clsx(styles.paginationWrapper, className)}\n ref={ref}\n id={id || \"pagination-wrapper\"}\n data-testid={dataTestId || \"pagination\"}\n aria-labelledby={paginationId}\n >\n <VisuallyHidden id={paginationId} aria-hidden>\n pagination\n </VisuallyHidden>\n <div className={styles.paginationList}>\n <div className={styles.paginationMenu}>\n {showFirstAndLastNav && (\n <ClickableIcon\n className={clsx(\n styles.iconButton,\n styles.iconButtonNavigation\n )}\n onClick={firstPaginationItem}\n disabled={isFirstPage}\n focusHighlight={false}\n aria-label=\"First page\"\n focusOutline=\"browser\"\n >\n <ChevronLineLeftIcon aria-hidden />\n </ClickableIcon>\n )}\n <ClickableIcon\n className={clsx(\n styles.iconButton,\n styles.iconButtonNavigation\n )}\n onClick={prevPaginationItem}\n disabled={isFirstPage}\n focusHighlight={false}\n aria-label=\"Previous page\"\n focusOutline=\"browser\"\n >\n <ChevronLeftIcon aria-hidden />\n </ClickableIcon>\n {isMounted && isMobile\n ? renderMobile()\n : renderPaginationItems()}\n <ClickableIcon\n className={clsx(\n styles.iconButton,\n styles.iconButtonNavigation\n )}\n onClick={nextPaginationItem}\n disabled={isLastPage}\n focusHighlight={false}\n aria-label=\"Next page\"\n focusOutline=\"browser\"\n >\n <ChevronRightIcon aria-hidden />\n </ClickableIcon>\n {showFirstAndLastNav && (\n <ClickableIcon\n className={clsx(\n styles.iconButton,\n styles.iconButtonNavigation\n )}\n onClick={lastPaginationItem}\n disabled={isLastPage}\n focusHighlight={false}\n aria-label=\"Last page\"\n focusOutline=\"browser\"\n >\n <ChevronLineRightIcon aria-hidden />\n </ClickableIcon>\n )}\n </div>\n </div>\n {showPageSizeChanger && !isMobile && (\n <div className={styles.inputSelectWrapper}>\n <VisuallyHidden id={`${paginationId}-page-size`}>\n Items per page\n </VisuallyHidden>\n <InputSelect\n aria-labelledby={`${paginationId}-page-size`}\n options={pageSizeOptions}\n valueExtractor={(item) => item.value}\n listExtractor={(item) => item.label}\n displayValueExtractor={(item) => item.label}\n selectedOption={selectedOption}\n onSelectOption={handleListItemClick}\n />\n </div>\n )}\n </nav>\n );\n};\n// @catalog\n/**\n * A navigation control for moving through paged content.\n *\n * Use `Pagination` to let users navigate between pages of a data set.\n */\nexport const Pagination = React.forwardRef(Component);\n\nconst DEFAULT_OPTIONS: PageSizeItemProps[] = [\n { value: 10, label: \"10 per page\" },\n { value: 20, label: \"20 per page\" },\n { value: 30, label: \"30 per page\" },\n];\n"],"names":["Pagination","React","forwardRef","id","dataTestId","className","pageSize","totalItems","activePage","pageSizeOptions","DEFAULT_OPTIONS","showFirstAndLastNav","showPageSizeChanger","onPageChange","onPageSizeChange","ref","isMounted","useIsMounted","isMobile","useMaxWidthMediaQuery","hoverRightButton","setHoverRightButton","useState","hoverLeftButton","setHoverLeftButton","inputText","setInputText","selectedOption","setSelectedOption","pageSizeLocal","setPageSize","value","paginationId","useId","totalPages","Math","ceil","isFirstPage","isLastPage","firstPaginationItem","handlePaginationItemClick","undefined","lastPaginationItem","prevPaginationItem","nextPaginationItem","parseInt","toString","useEffect","setInputValue","find","option","closeAllTooltips","pageIndex","handleFastForwardClick","currentIndex","min","handleFastBackwardClick","max","handleInput","event","target","length","test","newPage","replace","handleInputSubmit","preventDefault","onHoverRightButton","onBlurRightButton","onHoverLeftButton","onBlurLeftButton","renderStartEllipsis","_jsxs","styles.ellipsisContainer","children","_jsx","ClickableIcon","styles.iconButton","focusHighlight","focusOutline","onMouseEnter","onMouseLeave","onFocus","onBlur","onClick","Chevron2LeftIcon","EllipsisHorizontalIcon","styles.hover","renderEndEllipsis","Chevron2RightIcon","clsx","styles.paginationWrapper","VisuallyHidden","styles.paginationList","styles.paginationMenu","styles.iconButtonNavigation","disabled","ChevronLineLeftIcon","ChevronLeftIcon","styles.paginationMobileInput","onSubmit","Input","styles.inputView","onChange","autoComplete","type","Typography","BodyBL","styles.labelDivider","styles.label","Array","map","e","i","endRange","boundaryRange","active","styles.pageItem","styles.pageItemSelected","onMouseOver","ChevronRightIcon","ChevronLineRightIcon","styles.inputSelectWrapper","InputSelect","options","valueExtractor","item","listExtractor","label","displayValueExtractor","onSelectOption","pagesize","totalPage"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pagination.js","sources":["../../src/pagination/pagination.tsx"],"sourcesContent":["import { Chevron2LeftIcon } from \"@lifesg/react-icons/chevron-2-left\";\nimport { Chevron2RightIcon } from \"@lifesg/react-icons/chevron-2-right\";\nimport { ChevronLeftIcon } from \"@lifesg/react-icons/chevron-left\";\nimport { ChevronLineLeftIcon } from \"@lifesg/react-icons/chevron-line-left\";\nimport { ChevronLineRightIcon } from \"@lifesg/react-icons/chevron-line-right\";\nimport { ChevronRightIcon } from \"@lifesg/react-icons/chevron-right\";\nimport { EllipsisHorizontalIcon } from \"@lifesg/react-icons/ellipsis-horizontal\";\nimport clsx from \"clsx\";\nimport React, { useEffect, useState } from \"react\";\n\nimport { Input } from \"../input\";\nimport { InputSelect } from \"../input-select\";\nimport { VisuallyHidden } from \"../shared/accessibility\";\nimport { ClickableIcon } from \"../shared/clickable-icon\";\nimport { useMaxWidthMediaQuery } from \"../theme\";\nimport { Typography } from \"../typography\";\nimport { useId, useIsMounted } from \"../util\";\nimport * as styles from \"./pagination.styles\";\nimport type { PageSizeItemProps, PaginationProps } from \"./types\";\n\nconst Component = (\n {\n id,\n \"data-testid\": dataTestId,\n className,\n pageSize = 10,\n totalItems,\n activePage,\n pageSizeOptions = DEFAULT_OPTIONS,\n showFirstAndLastNav,\n showPageSizeChanger = false,\n onPageChange,\n onPageSizeChange,\n }: PaginationProps,\n ref: React.Ref<HTMLDivElement>\n) => {\n // =============================================================================\n // CONST, STATE, REF\n // =============================================================================\n\n const isMounted = useIsMounted();\n const isMobile = useMaxWidthMediaQuery(\"sm\");\n const [hoverRightButton, setHoverRightButton] = useState(false);\n const [hoverLeftButton, setHoverLeftButton] = useState(false);\n const [inputText, setInputText] = useState<string>(\"\");\n\n const [selectedOption, setSelectedOption] = useState<\n PageSizeItemProps | undefined\n >(pageSizeOptions[0]);\n const [pageSizeLocal, setPageSize] = useState<number>(\n !isMobile && showPageSizeChanger\n ? selectedOption\n ? selectedOption.value\n : pageSize\n : pageSize\n );\n\n const boundaryRange = 1;\n const siblingRange = 1;\n\n const internalId = useId();\n const paginationId = `${internalId}-pagination`;\n\n const totalPages = Math.ceil(totalItems / pageSizeLocal);\n const isFirstPage = activePage === 1;\n const isLastPage = activePage === totalPages;\n\n const firstPaginationItem =\n activePage > 1 ? () => handlePaginationItemClick(1) : undefined;\n const lastPaginationItem =\n activePage < totalPages\n ? () => handlePaginationItemClick(totalPages)\n : undefined;\n const prevPaginationItem =\n activePage > 1\n ? () => handlePaginationItemClick(activePage - 1)\n : undefined;\n const nextPaginationItem =\n activePage < totalPages\n ? () =>\n handlePaginationItemClick(parseInt(activePage.toString()) + 1)\n : undefined;\n\n // =============================================================================\n // EFFECTS\n // =============================================================================\n useEffect(() => {\n if (activePage) {\n setInputValue(activePage);\n }\n }, [activePage]);\n\n useEffect(() => {\n setPageSize(pageSize);\n setSelectedOption(\n pageSizeOptions.find((option) => option.value === pageSize)\n );\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [pageSize]);\n\n // =============================================================================\n // HELPER FUNCTIONS\n // =============================================================================\n const setInputValue = (value: number) => {\n setInputText(value.toString());\n };\n\n const closeAllTooltips = () => {\n setHoverRightButton(false);\n setHoverLeftButton(false);\n };\n\n // =============================================================================\n // EVENT HANDLERS\n // =============================================================================\n const handlePaginationItemClick = (pageIndex: number) => {\n if (onPageChange) {\n onPageChange(pageIndex);\n setInputValue(pageIndex);\n }\n };\n\n const handleFastForwardClick = () => {\n const currentIndex = Math.min(totalPages, activePage + 5);\n handlePaginationItemClick(currentIndex);\n setInputValue(currentIndex);\n setHoverRightButton(true);\n setHoverLeftButton(false);\n };\n\n const handleFastBackwardClick = () => {\n const currentIndex = Math.max(1, activePage - 5);\n handlePaginationItemClick(currentIndex);\n setInputValue(currentIndex);\n setHoverRightButton(false);\n setHoverLeftButton(true);\n };\n\n const handleInput = (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = event.target.value;\n const numericRegex = /^[0-9]+$/;\n if (value === undefined || value.length === 0) {\n setInputText(\"\");\n } else if (!numericRegex.test(value)) {\n setInputText(value.replace(/[^0-9]/g, \"\"));\n } else {\n const newPage = parseInt(value.replace(/[^0-9]/g, \"\"));\n setInputValue(Math.max(1, Math.min(totalPages, newPage)));\n }\n };\n\n const handleInputSubmit = (event: React.FormEvent) => {\n event.preventDefault();\n if (inputText) {\n onPageChange?.(parseInt(inputText));\n }\n };\n\n const onHoverRightButton = () => {\n setHoverRightButton(true);\n };\n\n const onBlurRightButton = () => {\n setHoverRightButton(false);\n };\n\n const onHoverLeftButton = () => {\n setHoverLeftButton(true);\n };\n\n const onBlurLeftButton = () => {\n setHoverLeftButton(false);\n };\n\n const handleListItemClick = (item: PageSizeItemProps) => {\n setSelectedOption(item);\n const pagesize = item.value;\n const totalPage = Math.ceil(totalItems / pagesize);\n\n setPageSize(pagesize);\n\n const page = activePage >= totalPage ? totalPage : activePage;\n\n if (onPageSizeChange) {\n onPageSizeChange(page, pagesize);\n }\n };\n\n // =============================================================================\n // RENDER FUNCTIONS\n // =============================================================================\n const renderPaginationItems = () => {\n return [...Array(totalPages)].map((e, i) => {\n const pageIndex = i + 1;\n const startRange = boundaryRange + 2 + siblingRange * 2;\n const endRange = totalPages - startRange;\n const totalRange = (boundaryRange + siblingRange) * 2 + 3;\n const active = activePage === pageIndex;\n if (totalPages <= totalRange) {\n return (\n <button\n key={pageIndex}\n onClick={() => handlePaginationItemClick(pageIndex)}\n className={clsx(\n styles.pageItem,\n active && styles.pageItemSelected\n )}\n aria-label={`page ${pageIndex} of ${totalPages}`}\n aria-current={active ? \"page\" : false}\n onMouseOver={closeAllTooltips}\n onFocus={closeAllTooltips}\n >\n {pageIndex}\n </button>\n );\n }\n\n const ellipsisStart =\n activePage + siblingRange > startRange &&\n pageIndex === boundaryRange + 1;\n if (ellipsisStart) {\n return renderStartEllipsis(pageIndex);\n }\n\n const ellipsisEnd =\n activePage - siblingRange <= endRange &&\n pageIndex === totalPages - boundaryRange - 1;\n if (ellipsisEnd) {\n return renderEndEllipsis(pageIndex);\n }\n\n const paginationStart =\n (pageIndex <= startRange &&\n activePage + siblingRange <= startRange) ||\n pageIndex <= boundaryRange;\n const paginationMiddle =\n pageIndex === activePage ||\n (pageIndex <= activePage + siblingRange &&\n pageIndex >= activePage - siblingRange - 1);\n const paginationEnd =\n (pageIndex > endRange &&\n activePage - siblingRange > endRange) ||\n pageIndex > totalPages - boundaryRange;\n if (paginationStart || paginationMiddle || paginationEnd) {\n return (\n <button\n key={pageIndex}\n onClick={() => handlePaginationItemClick(pageIndex)}\n className={clsx(\n styles.pageItem,\n active && styles.pageItemSelected\n )}\n aria-label={`page ${pageIndex} of ${totalPages}`}\n aria-current={active ? \"page\" : false}\n onMouseOver={closeAllTooltips}\n onFocus={closeAllTooltips}\n >\n {pageIndex}\n </button>\n );\n }\n\n return null;\n });\n };\n\n const renderStartEllipsis = (pageIndex: number) => (\n <div key={pageIndex} className={styles.ellipsisContainer}>\n <ClickableIcon\n className={styles.iconButton}\n focusHighlight={false}\n focusOutline=\"browser\"\n aria-label={\"Previous 5 pages\"}\n onMouseEnter={onHoverLeftButton}\n onMouseLeave={onBlurLeftButton}\n onFocus={onHoverLeftButton}\n onBlur={onBlurLeftButton}\n onClick={handleFastBackwardClick}\n >\n {hoverLeftButton ? (\n <Chevron2LeftIcon aria-hidden />\n ) : (\n <EllipsisHorizontalIcon aria-hidden />\n )}\n </ClickableIcon>\n {hoverLeftButton && (\n <div className={styles.hover}>Previous 5 pages</div>\n )}\n </div>\n );\n\n const renderEndEllipsis = (pageIndex: number) => (\n <div key={pageIndex} className={styles.ellipsisContainer}>\n <ClickableIcon\n className={styles.iconButton}\n focusHighlight={false}\n focusOutline=\"browser\"\n aria-label={\"Next 5 pages\"}\n onMouseEnter={onHoverRightButton}\n onMouseLeave={onBlurRightButton}\n onFocus={onHoverRightButton}\n onBlur={onBlurRightButton}\n onClick={handleFastForwardClick}\n >\n {hoverRightButton ? (\n <Chevron2RightIcon aria-hidden />\n ) : (\n <EllipsisHorizontalIcon aria-hidden />\n )}\n </ClickableIcon>\n {hoverRightButton && (\n <div className={styles.hover}>Next 5 pages</div>\n )}\n </div>\n );\n\n const renderMobile = () => (\n <div className={styles.paginationMobileInput}>\n <form onSubmit={handleInputSubmit}>\n <Input\n className={styles.inputView}\n value={inputText}\n onChange={handleInput}\n autoComplete=\"off\"\n type=\"numeric\"\n id={(id || \"pagination\") + \"-input\"}\n data-testid={(dataTestId || \"pagination\") + \"-input\"}\n aria-label={`Page ${activePage} of ${totalPages}`}\n />\n </form>\n <Typography.BodyBL className={styles.labelDivider}>\n /\n </Typography.BodyBL>\n <Typography.BodyBL className={styles.label}>\n {totalPages}\n </Typography.BodyBL>\n </div>\n );\n\n return (\n <nav\n className={clsx(styles.paginationWrapper, className)}\n ref={ref}\n id={id || \"pagination-wrapper\"}\n data-testid={dataTestId || \"pagination\"}\n aria-labelledby={paginationId}\n >\n <VisuallyHidden id={paginationId} aria-hidden>\n pagination\n </VisuallyHidden>\n <div className={styles.paginationList}>\n <div className={styles.paginationMenu}>\n {showFirstAndLastNav && (\n <ClickableIcon\n className={clsx(\n styles.iconButton,\n styles.iconButtonNavigation\n )}\n onClick={firstPaginationItem}\n disabled={isFirstPage}\n focusHighlight={false}\n aria-label=\"First page\"\n focusOutline=\"browser\"\n >\n <ChevronLineLeftIcon aria-hidden />\n </ClickableIcon>\n )}\n <ClickableIcon\n className={clsx(\n styles.iconButton,\n styles.iconButtonNavigation\n )}\n onClick={prevPaginationItem}\n disabled={isFirstPage}\n focusHighlight={false}\n aria-label=\"Previous page\"\n focusOutline=\"browser\"\n >\n <ChevronLeftIcon aria-hidden />\n </ClickableIcon>\n {isMounted && isMobile\n ? renderMobile()\n : renderPaginationItems()}\n <ClickableIcon\n className={clsx(\n styles.iconButton,\n styles.iconButtonNavigation\n )}\n onClick={nextPaginationItem}\n disabled={isLastPage}\n focusHighlight={false}\n aria-label=\"Next page\"\n focusOutline=\"browser\"\n >\n <ChevronRightIcon aria-hidden />\n </ClickableIcon>\n {showFirstAndLastNav && (\n <ClickableIcon\n className={clsx(\n styles.iconButton,\n styles.iconButtonNavigation\n )}\n onClick={lastPaginationItem}\n disabled={isLastPage}\n focusHighlight={false}\n aria-label=\"Last page\"\n focusOutline=\"browser\"\n >\n <ChevronLineRightIcon aria-hidden />\n </ClickableIcon>\n )}\n </div>\n </div>\n {showPageSizeChanger && !isMobile && (\n <div className={styles.inputSelectWrapper}>\n <VisuallyHidden id={`${paginationId}-page-size`}>\n Items per page\n </VisuallyHidden>\n <InputSelect\n aria-labelledby={`${paginationId}-page-size`}\n options={pageSizeOptions}\n valueExtractor={(item) => item.value}\n listExtractor={(item) => item.label}\n displayValueExtractor={(item) => item.label}\n selectedOption={selectedOption}\n onSelectOption={handleListItemClick}\n />\n </div>\n )}\n </nav>\n );\n};\n// @catalog\n/**\n * A navigation control for moving through paged content.\n *\n * Use `Pagination` to let users navigate between pages of a data set.\n */\nexport const Pagination = React.forwardRef(Component);\n\nconst DEFAULT_OPTIONS: PageSizeItemProps[] = [\n { value: 10, label: \"10 per page\" },\n { value: 20, label: \"20 per page\" },\n { value: 30, label: \"30 per page\" },\n];\n"],"names":["Pagination","React","forwardRef","id","dataTestId","className","pageSize","totalItems","activePage","pageSizeOptions","DEFAULT_OPTIONS","showFirstAndLastNav","showPageSizeChanger","onPageChange","onPageSizeChange","ref","isMounted","useIsMounted","isMobile","useMaxWidthMediaQuery","hoverRightButton","setHoverRightButton","useState","hoverLeftButton","setHoverLeftButton","inputText","setInputText","selectedOption","setSelectedOption","pageSizeLocal","setPageSize","value","paginationId","useId","totalPages","Math","ceil","isFirstPage","isLastPage","firstPaginationItem","handlePaginationItemClick","undefined","lastPaginationItem","prevPaginationItem","nextPaginationItem","parseInt","toString","useEffect","setInputValue","find","option","closeAllTooltips","pageIndex","handleFastForwardClick","currentIndex","min","handleFastBackwardClick","max","handleInput","event","target","length","test","newPage","replace","handleInputSubmit","preventDefault","onHoverRightButton","onBlurRightButton","onHoverLeftButton","onBlurLeftButton","renderStartEllipsis","_jsxs","styles.ellipsisContainer","children","_jsx","ClickableIcon","styles.iconButton","focusHighlight","focusOutline","onMouseEnter","onMouseLeave","onFocus","onBlur","onClick","Chevron2LeftIcon","EllipsisHorizontalIcon","styles.hover","renderEndEllipsis","Chevron2RightIcon","clsx","styles.paginationWrapper","VisuallyHidden","styles.paginationList","styles.paginationMenu","styles.iconButtonNavigation","disabled","ChevronLineLeftIcon","ChevronLeftIcon","styles.paginationMobileInput","onSubmit","Input","styles.inputView","onChange","autoComplete","type","Typography","BodyBL","styles.labelDivider","styles.label","Array","map","e","i","endRange","boundaryRange","active","styles.pageItem","styles.pageItemSelected","onMouseOver","ChevronRightIcon","ChevronLineRightIcon","styles.inputSelectWrapper","InputSelect","options","valueExtractor","item","listExtractor","label","displayValueExtractor","onSelectOption","pagesize","totalPage"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"phone-number-input.js","sources":["../../src/phone-number-input/phone-number-input.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\nimport { InputGroup } from \"../input-group/input-group\";\nimport type { AddonProps, LabelAddon, ListAddon } from \"../input-group/types\";\nimport { useNextInputState } from \"../util\";\nimport { PhoneNumberInputHelper } from \"./phone-number-input-helper\";\nimport type { CountryValue, PhoneNumberInputProps } from \"./types\";\n\n// @catalog\n/**\n * A phone number field composed of a country code selector and a number input.\n *\n * Use `PhoneNumberInput` when collecting an internationalised phone number.\n * The country code can be selected from a searchable dropdown or locked to a\n * fixed value via `fixedCountry`.\n */\nexport const PhoneNumberInput = ({\n id,\n onChange,\n value,\n allowClear,\n onClear,\n onBlur,\n error,\n fixedCountry = false,\n optionPlaceholder = \"Select\",\n optionSearchPlaceholder,\n enableSearch,\n onHideOptions,\n onShowOptions,\n placeholder,\n autoComplete,\n noBorder = false,\n getAriaLabel,\n ...otherProps\n}: PhoneNumberInputProps) => {\n // =============================================================================\n // CONST, STATE, REF\n // =============================================================================\n const [options] = useState<CountryValue[]>(\n PhoneNumberInputHelper.getCountries()\n );\n\n const [selectedCountry, setSelectedCountry] = useState<\n CountryValue | undefined\n >(undefined);\n const [inputValue, setInputValue] = useState<string>(\"\");\n\n const nodeRef = useRef<HTMLInputElement>(null);\n\n const getNextInputState = useNextInputState({\n ref: nodeRef,\n formatter: (value) =>\n PhoneNumberInputHelper.formatNumber(\n value.replace(/[^0-9]/g, \"\"),\n selectedCountry\n ),\n });\n\n const resolvedAriaLabel =\n getAriaLabel?.(selectedCountry) ?? \"Enter phone number\";\n\n // =============================================================================\n // EFFECTS\n // =============================================================================\n useEffect(() => {\n const selectedOption = options.filter(\n (country) =>\n country.countryCode === normaliseCountryCode(value?.countryCode)\n )[0];\n\n setSelectedCountry(selectedOption);\n setInputValue(\n PhoneNumberInputHelper.formatNumber(value?.number, selectedOption)\n );\n }, [value]);\n\n // =============================================================================\n // EVENT HANDLERS\n // =============================================================================\n const handleClear = () => {\n if (onClear) {\n onClear();\n } else {\n setInputValue(\"\");\n }\n };\n\n const handleInputChange = () => {\n const nextState = getNextInputState();\n if (!nextState) return;\n\n const { nextValue, updateCaretPosition } = nextState;\n\n updateCaretPosition();\n performLocalChangeHandler(nextValue, selectedCountry);\n if (onChange) {\n performOnChangeHandler(nextValue, selectedCountry);\n }\n };\n\n const handleSelectOption = (country: CountryValue) => {\n performLocalChangeHandler(inputValue, country);\n if (onChange) {\n performOnChangeHandler(inputValue, country);\n }\n };\n\n // =============================================================================\n // HELPER FUNCTIONS\n // =============================================================================\n const performOnChangeHandler = (\n inputValue: string,\n selectedCountry: CountryValue | undefined\n ) => {\n const formatedInputValue = PhoneNumberInputHelper.formatNumber(\n inputValue,\n selectedCountry\n );\n onChange?.({\n number: formatedInputValue.replace(/[\\s()]+/g, \"\"), // strip formatted spaces\n countryCode:\n selectedCountry && addPlusPrefix(selectedCountry.countryCode),\n });\n };\n\n const performLocalChangeHandler = (\n inputValue: string,\n selectedCountry: CountryValue | undefined\n ) => {\n setInputValue(\n PhoneNumberInputHelper.formatNumber(inputValue, selectedCountry)\n );\n\n setSelectedCountry(selectedCountry);\n };\n\n const getAddonProps = (): AddonProps<CountryValue, string> => {\n if (fixedCountry) {\n return {\n type: \"label\",\n attributes: {\n value: addPlusPrefix(selectedCountry?.countryCode),\n } as LabelAddon,\n };\n } else {\n return {\n type: \"list\",\n attributes: {\n \"aria-label\": \"Country code\",\n placeholder: optionPlaceholder,\n options,\n selectedOption: selectedCountry,\n enableSearch: enableSearch,\n searchPlaceholder: optionSearchPlaceholder,\n valueExtractor: (option) => `+${option.countryCode}`,\n listExtractor: (option) => ({\n title: option.name,\n secondaryLabel: addPlusPrefix(option.countryCode),\n }),\n onSelectOption: handleSelectOption,\n onHideOptions: onHideOptions,\n onShowOptions: onShowOptions,\n } as ListAddon<CountryValue, string>,\n };\n }\n };\n\n // =========================================================================\n // RENDER FUNCTIONS\n // =========================================================================\n return (\n <InputGroup\n id={id}\n ref={nodeRef}\n value={inputValue}\n onChange={handleInputChange}\n allowClear={allowClear && !!inputValue}\n onClear={handleClear}\n onBlur={onBlur}\n error={error}\n placeholder={placeholder}\n addon={getAddonProps()}\n inputMode=\"numeric\"\n autoComplete={autoComplete}\n noBorder={noBorder}\n aria-label={resolvedAriaLabel}\n {...otherProps}\n />\n );\n};\n\n// =============================================================================\n// HELPER FUNCTIONS\n// =============================================================================\n/**\n * This strips the + off the specified country code if it\n * is present.\n */\nconst normaliseCountryCode = (countryCode: string | undefined): string => {\n return countryCode ? countryCode.replace(\"+\", \"\") : \"\";\n};\n\nconst addPlusPrefix = (countryCode?: string): string => {\n if (!countryCode) return \"\";\n return countryCode.includes(\"+\") ? countryCode : `+${countryCode}`;\n};\n"],"names":["PhoneNumberInput","_a","id","onChange","value","allowClear","onClear","onBlur","error","fixedCountry","optionPlaceholder","optionSearchPlaceholder","enableSearch","onHideOptions","onShowOptions","placeholder","autoComplete","noBorder","getAriaLabel","otherProps","__rest","options","useState","PhoneNumberInputHelper","getCountries","selectedCountry","setSelectedCountry","undefined","inputValue","setInputValue","nodeRef","useRef","getNextInputState","useNextInputState","ref","formatter","formatNumber","replace","resolvedAriaLabel","_b","useEffect","selectedOption","filter","country","countryCode","normaliseCountryCode","number","handleSelectOption","performLocalChangeHandler","performOnChangeHandler","formatedInputValue","addPlusPrefix","_jsx","InputGroup","Object","assign","nextState","nextValue","updateCaretPosition","addon","type","attributes","searchPlaceholder","valueExtractor","option","listExtractor","title","name","secondaryLabel","onSelectOption","inputMode","includes"],"mappings":"+gBAgBO,MAAMA,EAAoBC,WAAAC,GAC7BA,EAAEC,SACFA,EAAQC,MACRA,EAAKC,WACLA,EAAUC,QACVA,EAAOC,OACPA,EAAMC,MACNA,EAAKC,aACLA,GAAe,EAAKC,kBACpBA,EAAoB,SAAQC,wBAC5BA,EAAuBC,aACvBA,EAAYC,cACZA,EAAaC,cACbA,EAAaC,YACbA,EAAWC,aACXA,EAAYC,SACZA,GAAW,EAAKC,aAChBA,GAAYjB,EACTkB,EAAUC,EAAAnB,EAlBgB,sOAuB7B,MAAOoB,GAAWC,EACdC,EAAuBC,iBAGpBC,EAAiBC,GAAsBJ,OAE5CK,IACKC,EAAYC,GAAiBP,EAAiB,IAE/CQ,EAAUC,EAAyB,MAEnCC,EAAoBC,EAAkB,CACxCC,IAAKJ,EACLK,UAAY/B,GACRmB,EAAuBa,aACnBhC,EAAMiC,QAAQ,UAAW,IACzBZ,KAINa,EAC6B,QAA/BC,EAAArB,aAAY,EAAZA,EAAeO,UAAgB,IAAAc,EAAAA,EAAI,qBAKvCC,GAAU,KACN,MAAMC,EAAiBpB,EAAQqB,QAC1BC,GACGA,EAAQC,cAAgBC,EAAqBzC,aAAK,EAALA,EAAOwC,eAC1D,GAEFlB,EAAmBe,GACnBZ,EACIN,EAAuBa,aAAahC,aAAK,EAALA,EAAO0C,OAAQL,GACtD,GACF,CAACrC,IAKJ,MAqBM2C,EAAsBJ,IACxBK,EAA0BpB,EAAYe,GAClCxC,GACA8C,EAAuBrB,EAAYe,EACvC,EAMEM,EAAyB,CAC3BrB,EACAH,KAEA,MAAMyB,EAAqB3B,EAAuBa,aAC9CR,EACAH,GAEJtB,SAAAA,EAAW,CACP2C,OAAQI,EAAmBb,QAAQ,WAAY,IAC/CO,YACInB,GAAmB0B,EAAc1B,EAAgBmB,cACvD,EAGAI,EAA4B,CAC9BpB,EACAH,KAEAI,EACIN,EAAuBa,aAAaR,EAAYH,IAGpDC,EAAmBD,EAAgB,EAqCvC,OACI2B,EAACC,EAAUC,OAAAC,OAAA,CACPrD,GAAIA,EACJgC,IAAKJ,EACL1B,MAAOwB,EACPzB,SAxFkB,KACtB,MAAMqD,EAAYxB,IAClB,IAAKwB,EAAW,OAEhB,MAAMC,UAAEA,EAASC,oBAAEA,GAAwBF,EAE3CE,IACAV,EAA0BS,EAAWhC,GACjCtB,GACA8C,EAAuBQ,EAAWhC,EACtC,EA+EIpB,WAAYA,KAAgBuB,EAC5BtB,QAlGY,KACZA,EACAA,IAEAuB,EAAc,GAClB,EA8FItB,OAAQA,EACRC,MAAOA,EACPO,YAAaA,EACb4C,MA5CAlD,EACO,CACHmD,KAAM,QACNC,WAAY,CACRzD,MAAO+C,EAAc1B,aAAe,EAAfA,EAAiBmB,eAIvC,CACHgB,KAAM,OACNC,WAAY,CACR,aAAc,eACd9C,YAAaL,EACbW,UACAoB,eAAgBhB,EAChBb,aAAcA,EACdkD,kBAAmBnD,EACnBoD,eAAiBC,GAAW,IAAIA,EAAOpB,cACvCqB,cAAgBD,IAAM,CAClBE,MAAOF,EAAOG,KACdC,eAAgBjB,EAAca,EAAOpB,eAEzCyB,eAAgBtB,EAChBlC,cAAeA,EACfC,cAAeA,IAqBvBwD,UAAU,UACVtD,aAAcA,EACdC,SAAUA,EAAQ,aACNqB,GACRnB,GACN,EAWJ0B,EAAwBD,GACnBA,EAAcA,EAAYP,QAAQ,IAAK,IAAM,GAGlDc,EAAiBP,GACdA,EACEA,EAAY2B,SAAS,KAAO3B,EAAc,IAAIA,IAD5B"}
|
|
1
|
+
{"version":3,"file":"phone-number-input.js","sources":["../../src/phone-number-input/phone-number-input.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\nimport { InputGroup } from \"../input-group/input-group\";\nimport type { AddonProps, LabelAddon, ListAddon } from \"../input-group/types\";\nimport { useNextInputState } from \"../util\";\nimport { PhoneNumberInputHelper } from \"./phone-number-input-helper\";\nimport type { CountryValue, PhoneNumberInputProps } from \"./types\";\n\n// @catalog\n/**\n * A phone number field composed of a country code selector and a number input.\n *\n * Use `PhoneNumberInput` when collecting an internationalised phone number.\n * The country code can be selected from a searchable dropdown or locked to a\n * fixed value via `fixedCountry`.\n */\nexport const PhoneNumberInput = ({\n id,\n onChange,\n value,\n allowClear,\n onClear,\n onBlur,\n error,\n fixedCountry = false,\n optionPlaceholder = \"Select\",\n optionSearchPlaceholder,\n enableSearch,\n onHideOptions,\n onShowOptions,\n placeholder,\n autoComplete,\n noBorder = false,\n getAriaLabel,\n ...otherProps\n}: PhoneNumberInputProps) => {\n // =============================================================================\n // CONST, STATE, REF\n // =============================================================================\n const [options] = useState<CountryValue[]>(\n PhoneNumberInputHelper.getCountries()\n );\n\n const [selectedCountry, setSelectedCountry] = useState<\n CountryValue | undefined\n >(undefined);\n const [inputValue, setInputValue] = useState<string>(\"\");\n\n const nodeRef = useRef<HTMLInputElement>(null);\n\n const getNextInputState = useNextInputState({\n ref: nodeRef,\n formatter: (value) =>\n PhoneNumberInputHelper.formatNumber(\n value.replace(/[^0-9]/g, \"\"),\n selectedCountry\n ),\n });\n\n const resolvedAriaLabel =\n getAriaLabel?.(selectedCountry) ?? \"Enter phone number\";\n\n // =============================================================================\n // EFFECTS\n // =============================================================================\n useEffect(() => {\n const selectedOption = options.filter(\n (country) =>\n country.countryCode === normaliseCountryCode(value?.countryCode)\n )[0];\n\n setSelectedCountry(selectedOption);\n setInputValue(\n PhoneNumberInputHelper.formatNumber(value?.number, selectedOption)\n );\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [value]);\n\n // =============================================================================\n // EVENT HANDLERS\n // =============================================================================\n const handleClear = () => {\n if (onClear) {\n onClear();\n } else {\n setInputValue(\"\");\n }\n };\n\n const handleInputChange = () => {\n const nextState = getNextInputState();\n if (!nextState) return;\n\n const { nextValue, updateCaretPosition } = nextState;\n\n updateCaretPosition();\n performLocalChangeHandler(nextValue, selectedCountry);\n if (onChange) {\n performOnChangeHandler(nextValue, selectedCountry);\n }\n };\n\n const handleSelectOption = (country: CountryValue) => {\n performLocalChangeHandler(inputValue, country);\n if (onChange) {\n performOnChangeHandler(inputValue, country);\n }\n };\n\n // =============================================================================\n // HELPER FUNCTIONS\n // =============================================================================\n const performOnChangeHandler = (\n inputValue: string,\n selectedCountry: CountryValue | undefined\n ) => {\n const formatedInputValue = PhoneNumberInputHelper.formatNumber(\n inputValue,\n selectedCountry\n );\n onChange?.({\n number: formatedInputValue.replace(/[\\s()]+/g, \"\"), // strip formatted spaces\n countryCode:\n selectedCountry && addPlusPrefix(selectedCountry.countryCode),\n });\n };\n\n const performLocalChangeHandler = (\n inputValue: string,\n selectedCountry: CountryValue | undefined\n ) => {\n setInputValue(\n PhoneNumberInputHelper.formatNumber(inputValue, selectedCountry)\n );\n\n setSelectedCountry(selectedCountry);\n };\n\n const getAddonProps = (): AddonProps<CountryValue, string> => {\n if (fixedCountry) {\n return {\n type: \"label\",\n attributes: {\n value: addPlusPrefix(selectedCountry?.countryCode),\n } as LabelAddon,\n };\n } else {\n return {\n type: \"list\",\n attributes: {\n \"aria-label\": \"Country code\",\n placeholder: optionPlaceholder,\n options,\n selectedOption: selectedCountry,\n enableSearch: enableSearch,\n searchPlaceholder: optionSearchPlaceholder,\n valueExtractor: (option) => `+${option.countryCode}`,\n listExtractor: (option) => ({\n title: option.name,\n secondaryLabel: addPlusPrefix(option.countryCode),\n }),\n onSelectOption: handleSelectOption,\n onHideOptions: onHideOptions,\n onShowOptions: onShowOptions,\n } as ListAddon<CountryValue, string>,\n };\n }\n };\n\n // =========================================================================\n // RENDER FUNCTIONS\n // =========================================================================\n return (\n <InputGroup\n id={id}\n ref={nodeRef}\n value={inputValue}\n onChange={handleInputChange}\n allowClear={allowClear && !!inputValue}\n onClear={handleClear}\n onBlur={onBlur}\n error={error}\n placeholder={placeholder}\n addon={getAddonProps()}\n inputMode=\"numeric\"\n autoComplete={autoComplete}\n noBorder={noBorder}\n aria-label={resolvedAriaLabel}\n {...otherProps}\n />\n );\n};\n\n// =============================================================================\n// HELPER FUNCTIONS\n// =============================================================================\n/**\n * This strips the + off the specified country code if it\n * is present.\n */\nconst normaliseCountryCode = (countryCode: string | undefined): string => {\n return countryCode ? countryCode.replace(\"+\", \"\") : \"\";\n};\n\nconst addPlusPrefix = (countryCode?: string): string => {\n if (!countryCode) return \"\";\n return countryCode.includes(\"+\") ? countryCode : `+${countryCode}`;\n};\n"],"names":["PhoneNumberInput","_a","id","onChange","value","allowClear","onClear","onBlur","error","fixedCountry","optionPlaceholder","optionSearchPlaceholder","enableSearch","onHideOptions","onShowOptions","placeholder","autoComplete","noBorder","getAriaLabel","otherProps","__rest","options","useState","PhoneNumberInputHelper","getCountries","selectedCountry","setSelectedCountry","undefined","inputValue","setInputValue","nodeRef","useRef","getNextInputState","useNextInputState","ref","formatter","formatNumber","replace","resolvedAriaLabel","_b","useEffect","selectedOption","filter","country","countryCode","normaliseCountryCode","number","handleSelectOption","performLocalChangeHandler","performOnChangeHandler","formatedInputValue","addPlusPrefix","_jsx","InputGroup","Object","assign","nextState","nextValue","updateCaretPosition","addon","type","attributes","searchPlaceholder","valueExtractor","option","listExtractor","title","name","secondaryLabel","onSelectOption","inputMode","includes"],"mappings":"+gBAgBO,MAAMA,EAAoBC,WAAAC,GAC7BA,EAAEC,SACFA,EAAQC,MACRA,EAAKC,WACLA,EAAUC,QACVA,EAAOC,OACPA,EAAMC,MACNA,EAAKC,aACLA,GAAe,EAAKC,kBACpBA,EAAoB,SAAQC,wBAC5BA,EAAuBC,aACvBA,EAAYC,cACZA,EAAaC,cACbA,EAAaC,YACbA,EAAWC,aACXA,EAAYC,SACZA,GAAW,EAAKC,aAChBA,GAAYjB,EACTkB,EAAUC,EAAAnB,EAlBgB,sOAuB7B,MAAOoB,GAAWC,EACdC,EAAuBC,iBAGpBC,EAAiBC,GAAsBJ,OAE5CK,IACKC,EAAYC,GAAiBP,EAAiB,IAE/CQ,EAAUC,EAAyB,MAEnCC,EAAoBC,EAAkB,CACxCC,IAAKJ,EACLK,UAAY/B,GACRmB,EAAuBa,aACnBhC,EAAMiC,QAAQ,UAAW,IACzBZ,KAINa,EAC6B,QAA/BC,EAAArB,aAAY,EAAZA,EAAeO,UAAgB,IAAAc,EAAAA,EAAI,qBAKvCC,GAAU,KACN,MAAMC,EAAiBpB,EAAQqB,QAC1BC,GACGA,EAAQC,cAAgBC,EAAqBzC,aAAK,EAALA,EAAOwC,eAC1D,GAEFlB,EAAmBe,GACnBZ,EACIN,EAAuBa,aAAahC,aAAK,EAALA,EAAO0C,OAAQL,GACtD,GAEF,CAACrC,IAKJ,MAqBM2C,EAAsBJ,IACxBK,EAA0BpB,EAAYe,GAClCxC,GACA8C,EAAuBrB,EAAYe,EACvC,EAMEM,EAAyB,CAC3BrB,EACAH,KAEA,MAAMyB,EAAqB3B,EAAuBa,aAC9CR,EACAH,GAEJtB,SAAAA,EAAW,CACP2C,OAAQI,EAAmBb,QAAQ,WAAY,IAC/CO,YACInB,GAAmB0B,EAAc1B,EAAgBmB,cACvD,EAGAI,EAA4B,CAC9BpB,EACAH,KAEAI,EACIN,EAAuBa,aAAaR,EAAYH,IAGpDC,EAAmBD,EAAgB,EAqCvC,OACI2B,EAACC,EAAUC,OAAAC,OAAA,CACPrD,GAAIA,EACJgC,IAAKJ,EACL1B,MAAOwB,EACPzB,SAxFkB,KACtB,MAAMqD,EAAYxB,IAClB,IAAKwB,EAAW,OAEhB,MAAMC,UAAEA,EAASC,oBAAEA,GAAwBF,EAE3CE,IACAV,EAA0BS,EAAWhC,GACjCtB,GACA8C,EAAuBQ,EAAWhC,EACtC,EA+EIpB,WAAYA,KAAgBuB,EAC5BtB,QAlGY,KACZA,EACAA,IAEAuB,EAAc,GAClB,EA8FItB,OAAQA,EACRC,MAAOA,EACPO,YAAaA,EACb4C,MA5CAlD,EACO,CACHmD,KAAM,QACNC,WAAY,CACRzD,MAAO+C,EAAc1B,aAAe,EAAfA,EAAiBmB,eAIvC,CACHgB,KAAM,OACNC,WAAY,CACR,aAAc,eACd9C,YAAaL,EACbW,UACAoB,eAAgBhB,EAChBb,aAAcA,EACdkD,kBAAmBnD,EACnBoD,eAAiBC,GAAW,IAAIA,EAAOpB,cACvCqB,cAAgBD,IAAM,CAClBE,MAAOF,EAAOG,KACdC,eAAgBjB,EAAca,EAAOpB,eAEzCyB,eAAgBtB,EAChBlC,cAAeA,EACfC,cAAeA,IAqBvBwD,UAAU,UACVtD,aAAcA,EACdC,SAAUA,EAAQ,aACNqB,GACRnB,GACN,EAWJ0B,EAAwBD,GACnBA,EAAcA,EAAYP,QAAQ,IAAK,IAAM,GAGlDc,EAAiBP,GACdA,EACEA,EAAY2B,SAAS,KAAO3B,EAAc,IAAIA,IAD5B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"predictive-text-input.js","sources":["../../src/predictive-text-input/predictive-text-input.tsx"],"sourcesContent":["import debounce from \"lodash/debounce\";\nimport type React from \"react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { Input } from \"../input\";\nimport { concatIds, VisuallyHidden } from \"../shared/accessibility\";\nimport { DropdownList, DropdownListState } from \"../shared/dropdown-list\";\nimport type { ItemsLoadStateType } from \"../shared/dropdown-list/types\";\nimport { ElementWithDropdown } from \"../shared/dropdown-wrapper\";\nimport { InputWrapper } from \"../shared/input-wrapper\";\nimport { useId } from \"../util\";\nimport type { PredictiveTextInputProps } from \"./types\";\n\n// @catalog\n/**\n * A text input that fetches and displays matching suggestions as the user types.\n *\n * Use `PredictiveTextInput` for search-as-you-type or autocomplete scenarios\n * where options are loaded asynchronously based on the current input.\n */\nexport const PredictiveTextInput = <T, V>({\n className,\n \"data-testid\": testId,\n selectedOption,\n minimumCharacters = 3,\n fetchOptions,\n placeholder = \"Enter here...\",\n readOnly = false,\n disabled = false,\n error,\n valueExtractor,\n listExtractor,\n displayValueExtractor,\n onSelectOption,\n alignment,\n dropdownZIndex,\n dropdownRootNode,\n dropdownWidth,\n \"aria-describedby\": ariaDescribedBy,\n ...otherProps\n}: PredictiveTextInputProps<T, V>): JSX.Element => {\n const getDisplayValue = (item: T | undefined): string => {\n if (!item) return \"\";\n return displayValueExtractor\n ? displayValueExtractor(item)\n : item.toString();\n };\n\n // =============================================================================\n // CONST, STATE\n // =============================================================================\n const [input, setInput] = useState<string>(() =>\n selectedOption ? getDisplayValue(selectedOption) : \"\"\n );\n const [searchedInput, setSearchedInput] = useState<string>(() =>\n selectedOption ? getDisplayValue(selectedOption) : \"\"\n );\n const [options, setOptions] = useState<T[]>([]);\n const [isLoading, setIsLoading] = useState<boolean>(true);\n const [isError, setIsError] = useState<boolean>(false);\n const [isOptionSelected, setIsOptionSelected] = useState<boolean>(\n !!selectedOption\n );\n const [prevOptionSelected, setPrevOptionSelected] = useState<T | undefined>(\n selectedOption\n );\n const [isOpen, setIsOpen] = useState(false);\n const [isFocused, setIsFocused] = useState(false);\n\n const internalId = useId();\n const [resultAnnouncement, setResultAnnouncement] = useState<string | null>(\n null\n );\n const instructionId = `${internalId}-instruction`;\n\n const nodeRef = useRef<HTMLDivElement | null>(null);\n const selectorRef = useRef<HTMLInputElement | null>(null);\n\n const fetchOptionsRef = useRef(fetchOptions);\n\n // =============================================================================\n // DEBOUNCE FUNCTIONS\n // =============================================================================\n const handleFetchOptions = useCallback(async (input: string) => {\n if (!fetchOptionsRef.current) return;\n setIsError(false);\n setIsLoading(true);\n try {\n const fetchedOptions = await fetchOptionsRef.current(input);\n setSearchedInput(input);\n setOptions(fetchedOptions ?? []);\n setIsLoading(false);\n } catch {\n setIsError(true);\n setIsLoading(false);\n }\n }, []);\n\n const fetchOptionsDebounced = useCallback(\n debounce((input: string) => {\n handleFetchOptions(input);\n }, 500),\n [handleFetchOptions]\n );\n\n // =============================================================================\n // EFFECTS\n // =============================================================================\n\n useEffect(() => {\n fetchOptionsRef.current = fetchOptions;\n }, [fetchOptions]);\n\n useEffect(() => {\n /***\n * Should not fetch options for the following the scenario :\n * 1. When input < minimumCharacters\n * 2. When user selected option from dropdown becomes input\n * 3. Initial selectedOption passed in as input\n */\n if (input && input.length >= minimumCharacters) {\n if (!isOptionSelected) {\n setIsOpen(true);\n fetchOptionsDebounced(input);\n } else if (input !== searchedInput) {\n fetchOptionsDebounced(input);\n }\n } else {\n fetchOptionsDebounced.cancel();\n }\n\n if (input === \"\" && prevOptionSelected) {\n onSelectOption?.(undefined, undefined);\n handleDropdownDismiss();\n setPrevOptionSelected(undefined);\n }\n\n if (selectedOption && input !== getDisplayValue(selectedOption)) {\n setIsOptionSelected(false);\n }\n }, [input, selectedOption]);\n\n useEffect(() => {\n setInput(selectedOption ? getDisplayValue(selectedOption) : \"\");\n setSearchedInput(selectedOption ? getDisplayValue(selectedOption) : \"\");\n setPrevOptionSelected(selectedOption);\n setIsOptionSelected(!!selectedOption);\n }, [selectedOption]);\n\n useEffect(() => {\n const state = getItemsLoadState();\n const count = options?.length ?? 0;\n\n if (state === \"loading\" && input.length >= minimumCharacters) {\n setResultAnnouncement(\"Loading suggested results\");\n return;\n }\n\n if (state === \"fail\") {\n setResultAnnouncement(\"Suggestions failed to load. Try again.\");\n return;\n }\n\n if (isOpen && !isLoading && !isError) {\n if (count === 0) {\n setResultAnnouncement(input ? \"No results found.\" : null);\n } else {\n setResultAnnouncement(\n `${count} result${\n count > 1 ? \"s\" : \"\"\n } found. Press down arrow to scroll through the list.`\n );\n }\n }\n }, [options, input, isError, isLoading]);\n\n // =============================================================================\n // Cleanup: cancel debounce on unmount\n // =============================================================================\n useEffect(() => {\n return () => {\n fetchOptionsDebounced.cancel();\n };\n }, [fetchOptionsDebounced]);\n\n // =============================================================================\n // EVENT HANDLERS\n // =============================================================================\n const handleListItemClick = (item: T, extractedValue: V) => {\n selectorRef.current?.focus();\n setInput(getDisplayValue(item));\n setSearchedInput(item ? getDisplayValue(item) : \"\");\n setIsOptionSelected(true);\n setPrevOptionSelected(item);\n setIsOpen(false);\n onSelectOption?.(item, extractedValue);\n };\n\n const handleOpen = () => {\n if (!isOptionSelected && input.length >= minimumCharacters) {\n setIsFocused(true);\n }\n };\n\n const handleClose = () => {\n setIsOpen(false);\n setIsFocused(false);\n handleOnBlur();\n };\n\n const handleNodeFocus = () => {\n setIsFocused(true);\n };\n\n const handleNodeBlur = (e: React.FocusEvent) => {\n if (\n !isOpen &&\n nodeRef.current &&\n !nodeRef.current.contains(e.relatedTarget as Node)\n ) {\n setIsFocused(false);\n handleOnBlur();\n }\n };\n\n const handleListDismiss = () => {\n setIsOpen(false);\n setIsFocused(false);\n };\n\n const handleDismiss = () => {\n setIsOpen(false);\n setIsFocused(false);\n handleOnBlur();\n selectorRef.current?.focus();\n };\n\n const handleHide = () => {\n // reset dropdown state once it fully closes\n setOptions([]);\n setIsLoading(true);\n setIsError(false);\n };\n\n const handleOnClear = () => {\n setInput(\"\");\n setIsOptionSelected(false);\n setIsOpen(false);\n onSelectOption?.(undefined, undefined);\n };\n\n const handleOnBlur = () => {\n if (!isOptionSelected) {\n if (prevOptionSelected) {\n const prevValue = getDisplayValue(prevOptionSelected);\n setInput(prevValue);\n setIsOpen(false);\n } else {\n handleOnClear();\n }\n }\n };\n\n const handleTyping = (e: React.ChangeEvent<HTMLInputElement>) => {\n setInput(e.target.value);\n setIsOptionSelected(false);\n };\n\n const handleDropdownDismiss = (item?: T) => {\n setSearchedInput(item ? getDisplayValue(item) : \"\");\n setIsOptionSelected(!!item);\n setIsLoading(true);\n };\n\n // =============================================================================\n // HELPER FUNCTION\n // =============================================================================\n const getItemsLoadState = (): ItemsLoadStateType => {\n if (isError) return \"fail\";\n return isLoading ? \"loading\" : \"success\";\n };\n\n // =============================================================================\n // RENDER FUNCTION\n // =============================================================================\n const renderInputElement = () => {\n return (\n <InputWrapper\n className={className}\n data-testid={testId}\n ref={nodeRef}\n tabIndex={-1}\n onFocus={handleNodeFocus}\n onBlur={handleNodeBlur}\n focused={isFocused}\n disabled={disabled}\n readOnly={readOnly}\n error={error}\n >\n <VisuallyHidden id={instructionId} aria-hidden>\n Type in {minimumCharacters} or more characters for suggested\n results.\n </VisuallyHidden>\n {resultAnnouncement && (\n <VisuallyHidden aria-live=\"polite\">\n {resultAnnouncement}\n </VisuallyHidden>\n )}\n <Input\n role=\"combobox\"\n ref={selectorRef}\n id={internalId}\n type=\"text\"\n value={input}\n onChange={handleTyping}\n placeholder={placeholder}\n readOnly={readOnly}\n disabled={disabled}\n allowClear\n onClear={handleOnClear}\n aria-expanded={isOpen}\n aria-controls={internalId}\n aria-autocomplete=\"list\"\n aria-haspopup=\"listbox\"\n onBlur={\n input.length < minimumCharacters\n ? handleOnBlur\n : undefined\n }\n styleType=\"no-border\"\n aria-describedby={concatIds(ariaDescribedBy, instructionId)}\n {...otherProps}\n />\n </InputWrapper>\n );\n };\n\n const renderDropdown = () => {\n return (\n <DropdownList\n listboxId={internalId}\n listItems={options}\n onSelectItem={handleListItemClick}\n onDismiss={handleListDismiss}\n valueExtractor={valueExtractor}\n listExtractor={listExtractor}\n itemsLoadState={getItemsLoadState()}\n itemTruncationType={\"end\"}\n itemMaxLines={1}\n labelDisplayType={\"next-line\"}\n disableItemFocus\n onRetry={() => handleFetchOptions(input)}\n width={dropdownWidth}\n matchElementWidth\n />\n );\n };\n\n return (\n <DropdownListState>\n <ElementWithDropdown\n enabled={!readOnly && !disabled}\n isOpen={isOpen}\n renderElement={renderInputElement}\n renderDropdown={renderDropdown}\n onOpen={handleOpen}\n onClose={handleClose}\n onDismiss={handleDismiss}\n onHide={handleHide}\n clickToToggle={false}\n offset={8}\n alignment={alignment}\n fitAvailableHeight\n customZIndex={dropdownZIndex}\n rootNode={dropdownRootNode}\n />\n </DropdownListState>\n );\n};\n"],"names":["PredictiveTextInput","_a","className","testId","selectedOption","minimumCharacters","fetchOptions","placeholder","readOnly","disabled","error","valueExtractor","listExtractor","displayValueExtractor","onSelectOption","alignment","dropdownZIndex","dropdownRootNode","dropdownWidth","ariaDescribedBy","otherProps","__rest","getDisplayValue","item","toString","input","setInput","useState","searchedInput","setSearchedInput","options","setOptions","isLoading","setIsLoading","isError","setIsError","isOptionSelected","setIsOptionSelected","prevOptionSelected","setPrevOptionSelected","isOpen","setIsOpen","isFocused","setIsFocused","internalId","useId","resultAnnouncement","setResultAnnouncement","instructionId","nodeRef","useRef","selectorRef","fetchOptionsRef","handleFetchOptions","useCallback","__awaiter","current","fetchedOptions","fetchOptionsDebounced","debounce","useEffect","length","cancel","undefined","handleDropdownDismiss","state","getItemsLoadState","count","handleListItemClick","extractedValue","focus","handleNodeFocus","handleNodeBlur","e","contains","relatedTarget","handleOnBlur","handleListDismiss","handleOnClear","prevValue","handleTyping","target","value","_jsx","DropdownListState","children","ElementWithDropdown","enabled","renderElement","_jsxs","InputWrapper","ref","tabIndex","onFocus","onBlur","focused","VisuallyHidden","id","Input","Object","assign","role","type","onChange","allowClear","onClear","styleType","concatIds","renderDropdown","DropdownList","listboxId","listItems","onSelectItem","onDismiss","itemsLoadState","itemTruncationType","itemMaxLines","labelDisplayType","disableItemFocus","onRetry","width","matchElementWidth","onOpen","onClose","onHide","clickToToggle","offset","fitAvailableHeight","customZIndex","rootNode"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"predictive-text-input.js","sources":["../../src/predictive-text-input/predictive-text-input.tsx"],"sourcesContent":["import debounce from \"lodash/debounce\";\nimport type React from \"react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { Input } from \"../input\";\nimport { concatIds, VisuallyHidden } from \"../shared/accessibility\";\nimport { DropdownList, DropdownListState } from \"../shared/dropdown-list\";\nimport type { ItemsLoadStateType } from \"../shared/dropdown-list/types\";\nimport { ElementWithDropdown } from \"../shared/dropdown-wrapper\";\nimport { InputWrapper } from \"../shared/input-wrapper\";\nimport { useId } from \"../util\";\nimport type { PredictiveTextInputProps } from \"./types\";\n\n// @catalog\n/**\n * A text input that fetches and displays matching suggestions as the user types.\n *\n * Use `PredictiveTextInput` for search-as-you-type or autocomplete scenarios\n * where options are loaded asynchronously based on the current input.\n */\nexport const PredictiveTextInput = <T, V>({\n className,\n \"data-testid\": testId,\n selectedOption,\n minimumCharacters = 3,\n fetchOptions,\n placeholder = \"Enter here...\",\n readOnly = false,\n disabled = false,\n error,\n valueExtractor,\n listExtractor,\n displayValueExtractor,\n onSelectOption,\n alignment,\n dropdownZIndex,\n dropdownRootNode,\n dropdownWidth,\n \"aria-describedby\": ariaDescribedBy,\n ...otherProps\n}: PredictiveTextInputProps<T, V>): JSX.Element => {\n const getDisplayValue = (item: T | undefined): string => {\n if (!item) return \"\";\n return displayValueExtractor\n ? displayValueExtractor(item)\n : item.toString();\n };\n\n // =============================================================================\n // CONST, STATE\n // =============================================================================\n const [input, setInput] = useState<string>(() =>\n selectedOption ? getDisplayValue(selectedOption) : \"\"\n );\n const [searchedInput, setSearchedInput] = useState<string>(() =>\n selectedOption ? getDisplayValue(selectedOption) : \"\"\n );\n const [options, setOptions] = useState<T[]>([]);\n const [isLoading, setIsLoading] = useState<boolean>(true);\n const [isError, setIsError] = useState<boolean>(false);\n const [isOptionSelected, setIsOptionSelected] = useState<boolean>(\n !!selectedOption\n );\n const [prevOptionSelected, setPrevOptionSelected] = useState<T | undefined>(\n selectedOption\n );\n const [isOpen, setIsOpen] = useState(false);\n const [isFocused, setIsFocused] = useState(false);\n\n const internalId = useId();\n const [resultAnnouncement, setResultAnnouncement] = useState<string | null>(\n null\n );\n const instructionId = `${internalId}-instruction`;\n\n const nodeRef = useRef<HTMLDivElement | null>(null);\n const selectorRef = useRef<HTMLInputElement | null>(null);\n\n const fetchOptionsRef = useRef(fetchOptions);\n\n // =============================================================================\n // DEBOUNCE FUNCTIONS\n // =============================================================================\n const handleFetchOptions = useCallback(async (input: string) => {\n if (!fetchOptionsRef.current) return;\n setIsError(false);\n setIsLoading(true);\n try {\n const fetchedOptions = await fetchOptionsRef.current(input);\n setSearchedInput(input);\n setOptions(fetchedOptions ?? []);\n setIsLoading(false);\n } catch {\n setIsError(true);\n setIsLoading(false);\n }\n }, []);\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const fetchOptionsDebounced = useCallback(\n debounce((input: string) => {\n handleFetchOptions(input);\n }, 500),\n [handleFetchOptions]\n );\n\n // =============================================================================\n // EFFECTS\n // =============================================================================\n\n useEffect(() => {\n fetchOptionsRef.current = fetchOptions;\n }, [fetchOptions]);\n\n useEffect(() => {\n /***\n * Should not fetch options for the following the scenario :\n * 1. When input < minimumCharacters\n * 2. When user selected option from dropdown becomes input\n * 3. Initial selectedOption passed in as input\n */\n if (input && input.length >= minimumCharacters) {\n if (!isOptionSelected) {\n setIsOpen(true);\n fetchOptionsDebounced(input);\n } else if (input !== searchedInput) {\n fetchOptionsDebounced(input);\n }\n } else {\n fetchOptionsDebounced.cancel();\n }\n\n if (input === \"\" && prevOptionSelected) {\n onSelectOption?.(undefined, undefined);\n handleDropdownDismiss();\n setPrevOptionSelected(undefined);\n }\n\n if (selectedOption && input !== getDisplayValue(selectedOption)) {\n setIsOptionSelected(false);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [input, selectedOption]);\n\n useEffect(() => {\n setInput(selectedOption ? getDisplayValue(selectedOption) : \"\");\n setSearchedInput(selectedOption ? getDisplayValue(selectedOption) : \"\");\n setPrevOptionSelected(selectedOption);\n setIsOptionSelected(!!selectedOption);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [selectedOption]);\n\n useEffect(() => {\n const state = getItemsLoadState();\n const count = options?.length ?? 0;\n\n if (state === \"loading\" && input.length >= minimumCharacters) {\n setResultAnnouncement(\"Loading suggested results\");\n return;\n }\n\n if (state === \"fail\") {\n setResultAnnouncement(\"Suggestions failed to load. Try again.\");\n return;\n }\n\n if (isOpen && !isLoading && !isError) {\n if (count === 0) {\n setResultAnnouncement(input ? \"No results found.\" : null);\n } else {\n setResultAnnouncement(\n `${count} result${\n count > 1 ? \"s\" : \"\"\n } found. Press down arrow to scroll through the list.`\n );\n }\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [options, input, isError, isLoading]);\n\n // =============================================================================\n // Cleanup: cancel debounce on unmount\n // =============================================================================\n useEffect(() => {\n return () => {\n fetchOptionsDebounced.cancel();\n };\n }, [fetchOptionsDebounced]);\n\n // =============================================================================\n // EVENT HANDLERS\n // =============================================================================\n const handleListItemClick = (item: T, extractedValue: V) => {\n selectorRef.current?.focus();\n setInput(getDisplayValue(item));\n setSearchedInput(item ? getDisplayValue(item) : \"\");\n setIsOptionSelected(true);\n setPrevOptionSelected(item);\n setIsOpen(false);\n onSelectOption?.(item, extractedValue);\n };\n\n const handleOpen = () => {\n if (!isOptionSelected && input.length >= minimumCharacters) {\n setIsFocused(true);\n }\n };\n\n const handleClose = () => {\n setIsOpen(false);\n setIsFocused(false);\n handleOnBlur();\n };\n\n const handleNodeFocus = () => {\n setIsFocused(true);\n };\n\n const handleNodeBlur = (e: React.FocusEvent) => {\n if (\n !isOpen &&\n nodeRef.current &&\n !nodeRef.current.contains(e.relatedTarget as Node)\n ) {\n setIsFocused(false);\n handleOnBlur();\n }\n };\n\n const handleListDismiss = () => {\n setIsOpen(false);\n setIsFocused(false);\n };\n\n const handleDismiss = () => {\n setIsOpen(false);\n setIsFocused(false);\n handleOnBlur();\n selectorRef.current?.focus();\n };\n\n const handleHide = () => {\n // reset dropdown state once it fully closes\n setOptions([]);\n setIsLoading(true);\n setIsError(false);\n };\n\n const handleOnClear = () => {\n setInput(\"\");\n setIsOptionSelected(false);\n setIsOpen(false);\n onSelectOption?.(undefined, undefined);\n };\n\n const handleOnBlur = () => {\n if (!isOptionSelected) {\n if (prevOptionSelected) {\n const prevValue = getDisplayValue(prevOptionSelected);\n setInput(prevValue);\n setIsOpen(false);\n } else {\n handleOnClear();\n }\n }\n };\n\n const handleTyping = (e: React.ChangeEvent<HTMLInputElement>) => {\n setInput(e.target.value);\n setIsOptionSelected(false);\n };\n\n const handleDropdownDismiss = (item?: T) => {\n setSearchedInput(item ? getDisplayValue(item) : \"\");\n setIsOptionSelected(!!item);\n setIsLoading(true);\n };\n\n // =============================================================================\n // HELPER FUNCTION\n // =============================================================================\n const getItemsLoadState = (): ItemsLoadStateType => {\n if (isError) return \"fail\";\n return isLoading ? \"loading\" : \"success\";\n };\n\n // =============================================================================\n // RENDER FUNCTION\n // =============================================================================\n const renderInputElement = () => {\n return (\n <InputWrapper\n className={className}\n data-testid={testId}\n ref={nodeRef}\n tabIndex={-1}\n onFocus={handleNodeFocus}\n onBlur={handleNodeBlur}\n focused={isFocused}\n disabled={disabled}\n readOnly={readOnly}\n error={error}\n >\n <VisuallyHidden id={instructionId} aria-hidden>\n Type in {minimumCharacters} or more characters for suggested\n results.\n </VisuallyHidden>\n {resultAnnouncement && (\n <VisuallyHidden aria-live=\"polite\">\n {resultAnnouncement}\n </VisuallyHidden>\n )}\n <Input\n role=\"combobox\"\n ref={selectorRef}\n id={internalId}\n type=\"text\"\n value={input}\n onChange={handleTyping}\n placeholder={placeholder}\n readOnly={readOnly}\n disabled={disabled}\n allowClear\n onClear={handleOnClear}\n aria-expanded={isOpen}\n aria-controls={internalId}\n aria-autocomplete=\"list\"\n aria-haspopup=\"listbox\"\n onBlur={\n input.length < minimumCharacters\n ? handleOnBlur\n : undefined\n }\n styleType=\"no-border\"\n aria-describedby={concatIds(ariaDescribedBy, instructionId)}\n {...otherProps}\n />\n </InputWrapper>\n );\n };\n\n const renderDropdown = () => {\n return (\n <DropdownList\n listboxId={internalId}\n listItems={options}\n onSelectItem={handleListItemClick}\n onDismiss={handleListDismiss}\n valueExtractor={valueExtractor}\n listExtractor={listExtractor}\n itemsLoadState={getItemsLoadState()}\n itemTruncationType={\"end\"}\n itemMaxLines={1}\n labelDisplayType={\"next-line\"}\n disableItemFocus\n onRetry={() => handleFetchOptions(input)}\n width={dropdownWidth}\n matchElementWidth\n />\n );\n };\n\n return (\n <DropdownListState>\n <ElementWithDropdown\n enabled={!readOnly && !disabled}\n isOpen={isOpen}\n renderElement={renderInputElement}\n renderDropdown={renderDropdown}\n onOpen={handleOpen}\n onClose={handleClose}\n onDismiss={handleDismiss}\n onHide={handleHide}\n clickToToggle={false}\n offset={8}\n alignment={alignment}\n fitAvailableHeight\n customZIndex={dropdownZIndex}\n rootNode={dropdownRootNode}\n />\n </DropdownListState>\n );\n};\n"],"names":["PredictiveTextInput","_a","className","testId","selectedOption","minimumCharacters","fetchOptions","placeholder","readOnly","disabled","error","valueExtractor","listExtractor","displayValueExtractor","onSelectOption","alignment","dropdownZIndex","dropdownRootNode","dropdownWidth","ariaDescribedBy","otherProps","__rest","getDisplayValue","item","toString","input","setInput","useState","searchedInput","setSearchedInput","options","setOptions","isLoading","setIsLoading","isError","setIsError","isOptionSelected","setIsOptionSelected","prevOptionSelected","setPrevOptionSelected","isOpen","setIsOpen","isFocused","setIsFocused","internalId","useId","resultAnnouncement","setResultAnnouncement","instructionId","nodeRef","useRef","selectorRef","fetchOptionsRef","handleFetchOptions","useCallback","__awaiter","current","fetchedOptions","fetchOptionsDebounced","debounce","useEffect","length","cancel","undefined","handleDropdownDismiss","state","getItemsLoadState","count","handleListItemClick","extractedValue","focus","handleNodeFocus","handleNodeBlur","e","contains","relatedTarget","handleOnBlur","handleListDismiss","handleOnClear","prevValue","handleTyping","target","value","_jsx","DropdownListState","children","ElementWithDropdown","enabled","renderElement","_jsxs","InputWrapper","ref","tabIndex","onFocus","onBlur","focused","VisuallyHidden","id","Input","Object","assign","role","type","onChange","allowClear","onClear","styleType","concatIds","renderDropdown","DropdownList","listboxId","listItems","onSelectItem","onDismiss","itemsLoadState","itemTruncationType","itemMaxLines","labelDisplayType","disableItemFocus","onRetry","width","matchElementWidth","onOpen","onClose","onHide","clickToToggle","offset","fitAvailableHeight","customZIndex","rootNode"],"mappings":"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"}
|