@bloom-housing/ui-components 2.0.0-pre-tailwind

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.
Files changed (223) hide show
  1. package/.jest/setup-tests.js +24 -0
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +195 -0
  4. package/index.ts +148 -0
  5. package/jest.config.js +41 -0
  6. package/package.json +98 -0
  7. package/public/images/alameda-logo-white.svg +1 -0
  8. package/public/images/arrow-down.png +0 -0
  9. package/public/images/arrow-down.svg +1 -0
  10. package/public/images/check.png +0 -0
  11. package/public/images/check.svg +11 -0
  12. package/public/images/eho-logo-white.svg +1 -0
  13. package/public/images/eho-logo.svg +1 -0
  14. package/public/images/logo_glyph.svg +11 -0
  15. package/src/actions/Button.scss +157 -0
  16. package/src/actions/Button.tsx +80 -0
  17. package/src/actions/ExpandableContent.tsx +29 -0
  18. package/src/actions/ExpandableText.scss +18 -0
  19. package/src/actions/ExpandableText.tsx +52 -0
  20. package/src/actions/LinkButton.tsx +30 -0
  21. package/src/actions/LocalizedLink.tsx +11 -0
  22. package/src/authentication/AuthContext.ts +327 -0
  23. package/src/authentication/RequireLogin.tsx +62 -0
  24. package/src/authentication/index.ts +5 -0
  25. package/src/authentication/timeout.tsx +127 -0
  26. package/src/authentication/token.ts +17 -0
  27. package/src/authentication/useRequireLoggedInUser.ts +19 -0
  28. package/src/blocks/ActionBlock.scss +108 -0
  29. package/src/blocks/ActionBlock.tsx +51 -0
  30. package/src/blocks/AppStatusItem.scss +140 -0
  31. package/src/blocks/AppStatusItem.tsx +75 -0
  32. package/src/blocks/DashBlock.tsx +42 -0
  33. package/src/blocks/DashBlocks.scss +56 -0
  34. package/src/blocks/DashBlocks.tsx +7 -0
  35. package/src/blocks/FormCard.scss +201 -0
  36. package/src/blocks/FormCard.tsx +29 -0
  37. package/src/blocks/HousingCounselor.tsx +51 -0
  38. package/src/blocks/ImageCard.scss +91 -0
  39. package/src/blocks/ImageCard.tsx +77 -0
  40. package/src/blocks/InfoCard.scss +42 -0
  41. package/src/blocks/InfoCard.tsx +44 -0
  42. package/src/blocks/StatusBar.scss +30 -0
  43. package/src/blocks/StatusBar.tsx +31 -0
  44. package/src/blocks/ViewItem.scss +59 -0
  45. package/src/blocks/ViewItem.tsx +32 -0
  46. package/src/config/ConfigContext.tsx +36 -0
  47. package/src/config/NavigationContext.tsx +54 -0
  48. package/src/config/index.ts +2 -0
  49. package/src/footers/ExygyFooter.tsx +12 -0
  50. package/src/footers/SiteFooter.scss +28 -0
  51. package/src/footers/SiteFooter.tsx +10 -0
  52. package/src/forms/CloudinaryUpload.ts +50 -0
  53. package/src/forms/DOBField.tsx +132 -0
  54. package/src/forms/DateField.tsx +120 -0
  55. package/src/forms/Dropzone.scss +17 -0
  56. package/src/forms/Dropzone.tsx +67 -0
  57. package/src/forms/Field.tsx +115 -0
  58. package/src/forms/FieldGroup.tsx +82 -0
  59. package/src/forms/Form.tsx +22 -0
  60. package/src/forms/HouseholdMemberForm.tsx +41 -0
  61. package/src/forms/HouseholdSizeField.tsx +74 -0
  62. package/src/forms/PhoneField.tsx +69 -0
  63. package/src/forms/PhoneMask.tsx +24 -0
  64. package/src/forms/Select.tsx +80 -0
  65. package/src/forms/Textarea.scss +40 -0
  66. package/src/forms/Textarea.tsx +64 -0
  67. package/src/forms/TimeField.tsx +176 -0
  68. package/src/global/AppearanceTypes.ts +46 -0
  69. package/src/global/ApplicationStatusType.ts +6 -0
  70. package/src/global/accordion.scss +4 -0
  71. package/src/global/blocks.scss +137 -0
  72. package/src/global/custom_counter.scss +50 -0
  73. package/src/global/forms.scss +362 -0
  74. package/src/global/headers.scss +89 -0
  75. package/src/global/homepage.scss +8 -0
  76. package/src/global/index.scss +72 -0
  77. package/src/global/lists.scss +21 -0
  78. package/src/global/markdown.scss +33 -0
  79. package/src/global/mixins.scss +175 -0
  80. package/src/global/navbar.scss +280 -0
  81. package/src/global/print.scss +59 -0
  82. package/src/global/tables.scss +197 -0
  83. package/src/global/text.scss +141 -0
  84. package/src/global/vendor/AgPagination.tsx +133 -0
  85. package/src/global/vendor/_setup_bulma.scss +31 -0
  86. package/src/global/vendor/ag_grid.scss +140 -0
  87. package/src/headers/Hero.scss +56 -0
  88. package/src/headers/Hero.tsx +76 -0
  89. package/src/headers/PageHeader.scss +31 -0
  90. package/src/headers/PageHeader.tsx +39 -0
  91. package/src/headers/SiteHeader.tsx +136 -0
  92. package/src/helpers/address.tsx +46 -0
  93. package/src/helpers/blankApplication.ts +108 -0
  94. package/src/helpers/capitalize.tsx +7 -0
  95. package/src/helpers/dateToString.ts +11 -0
  96. package/src/helpers/debounce.ts +12 -0
  97. package/src/helpers/formOptions.tsx +229 -0
  98. package/src/helpers/formatYesNoLabel.ts +9 -0
  99. package/src/helpers/getTranslationWithArguments.ts +14 -0
  100. package/src/helpers/links.ts +7 -0
  101. package/src/helpers/localeRoute.tsx +13 -0
  102. package/src/helpers/mergeDeep.ts +12 -0
  103. package/src/helpers/nextjs.ts +7 -0
  104. package/src/helpers/numberOrdinal.ts +17 -0
  105. package/src/helpers/occupancyFormatting.tsx +46 -0
  106. package/src/helpers/pdfs.ts +19 -0
  107. package/src/helpers/photos.ts +19 -0
  108. package/src/helpers/preferences.tsx +426 -0
  109. package/src/helpers/resolveObject.ts +5 -0
  110. package/src/helpers/state.tsx +7 -0
  111. package/src/helpers/tableSummaries.tsx +80 -0
  112. package/src/helpers/translator.tsx +37 -0
  113. package/src/helpers/useKeyPress.ts +17 -0
  114. package/src/helpers/useMutate.ts +40 -0
  115. package/src/helpers/useOutsideClick.ts +25 -0
  116. package/src/helpers/validators.ts +3 -0
  117. package/src/icons/HeaderBadge.scss +29 -0
  118. package/src/icons/HeaderBadge.tsx +38 -0
  119. package/src/icons/Icon.scss +76 -0
  120. package/src/icons/Icon.tsx +145 -0
  121. package/src/icons/Icons.tsx +556 -0
  122. package/src/lists/PreferencesList.scss +72 -0
  123. package/src/lists/PreferencesList.tsx +60 -0
  124. package/src/locales/es.json +745 -0
  125. package/src/locales/general.json +1307 -0
  126. package/src/locales/general_OLD.json +868 -0
  127. package/src/locales/vi.json +745 -0
  128. package/src/locales/zh.json +745 -0
  129. package/src/navigation/Breadcrumbs.scss +25 -0
  130. package/src/navigation/Breadcrumbs.tsx +27 -0
  131. package/src/navigation/FooterNav.scss +47 -0
  132. package/src/navigation/FooterNav.tsx +19 -0
  133. package/src/navigation/LanguageNav.scss +32 -0
  134. package/src/navigation/LanguageNav.tsx +53 -0
  135. package/src/navigation/ProgressNav.scss +102 -0
  136. package/src/navigation/ProgressNav.tsx +50 -0
  137. package/src/navigation/TabNav.scss +38 -0
  138. package/src/navigation/TabNav.tsx +69 -0
  139. package/src/navigation/Tabs.scss +65 -0
  140. package/src/navigation/Tabs.tsx +93 -0
  141. package/src/navigation/UserNav.tsx +37 -0
  142. package/src/notifications/AlertBox.scss +78 -0
  143. package/src/notifications/AlertBox.tsx +79 -0
  144. package/src/notifications/AlertNotice.scss +58 -0
  145. package/src/notifications/AlertNotice.tsx +37 -0
  146. package/src/notifications/ApplicationStatus.scss +10 -0
  147. package/src/notifications/ApplicationStatus.tsx +64 -0
  148. package/src/notifications/ErrorMessage.tsx +15 -0
  149. package/src/notifications/SiteAlert.tsx +54 -0
  150. package/src/notifications/StatusAside.scss +11 -0
  151. package/src/notifications/StatusAside.tsx +25 -0
  152. package/src/notifications/StatusMessage.scss +25 -0
  153. package/src/notifications/StatusMessage.tsx +59 -0
  154. package/src/notifications/alertTypes.ts +7 -0
  155. package/src/notifications/index.ts +4 -0
  156. package/src/overlays/Drawer.scss +105 -0
  157. package/src/overlays/Drawer.tsx +51 -0
  158. package/src/overlays/LoadingOverlay.scss +25 -0
  159. package/src/overlays/LoadingOverlay.tsx +29 -0
  160. package/src/overlays/Modal.scss +55 -0
  161. package/src/overlays/Modal.tsx +61 -0
  162. package/src/overlays/Overlay.scss +50 -0
  163. package/src/overlays/Overlay.tsx +100 -0
  164. package/src/page_components/listing/AdditionalFees.tsx +56 -0
  165. package/src/page_components/listing/ListingCard.scss +47 -0
  166. package/src/page_components/listing/ListingCard.tsx +34 -0
  167. package/src/page_components/listing/ListingDetailHeader.tsx +25 -0
  168. package/src/page_components/listing/ListingDetails.tsx +29 -0
  169. package/src/page_components/listing/ListingMap.scss +36 -0
  170. package/src/page_components/listing/ListingMap.tsx +138 -0
  171. package/src/page_components/listing/ListingsGroup.scss +65 -0
  172. package/src/page_components/listing/ListingsGroup.tsx +49 -0
  173. package/src/page_components/listing/UnitTables.tsx +111 -0
  174. package/src/page_components/listing/listing_sidebar/ApplicationSection.tsx +49 -0
  175. package/src/page_components/listing/listing_sidebar/Apply.tsx +225 -0
  176. package/src/page_components/listing/listing_sidebar/LeasingAgent.tsx +77 -0
  177. package/src/page_components/listing/listing_sidebar/ListingUpdated.tsx +20 -0
  178. package/src/page_components/listing/listing_sidebar/ReferralApplication.tsx +28 -0
  179. package/src/page_components/listing/listing_sidebar/SidebarAddress.tsx +56 -0
  180. package/src/page_components/listing/listing_sidebar/Waitlist.tsx +94 -0
  181. package/src/page_components/listing/listing_sidebar/WhatToExpect.tsx +22 -0
  182. package/src/page_components/listing/listing_sidebar/events/DownloadLotteryResults.tsx +34 -0
  183. package/src/page_components/listing/listing_sidebar/events/EventDateSection.tsx +24 -0
  184. package/src/page_components/listing/listing_sidebar/events/LotteryResultsEvent.tsx +26 -0
  185. package/src/page_components/listing/listing_sidebar/events/OpenHouseEvent.tsx +27 -0
  186. package/src/page_components/listing/listing_sidebar/events/PublicLotteryEvent.tsx +22 -0
  187. package/src/prototypes/AppCard.scss +64 -0
  188. package/src/prototypes/Back.scss +19 -0
  189. package/src/prototypes/ButtonGroup.scss +6 -0
  190. package/src/prototypes/ButtonPager.scss +22 -0
  191. package/src/prototypes/FieldSection.scss +35 -0
  192. package/src/prototypes/FieldSection.tsx +31 -0
  193. package/src/prototypes/GridItem.tsx +15 -0
  194. package/src/prototypes/SideNav.scss +32 -0
  195. package/src/prototypes/SideNav.tsx +14 -0
  196. package/src/prototypes/SummaryCard.scss +34 -0
  197. package/src/sections/ContentSection.scss +15 -0
  198. package/src/sections/ContentSection.tsx +25 -0
  199. package/src/sections/FooterSection.scss +6 -0
  200. package/src/sections/FooterSection.tsx +16 -0
  201. package/src/sections/GridSection.scss +72 -0
  202. package/src/sections/GridSection.tsx +82 -0
  203. package/src/sections/InfoCardGrid.scss +45 -0
  204. package/src/sections/InfoCardGrid.tsx +20 -0
  205. package/src/sections/ListSection.scss +7 -0
  206. package/src/sections/ListSection.tsx +23 -0
  207. package/src/sections/MarkdownSection.scss +13 -0
  208. package/src/sections/MarkdownSection.tsx +21 -0
  209. package/src/sections/ResponsiveContentList.tsx +67 -0
  210. package/src/sections/ResponsiveWrappers.tsx +23 -0
  211. package/src/tables/GroupedTable.tsx +86 -0
  212. package/src/tables/MinimalTable.tsx +32 -0
  213. package/src/tables/ResponsiveTable.tsx +24 -0
  214. package/src/tables/StandardTable.tsx +229 -0
  215. package/src/text/Description.scss +52 -0
  216. package/src/text/Description.tsx +24 -0
  217. package/src/text/Message.scss +16 -0
  218. package/src/text/Message.tsx +16 -0
  219. package/src/text/Tag.scss +94 -0
  220. package/src/text/Tag.tsx +22 -0
  221. package/tailwind.config.js +128 -0
  222. package/tailwind.tosass.js +29 -0
  223. package/tsconfig.json +31 -0
