diff --git a/packages/framework/esm-framework/docs/API.md b/packages/framework/esm-framework/docs/API.md index bd612e714..809015f34 100644 --- a/packages/framework/esm-framework/docs/API.md +++ b/packages/framework/esm-framework/docs/API.md @@ -695,7 +695,7 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:648](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L648) +[packages/framework/esm-styleguide/src/icons/icons.tsx:664](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L664) ___ @@ -1173,7 +1173,7 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:630](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L638) +[packages/framework/esm-styleguide/src/icons/icons.tsx:638](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L638) ___ @@ -1233,7 +1233,7 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:635](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L643) +[packages/framework/esm-styleguide/src/icons/icons.tsx:643](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L643) ___ @@ -1427,7 +1427,7 @@ Note this is an alias for ListCheckedIcon #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:642](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L650) +[packages/framework/esm-styleguide/src/icons/icons.tsx:650](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L650) ___ @@ -1549,7 +1549,7 @@ This is a utility type for custom icons that use the svg-sprite-loader to bundle #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:656](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L656) +[packages/framework/esm-styleguide/src/icons/icons.tsx:672](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L672) ___ @@ -1815,7 +1815,7 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:639](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L655) +[packages/framework/esm-styleguide/src/icons/icons.tsx:655](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L655) ___ @@ -1899,7 +1899,7 @@ Note this is an alias for ShoppingCartArrowDownIcon #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:654](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L662) +[packages/framework/esm-styleguide/src/icons/icons.tsx:662](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L662) ___ @@ -1955,9 +1955,10 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/icons/icons.tsx:471](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L627) +[packages/framework/esm-styleguide/src/icons/icons.tsx:627](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/icons/icons.tsx#L627) ___ + ### TableIcon • `Const` **TableIcon**: `MemoExoticComponent`<`ForwardRefExoticComponent`<[`IconProps`](API.md#iconprops) & `RefAttributes`<`SVGSVGElement`\>\>\> @@ -6718,7 +6719,7 @@ invalid key to this function will result in a type error. | Name | Type | Description | | :------ | :------ | :------ | -| `key` | ``"error"`` \| ``"change"`` \| ``"close"`` \| ``"other"`` \| ``"actions"`` \| ``"address"`` \| ``"age"`` \| ``"cancel"`` \| ``"confirm"`` \| ``"contactAdministratorIfIssuePersists"`` \| ``"contactDetails"`` \| ``"errorCopy"`` \| ``"female"`` \| ``"hideDetails"`` \| ``"loading"`` \| ``"male"`` \| ``"patientIdentifierSticker"`` \| ``"patientLists"`` \| ``"print"`` \| ``"printError"`` \| ``"printErrorExplainer"`` \| ``"printIdentifierSticker"`` \| ``"printing"`` \| ``"relationships"`` \| ``"resetOverrides"`` \| ``"scriptLoadingFailed"`` \| ``"scriptLoadingError"`` \| ``"seeMoreLists"`` \| ``"sex"`` \| ``"showDetails"`` \| ``"unknown"`` \| ``"closeAllOpenedWorkspaces"`` \| ``"closingAllWorkspacesPromptBody"`` \| ``"closingAllWorkspacesPromptTitle"`` \| ``"discard"`` \| ``"hide"`` \| ``"maximize"`` \| ``"minimize"`` \| ``"openAnyway"`` \| ``"unsavedChangesInOpenedWorkspace"`` \| ``"unsavedChangesInWorkspace"`` \| ``"unsavedChangesTitleText"`` \| ``"workspaceHeader"`` \| ``"address1"`` \| ``"address2"`` \| ``"address3"`` \| ``"address4"`` \| ``"address5"`` \| ``"address6"`` \| ``"city"`` \| ``"cityVillage"`` \| ``"country"`` \| ``"countyDistrict"`` \| ``"postalCode"`` \| ``"state"`` \| ``"stateProvince"`` | - | +| `key` | ``"error"`` \| ``"change"`` \| ``"close"`` \| ``"other"`` \| ``"actions"`` \| ``"address"`` \| ``"age"`` \| ``"cancel"`` \| ``"confirm"`` \| ``"contactAdministratorIfIssuePersists"`` \| ``"contactDetails"`` \| ``"errorCopy"`` \| ``"female"`` \| ``"loading"`` \| ``"male"`` \| ``"patientIdentifierSticker"`` \| ``"patientLists"`` \| ``"print"`` \| ``"printError"`` \| ``"printErrorExplainer"`` \| ``"printIdentifierSticker"`` \| ``"printing"`` \| ``"relationships"`` \| ``"resetOverrides"`` \| ``"scriptLoadingFailed"`` \| ``"scriptLoadingError"`` \| ``"seeMoreLists"`` \| ``"sex"`` \| ``"showLess"`` \| ``"showMore"`` \| ``"unknown"`` \| ``"closeAllOpenedWorkspaces"`` \| ``"closingAllWorkspacesPromptBody"`` \| ``"closingAllWorkspacesPromptTitle"`` \| ``"discard"`` \| ``"hide"`` \| ``"maximize"`` \| ``"minimize"`` \| ``"openAnyway"`` \| ``"unsavedChangesInOpenedWorkspace"`` \| ``"unsavedChangesInWorkspace"`` \| ``"unsavedChangesTitleText"`` \| ``"workspaceHeader"`` \| ``"address1"`` \| ``"address2"`` \| ``"address3"`` \| ``"address4"`` \| ``"address5"`` \| ``"address6"`` \| ``"city"`` \| ``"cityVillage"`` \| ``"country"`` \| ``"countyDistrict"`` \| ``"district"`` \| ``"postalCode"`` \| ``"state"`` \| ``"stateProvince"`` | - | | `defaultText?` | `string` | - | | `options?` | `Omit`<`TOptions`<`StringMap`\>, ``"defaultValue"`` \| ``"ns"``\> | Object passed to the i18next `t` function. See https://www.i18next.com/translation-function/essentials#overview-options for more information. `ns` and `defaultValue` are already set and may not be used. | @@ -6850,7 +6851,7 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-identifiers.component.tsx:13](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-identifiers.component.tsx#L13) +[packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-identifiers.component.tsx:41](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-identifiers.component.tsx#L41) ___ @@ -6870,7 +6871,7 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-info.component.tsx:14](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-info.component.tsx#L14) +[packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-info.component.tsx:43](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-info.component.tsx#L43) ___ @@ -6898,9 +6899,7 @@ ___ ▸ **PatientPhoto**(`__namedParameters`): `Element` -A component which displays the patient photo. If there is no photo, it will display -a generated avatar. The default size is 80px. Set the size prop to 'small' to display -a 48px avatar. +A component which displays the patient photo https://zeroheight.com/23a080e38/p/6663f3-patient-header. If there is no photo, it will display a generated avatar. The default size is 56px. #### Parameters @@ -6914,7 +6913,7 @@ a 48px avatar. #### Defined in -[packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx:18](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx#L18) +[packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx:21](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx#L21) ___ diff --git a/packages/framework/esm-framework/docs/interfaces/PatientBannerPatientInfoProps.md b/packages/framework/esm-framework/docs/interfaces/PatientBannerPatientInfoProps.md index 78c2cbfc0..e81b69cf2 100644 --- a/packages/framework/esm-framework/docs/interfaces/PatientBannerPatientInfoProps.md +++ b/packages/framework/esm-framework/docs/interfaces/PatientBannerPatientInfoProps.md @@ -16,4 +16,4 @@ #### Defined in -[packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-info.component.tsx:11](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-info.component.tsx#L11) +[packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-info.component.tsx:12](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-info.component.tsx#L12) diff --git a/packages/framework/esm-framework/docs/interfaces/PatientPhotoProps.md b/packages/framework/esm-framework/docs/interfaces/PatientPhotoProps.md index f33328436..34fffd26c 100644 --- a/packages/framework/esm-framework/docs/interfaces/PatientPhotoProps.md +++ b/packages/framework/esm-framework/docs/interfaces/PatientPhotoProps.md @@ -18,7 +18,7 @@ #### Defined in -[packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx:8](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx#L8) +[packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx:12](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx#L12) ___ @@ -28,7 +28,7 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx:9](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx#L9) +[packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx:13](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx#L13) ___ @@ -38,4 +38,4 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx:10](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx#L10) +[packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx:15](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx#L15) diff --git a/packages/framework/esm-styleguide/src/patient-banner/contact-details/patient-banner-toggle-contact-details-button.component.tsx b/packages/framework/esm-styleguide/src/patient-banner/contact-details/patient-banner-toggle-contact-details-button.component.tsx index 855296c89..8dccab661 100644 --- a/packages/framework/esm-styleguide/src/patient-banner/contact-details/patient-banner-toggle-contact-details-button.component.tsx +++ b/packages/framework/esm-styleguide/src/patient-banner/contact-details/patient-banner-toggle-contact-details-button.component.tsx @@ -26,9 +26,7 @@ export function PatientBannerToggleContactDetailsButton({ onClick={toggleContactDetails} renderIcon={showContactDetails ? ChevronUpIcon : ChevronDownIcon} > - {showContactDetails - ? getCoreTranslation('hideDetails', 'Hide details') - : getCoreTranslation('showDetails', 'Show details')} + {showContactDetails ? getCoreTranslation('showLess', 'Show less') : getCoreTranslation('showMore', 'Show more')} ); } diff --git a/packages/framework/esm-styleguide/src/patient-banner/patient-info/gender-icons.component.tsx b/packages/framework/esm-styleguide/src/patient-banner/patient-info/gender-icons.component.tsx new file mode 100644 index 000000000..b94bfedd1 --- /dev/null +++ b/packages/framework/esm-styleguide/src/patient-banner/patient-info/gender-icons.component.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +export const FemaleIcon: React.FC = () => ( + + + +); + +export const MaleIcon: React.FC = () => ( + + + + +); + +export const UnknownIcon: React.FC = () => ( + + + + + + +); + +export const OtherIcon: React.FC = () => ( + + + + +); diff --git a/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-identifiers.component.tsx b/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-identifiers.component.tsx index f9a21c9ec..5df8d67b0 100644 --- a/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-identifiers.component.tsx +++ b/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-identifiers.component.tsx @@ -1,21 +1,50 @@ /** @module @category UI */ import React from 'react'; -import { Tag } from '@carbon/react'; +import { FormLabel, Tag } from '@carbon/react'; import { useConfig, usePrimaryIdentifierCode } from '@openmrs/esm-react-utils'; import { type StyleguideConfigObject } from '../../config-schema'; import styles from './patient-banner-patient-info.module.scss'; +interface IdentifierProps { + showIdentifierLabel: boolean; + type: fhir.CodeableConcept | undefined; + value: string | undefined; +} + interface PatientBannerPatientIdentifierProps { identifier: fhir.Identifier[] | undefined; showIdentifierLabel: boolean; } +function PrimaryIdentifier({ showIdentifierLabel, type, value }: IdentifierProps) { + return ( + + {showIdentifierLabel && ( + + {type?.text && {type.text}: } + {value} + + )} + + ); +} + +function SecondaryIdentifier({ showIdentifierLabel, type, value }: IdentifierProps) { + return ( + + {showIdentifierLabel && {type?.text}: } + {value} + + ); +} + export function PatientBannerPatientIdentifier({ identifier, showIdentifierLabel, }: PatientBannerPatientIdentifierProps) { const { excludePatientIdentifierCodeTypes } = useConfig(); const { primaryIdentifierCode } = usePrimaryIdentifierCode(); + const filteredIdentifiers = identifier?.filter((identifier) => { const code = identifier.type?.coding?.[0]?.code; @@ -23,32 +52,22 @@ export function PatientBannerPatientIdentifier({ }) ?? []; return ( -
+ <> {filteredIdentifiers?.length ? filteredIdentifiers.map(({ value, type }, index) => ( - -
{index > 0 && ·}
- {type?.coding?.[0]?.code === primaryIdentifierCode ? ( -
- {showIdentifierLabel && ( - - {type?.text}: - - )} - {value} -
- ) : ( - - )} -
+ + + {type?.coding?.[0]?.code === primaryIdentifierCode ? ( + + ) : ( + + )} + + {index < filteredIdentifiers.length - 1 && ·} + )) : ''} -
+ ); } diff --git a/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-info.component.tsx b/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-info.component.tsx index c75182860..c66b26c7c 100644 --- a/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-info.component.tsx +++ b/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-info.component.tsx @@ -1,9 +1,10 @@ /** @module @category UI */ +import React, { useMemo } from 'react'; +import classNames from 'classnames'; import { ExtensionSlot } from '@openmrs/esm-react-utils'; import { getCoreTranslation } from '@openmrs/esm-translations'; import { age, formatDate, parseDate } from '@openmrs/esm-utils'; -import classNames from 'classnames'; -import React from 'react'; +import { FemaleIcon, MaleIcon, OtherIcon, UnknownIcon } from './gender-icons.component'; import PatientBannerPatientIdentifier from './patient-banner-patient-identifiers.component'; import styles from './patient-banner-patient-info.module.scss'; @@ -11,51 +12,67 @@ export interface PatientBannerPatientInfoProps { patient: fhir.Patient; } +type Gender = 'female' | 'male' | 'other' | 'unknown'; + +interface GenderIconProps { + gender: keyof typeof GENDER_ICONS; +} + +const GENDER_ICONS = { + Female: FemaleIcon, + Male: MaleIcon, + Other: OtherIcon, + Unknown: UnknownIcon, +} as const; + +const GenderIcon = ({ gender }: GenderIconProps) => { + const IconComponent = GENDER_ICONS[gender]; + + if (!IconComponent) { + return null; + } + + return ; +}; + +const getGender = (gender: string): string => { + const key = gender.toLowerCase() as Gender; + return getCoreTranslation(key, gender); +}; + export function PatientBannerPatientInfo({ patient }: PatientBannerPatientInfoProps) { - const name = `${patient?.name?.[0]?.given?.join(' ')} ${patient?.name?.[0].family}`; + const name = `${patient?.name?.[0]?.given?.join(' ')} ${patient?.name?.[0]?.family}`; const gender = patient?.gender && getGender(patient.gender); + const extensionState = useMemo(() => ({ patientUuid: patient.id, patient }), [patient.id, patient]); + return (
{name} - + + {gender && ( +
+ + {gender} +
+ )} + +
- {gender} {patient.birthDate && ( <> - · {age(patient.birthDate)} · - {formatDate(parseDate(patient.birthDate), { mode: 'wide', time: false })} + {formatDate(parseDate(patient.birthDate), { time: false })} + · )} -
-
); } - -const getGender = (gender: string): string => { - switch (gender) { - case 'male': - return getCoreTranslation('male', 'Male'); - case 'female': - return getCoreTranslation('female', 'Female'); - case 'other': - return getCoreTranslation('other', 'Other'); - case 'unknown': - return getCoreTranslation('unknown', 'Unknown'); - default: - return gender; - } -}; diff --git a/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-info.module.scss b/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-info.module.scss index 937f855bc..b51ffe4ab 100644 --- a/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-info.module.scss +++ b/packages/framework/esm-styleguide/src/patient-banner/patient-info/patient-banner-patient-info.module.scss @@ -1,10 +1,11 @@ @use '@carbon/colors'; -@use '@carbon/styles/scss/type'; -@import '~@openmrs/esm-styleguide/src/vars'; +@use '@carbon/layout'; +@use '@carbon/type'; +@use '@openmrs/esm-styleguide/src/vars' as *; .container { - border-top: 0.063rem solid $ui-03; - border-bottom: 0.063rem solid $ui-03; + border-top: 1px solid $ui-03; + border-bottom: 1px solid $ui-03; background-color: $ui-01; } @@ -18,12 +19,14 @@ .patientName { @include type.type-style('heading-03'); - margin-right: 0.25rem; + color: colors.$gray-100; + font-weight: 600; + margin-right: layout.$spacing-02; } .patientAvatar { - width: 5rem; - height: 5rem; + width: 3.5rem; + height: 3.5rem; margin: 1rem; border-radius: 1px; } @@ -31,18 +34,25 @@ .patientAvatarButton { cursor: pointer; border: none; - padding: 0px; + padding: 0; background: none; } .patientInfo { + display: flex; + flex-direction: column; + padding: layout.$spacing-05 0 layout.$spacing-05 0; width: 100%; } .demographics { @include type.type-style('body-compact-02'); color: $text-02; - margin-top: 0.25rem; + display: flex; + align-items: center; + column-gap: layout.$spacing-02; + margin-top: layout.$spacing-02; + flex-wrap: wrap; } .row { @@ -50,11 +60,6 @@ flex-direction: row; justify-content: space-between; align-items: baseline; - margin-top: 0.25rem; -} - -.patientNameRow { - margin-top: 0.875rem; } .flexRow { @@ -63,11 +68,17 @@ align-items: center; } +.tagsSlot { + @extend .flexRow; + margin: 0 layout.$spacing-03; +} + .identifiers { @include type.type-style('body-compact-02'); color: colors.$gray-70; display: flex; flex-wrap: wrap; + align-items: center; } .identifierTag { @@ -75,33 +86,46 @@ align-items: center; } -.separator { - margin: 0 0.5rem; -} - -.primaryIdentifier { +.primaryIdentifier, +.secondaryIdentifier { + @include type.type-style('body-compact-02'); display: flex; align-items: center; - gap: 0.25rem; + gap: layout.$spacing-02; } -.secondaryIdentifier { - display: flex; +.primaryIdentifier { + color: colors.$gray-100; - .label { - @include type.type-style('label-01'); - margin: 0.25rem 0.25rem 0.25rem 0rem; - align-self: auto; + .value { + font-weight: bold; } +} +.secondaryIdentifier { .identifier { @include type.type-style('body-compact-02'); align-self: baseline; } } +.gender { + display: flex; + align-items: center; + color: colors.$gray-70; + gap: layout.$spacing-01; +} + +.separator { + margin: layout.$spacing-01; +} + +.tag { + font-size: inherit !important; +} + .tooltipPadding { - padding: 0.25rem; + padding: layout.$spacing-02; } .tooltipSmallText { @@ -117,15 +141,15 @@ html[dir='rtl'] { .demographics { display: flex; - gap: 0.25rem; + gap: layout.$spacing-02; } .tag { - margin: 0.25rem 0rem 0.25rem 0.25rem; + margin: layout.$spacing-02 0 layout.$spacing-02 layout.$spacing-02; } .patientName { margin-right: unset; - margin-left: 0.25rem; + margin-left: layout.$spacing-02; } } diff --git a/packages/framework/esm-styleguide/src/patient-photo/loader-icon.component.tsx b/packages/framework/esm-styleguide/src/patient-photo/loader-icon.component.tsx new file mode 100644 index 000000000..d34a5919b --- /dev/null +++ b/packages/framework/esm-styleguide/src/patient-photo/loader-icon.component.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +interface LoaderIconProps { + height?: number; + width?: number; +} + +const LoaderIcon: React.FC = ({ width = 56, height = 56 }) => ( + + + + + +); + +export default LoaderIcon; diff --git a/packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx b/packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx index 43dbdb125..84a3a5441 100644 --- a/packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx +++ b/packages/framework/esm-styleguide/src/patient-photo/patient-photo.component.tsx @@ -1,40 +1,65 @@ /** @module @category UI */ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import Avatar from 'react-avatar'; import GeoPattern from 'geopattern'; +import { SkeletonIcon } from '@carbon/react'; +import { useTranslation } from 'react-i18next'; import { usePatientPhoto } from './usePatientPhoto'; +import PlaceholderIcon from './placeholder-icon.component'; +import styles from './patient-photo.module.scss'; export interface PatientPhotoProps { patientName: string; patientUuid: string; + // TODO: Remove this prop size?: 'small' | undefined; } /** - * A component which displays the patient photo. If there is no photo, it will display - * a generated avatar. The default size is 80px. Set the size prop to 'small' to display - * a 48px avatar. + * A component which displays the patient photo https://zeroheight.com/23a080e38/p/6663f3-patient-header. If there is no photo, it will display a generated avatar. The default size is 56px. */ export function PatientPhoto({ patientUuid, patientName, size }: PatientPhotoProps) { - const { data: photo } = usePatientPhoto(patientUuid); + const { t } = useTranslation(); + const { data: photo, isLoading } = usePatientPhoto(patientUuid); + const [validImageSrc, setValidImageSrc] = useState(null); const pattern = useMemo(() => GeoPattern.generate(patientUuid), [patientUuid]); + useEffect(() => { + if (photo?.imageSrc) { + const img = new Image(); + img.onload = () => setValidImageSrc(photo.imageSrc); + img.onerror = () => setValidImageSrc(null); + img.src = photo.imageSrc; + } + }, [photo?.imageSrc]); + + const altText = t('avatarAltText', 'Profile photo unavailable - grey placeholder image'); + + if (isLoading) { + return ; + } + + if (photo?.imageSrc && !validImageSrc) { + return ; + } + return ( ); } diff --git a/packages/framework/esm-styleguide/src/patient-photo/patient-photo.module.scss b/packages/framework/esm-styleguide/src/patient-photo/patient-photo.module.scss new file mode 100644 index 000000000..78337a904 --- /dev/null +++ b/packages/framework/esm-styleguide/src/patient-photo/patient-photo.module.scss @@ -0,0 +1,4 @@ +.skeleton { + height: 3.5rem; + width: 3.5rem; +} diff --git a/packages/framework/esm-styleguide/src/patient-photo/patient-photo.test.tsx b/packages/framework/esm-styleguide/src/patient-photo/patient-photo.test.tsx index 1aa390fb4..e1bcf8ce5 100644 --- a/packages/framework/esm-styleguide/src/patient-photo/patient-photo.test.tsx +++ b/packages/framework/esm-styleguide/src/patient-photo/patient-photo.test.tsx @@ -1,9 +1,12 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { PatientPhoto } from './patient-photo.component'; +import { usePatientPhoto } from './usePatientPhoto'; + +const mockedUsePatientPhoto = jest.mocked(usePatientPhoto); jest.mock('./usePatientPhoto', () => ({ - usePatientPhoto: jest.fn().mockReturnValue({ data: { imageSrc: 'test-image-src' } }), + usePatientPhoto: jest.fn(), })); jest.mock('geopattern', () => ({ @@ -15,22 +18,73 @@ jest.mock('geopattern', () => ({ const patientUuid = 'test-patient-uuid'; const patientName = 'Freddy Mercury'; -describe('PatientPhoto Component', () => { - it('should render the component with the patient photo and size should not be small', () => { +describe('PatientPhoto', () => { + it('renders a progressbar when the patient photo is loading', () => { + mockedUsePatientPhoto.mockReturnValue({ + isLoading: true, + data: null, + error: undefined, + }); + render(); - const avatarImage = screen.getByTitle(`${patientName}`); + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + }); + + it('renders a placeholder image if the patient photo fails to load', async () => { + mockedUsePatientPhoto.mockReturnValue({ + isLoading: false, + data: { imageSrc: 'invalid-url.jpg', dateTime: '2024-01-01' }, + error: undefined, + }); - expect(avatarImage).toBeInTheDocument(); - expect(avatarImage).toHaveAttribute('style', expect.stringContaining('width: 80px; height: 80px')); + render(); + + expect(screen.getByLabelText(/patient photo placeholder/i)).toBeInTheDocument(); }); - it('should render the component with the patient photo and size should be small i.e. 48px', () => { - render(); + it('renders the avatar image when image successfully loads', async () => { + // Mock the Image loading process + const originalImage = window.Image; + const mockImage = function () { + return { + onload: null, + _src: '', + get src() { + return this._src; + }, + set src(value: string) { + this._src = value; + // Simulate successful image load + setTimeout(() => { + if (this.onload) { + this.onload.call(this, new Event('load')); + } + }, 0); + }, + }; + }; + window.Image = mockImage as any; - const avatarImage = screen.getByTitle(`${patientName}`); + mockedUsePatientPhoto.mockReturnValue({ + isLoading: false, + data: { imageSrc: 'valid-image.jpg', dateTime: '2024-01-01' }, + error: undefined, + }); + render(); + + const altText = 'Profile photo unavailable - grey placeholder image'; + + // Wait for the image to "load" + await screen.findByAltText(altText); + + const avatarImage = screen.getByRole('img', { name: altText }); expect(avatarImage).toBeInTheDocument(); - expect(avatarImage).toHaveAttribute('style', expect.stringContaining('width: 48px; height: 48px')); + expect(avatarImage).toHaveAttribute('src', 'valid-image.jpg'); + expect(avatarImage).toHaveAttribute('alt', altText); + + // Restore the original Image constructor + window.Image = originalImage; }); }); diff --git a/packages/framework/esm-styleguide/src/patient-photo/placeholder-icon.component.tsx b/packages/framework/esm-styleguide/src/patient-photo/placeholder-icon.component.tsx new file mode 100644 index 000000000..4ded53b4f --- /dev/null +++ b/packages/framework/esm-styleguide/src/patient-photo/placeholder-icon.component.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface PlaceholderIconProps { + width?: number; + height?: number; +} + +const PlaceholderIcon: React.FC = ({ width = 56, height = 56 }) => { + const { t } = useTranslation(); + + return ( + + + + + ); +}; + +export default PlaceholderIcon; diff --git a/packages/framework/esm-translations/src/translations.ts b/packages/framework/esm-translations/src/translations.ts index fd57bd939..d3f9d1c3f 100644 --- a/packages/framework/esm-translations/src/translations.ts +++ b/packages/framework/esm-translations/src/translations.ts @@ -50,7 +50,6 @@ export const coreTranslations = { errorCopy: 'Sorry, there was a problem displaying this information. You can try to reload this page, or contact the site administrator and quote the error code above.', female: 'Female', - hideDetails: 'Hide details', loading: 'Loading', male: 'Male', other: 'Other', @@ -68,6 +67,7 @@ export const coreTranslations = { 'Failed to load overridden script from {{url}}. Please check that the bundled script is available at the expected URL. Click the button below to reset all import map overrides.', seeMoreLists: 'See {{count}} more lists', sex: 'Sex', - showDetails: 'Show details', + showLess: 'Show less', + showMore: 'Show more', unknown: 'Unknown', }; diff --git a/packages/framework/esm-translations/translations/en.json b/packages/framework/esm-translations/translations/en.json index e6e76392d..e1a140c6d 100644 --- a/packages/framework/esm-translations/translations/en.json +++ b/packages/framework/esm-translations/translations/en.json @@ -22,11 +22,11 @@ "country": "Country", "countyDistrict": "District", "discard": "Discard", + "district": "District", "error": "Error", "errorCopy": "Sorry, there was a problem displaying this information. You can try to reload this page, or contact the site administrator and quote the error code above.", "female": "Female", "hide": "Hide", - "hideDetails": "Hide details", "loading": "Loading", "male": "Male", "maximize": "Maximize", @@ -47,7 +47,8 @@ "scriptLoadingFailed": "Error: Script failed to load", "seeMoreLists": "See {{count}} more lists", "sex": "Sex", - "showDetails": "Show details", + "showLess": "Show less", + "showMore": "Show more", "state": "State", "stateProvince": "State", "unknown": "Unknown",