@bloom-housing/ui-components 4.4.1-alpha.9 → 5.0.0

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 (43) hide show
  1. package/CHANGELOG.md +930 -0
  2. package/index.ts +6 -5
  3. package/package.json +8 -3
  4. package/src/actions/Button.tsx +2 -2
  5. package/src/actions/ExpandableContent.tsx +9 -5
  6. package/src/blocks/ImageCard.tsx +3 -3
  7. package/src/blocks/StandardCard.docs.mdx +34 -0
  8. package/src/blocks/StandardCard.scss +33 -0
  9. package/src/blocks/StandardCard.tsx +37 -0
  10. package/src/config/index.ts +0 -1
  11. package/src/forms/FieldGroup.tsx +1 -1
  12. package/src/forms/HouseholdSizeField.tsx +2 -1
  13. package/src/global/tables.scss +7 -1
  14. package/src/helpers/formOptions.tsx +0 -9
  15. package/src/helpers/preferences.tsx +3 -314
  16. package/src/icons/Icon.tsx +22 -3
  17. package/src/locales/es.json +18 -0
  18. package/src/locales/general.json +23 -0
  19. package/src/{prototypes → navigation}/SideNav.scss +15 -9
  20. package/src/navigation/SideNav.tsx +39 -0
  21. package/src/notifications/ApplicationStatus.tsx +2 -2
  22. package/src/overlays/Drawer.tsx +1 -1
  23. package/src/overlays/Modal.scss +5 -0
  24. package/src/overlays/Modal.tsx +19 -3
  25. package/src/page_components/listing/ListingsGroup.tsx +2 -2
  26. package/src/page_components/listing/listing_sidebar/ExpandableSection.tsx +34 -0
  27. package/src/page_components/listing/listing_sidebar/QuantityRowSection.tsx +1 -0
  28. package/src/page_components/sign-in/FormSignInMFACode.tsx +7 -3
  29. package/src/page_components/sign-in/FormSignInMFAType.tsx +7 -8
  30. package/src/page_components/sign-in/FormTerms.tsx +9 -27
  31. package/src/tables/StandardTable.tsx +17 -4
  32. package/src/authentication/AuthContext.ts +0 -386
  33. package/src/authentication/RequireLogin.tsx +0 -89
  34. package/src/authentication/index.ts +0 -5
  35. package/src/authentication/timeout.tsx +0 -128
  36. package/src/authentication/token.ts +0 -17
  37. package/src/authentication/useRequireLoggedInUser.ts +0 -19
  38. package/src/config/ConfigContext.tsx +0 -36
  39. package/src/helpers/tableSummaries.tsx +0 -104
  40. package/src/notifications/index.ts +0 -4
  41. package/src/page_components/listing/UnitTables.tsx +0 -122
  42. package/src/page_components/listing/listing_sidebar/WhatToExpect.tsx +0 -22
  43. package/src/prototypes/SideNav.tsx +0 -14
@@ -1,11 +1,4 @@
1
1
  import React from "react"
2
- import {
3
- InputType,
4
- ApplicationPreference,
5
- FormMetadataOptions,
6
- Preference,
7
- ListingPreference,
8
- } from "@bloom-housing/backend-core/types"
9
2
  import { UseFormMethods } from "react-hook-form"
