@openmrs/esm-styleguide 10.0.1-pre.5702 → 10.0.1-pre.5704
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/.turbo/turbo-build.log +1 -1
- package/dist/config-schema.js +2 -2
- package/dist/diagnosis-tags/diagnosis-tags.component.d.ts +10 -9
- package/dist/diagnosis-tags/diagnosis-tags.component.d.ts.map +1 -1
- package/dist/diagnosis-tags/diagnosis-tags.component.js +96 -17
- package/dist/diagnosis-tags/diagnosis-tags.module.scss +25 -1
- package/dist/diagnosis-tags/diagnosis-tags.stories.js +24 -0
- package/mock-jest.tsx +12 -7
- package/mock.tsx +12 -7
- package/package.json +25 -25
- package/src/config-schema.ts +2 -2
- package/src/diagnosis-tags/diagnosis-tags.component.tsx +103 -23
- package/src/diagnosis-tags/diagnosis-tags.module.scss +25 -1
- package/src/diagnosis-tags/diagnosis-tags.stories.tsx +11 -0
- package/src/diagnosis-tags/diagnosis-tags.test.tsx +174 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
[0] Successfully compiled: 115 files, copied 169 files with swc (
|
|
1
|
+
[0] Successfully compiled: 115 files, copied 169 files with swc (278.14ms)
|
|
2
2
|
[0] swc --strip-leading-paths --copy-files src -d dist && svgo -rf dist --quiet exited with code 0
|
|
3
3
|
[1] tsc --project tsconfig.build.json exited with code 0
|
package/dist/config-schema.js
CHANGED
|
@@ -4,7 +4,7 @@ const diagnosisTagConfigSchema = {
|
|
|
4
4
|
primaryColor: {
|
|
5
5
|
_type: Type.String,
|
|
6
6
|
_description: 'The color for displaying primary diagnoses tags',
|
|
7
|
-
_default: '
|
|
7
|
+
_default: 'blue',
|
|
8
8
|
_validators: [
|
|
9
9
|
validators.oneOf(carbonTagColors)
|
|
10
10
|
]
|
|
@@ -12,7 +12,7 @@ const diagnosisTagConfigSchema = {
|
|
|
12
12
|
secondaryColor: {
|
|
13
13
|
_type: Type.String,
|
|
14
14
|
_description: 'The color for displaying secondary diagnoses tags',
|
|
15
|
-
_default: '
|
|
15
|
+
_default: 'gray',
|
|
16
16
|
_validators: [
|
|
17
17
|
validators.oneOf(carbonTagColors)
|
|
18
18
|
]
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
interface DiagnosisTagsProps {
|
|
4
|
-
diagnoses: Array<Diagnosis
|
|
2
|
+
import type { Diagnosis } from '@openmrs/esm-emr-api';
|
|
3
|
+
export interface DiagnosisTagsProps {
|
|
4
|
+
diagnoses: Array<Pick<Diagnosis, 'uuid' | 'display' | 'rank' | 'certainty'>>;
|
|
5
|
+
/**
|
|
6
|
+
* Mark provisional diagnoses with a leading "?". Confirmed diagnoses, and any with missing or
|
|
7
|
+
* unknown certainty, show just the name. Off by default because diagnoses saved before
|
|
8
|
+
* certainty was captured are stored as provisional.
|
|
9
|
+
*/
|
|
10
|
+
showCertainty?: boolean;
|
|
5
11
|
}
|
|
6
|
-
/**
|
|
7
|
-
* This component takes a list of diagnoses and displays them as
|
|
8
|
-
* Carbon tags, with colors configured base on whether the diagnoses are primary
|
|
9
|
-
* or secondary.
|
|
10
|
-
*/
|
|
12
|
+
/** Displays diagnoses with configured rank colours, optionally marking provisional ones. */
|
|
11
13
|
export declare const DiagnosisTags: React.FC<DiagnosisTagsProps>;
|
|
12
|
-
export {};
|
|
13
14
|
//# sourceMappingURL=diagnosis-tags.component.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diagnosis-tags.component.d.ts","sourceRoot":"","sources":["../../src/diagnosis-tags/diagnosis-tags.component.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"diagnosis-tags.component.d.ts","sourceRoot":"","sources":["../../src/diagnosis-tags/diagnosis-tags.component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4C,MAAM,OAAO,CAAC;AAKjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAKtD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC;IAC7E;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,4FAA4F;AAC5F,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAgBtD,CAAC"}
|
|
@@ -1,28 +1,107 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import React from "react";
|
|
3
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React, { useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { Button, Tooltip, TooltipTrigger } from "react-aria-components";
|
|
4
|
+
import classNames from "classnames";
|
|
5
|
+
import { getCoreTranslation } from "@openmrs/esm-translations";
|
|
4
6
|
import { useConfig } from "@openmrs/esm-react-utils";
|
|
5
7
|
import styles from "./diagnosis-tags.module.scss";
|
|
6
|
-
/**
|
|
7
|
-
* This component takes a list of diagnoses and displays them as
|
|
8
|
-
* Carbon tags, with colors configured base on whether the diagnoses are primary
|
|
9
|
-
* or secondary.
|
|
10
|
-
*/ export const DiagnosisTags = ({ diagnoses })=>{
|
|
8
|
+
/** Displays diagnoses with configured rank colours, optionally marking provisional ones. */ export const DiagnosisTags = ({ diagnoses, showCertainty = false })=>{
|
|
11
9
|
const { diagnosisTags } = useConfig({
|
|
12
10
|
externalModuleName: '@openmrs/esm-styleguide'
|
|
13
11
|
});
|
|
14
12
|
return /*#__PURE__*/ _jsx("div", {
|
|
15
13
|
className: styles.container,
|
|
16
14
|
children: diagnoses.map((diagnosis)=>{
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
children: display
|
|
25
|
-
}, uuid);
|
|
15
|
+
const isProvisional = showCertainty && diagnosis.certainty === 'PROVISIONAL';
|
|
16
|
+
const color = diagnosis.rank === 1 ? diagnosisTags?.primaryColor ?? 'blue' : diagnosisTags?.secondaryColor ?? 'gray';
|
|
17
|
+
return /*#__PURE__*/ _jsx(DiagnosisPill, {
|
|
18
|
+
color: color,
|
|
19
|
+
isProvisional: isProvisional,
|
|
20
|
+
name: diagnosis.display
|
|
21
|
+
}, diagnosis.uuid);
|
|
26
22
|
})
|
|
27
23
|
});
|
|
28
24
|
};
|
|
25
|
+
function DiagnosisPill({ color, isProvisional, name }) {
|
|
26
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
27
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
28
|
+
const [isTruncated, setIsTruncated] = useState(false);
|
|
29
|
+
const isInteractive = isTruncated || isFocused;
|
|
30
|
+
const labelRef = useRef(null);
|
|
31
|
+
useLayoutEffect(()=>{
|
|
32
|
+
const label = labelRef.current;
|
|
33
|
+
if (!label) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const measure = ()=>{
|
|
37
|
+
const truncated = label.scrollWidth > label.clientWidth;
|
|
38
|
+
setIsTruncated(truncated);
|
|
39
|
+
if (!truncated) {
|
|
40
|
+
setIsOpen(false);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
measure();
|
|
44
|
+
const observer = new ResizeObserver(measure);
|
|
45
|
+
observer.observe(label);
|
|
46
|
+
return ()=>observer.disconnect();
|
|
47
|
+
}, [
|
|
48
|
+
name,
|
|
49
|
+
isProvisional,
|
|
50
|
+
isInteractive
|
|
51
|
+
]);
|
|
52
|
+
const className = classNames('cds--tag', 'cds--tag--md', 'cds--layout--size-md', `cds--tag--${color}`, styles.diagnosisTag);
|
|
53
|
+
const content = /*#__PURE__*/ _jsxs(_Fragment, {
|
|
54
|
+
children: [
|
|
55
|
+
isProvisional && /*#__PURE__*/ _jsxs(_Fragment, {
|
|
56
|
+
children: [
|
|
57
|
+
/*#__PURE__*/ _jsx("span", {
|
|
58
|
+
"aria-hidden": "true",
|
|
59
|
+
className: styles.provisionalMark,
|
|
60
|
+
children: "?"
|
|
61
|
+
}),
|
|
62
|
+
/*#__PURE__*/ _jsxs("span", {
|
|
63
|
+
className: "cds--visually-hidden",
|
|
64
|
+
children: [
|
|
65
|
+
getCoreTranslation('provisional', 'Provisional'),
|
|
66
|
+
" "
|
|
67
|
+
]
|
|
68
|
+
})
|
|
69
|
+
]
|
|
70
|
+
}),
|
|
71
|
+
/*#__PURE__*/ _jsx("span", {
|
|
72
|
+
ref: labelRef,
|
|
73
|
+
className: "cds--tag__label",
|
|
74
|
+
children: name
|
|
75
|
+
})
|
|
76
|
+
]
|
|
77
|
+
});
|
|
78
|
+
if (!isInteractive) {
|
|
79
|
+
return /*#__PURE__*/ _jsx("span", {
|
|
80
|
+
className: className,
|
|
81
|
+
"data-testid": "diagnosis-tag",
|
|
82
|
+
children: content
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return /*#__PURE__*/ _jsxs(TooltipTrigger, {
|
|
86
|
+
delay: 0,
|
|
87
|
+
closeDelay: 150,
|
|
88
|
+
isOpen: isOpen,
|
|
89
|
+
onOpenChange: setIsOpen,
|
|
90
|
+
children: [
|
|
91
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
92
|
+
className: className,
|
|
93
|
+
"data-testid": "diagnosis-tag",
|
|
94
|
+
onFocusChange: setIsFocused,
|
|
95
|
+
onPress: ()=>setIsOpen(true),
|
|
96
|
+
children: content
|
|
97
|
+
}),
|
|
98
|
+
/*#__PURE__*/ _jsx(Tooltip, {
|
|
99
|
+
className: styles.tagTooltip,
|
|
100
|
+
placement: "bottom",
|
|
101
|
+
offset: 4,
|
|
102
|
+
containerPadding: 8,
|
|
103
|
+
children: name
|
|
104
|
+
})
|
|
105
|
+
]
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -1,8 +1,32 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
1
2
|
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
2
4
|
|
|
3
5
|
.container {
|
|
4
6
|
display: flex;
|
|
5
|
-
flex-
|
|
7
|
+
flex-wrap: wrap;
|
|
6
8
|
align-items: center;
|
|
7
9
|
gap: layout.$spacing-02;
|
|
8
10
|
}
|
|
11
|
+
|
|
12
|
+
.diagnosisTag:focus-visible {
|
|
13
|
+
outline: 2px solid colors.$blue-60;
|
|
14
|
+
outline-offset: 1px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.tagTooltip {
|
|
18
|
+
@include type.type-style('body-compact-01');
|
|
19
|
+
max-width: min(18rem, calc(100vw - 1rem));
|
|
20
|
+
overflow-wrap: anywhere;
|
|
21
|
+
padding: layout.$spacing-01 layout.$spacing-03;
|
|
22
|
+
border-radius: 2px;
|
|
23
|
+
background-color: colors.$gray-80;
|
|
24
|
+
color: colors.$white-0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The name truncates; the "?" marking a provisional diagnosis must stay fully visible inside the tag
|
|
28
|
+
.provisionalMark {
|
|
29
|
+
margin-inline-end: layout.$spacing-01;
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
flex-shrink: 0;
|
|
32
|
+
}
|
|
@@ -71,3 +71,27 @@ export const MultipleDiagnoses = {
|
|
|
71
71
|
]
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
|
+
export const WithCertainty = {
|
|
75
|
+
args: {
|
|
76
|
+
showCertainty: true,
|
|
77
|
+
diagnoses: [
|
|
78
|
+
{
|
|
79
|
+
uuid: '1',
|
|
80
|
+
display: 'Extrinsic Asthma with Asthma Attack',
|
|
81
|
+
rank: 1,
|
|
82
|
+
certainty: 'CONFIRMED'
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
uuid: '2',
|
|
86
|
+
display: 'Malaria due to Plasmodium falciparum',
|
|
87
|
+
rank: 2,
|
|
88
|
+
certainty: 'PROVISIONAL'
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
uuid: '3',
|
|
92
|
+
display: 'Anemia',
|
|
93
|
+
rank: 2
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
};
|
package/mock-jest.tsx
CHANGED
|
@@ -202,13 +202,18 @@ export const LocationPicker = jest.fn(({ onChange, selectedLocationUuid }) => {
|
|
|
202
202
|
);
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
-
export const DiagnosisTags = jest.fn(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
205
|
+
export const DiagnosisTags = jest.fn(
|
|
206
|
+
({ diagnoses, showCertainty = false }: { diagnoses: Array<Diagnosis>; showCertainty?: boolean }) => (
|
|
207
|
+
<>
|
|
208
|
+
{diagnoses.map((d) => (
|
|
209
|
+
<span key={d.uuid}>
|
|
210
|
+
{showCertainty && d.certainty === 'PROVISIONAL' ? '? ' : ''}
|
|
211
|
+
{d.display}
|
|
212
|
+
</span>
|
|
213
|
+
))}
|
|
214
|
+
</>
|
|
215
|
+
),
|
|
216
|
+
);
|
|
212
217
|
|
|
213
218
|
export const Workspace2 = jest.fn(({ title, children }) => (
|
|
214
219
|
<div>
|
package/mock.tsx
CHANGED
|
@@ -203,13 +203,18 @@ export const LocationPicker = vi.fn(({ onChange, selectedLocationUuid }) => {
|
|
|
203
203
|
);
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
-
export const DiagnosisTags = vi.fn(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
206
|
+
export const DiagnosisTags = vi.fn(
|
|
207
|
+
({ diagnoses, showCertainty = false }: { diagnoses: Array<Diagnosis>; showCertainty?: boolean }) => (
|
|
208
|
+
<>
|
|
209
|
+
{diagnoses.map((d) => (
|
|
210
|
+
<span key={d.uuid}>
|
|
211
|
+
{showCertainty && d.certainty === 'PROVISIONAL' ? '? ' : ''}
|
|
212
|
+
{d.display}
|
|
213
|
+
</span>
|
|
214
|
+
))}
|
|
215
|
+
</>
|
|
216
|
+
),
|
|
217
|
+
);
|
|
213
218
|
|
|
214
219
|
export const Workspace2 = vi.fn(({ title, children }) => (
|
|
215
220
|
<div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-styleguide",
|
|
3
|
-
"version": "10.0.1-pre.
|
|
3
|
+
"version": "10.0.1-pre.5704",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "The styleguide for OpenMRS SPA",
|
|
6
6
|
"module": "dist/internal.js",
|
|
@@ -79,18 +79,18 @@
|
|
|
79
79
|
"uuid": "^11.1.1"
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
|
-
"@openmrs/esm-api": "^10.0.1-pre.
|
|
83
|
-
"@openmrs/esm-config": "^10.0.1-pre.
|
|
84
|
-
"@openmrs/esm-emr-api": "^10.0.1-pre.
|
|
85
|
-
"@openmrs/esm-error-handling": "^10.0.1-pre.
|
|
86
|
-
"@openmrs/esm-extensions": "^10.0.1-pre.
|
|
87
|
-
"@openmrs/esm-globals": "^10.0.1-pre.
|
|
88
|
-
"@openmrs/esm-navigation": "^10.0.1-pre.
|
|
89
|
-
"@openmrs/esm-react-utils": "^10.0.1-pre.
|
|
90
|
-
"@openmrs/esm-routes": "^10.0.1-pre.
|
|
91
|
-
"@openmrs/esm-state": "^10.0.1-pre.
|
|
92
|
-
"@openmrs/esm-translations": "^10.0.1-pre.
|
|
93
|
-
"@openmrs/esm-utils": "^10.0.1-pre.
|
|
82
|
+
"@openmrs/esm-api": "^10.0.1-pre.5704",
|
|
83
|
+
"@openmrs/esm-config": "^10.0.1-pre.5704",
|
|
84
|
+
"@openmrs/esm-emr-api": "^10.0.1-pre.5704",
|
|
85
|
+
"@openmrs/esm-error-handling": "^10.0.1-pre.5704",
|
|
86
|
+
"@openmrs/esm-extensions": "^10.0.1-pre.5704",
|
|
87
|
+
"@openmrs/esm-globals": "^10.0.1-pre.5704",
|
|
88
|
+
"@openmrs/esm-navigation": "^10.0.1-pre.5704",
|
|
89
|
+
"@openmrs/esm-react-utils": "^10.0.1-pre.5704",
|
|
90
|
+
"@openmrs/esm-routes": "^10.0.1-pre.5704",
|
|
91
|
+
"@openmrs/esm-state": "^10.0.1-pre.5704",
|
|
92
|
+
"@openmrs/esm-translations": "^10.0.1-pre.5704",
|
|
93
|
+
"@openmrs/esm-utils": "^10.0.1-pre.5704",
|
|
94
94
|
"dayjs": "1.x",
|
|
95
95
|
"i18next": "25.x",
|
|
96
96
|
"react": "18.x",
|
|
@@ -99,18 +99,18 @@
|
|
|
99
99
|
"swr": "2.x"
|
|
100
100
|
},
|
|
101
101
|
"devDependencies": {
|
|
102
|
-
"@openmrs/esm-api": "10.0.1-pre.
|
|
103
|
-
"@openmrs/esm-config": "10.0.1-pre.
|
|
104
|
-
"@openmrs/esm-emr-api": "10.0.1-pre.
|
|
105
|
-
"@openmrs/esm-error-handling": "10.0.1-pre.
|
|
106
|
-
"@openmrs/esm-extensions": "10.0.1-pre.
|
|
107
|
-
"@openmrs/esm-globals": "10.0.1-pre.
|
|
108
|
-
"@openmrs/esm-navigation": "10.0.1-pre.
|
|
109
|
-
"@openmrs/esm-react-utils": "10.0.1-pre.
|
|
110
|
-
"@openmrs/esm-routes": "10.0.1-pre.
|
|
111
|
-
"@openmrs/esm-state": "10.0.1-pre.
|
|
112
|
-
"@openmrs/esm-translations": "10.0.1-pre.
|
|
113
|
-
"@openmrs/esm-utils": "10.0.1-pre.
|
|
102
|
+
"@openmrs/esm-api": "10.0.1-pre.5704",
|
|
103
|
+
"@openmrs/esm-config": "10.0.1-pre.5704",
|
|
104
|
+
"@openmrs/esm-emr-api": "10.0.1-pre.5704",
|
|
105
|
+
"@openmrs/esm-error-handling": "10.0.1-pre.5704",
|
|
106
|
+
"@openmrs/esm-extensions": "10.0.1-pre.5704",
|
|
107
|
+
"@openmrs/esm-globals": "10.0.1-pre.5704",
|
|
108
|
+
"@openmrs/esm-navigation": "10.0.1-pre.5704",
|
|
109
|
+
"@openmrs/esm-react-utils": "10.0.1-pre.5704",
|
|
110
|
+
"@openmrs/esm-routes": "10.0.1-pre.5704",
|
|
111
|
+
"@openmrs/esm-state": "10.0.1-pre.5704",
|
|
112
|
+
"@openmrs/esm-translations": "10.0.1-pre.5704",
|
|
113
|
+
"@openmrs/esm-utils": "10.0.1-pre.5704",
|
|
114
114
|
"@swc/cli": "0.8.1",
|
|
115
115
|
"@swc/core": "1.15.21",
|
|
116
116
|
"@types/geopattern": "^1.2.9",
|
package/src/config-schema.ts
CHANGED
|
@@ -30,13 +30,13 @@ const diagnosisTagConfigSchema: ConfigSchema = {
|
|
|
30
30
|
primaryColor: {
|
|
31
31
|
_type: Type.String,
|
|
32
32
|
_description: 'The color for displaying primary diagnoses tags',
|
|
33
|
-
_default: '
|
|
33
|
+
_default: 'blue',
|
|
34
34
|
_validators: [validators.oneOf(carbonTagColors)],
|
|
35
35
|
},
|
|
36
36
|
secondaryColor: {
|
|
37
37
|
_type: Type.String,
|
|
38
38
|
_description: 'The color for displaying secondary diagnoses tags',
|
|
39
|
-
_default: '
|
|
39
|
+
_default: 'gray',
|
|
40
40
|
_validators: [validators.oneOf(carbonTagColors)],
|
|
41
41
|
},
|
|
42
42
|
};
|
|
@@ -1,38 +1,118 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import React, { useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Button, Tooltip, TooltipTrigger } from 'react-aria-components';
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import { getCoreTranslation } from '@openmrs/esm-translations';
|
|
3
5
|
import { useConfig } from '@openmrs/esm-react-utils';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
+
import type { Diagnosis } from '@openmrs/esm-emr-api';
|
|
7
|
+
import type { StyleguideConfigObject } from '../config-schema';
|
|
8
|
+
import type { CarbonTagColor } from '../utils';
|
|
6
9
|
import styles from './diagnosis-tags.module.scss';
|
|
7
10
|
|
|
8
|
-
interface DiagnosisTagsProps {
|
|
9
|
-
diagnoses: Array<Diagnosis
|
|
11
|
+
export interface DiagnosisTagsProps {
|
|
12
|
+
diagnoses: Array<Pick<Diagnosis, 'uuid' | 'display' | 'rank' | 'certainty'>>;
|
|
13
|
+
/**
|
|
14
|
+
* Mark provisional diagnoses with a leading "?". Confirmed diagnoses, and any with missing or
|
|
15
|
+
* unknown certainty, show just the name. Off by default because diagnoses saved before
|
|
16
|
+
* certainty was captured are stored as provisional.
|
|
17
|
+
*/
|
|
18
|
+
showCertainty?: boolean;
|
|
10
19
|
}
|
|
11
20
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* or secondary.
|
|
16
|
-
*/
|
|
17
|
-
export const DiagnosisTags: React.FC<DiagnosisTagsProps> = ({ diagnoses }) => {
|
|
18
|
-
const { diagnosisTags } = useConfig<StyleguideConfigObject>({
|
|
19
|
-
externalModuleName: '@openmrs/esm-styleguide',
|
|
20
|
-
});
|
|
21
|
+
/** Displays diagnoses with configured rank colours, optionally marking provisional ones. */
|
|
22
|
+
export const DiagnosisTags: React.FC<DiagnosisTagsProps> = ({ diagnoses, showCertainty = false }) => {
|
|
23
|
+
const { diagnosisTags } = useConfig<StyleguideConfigObject>({ externalModuleName: '@openmrs/esm-styleguide' });
|
|
21
24
|
|
|
22
25
|
return (
|
|
23
26
|
<div className={styles.container}>
|
|
24
27
|
{diagnoses.map((diagnosis) => {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const color = diagnosisTags[key];
|
|
28
|
+
const isProvisional = showCertainty && diagnosis.certainty === 'PROVISIONAL';
|
|
29
|
+
const color =
|
|
30
|
+
diagnosis.rank === 1 ? (diagnosisTags?.primaryColor ?? 'blue') : (diagnosisTags?.secondaryColor ?? 'gray');
|
|
31
|
+
|
|
30
32
|
return (
|
|
31
|
-
<
|
|
32
|
-
{display}
|
|
33
|
-
</Tag>
|
|
33
|
+
<DiagnosisPill key={diagnosis.uuid} color={color} isProvisional={isProvisional} name={diagnosis.display} />
|
|
34
34
|
);
|
|
35
35
|
})}
|
|
36
36
|
</div>
|
|
37
37
|
);
|
|
38
38
|
};
|
|
39
|
+
|
|
40
|
+
interface DiagnosisPillProps {
|
|
41
|
+
color: CarbonTagColor;
|
|
42
|
+
isProvisional: boolean;
|
|
43
|
+
name?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function DiagnosisPill({ color, isProvisional, name }: DiagnosisPillProps) {
|
|
47
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
48
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
49
|
+
|
|
50
|
+
const [isTruncated, setIsTruncated] = useState(false);
|
|
51
|
+
const isInteractive = isTruncated || isFocused;
|
|
52
|
+
const labelRef = useRef<HTMLSpanElement>(null);
|
|
53
|
+
|
|
54
|
+
useLayoutEffect(() => {
|
|
55
|
+
const label = labelRef.current;
|
|
56
|
+
if (!label) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const measure = () => {
|
|
60
|
+
const truncated = label.scrollWidth > label.clientWidth;
|
|
61
|
+
setIsTruncated(truncated);
|
|
62
|
+
if (!truncated) {
|
|
63
|
+
setIsOpen(false);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
measure();
|
|
67
|
+
const observer = new ResizeObserver(measure);
|
|
68
|
+
observer.observe(label);
|
|
69
|
+
return () => observer.disconnect();
|
|
70
|
+
}, [name, isProvisional, isInteractive]);
|
|
71
|
+
|
|
72
|
+
const className = classNames(
|
|
73
|
+
'cds--tag',
|
|
74
|
+
'cds--tag--md',
|
|
75
|
+
'cds--layout--size-md',
|
|
76
|
+
`cds--tag--${color}`,
|
|
77
|
+
styles.diagnosisTag,
|
|
78
|
+
);
|
|
79
|
+
const content = (
|
|
80
|
+
<>
|
|
81
|
+
{isProvisional && (
|
|
82
|
+
<>
|
|
83
|
+
<span aria-hidden="true" className={styles.provisionalMark}>
|
|
84
|
+
?
|
|
85
|
+
</span>
|
|
86
|
+
<span className="cds--visually-hidden">{getCoreTranslation('provisional', 'Provisional')} </span>
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
<span ref={labelRef} className="cds--tag__label">
|
|
90
|
+
{name}
|
|
91
|
+
</span>
|
|
92
|
+
</>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (!isInteractive) {
|
|
96
|
+
return (
|
|
97
|
+
<span className={className} data-testid="diagnosis-tag">
|
|
98
|
+
{content}
|
|
99
|
+
</span>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<TooltipTrigger delay={0} closeDelay={150} isOpen={isOpen} onOpenChange={setIsOpen}>
|
|
105
|
+
<Button
|
|
106
|
+
className={className}
|
|
107
|
+
data-testid="diagnosis-tag"
|
|
108
|
+
onFocusChange={setIsFocused}
|
|
109
|
+
onPress={() => setIsOpen(true)}
|
|
110
|
+
>
|
|
111
|
+
{content}
|
|
112
|
+
</Button>
|
|
113
|
+
<Tooltip className={styles.tagTooltip} placement="bottom" offset={4} containerPadding={8}>
|
|
114
|
+
{name}
|
|
115
|
+
</Tooltip>
|
|
116
|
+
</TooltipTrigger>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -1,8 +1,32 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
1
2
|
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
2
4
|
|
|
3
5
|
.container {
|
|
4
6
|
display: flex;
|
|
5
|
-
flex-
|
|
7
|
+
flex-wrap: wrap;
|
|
6
8
|
align-items: center;
|
|
7
9
|
gap: layout.$spacing-02;
|
|
8
10
|
}
|
|
11
|
+
|
|
12
|
+
.diagnosisTag:focus-visible {
|
|
13
|
+
outline: 2px solid colors.$blue-60;
|
|
14
|
+
outline-offset: 1px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.tagTooltip {
|
|
18
|
+
@include type.type-style('body-compact-01');
|
|
19
|
+
max-width: min(18rem, calc(100vw - 1rem));
|
|
20
|
+
overflow-wrap: anywhere;
|
|
21
|
+
padding: layout.$spacing-01 layout.$spacing-03;
|
|
22
|
+
border-radius: 2px;
|
|
23
|
+
background-color: colors.$gray-80;
|
|
24
|
+
color: colors.$white-0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The name truncates; the "?" marking a provisional diagnosis must stay fully visible inside the tag
|
|
28
|
+
.provisionalMark {
|
|
29
|
+
margin-inline-end: layout.$spacing-01;
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
flex-shrink: 0;
|
|
32
|
+
}
|
|
@@ -38,3 +38,14 @@ export const MultipleDiagnoses: Story = {
|
|
|
38
38
|
],
|
|
39
39
|
},
|
|
40
40
|
};
|
|
41
|
+
|
|
42
|
+
export const WithCertainty: Story = {
|
|
43
|
+
args: {
|
|
44
|
+
showCertainty: true,
|
|
45
|
+
diagnoses: [
|
|
46
|
+
{ uuid: '1', display: 'Extrinsic Asthma with Asthma Attack', rank: 1, certainty: 'CONFIRMED' },
|
|
47
|
+
{ uuid: '2', display: 'Malaria due to Plasmodium falciparum', rank: 2, certainty: 'PROVISIONAL' },
|
|
48
|
+
{ uuid: '3', display: 'Anemia', rank: 2 },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/** @vitest-environment jsdom */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
|
4
|
+
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
5
|
+
import userEvent from '@testing-library/user-event';
|
|
6
|
+
import { useConfig } from '@openmrs/esm-react-utils';
|
|
7
|
+
import { DiagnosisTags } from './diagnosis-tags.component';
|
|
8
|
+
|
|
9
|
+
const diagnoses = [
|
|
10
|
+
{ uuid: '1', display: 'Asthma', rank: 1, certainty: 'CONFIRMED' },
|
|
11
|
+
{ uuid: '2', display: 'Malaria', rank: 2, certainty: 'PROVISIONAL' },
|
|
12
|
+
{ uuid: '3', display: 'Anemia', rank: 2, certainty: 'UNKNOWN' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.stubGlobal(
|
|
17
|
+
'ResizeObserver',
|
|
18
|
+
class {
|
|
19
|
+
observe() {}
|
|
20
|
+
unobserve() {}
|
|
21
|
+
disconnect() {}
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
vi.mocked(useConfig).mockReturnValue({ diagnosisTags: { primaryColor: 'purple', secondaryColor: 'teal' } });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
vi.restoreAllMocks();
|
|
29
|
+
vi.unstubAllGlobals();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function mockTruncation() {
|
|
33
|
+
vi.spyOn(HTMLElement.prototype, 'scrollWidth', 'get').mockReturnValue(300);
|
|
34
|
+
return vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(200);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test('preserves name-only display by default', () => {
|
|
38
|
+
render(<DiagnosisTags diagnoses={diagnoses} />);
|
|
39
|
+
expect(screen.getByText('Malaria')).toBeInTheDocument();
|
|
40
|
+
expect(screen.queryByText('?')).not.toBeInTheDocument();
|
|
41
|
+
expect(screen.queryByText(/provisional/i)).not.toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('marks only provisional diagnoses when enabled and preserves configured colours', () => {
|
|
45
|
+
render(<DiagnosisTags diagnoses={diagnoses} showCertainty />);
|
|
46
|
+
const [asthma, malaria, anemia] = screen.getAllByTestId('diagnosis-tag');
|
|
47
|
+
expect(malaria).toHaveTextContent(/^\?Provisional Malaria$/);
|
|
48
|
+
expect(asthma).toHaveTextContent(/^Asthma$/);
|
|
49
|
+
expect(anemia).toHaveTextContent(/^Anemia$/);
|
|
50
|
+
expect(screen.getAllByText('?')).toHaveLength(1);
|
|
51
|
+
expect(asthma).toHaveClass('cds--tag--purple');
|
|
52
|
+
expect(malaria).toHaveClass('cds--tag--teal');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('defaults to blue for primary and gray for secondary diagnoses', () => {
|
|
56
|
+
vi.mocked(useConfig).mockReturnValue({});
|
|
57
|
+
render(<DiagnosisTags diagnoses={diagnoses} />);
|
|
58
|
+
const [asthma, malaria] = screen.getAllByTestId('diagnosis-tag');
|
|
59
|
+
expect(asthma).toHaveClass('cds--tag--blue');
|
|
60
|
+
expect(malaria).toHaveClass('cds--tag--gray');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('names a truncated provisional diagnosis as provisional for assistive technology', () => {
|
|
64
|
+
mockTruncation();
|
|
65
|
+
render(<DiagnosisTags diagnoses={[diagnoses[1]]} showCertainty />);
|
|
66
|
+
// Browsers read this as "Provisional Malaria"; jsdom's name computation drops the space
|
|
67
|
+
expect(screen.getByRole('button', { name: /^Provisional\s?Malaria$/ })).toBeInTheDocument();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('reveals the full name on keyboard focus and dismisses it with Escape', async () => {
|
|
71
|
+
mockTruncation();
|
|
72
|
+
const user = userEvent.setup();
|
|
73
|
+
render(<DiagnosisTags diagnoses={[diagnoses[0]]} showCertainty />);
|
|
74
|
+
await user.tab();
|
|
75
|
+
expect(screen.getByTestId('diagnosis-tag')).toHaveFocus();
|
|
76
|
+
expect(screen.getAllByText('Asthma')).toHaveLength(2);
|
|
77
|
+
await user.keyboard('{Escape}');
|
|
78
|
+
expect(screen.getAllByText('Asthma')).toHaveLength(1);
|
|
79
|
+
expect(screen.getByTestId('diagnosis-tag')).toHaveFocus();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('allows moving onto the tooltip and dismisses a hover-open tooltip with Escape', async () => {
|
|
83
|
+
mockTruncation();
|
|
84
|
+
const user = userEvent.setup();
|
|
85
|
+
render(<DiagnosisTags diagnoses={[diagnoses[0]]} showCertainty />);
|
|
86
|
+
fireEvent.mouseMove(document.body);
|
|
87
|
+
fireEvent.pointerMove(document.body, { pointerType: 'mouse' });
|
|
88
|
+
fireEvent.pointerEnter(screen.getByTestId('diagnosis-tag'), { pointerType: 'mouse' });
|
|
89
|
+
fireEvent.mouseEnter(screen.getByTestId('diagnosis-tag'));
|
|
90
|
+
const tooltip = await screen.findByRole('tooltip');
|
|
91
|
+
await user.hover(tooltip);
|
|
92
|
+
expect(tooltip).toBeInTheDocument();
|
|
93
|
+
fireEvent.keyDown(document.body, { key: 'Escape' });
|
|
94
|
+
await waitFor(() => expect(screen.queryByRole('tooltip')).not.toBeInTheDocument());
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('opens the full name on activation without requiring certainty', async () => {
|
|
98
|
+
mockTruncation();
|
|
99
|
+
const user = userEvent.setup();
|
|
100
|
+
render(<DiagnosisTags diagnoses={[diagnoses[0]]} />);
|
|
101
|
+
await user.click(screen.getByRole('button', { name: 'Asthma' }));
|
|
102
|
+
expect(await screen.findByRole('tooltip')).toHaveTextContent('Asthma');
|
|
103
|
+
expect(screen.queryByText('?')).not.toBeInTheDocument();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('short names have no tab stop or tooltip', async () => {
|
|
107
|
+
const user = userEvent.setup();
|
|
108
|
+
render(<DiagnosisTags diagnoses={[diagnoses[0]]} showCertainty />);
|
|
109
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
110
|
+
await user.hover(screen.getByTestId('diagnosis-tag'));
|
|
111
|
+
await user.tab();
|
|
112
|
+
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
|
113
|
+
expect(screen.getByTestId('diagnosis-tag')).not.toHaveFocus();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('updates truncation after resizing and changing certainty', () => {
|
|
117
|
+
const width = mockTruncation();
|
|
118
|
+
let resize: () => void;
|
|
119
|
+
const disconnect = vi.fn();
|
|
120
|
+
vi.stubGlobal(
|
|
121
|
+
'ResizeObserver',
|
|
122
|
+
class {
|
|
123
|
+
constructor(callback: () => void) {
|
|
124
|
+
resize = callback;
|
|
125
|
+
}
|
|
126
|
+
observe() {}
|
|
127
|
+
disconnect = disconnect;
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
const { rerender, unmount } = render(<DiagnosisTags diagnoses={[diagnoses[1]]} showCertainty />);
|
|
131
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
132
|
+
width.mockReturnValue(400);
|
|
133
|
+
act(() => resize());
|
|
134
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
135
|
+
width.mockReturnValue(200);
|
|
136
|
+
rerender(<DiagnosisTags diagnoses={[diagnoses[1]]} />);
|
|
137
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
138
|
+
unmount();
|
|
139
|
+
expect(disconnect).toHaveBeenCalled();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('keeps a resized pill focused until the user tabs away', async () => {
|
|
143
|
+
const width = mockTruncation();
|
|
144
|
+
let resize: () => void;
|
|
145
|
+
vi.stubGlobal(
|
|
146
|
+
'ResizeObserver',
|
|
147
|
+
class {
|
|
148
|
+
constructor(private callback: () => void) {}
|
|
149
|
+
observe(element: Element) {
|
|
150
|
+
if (element.classList.contains('cds--tag__label')) {
|
|
151
|
+
resize = this.callback;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
unobserve() {}
|
|
155
|
+
disconnect() {}
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
const user = userEvent.setup();
|
|
159
|
+
render(
|
|
160
|
+
<>
|
|
161
|
+
<DiagnosisTags diagnoses={[diagnoses[0]]} />
|
|
162
|
+
<button>Next</button>
|
|
163
|
+
</>,
|
|
164
|
+
);
|
|
165
|
+
await user.tab();
|
|
166
|
+
const tag = screen.getByRole('button', { name: 'Asthma' });
|
|
167
|
+
expect(tag).toHaveFocus();
|
|
168
|
+
width.mockReturnValue(400);
|
|
169
|
+
act(() => resize());
|
|
170
|
+
expect(tag).toHaveFocus();
|
|
171
|
+
await user.tab();
|
|
172
|
+
expect(screen.getByRole('button', { name: 'Next' })).toHaveFocus();
|
|
173
|
+
expect(screen.queryByRole('button', { name: 'Asthma' })).not.toBeInTheDocument();
|
|
174
|
+
});
|