Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ const ItemLabel = memo(function ItemLabel(props: ItemLabelProps) {
sx={{
mt: 0.5,
flexGrow: 1,
// A question label is a single run of text, so the outer paragraph margins that xhtml/markdown
// rendering adds would only push it out of line with its field. Display items are standalone
// blocks of prose, so they keep their paragraph spacing.
...(isDisplayItem
? {}
: {
'& p:first-of-type': { marginBlockStart: 0 },
'& p:last-of-type': { marginBlockEnd: 0 }
}),
...(parentStyles || {})
}}>
{/* Required asterisk position is in front of text */}
Expand All @@ -79,12 +88,17 @@ const ItemLabel = memo(function ItemLabel(props: ItemLabelProps) {
</RequiredAsterisk>
) : null}

{/* verticalAlign: top is required because an inline-flex box takes its baseline from its first
flex item. Once the label text wraps onto multiple lines, that baseline sits well below the
surrounding line box's baseline, which pushes the whole label down and away from its field.
Aligning the box to the top of the line box keeps multi-line labels in line with their fields. */}
<Box
component="span"
display="inline-flex"
alignItems="baseline"
gap={0.5}
flexWrap="wrap">
flexWrap="wrap"
sx={{ verticalAlign: 'top' }}>
<ItemPrefixSwitcher qItem={qItem} />
<ItemTextSwitcher qItem={qItem} />
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -905,3 +905,40 @@ export const qRenderingAnswerOptionValueString: Questionnaire = {
}
]
};

// Labels long enough to wrap onto multiple lines, both as plain text and as rendering-xhtml.
// Guards against https://github.com/aehrc/smart-forms/issues/2048, where a wrapping label was
// pushed below its field instead of staying in line with it.
export const qWrappingItemLabels: Questionnaire = {
resourceType: 'Questionnaire',
status: 'draft',
item: [
{
linkId: 'wrapping-label-plain',
text: 'Do you have access to the Aus CVD Risk-i application from your clinical system?',
type: 'boolean',
repeats: false
},
{
linkId: 'wrapping-label-xhtml',
text: 'Do you have access to the Aus CVD Risk-i application from your clinical system?',
_text: {
extension: [
{
url: 'http://hl7.org/fhir/StructureDefinition/rendering-xhtml',
valueString:
'<div xmlns="http://www.w3.org/1999/xhtml"><p>Do you have access to the <em>Aus&nbsp;CVD&nbsp;Risk&#8209;i</em> application from your clinical system?</p></div>'
}
]
},
type: 'boolean',
repeats: false
},
{
linkId: 'wrapping-label-string',
text: 'Do you have access to the Aus CVD Risk-i application from your clinical system, and if so, which version?',
type: 'string',
repeats: false
}
]
};
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ import {
qRenderingXhtmlGroupPropagationInlineStyles,
qRenderingXhtmlGroupPropagationNested,
qRenderingXhtmlPrefix,
qRenderingXhtmlTitle
qRenderingXhtmlTitle,
qWrappingItemLabels
} from '../assets/questionnaires';
import { createStory } from '../storybookWrappers/createStory';
import { expect } from 'storybook/test';

// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
const meta = {
Expand Down Expand Up @@ -170,3 +172,63 @@ export const PrefixOnlyNoText: Story = createStory({
questionnaire: qPrefixOnlyNoText
}
}) as Story;

// ── Wrapping item labels (Issue #2048) ───────────────────────────────────────

export const WrappingItemLabels: Story = createStory({
args: {
questionnaire: qWrappingItemLabels
},
play: async ({ canvasElement }) => {
// Client rects of every glyph run inside an element, top-most first. Ranges over the element
// itself are no good here - they also report zero-width rects for the empty inlines that xhtml
// rendering introduces, which sit above the text they wrap.
function getTextLineTops(element: Element): number[] {
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
const tops = new Set<number>();

let textNode = walker.nextNode();
while (textNode) {
if (textNode.textContent?.trim()) {
const range = document.createRange();
range.selectNodeContents(textNode);
for (const rect of range.getClientRects()) {
if (rect.height > 0 && rect.width > 0) {
tops.add(rect.top);
}
}
}
textNode = walker.nextNode();
}

return [...tops].sort((a, b) => a - b);
}

// A label that wraps onto multiple lines must still start at the top of its row, in line with
// its field, rather than being pushed below it.
for (const linkId of [
'wrapping-label-plain',
'wrapping-label-xhtml',
'wrapping-label-string'
]) {
const row = canvasElement.querySelector(`[data-linkid="${linkId}"]`);
if (!row) {
throw new Error(`Row was not found for [data-linkid="${linkId}"]`);
}

const label = row.querySelector(`#label-${linkId}`);
if (!label) {
throw new Error(`Label was not found for #label-${linkId}`);
}

const lineTops = getTextLineTops(label);

// Confirm the label actually wraps, otherwise this story isn't testing anything
expect(lineTops.length).toBeGreaterThan(1);

// Only ItemLabel's 4px nudge and the line's half-leading may sit above the first line of text
const offsetFromRowTop = lineTops[0] - row.getBoundingClientRect().top;
expect(offsetFromRowTop).toBeLessThanOrEqual(12);
}
}
}) as Story;
Loading