10
3
  import {
11
4
  t,
@@ -14,133 +7,23 @@ import {
14
7
  GridCell,
15
8
  Field,
16
9
  Select,
17
- SelectOption,
18
10
  resolveObject,
19
11
  } from "@bloom-housing/ui-components"
20
12
 
21
- type ExtraFieldProps = {
22
- metaKey: string
23
- optionKey: string
24
- extraKey: string
25
- type: InputType
26
- register: UseFormMethods["register"]
27
- errors?: UseFormMethods["errors"]
28
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
- hhMembersOptions?: SelectOption[]
30
- stateKeys: string[]
31
- }
32
-
33
13
  type FormAddressProps = {
34
14
  subtitle: string
35
15
  dataKey: string
36
- type: AddressType
16
+ enableMailCheckbox?: boolean
37
17
  register: UseFormMethods["register"]
38
18
  errors?: UseFormMethods["errors"]
39
19
  required?: boolean
40
20
  stateKeys: string[]
41
21
  }
42
22
 
43
- type AddressType =
44
- | "residence"
45
- | "residence-member"
46
- | "work"
47
- | "mailing"
48
- | "alternate"
49
- | "preference"
50
-
51
- /*
52
- Path to the preferences from listing object
53
- */
54
- export const PREFERENCES_FORM_PATH = "application.preferences.options"
55
- export const PREFERENCES_NONE_FORM_PATH = "application.preferences.none"
56
- /*
57
- It generates inner fields for preferences form
58
- */
59
- export const ExtraField = ({
60
- metaKey,
61
- optionKey,
62
- extraKey,
63
- type,
64
- register,
65
- errors,
66
- hhMembersOptions,
67
- stateKeys,
68
- }: ExtraFieldProps) => {
69
- const FIELD_NAME = `${PREFERENCES_FORM_PATH}.${metaKey}.${optionKey}.${extraKey}`
70
-
71
- return (
72
- <div className="my-4" key={FIELD_NAME}>
73
- {(() => {
74
- if (type === InputType.text) {
75
- return (
76
- <Field
77
- id={FIELD_NAME}
78
- name={FIELD_NAME}
79
- type="text"
80
- label={t(`application.preferences.options.${extraKey}`)}
81
- register={register}
82
- validation={{ required: true }}
83
- error={!!resolveObject(FIELD_NAME, errors)}
84
- errorMessage={t("errors.requiredFieldError")}
85
- />
86
- )
87
- } else if (type === InputType.address) {
88
- return (
89
- <div className="pb-4">
90
- <FormAddress
91
- subtitle={t("application.preferences.options.address")}
92
- dataKey={FIELD_NAME}
93
- type="preference"
94
- register={register}
95
- errors={errors}
96
- required={true}
97
- stateKeys={stateKeys}
98
- />
99
- </div>
100
- )
101
- } else if (type === InputType.hhMemberSelect) {
102
- if (!hhMembersOptions)
103
- return (
104
- <Field
105
- id={FIELD_NAME}
106
- name={FIELD_NAME}
107
- type="text"
108
- label={t(`application.preferences.options.${extraKey}`)}
109
- register={register}
110
- validation={{ required: true }}
111
- error={!!resolveObject(FIELD_NAME, errors)}
112
- errorMessage={t("errors.requiredFieldError")}
113
- />
114
- )
115
-
116
- return (
117
- <>
118
- <Select
119
- id={FIELD_NAME}
120
- name={FIELD_NAME}
121
- label={t(`application.preferences.options.${extraKey}`)}
122
- register={register}
123
- controlClassName="control"
124
- placeholder={t("t.selectOne")}
125
- options={hhMembersOptions}
126
- validation={{ required: true }}
127
- error={!!resolveObject(FIELD_NAME, errors)}
128
- errorMessage={t("errors.requiredFieldError")}
129
- />
130
- </>
131
- )
132
- }
133
-
134
- return <></>
135
- })()}
136
- </div>
137
- )
138
- }
139
-
140
23
  export const FormAddress = ({
141
24
  subtitle,
142
25
  dataKey,
143
- type,
26
+ enableMailCheckbox = false,
144
27
  register,
145
28
  errors,
146
29
  required,
@@ -225,7 +108,7 @@ export const FormAddress = ({
225
108
  </ViewItem>
226
109
  </GridCell>
227
110
 
228
- {type === "residence" && (
111
+ {enableMailCheckbox && (
229
112
  <GridCell span={2}>
230
113
  <Field
231
114
  id="application.sendMailToMailingAddress"
@@ -240,197 +123,3 @@ export const FormAddress = ({
240
123
  </>
241
124
  )
242
125
  }
243
-
244
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
245
- export const mapPreferencesToApi = (data: Record<string, any>) => {
246
- if (!data.application?.preferences) return []
247
-
248
- const CLAIMED_KEY = "claimed"
249
-
250
- const preferencesFormData = data.application.preferences.options
251
-
252
- const keys = Object.keys(preferencesFormData)
253
-
254
- return keys.map((key) => {
255
- const currentPreference = preferencesFormData[key]
256
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
257
- const currentPreferenceValues = Object.values(currentPreference) as Record<string, any>
258
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
259
- const claimed = currentPreferenceValues.map((c: { claimed: any }) => c.claimed).includes(true)
260
-
261
- const options = Object.keys(currentPreference).map((option) => {
262
- const currentOption = currentPreference[option]
263
-
264
- // count keys except claimed
265
- const extraKeys = Object.keys(currentOption).filter((item) => item !== CLAIMED_KEY)
266
-
267
- const response = {
268
- key: option,
269
- checked: currentOption[CLAIMED_KEY],
270
- }
271
-
272
- // assign extra data and detect data type
273
- if (extraKeys.length) {
274
- const extraData = extraKeys.map((extraKey) => {
275
- const type = (() => {
276
- if (typeof currentOption[extraKey] === "boolean") return InputType.boolean
277
- // if object includes "city" property, it should be an address
278
- if (Object.keys(currentOption[extraKey]).includes("city")) return InputType.address
279
-
280
- return InputType.text
281
- })()
282
-
283
- return {
284
- key: extraKey,
285
- type,
286
- value: currentOption[extraKey],
287
- }
288
- })
289
-
290
- Object.assign(response, { extraData })
291
- } else {
292
- Object.assign(response, { extraData: [] })
293
- }
294
-
295
- return response
296
- })
297
-
298
- return {
299
- key,
300
- claimed,
301
- options,
302
- }
303
- })
304
- }
305
-
306
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
307
- export const mapApiToPreferencesForm = (preferences: ApplicationPreference[]) => {
308
- const preferencesFormData = {}
309
-
310
- preferences.forEach((item) => {
311
- const options = item.options.reduce((acc, curr) => {
312
- // extraData which comes from the API is an array, in the form we expect an object
313
- const extraData =
314
- curr?.extraData?.reduce((extraAcc, extraCurr) => {
315
- // value - it can be string or nested address object
316
- const value = extraCurr.value
317
- Object.assign(extraAcc, {
318
- [extraCurr.key]: value,
319
- })
320
-
321
- return extraAcc
322
- }, {}) || {}
323
-
324
- // each form option has "claimed" property - it's "checked" property in the API
325
- const claimed = curr.checked
326
-
327
- Object.assign(acc, {
328
- [curr.key]: {
329
- claimed,
330
- ...extraData,
331
- },
332
- })
333
- return acc
334
- }, {})
335
-
336
- Object.assign(preferencesFormData, {
337
- [item.key]: options,
338
- })
339
- })
340
-
341
- const noneValues = preferences.reduce((acc, item) => {
342
- const isClaimed = item.claimed
343
-
344
- Object.assign(acc, {
345
- [`${item.key}-none`]: !isClaimed,
346
- })
347
-
348
- return acc
349
- }, {})
350
-
351
- return { options: preferencesFormData, none: noneValues }
352
- }
353
-
354
- /*
355
- It generates checkbox name in proper prefrences structure
356
- */
357
- export const getPreferenceOptionName = (key: string, metaKey: string, noneOption?: boolean) => {
358
- if (noneOption) return getExclusivePreferenceOptionName(key)
359
- else return getNormalPreferenceOptionName(metaKey, key)
360
- }
361
-
362
- export const getNormalPreferenceOptionName = (metaKey: string, key: string) => {
363
- return `${PREFERENCES_FORM_PATH}.${metaKey}.${key}.claimed`
364
- }
365
-
366
- export const getExclusivePreferenceOptionName = (key: string | undefined) => {
367
- return `${PREFERENCES_NONE_FORM_PATH}.${key}-none`
368
- }
369
-
370
- export type ExclusiveKey = {
371
- optionKey: string
372
- preferenceKey: string | undefined
373
- }
374
- /*
375
- Create an array of all exclusive keys from a preference set
376
- */
377
- export const getExclusiveKeys = (preferences: ListingPreference[]) => {
378
- const exclusive: ExclusiveKey[] = []
379
- preferences?.forEach((listingPreference) => {
380
- listingPreference.preference?.formMetadata?.options.forEach((option: FormMetadataOptions) => {
381
- if (option.exclusive)
382
- exclusive.push({
383
- optionKey: getPreferenceOptionName(
384
- option.key,
385
- listingPreference.preference?.formMetadata?.key ?? ""
386
- ),
387
- preferenceKey: listingPreference.preference?.formMetadata?.key,
388
- })
389
- })
390
- if (!listingPreference.preference?.formMetadata?.hideGenericDecline)
391
- exclusive.push({
392
- optionKey: getExclusivePreferenceOptionName(
393
- listingPreference.preference?.formMetadata?.key
394
- ),
395
- preferenceKey: listingPreference.preference?.formMetadata?.key,
396
- })
397
- })
398
- return exclusive
399
- }
400
-
401
- const uncheckPreference = (
402
- metaKey: string,
403
- options: FormMetadataOptions[] | undefined,
404
- setValue: (key: string, value: boolean) => void
405
- ) => {
406
- options?.forEach((option) => {
407
- setValue(getPreferenceOptionName(option.key, metaKey), false)
408
- })
409
- }
410
-
411
- /*
412
- Set the value of an exclusive checkbox, unchecking all the appropriate boxes in response to the value
413
- */
414
- export const setExclusive = (
415
- value: boolean,
416
- setValue: (key: string, value: boolean) => void,
417
- exclusiveKeys: ExclusiveKey[],
418
- key: string,
419
- preference: Preference
420
- ) => {
421
- if (value) {
422
- // Uncheck all other keys if setting an exclusive key to true
423
- uncheckPreference(
424
- preference?.formMetadata?.key ?? "",
425
- preference?.formMetadata?.options,
426
- setValue
427
- )
428
- setValue(key ?? "", true)
429
- } else {
430
- // Uncheck all exclusive keys if setting a normal key to true
431
- exclusiveKeys.forEach((thisKey) => {
432
- if (thisKey.preferenceKey === preference?.formMetadata?.key)
433
- setValue(thisKey.optionKey, false)
434
- })
435
- }
436
- }
@@ -1,4 +1,6 @@
1
1
  import * as React from "react"
2
+ import { IconDefinition } from "@fortawesome/fontawesome-svg-core"
3
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
2
4
  import "./Icon.scss"
3
5
  import {
4
6
  Application,
@@ -130,19 +132,22 @@ const IconMap = {
130
132
 
131
133
  export type IconTypes = keyof typeof IconMap
132
134
 
135
+ export type UniversalIconType = IconTypes | IconDefinition
136
+
133
137
  export type IconFill = "white" | "primary"
134
138
 
135
139
  export const IconFillColors = {
136
140
  white: "#ffffff",
137
141
  black: "#000000",
138
142
  primary: "#0077DA",
143
+ alert: "#b91c1c",
139
144
  }
140
145
 
141
146
  export type IconSize = "tiny" | "small" | "base" | "medium" | "large" | "xlarge" | "2xl" | "3xl"
142
147
 
143
148
  export interface IconProps {
144
149
  size: IconSize
145
- symbol: IconTypes
150
+ symbol: UniversalIconType
146
151
  className?: string
147
152
  fill?: string
148
153
  ariaHidden?: boolean
@@ -155,9 +160,14 @@ const Icon = (props: IconProps) => {
155
160
  if (props.className) wrapperClasses.push(props.className)
156
161
  if (props.symbol == "spinner") wrapperClasses.push("spinner-animation")
157
162
 
158
- const SpecificIcon = IconMap[props.symbol]
163
+ const SpecificIcon =
164
+ typeof props.symbol === "string" ? (
165
+ IconMap[props.symbol as string]
166
+ ) : (
167
+ <FontAwesomeIcon icon={props.symbol} />
168
+ )
159
169
 
160
- return (
170
+ return typeof props.symbol === "string" ? (
161
171
  <span
162
172
  className={wrapperClasses.join(" ")}
163
173
  aria-hidden={props.ariaHidden}
@@ -165,6 +175,15 @@ const Icon = (props: IconProps) => {
165
175
  >
166
176
  <SpecificIcon fill={props.fill ? props.fill : undefined} />
167
177
  </span>
178
+ ) : (
179
+ <span
180
+ className={wrapperClasses.join(" ")}
181
+ aria-hidden={props.ariaHidden}
182
+ data-test-id={props.dataTestId ?? null}
183
+ style={{ color: props.fill }}
184
+ >
185
+ {SpecificIcon}
186
+ </span>
168
187
  )
169
188
  }
170
189
 
@@ -394,6 +394,24 @@
394
394
  "authentication.timeout.signOutMessage": "Su seguridad es importante para nosotros. Concluimos su sesión debido a inactividad. Sírvase iniciar sesión para continuar.",
395
395
  "authentication.timeout.text": "Para proteger su identidad, su sesión concluirá en un minuto debido a inactividad. Si decide no responder, perderá toda la información que no haya guardado y concluirá su sesión.",
396
396
  "config.routePrefix": "es",
397
+ "eligibility.accessibility.acInUnit": "CA en la unidad",
398
+ "eligibility.accessibility.accessibleParking": "Estacionamiento Accesible",
399
+ "eligibility.accessibility.barrierFreeEntrance": "Entrada sin barreras",
400
+ "eligibility.accessibility.description": "Algunas propiedades tienen características de accesibilidad que otras pueden no tener.",
401
+ "eligibility.accessibility.elevator": "Ascensor",
402
+ "eligibility.accessibility.grabBars": "Barras de apoyo",
403
+ "eligibility.accessibility.hearing": "Audiencia",
404
+ "eligibility.accessibility.heatingInUnit": "Calefacción en Unidad",
405
+ "eligibility.accessibility.inUnitWasherDryer": "Lavadora y secadora en el apartamento",
406
+ "eligibility.accessibility.laundryInBuilding": "Lavandería en el edificio",
407
+ "eligibility.accessibility.mobility": "Movilidad",
408
+ "eligibility.accessibility.parkingOnSite": "Estacionamiento en el lugar",
409
+ "eligibility.accessibility.prompt": "¿Necesita funciones de accesibilidad adicionales?",
410
+ "eligibility.accessibility.rollInShower": "Rollo en la ducha",
411
+ "eligibility.accessibility.serviceAnimalsAllowed": "Se admiten animales de servicio",
412
+ "eligibility.accessibility.title": "Funciones de accesibilidad",
413
+ "eligibility.accessibility.visual": "Visual",
414
+ "eligibility.accessibility.wheelchairRamp": "Rampa para silla de ruedas",
397
415
  "errors.agreeError": "Debe estar de acuerdo con los términos para poder continuar",
398
416
  "errors.alert.badRequest": "¡Oops! Parece que algo salió mal. Por favor, inténtelo de nuevo. Comuníquese con su departamento de vivienda si sigue teniendo problemas.",
399
417
  "errors.alert.timeoutPleaseTryAgain": "¡Oops! Parece que algo salió mal. Por favor, inténtelo de nuevo.",
@@ -515,6 +515,24 @@
515
515
  "authentication.timeout.signOutMessage": "We care about your security. We logged you out due to inactivity. Please sign in to continue.",
516
516
  "authentication.timeout.text": "To protect your identity, your session will expire in one minute due to inactivity. You will lose any unsaved information and be logged out if you choose not to respond.",
517
517
  "config.routePrefix": "",
518
+ "eligibility.accessibility.acInUnit": "AC in Unit",
519
+ "eligibility.accessibility.accessibleParking": "Accessible Parking",
520
+ "eligibility.accessibility.barrierFreeEntrance": "Barrier Free Entrance",
521
+ "eligibility.accessibility.description": "Some properties have accessibility features that others may not have.",
522
+ "eligibility.accessibility.elevator": "Elevator",
523
+ "eligibility.accessibility.grabBars": "Grab Bars",
524
+ "eligibility.accessibility.hearing": "Hearing",
525
+ "eligibility.accessibility.heatingInUnit": "Heating in Unit",
526
+ "eligibility.accessibility.inUnitWasherDryer": "In Unit Washer Dryer",
527
+ "eligibility.accessibility.laundryInBuilding": "Laundry in Building",
528
+ "eligibility.accessibility.mobility": "Mobility",
529
+ "eligibility.accessibility.parkingOnSite": "Parking On Site",
530
+ "eligibility.accessibility.prompt": "Do you require additional accessibility features?",
531
+ "eligibility.accessibility.rollInShower": "Roll in Shower",
532
+ "eligibility.accessibility.serviceAnimalsAllowed": "Service Animals Allowed",
533
+ "eligibility.accessibility.title": "Accessibility Features",
534
+ "eligibility.accessibility.visual": "Visual",
535
+ "eligibility.accessibility.wheelchairRamp": "Wheelchair Ramp",
518
536
  "errors.agreeError": "You must agree to the terms in order to continue",
519
537
  "errors.alert.badRequest": "Looks like something went wrong. Please try again. \n\nContact your housing department if you're still experiencing issues.",
520
538
  "errors.alert.timeoutPleaseTryAgain": "Oops! Looks like something went wrong. Please try again.",
@@ -592,8 +610,10 @@
592
610
  "listings.apply.submitPaperNoDueDateNoPostMark": "%{developer} is not responsible for lost or delayed mail.",
593
611
  "listings.apply.submitPaperNoDueDatePostMark": "Applications must be received by the deadline. If sending by U.S. Mail, the application must be received by mail no later than %{postmarkReceivedByDate}. Applications received after %{postmarkReceivedByDate} via mail will not be accepted. %{developer} is not responsible for lost or delayed mail.",
594
612
  "listings.availableAndWaitlist": "Available Units & Open Waitlist",
613
+ "listings.availableUnitsDescription": "Applicants will be reviewed in order until all vacancies are filled.",
595
614
  "listings.availableUnitsAndWaitlist": "Available units and waitlist",
596
615
  "listings.availableUnitsAndWaitlistDesc": "Once applicants fill all available units, additional applicants will be placed on the waitlist for <span class='t-italic'>%{number} units</span>",
616
+ "listings.availableUnit": "Available Unit",
597
617
  "listings.availableUnits": "Available Units",
598
618
  "listings.bath": "bath",
599
619
  "listings.browseListings": "Browse Listings",
@@ -681,6 +701,7 @@
681
701
  "listings.sections.eligibilityTitle": "Eligibility",
682
702
  "listings.sections.featuresSubtitle": "Amenities, unit details and additional fees",
683
703
  "listings.sections.featuresTitle": "Features",
704
+ "listings.sections.accessibilityFeatures": "Accessibility Features",
684
705
  "listings.sections.housingPreferencesSubtitle": "Preference holders will be given highest ranking.",
685
706
  "listings.sections.housingPreferencesTitle": "Housing Preferences",
686
707
  "listings.sections.neighborhoodSubtitle": "Location and transportation",
@@ -810,6 +831,7 @@
810
831
  "states.WV": "West Virginia",
811
832
  "states.WY": "Wyoming",
812
833
  "t.accessibility": "Accessibility",
834
+ "t.additionalAccessibility": "Additional Accessibility",
813
835
  "t.additionalPhone": "Additional Phone",
814
836
  "t.am": "AM",
815
837
  "t.areYouStillWorking": "Are you still working?",
@@ -861,6 +883,7 @@
861
883
  "t.occupancy": "Occupancy",
862
884
  "t.ok": "Ok",
863
885
  "t.or": "or",
886
+ "t.order": "Order",
864
887
  "t.people": "people",
865
888
  "t.perMonth": "per month",
866
889
  "t.perYear": "per year",
@@ -3,13 +3,26 @@
3
3
  @apply rounded-lg;
4
4
 
5
5
  li {
6
- &:first-of-type a {
6
+ &:first-of-type {
7
7
  @apply rounded-t-lg;
8
8
  }
9
9
 
10
- &:last-of-type a {
10
+ &:last-of-type {
11
11
  @apply rounded-b-lg;
12
12
  }
13
+
14
+ &.is-current {
15
+ cursor: pointer;
16
+ @apply text-gray-900;
17
+ box-shadow: inset 3px 0px 0px 0px $tailwind-primary;
18
+ @apply block;
19
+ @apply px-4;
20
+ @apply py-2;
21
+ }
22
+
23
+ &:not(:last-child) {
24
+ @apply border-b;
25
+ }
13
26
  }
14
27
 
15
28
  a {
@@ -17,16 +30,9 @@
17
30
  @apply block;
18
31
  @apply px-4;
19
32
  @apply py-2;
20
- @apply border-b;
21
-
22
33
  &:hover {
23
34
  @apply bg-primary-lighter;
24
35
  @apply text-primary;
25
36
  }
26
-
27
- &.is-current {
28
- @apply text-gray-900;
29
- box-shadow: inset 3px 0px 0px 0px $tailwind-primary;
30
- }
31
37
  }
32
38
  }
@@ -0,0 +1,39 @@
1
+ import * as React from "react"
2
+ import { NavigationContext } from "../config/NavigationContext"
3
+ import "./SideNav.scss"
4
+
5
+ export interface SideNavItemProps {
6
+ current?: boolean
7
+ url: string
8
+ label: string
9
+ }
10
+
11
+ export interface SideNavProps {
12
+ navItems?: SideNavItemProps[]
13
+ }
14
+
15
+ const SideNav = (props: SideNavProps) => {
16
+ const { LinkComponent } = React.useContext(NavigationContext)
17
+
18
+ return (
19
+ <nav className="side-nav" aria-label="Secondary navigation">
20
+ <ul>
21
+ {props.navItems?.map((navItem: SideNavItemProps, index: number) => {
22
+ if (navItem.current) {
23
+ return (
24
+ <li className="is-current" key={index} aria-current="page">
25
+ {navItem.label}
26
+ </li>
27
+ )
28
+ }
29
+ return (
30
+ <li key={index}>
31
+ <LinkComponent href={navItem.url}>{navItem.label}</LinkComponent>
32
+ </li>
33
+ )
34
+ })}
35
+ </ul>
36
+ </nav>
37
+ )
38
+ }
39
+ export { SideNav as default, SideNav }
@@ -1,12 +1,12 @@
1
1
  import * as React from "react"
2
- import { Icon, IconFillColors, IconTypes } from "../icons/Icon"
2
+ import { Icon, IconFillColors, UniversalIconType } from "../icons/Icon"
3
3
  import { ApplicationStatusType } from "../global/ApplicationStatusType"
4
4
  import "./ApplicationStatus.scss"
5
5
 
6
6
  export interface ApplicationStatusProps {
7
7
  content: string
8
8
  iconColor?: string
9
- iconType?: IconTypes
9
+ iconType?: UniversalIconType
10
10
  status?: ApplicationStatusType
11
11
  subContent?: string
12
12
  vivid?: boolean
@@ -5,7 +5,7 @@ import { Overlay, OverlayProps } from "./Overlay"
5
5
  import { Tag } from "../text/Tag"
6
6
  import { AppearanceStyleType, AppearanceSizeType } from "../global/AppearanceTypes"
7
7
  import { AlertTypes } from "../notifications/alertTypes"
8
- import { AlertBox } from "../notifications"
8
+ import { AlertBox } from "../notifications/AlertBox"
9
9
  import { nanoid } from "nanoid"
10
10
 
11
11
  export enum DrawerSide {
@@ -34,6 +34,11 @@
34
34
  &:last-of-type {
35
35
  @apply rounded-b;
36
36
  }
37
+
38
+ &.is-scrollable {
39
+ max-height: calc(100vh - 200px);
40
+ @apply overflow-y-auto;
41
+ }
37
42
  }
38
43
 
39
44
  .modal__footer {
@@ -11,6 +11,10 @@ export interface ModalProps extends Omit<OverlayProps, "children"> {
11
11
  children?: React.ReactNode
12
12
  slim?: boolean
13
13
  role?: string
14
+ modalClassNames?: string
15
+ innerClassNames?: string
16
+ closeClassNames?: string
17
+ scrollable?: boolean
14
18
  }
15
19
 
16
20
  const ModalHeader = (props: { title: string; uniqueId?: string }) => (
@@ -35,6 +39,13 @@ const ModalFooter = (props: { actions: React.ReactNode[] }) => (
35
39
 
36
40
  export const Modal = (props: ModalProps) => {
37
41
  const uniqueIdRef = useRef(nanoid())
42
+ const modalClassNames = ["modal"]
43
+ const innerClassNames = ["modal__inner"]
44
+ const closeClassNames = ["modal__close"]
45
+ if (props.scrollable) innerClassNames.push("is-scrollable")
46
+ if (props.modalClassNames) modalClassNames.push(...props.modalClassNames.split(" "))
47
+ if (props.innerClassNames) innerClassNames.push(...props.innerClassNames.split(" "))
48
+ if (props.closeClassNames) closeClassNames.push(...props.closeClassNames.split(" "))
38
49
 
39
50
  return (
40
51
  <Overlay
@@ -46,10 +57,10 @@ export const Modal = (props: ModalProps) => {
46
57
  slim={props.slim}
47
58
  role={props.role ? props.role : "dialog"}
48
59
  >
49
- <div className="modal">
60
+ <div className={modalClassNames.join(" ")}>
50
61
  <ModalHeader title={props.title} uniqueId={uniqueIdRef.current} />
51
62
 
52
- <section className="modal__inner">
63
+ <section className={innerClassNames.join(" ")}>
53
64
  {typeof props.children === "string" ? (
54
65
  <p className="c-steel">{props.children}</p>
55
66
  ) : (
@@ -60,7 +71,12 @@ export const Modal = (props: ModalProps) => {
60
71
  {props.actions && <ModalFooter actions={props.actions} />}
61
72
 
62
73
  {!props.hideCloseIcon && (
63
- <button className="modal__close" aria-label="Close" onClick={props.onClose} tabIndex={0}>
74
+ <button
75
+ className={closeClassNames.join(" ")}
76
+ aria-label="Close"
77
+ onClick={props.onClose}
78
+ tabIndex={0}
79
+ >
64
80
  <Icon size="medium" symbol="close" />
65
81
  </button>
66
82
  )}