@@ -0,0 +1,115 @@
1
+ import React, { useMemo } from "react"
2
+ import { ErrorMessage } from "../notifications/ErrorMessage"
3
+ import { UseFormMethods } from "react-hook-form"
4
+
5
+ export interface FieldProps {
6
+ error?: boolean
7
+ errorMessage?: string
8
+ className?: string
9
+ controlClassName?: string
10
+ caps?: boolean
11
+ primary?: boolean
12
+ readerOnly?: boolean
13
+ type?: string
14
+ id?: string
15
+ name: string
16
+ note?: string
17
+ label?: string
18
+ defaultValue?: string | number
19
+ onDrop?: (e: any) => boolean
20
+ onPaste?: (e: any) => boolean
21
+ placeholder?: string
22
+ register?: UseFormMethods["register"]
23
+ validation?: Record<string, any>
24
+ disabled?: boolean
25
+ prepend?: string
26
+ inputProps?: Record<string, unknown>
27
+ describedBy?: string
28
+ getValues?: UseFormMethods["getValues"]
29
+ setValue?: UseFormMethods["setValue"]
30
+ }
31
+
32
+ const Field = (props: FieldProps) => {
33
+ const classes = ["field"]
34
+ if (props.error) {
35
+ classes.push("error")
36
+ }
37
+
38
+ if (props.className) {
39
+ classes.push(props.className)
40
+ }
41
+
42
+ const controlClasses = ["control"]
43
+ if (props.controlClassName) {
44
+ controlClasses.push(props.controlClassName)
45
+ }
46
+
47
+ const formatValue = () => {
48
+ if (props.getValues && props.setValue) {
49
+ const currencyValue = props.getValues(props.name)
50
+ const numericIncome = parseFloat(currencyValue)
51
+ if (!isNaN(numericIncome)) {
52
+ props.setValue(props.name, numericIncome.toFixed(2))
53
+ }
54
+ }
55
+ }
56
+
57
+ let inputProps = { ...props.inputProps }
58
+ if (props.type === "currency") inputProps = { ...inputProps, step: 0.01, onBlur: formatValue }
59
+
60
+ const type = (props.type === "currency" && "number") || props.type || "text"
61
+ const isRadioOrCheckbox = ["radio", "checkbox"].includes(type)
62
+
63
+ const label = useMemo(() => {
64
+ const labelClasses = ["label"]
65
+ if (props.caps) labelClasses.push("field-label--caps")
66
+ if (props.primary) labelClasses.push("text-primary")
67
+ if (props.readerOnly) labelClasses.push("sr-only")
68
+
69
+ return (
70
+ <label className={labelClasses.join(" ")} htmlFor={props.id || props.name}>
71
+ {props.label}
72
+ </label>
73
+ )
74
+ }, [props.caps, props.primary, props.readerOnly, props.id, props.name, props.label])
75
+
76
+ const idOrName = props.id || props.name
77
+
78
+ let note = <></>
79
+ if (props.note) {
80
+ note = <p className="field-note mb-4">{props.note}</p>
81
+ }
82
+
83
+ return (
84
+ <div className={classes.join(" ")}>
85
+ {!isRadioOrCheckbox && label}
86
+ {note}
87
+ <div className={controlClasses.join(" ")}>
88
+ {props.prepend && <span className="prepend">{props.prepend}</span>}
89
+ <input
90
+ aria-describedby={props.describedBy ? props.describedBy : `${idOrName}-error`}
91
+ aria-invalid={!!props.error || false}
92
+ className="input"
93
+ type={type}
94
+ id={idOrName}
95
+ name={props.name}
96
+ defaultValue={props.defaultValue}
97
+ placeholder={props.placeholder}
98
+ ref={props.register && props.register(props.validation)}
99
+ disabled={props.disabled}
100
+ onPaste={props.onPaste}
101
+ onDrop={props.onDrop}
102
+ {...inputProps}
103
+ />
104
+ {isRadioOrCheckbox && label}
105
+ </div>
106
+ {props.errorMessage && (
107
+ <ErrorMessage id={`${idOrName}-error`} error={props.error}>
108
+ {props.errorMessage}
109
+ </ErrorMessage>
110
+ )}
111
+ </div>
112
+ )
113
+ }
114
+
115
+ export { Field as default, Field }
@@ -0,0 +1,82 @@
1
+ import React from "react"
2
+ import { ErrorMessage } from "../notifications/ErrorMessage"
3
+ import { UseFormMethods } from "react-hook-form"
4
+
5
+ interface FieldSingle {
6
+ id: string
7
+ label: string
8
+ value?: string
9
+ defaultChecked?: boolean
10
+ note?: string
11
+ }
12
+
13
+ interface FieldGroupProps {
14
+ error?: boolean
15
+ errorMessage?: string
16
+ name: string
17
+ type?: string
18
+ groupLabel?: string
19
+ fields?: FieldSingle[]
20
+ groupNote?: string
21
+ register: UseFormMethods["register"]
22
+ validation?: Record<string, unknown>
23
+ fieldGroupClassName?: string
24
+ fieldClassName?: string
25
+ fieldLabelClassName?: string
26
+ }
27
+
28
+ const FieldGroup = ({
29
+ name,
30
+ groupLabel,
31
+ fields,
32
+ type = "checkbox",
33
+ validation = {},
34
+ error,
35
+ errorMessage,
36
+ groupNote,
37
+ register,
38
+ fieldGroupClassName,
39
+ fieldClassName,
40
+ fieldLabelClassName,
41
+ }: FieldGroupProps) => {
42
+ // Always align two-option radio groups side by side
43
+ if (fields?.length === 2) {
44
+ fieldGroupClassName = `${fieldGroupClassName} flex`
45
+ fieldClassName = `${fieldClassName} flex-initial mr-4`
46
+ }
47
+ return (
48
+ <>
49
+ {groupLabel && <label className="field-label--caps">{groupLabel}</label>}
50
+ {groupNote && <p className="field-note mb-4">{groupNote}</p>}
51
+
52
+ <div className={`field ${error && "error"} ${fieldGroupClassName || ""}`}>
53
+ {fields?.map((item) => (
54
+ <div className={`field ${fieldClassName || ""}`} key={item.id}>
55
+ <input
56
+ aria-describedby={`${name}-error`}
57
+ aria-invalid={!!error || false}
58
+ type={type}
59
+ id={item.id}
60
+ defaultValue={item.value || item.id}
61
+ name={name}
62
+ defaultChecked={item.defaultChecked || false}
63
+ ref={register(validation)}
64
+ />
65
+ <label htmlFor={item.id} className={`font-semibold ${fieldLabelClassName}`}>
66
+ {item.label}
67
+ </label>
68
+ {item.note && <span className={"field-note font-normal"}>{item.note}</span>}
69
+ </div>
70
+ ))}
71
+
72
+ {error && errorMessage && (
73
+ <ErrorMessage id={`${name}-error`} error={error}>
74
+ {errorMessage}
75
+ </ErrorMessage>
76
+ )}
77
+ </div>
78
+ </>
79
+ )
80
+ }
81
+
82
+ export { FieldGroup as default, FieldGroup }
@@ -0,0 +1,22 @@
1
+ import React from "react"
2
+
3
+ interface FormProps {
4
+ children: React.ReactNode
5
+ id?: string
6
+ className?: string
7
+ onSubmit: () => unknown
8
+ }
9
+
10
+ const Form = ({ id, children, className, onSubmit }: FormProps) => {
11
+ function onKeyPress(e: React.KeyboardEvent<HTMLElement>) {
12
+ return e.key === "Enter" && !(e.target instanceof HTMLButtonElement) && e.preventDefault()
13
+ }
14
+
15
+ return (
16
+ <form id={id} className={className} onSubmit={onSubmit} onKeyPress={onKeyPress} noValidate>
17
+ {children}
18
+ </form>
19
+ )
20
+ }
21
+
22
+ export { Form as default, Form }
@@ -0,0 +1,41 @@
1
+ import React from "react"
2
+ import { t } from "../helpers/translator"
3
+ import { Icon, IconFillColors } from "../icons/Icon"
4
+ import { ViewItem } from "../blocks/ViewItem"
5
+
6
+ export interface HouseholdMemberFormProps {
7
+ editMember?: (memberId: number | undefined) => void
8
+ editMode?: boolean
9
+ memberFirstName: string
10
+ memberId?: number
11
+ memberLastName: string
12
+ subtitle: string
13
+ }
14
+
15
+ const HouseholdMemberForm = (props: HouseholdMemberFormProps) => {
16
+ const editMode = props.editMode !== false && props.editMember // undefined should default to true
17
+
18
+ return (
19
+ <ViewItem helper={props.subtitle} className="pb-4 border-b text-left">
20
+ {props.memberFirstName} {props.memberLastName}
21
+ {editMode ? (
22
+ <button
23
+ id="edit-member"
24
+ className="edit-link"
25
+ onClick={() => props.editMember && props.editMember(props.memberId)}
26
+ >
27
+ {t("t.edit")}
28
+ </button>
29
+ ) : (
30
+ <Icon
31
+ className="ml-2 absolute top-0 right-0"
32
+ size="medium"
33
+ symbol="lock"
34
+ fill={IconFillColors.primary}
35
+ />
36
+ )}
37
+ </ViewItem>
38
+ )
39
+ }
40
+
41
+ export { HouseholdMemberForm as default, HouseholdMemberForm }
@@ -0,0 +1,74 @@
1
+ import React from "react"
2
+ import { t } from "../helpers/translator"
3
+ import { ErrorMessage } from "../notifications/ErrorMessage"
4
+ import { AlertBox, AlertNotice } from "../notifications"
5
+ import { UseFormMethods } from "react-hook-form"
6
+
7
+ export interface HouseholdSizeFieldProps {
8
+ assistanceUrl: string
9
+ clearErrors: () => void
10
+ error: any
11
+ householdSize: number
12
+ householdSizeMax: number
13
+ householdSizeMin: number
14
+ register: UseFormMethods["register"]
15
+ validate: boolean
16
+ }
17
+
18
+ const HouseholdSizeField = (props: HouseholdSizeFieldProps) => {
19
+ const {
20
+ householdSize,
21
+ householdSizeMax,
22
+ householdSizeMin,
23
+ validate,
24
+ register,
25
+ clearErrors,
26
+ error,
27
+ assistanceUrl,
28
+ } = props
29
+
30
+ return (
31
+ <>
32
+ {householdSizeMax && validate && (
33
+ <>
34
+ <span className="hidden">
35
+ <input
36
+ className="invisible"
37
+ type="number"
38
+ id="householdSize"
39
+ name="householdSize"
40
+ defaultValue={householdSize}
41
+ ref={
42
+ householdSizeMax
43
+ ? register({
44
+ min: {
45
+ value: householdSizeMin || 0,
46
+ message: t("errors.householdTooSmall"),
47
+ },
48
+ max: {
49
+ value: householdSizeMax,
50
+ message: t("errors.householdTooBig"),
51
+ },
52
+ })
53
+ : register
54
+ }
55
+ />
56
+ </span>
57
+ <ErrorMessage id={"householdsize-error"} error={error}>
58
+ <AlertBox type="alert" inverted onClose={() => clearErrors()}>
59
+ {t("application.household.dontQualifyHeader")}
60
+ </AlertBox>
61
+ <AlertNotice title={error?.message} type="alert" inverted>
62
+ <p className="mb-2">{t("application.household.dontQualifyInfo")}</p>
63
+ <p>
64
+ <a href={assistanceUrl}>{t("nav.getAssistance")}</a>
65
+ </p>
66
+ </AlertNotice>
67
+ </ErrorMessage>
68
+ </>
69
+ )}
70
+ </>
71
+ )
72
+ }
73
+
74
+ export { HouseholdSizeField as default, HouseholdSizeField }
@@ -0,0 +1,69 @@
1
+ import React from "react"
2
+ import { ErrorMessage } from "../notifications/ErrorMessage"
3
+ import { PhoneMask } from "./PhoneMask"
4
+ import { Controller } from "react-hook-form"
5
+
6
+ export const PhoneField = (props: {
7
+ error?: boolean
8
+ errorMessage?: string
9
+ controlClassName?: string
10
+ id?: string
11
+ name: string
12
+ label?: string
13
+ caps?: boolean
14
+ readerOnly?: boolean
15
+ placeholder?: string
16
+ defaultValue?: string
17
+ control?: any
18
+ disabled?: boolean
19
+ required?: boolean
20
+ mask?: (args: any) => JSX.Element
21
+ }) => {
22
+ const labelClasses = ["label"]
23
+ if (props.caps) labelClasses.push("field-label--caps")
24
+ if (props.readerOnly) labelClasses.push("sr-only")
25
+ /**
26
+ * we need to handle two different use cases,
27
+ * 1. supplying the render function
28
+ * 2. allowing for "as"
29
+ * So shared props below with two different Controller renders
30
+ */
31
+ const controllerProps = {
32
+ className: "input",
33
+ id: props.id,
34
+ name: props.name,
35
+ placeholder: props.placeholder,
36
+ defaultValue: props.defaultValue || "",
37
+ disabled: props.disabled,
38
+ control: props.control,
39
+ rules: {
40
+ validate: {
41
+ inputTel: (v: string) => {
42
+ if (!props.required && !v?.length) return true
43
+
44
+ const dropdown = document.querySelector<HTMLInputElement>(
45
+ "#" + props.name.replace(".", "\\.")
46
+ )
47
+ if (!dropdown || dropdown.disabled) return true
48
+ return v?.match(/\d/g)?.length == 10 ? true : false
49
+ },
50
+ },
51
+ },
52
+ }
53
+
54
+ return (
55
+ <div className={"field " + (props.error ? "error" : "")}>
56
+ {props.label && <label className={labelClasses.join(" ")}>{props.label}</label>}
57
+ <div className={props.controlClassName}>
58
+ {props.mask ? (
59
+ <Controller {...controllerProps} render={props.mask} />
60
+ ) : (
61
+ <Controller {...controllerProps} as={PhoneMask} />
62
+ )}
63
+ <ErrorMessage id={`${props.id}-error`} error={props.error}>
64
+ {props.errorMessage}
65
+ </ErrorMessage>
66
+ </div>
67
+ </div>
68
+ )
69
+ }
@@ -0,0 +1,24 @@
1
+ import React from "react"
2
+ import MaskedInput from "react-text-mask"
3
+
4
+ export const PhoneMask = (props: any) => {
5
+ const { value, onChange, name, disabled, placeholder } = props
6
+
7
+ return (
8
+ <MaskedInput
9
+ mask={["(", /[1-9]/, /\d/, /\d/, ")", " ", /\d/, /\d/, /\d/, "-", /\d/, /\d/, /\d/, /\d/]}
10
+ className="input"
11
+ type="tel"
12
+ placeholder={placeholder || "(555) 555-5555"}
13
+ guide={false}
14
+ id={name}
15
+ value={value}
16
+ name={name}
17
+ disabled={disabled}
18
+ onChange={(e: any) => {
19
+ e.persist()
20
+ onChange(e)
21
+ }}
22
+ />
23
+ )
24
+ }
@@ -0,0 +1,80 @@
1
+ import React from "react"
2
+ import { ErrorMessage } from "../notifications/ErrorMessage"
3
+ import { FormOptions, SelectOption } from "../helpers/formOptions"
4
+ import { UseFormMethods } from "react-hook-form"
5
+
6
+ interface SelectProps {
7
+ error?: boolean
8
+ errorMessage?: string
9
+ controlClassName?: string
10
+ labelClassName?: string
11
+ type?: string
12
+ id?: string
13
+ name: string
14
+ label?: string
15
+ defaultValue?: string
16
+ placeholder?: string
17
+ register?: UseFormMethods["register"]
18
+ validation?: Record<string, unknown>
19
+ disabled?: boolean
20
+ options: (string | SelectOption)[]
21
+ keyPrefix?: string
22
+ describedBy?: string
23
+ inputProps?: Record<string, unknown>
24
+ noDefault?: boolean
25
+ }
26
+
27
+ export const Select = ({
28
+ error,
29
+ errorMessage,
30
+ controlClassName,
31
+ labelClassName,
32
+ id,
33
+ name,
34
+ label,
35
+ placeholder,
36
+ register,
37
+ validation,
38
+ disabled,
39
+ options,
40
+ keyPrefix,
41
+ describedBy,
42
+ inputProps,
43
+ noDefault = false,
44
+ }: SelectProps) => {
45
+ if (noDefault === false) {
46
+ inputProps = inputProps ? { ...inputProps } : {}
47
+ inputProps.defaultValue = ""
48
+ }
49
+ return (
50
+ <div className={"field " + (error ? "error" : "")}>
51
+ <label className={labelClassName} htmlFor={id}>
52
+ {label}
53
+ </label>
54
+ <div className={controlClassName}>
55
+ <select
56
+ className="input"
57
+ id={id || name}
58
+ name={name}
59
+ aria-describedby={describedBy ? describedBy : `${id}-error`}
60
+ aria-invalid={!!error || false}
61
+ ref={register && register(validation)}
62
+ disabled={disabled}
63
+ {...inputProps}
64
+ >
65
+ {placeholder && (
66
+ <option value="" disabled>
67
+ {placeholder}
68
+ </option>
69
+ )}
70
+ <FormOptions options={options} keyPrefix={keyPrefix} />
71
+ </select>
72
+ </div>
73
+ {error && errorMessage && (
74
+ <ErrorMessage id={`${id}-error`} error={error}>
75
+ {errorMessage}
76
+ </ErrorMessage>
77
+ )}
78
+ </div>
79
+ )
80
+ }