Skip to content

Conversation

garethbowen
Copy link
Collaborator

@garethbowen garethbowen commented Oct 8, 2025

Closes #62 #198

I have verified this PR works in these browsers (latest versions):

  • Chrome
  • Firefox
  • Safari (macOS)
  • Safari (iOS)
  • Chrome for Android
  • Not applicable

What else has been done to verify that this works as intended?

Automated tests

Why is this the best possible solution? Were any other approaches considered?

How does this change affect users? Describe intentional changes to behavior and behavior that could have accidentally been affected by code changes. In other words, what are the regression risks?

Markdown processing runs automatically on all labels, so forms that include markdown styling accidentally will look different, for example, questions starting with a number will now be rendered as an ordered list which means they will be indented when they weren't before.

Do we need any specific form for testing your changes? If so, please attach one.

From this branch: /packages/common/src/fixtures/notes/3-notes-with-markdown.xml

What's changed

Copy link

changeset-bot bot commented Oct 8, 2025

🦋 Changeset detected

Latest commit: ebb6434

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@getodk/xforms-engine Minor
@getodk/web-forms Minor
@getodk/scenario Minor
@getodk/common Minor
@getodk/xpath Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

span,
label {
font-weight: normal;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why this was in the reset in the first place and I couldn't find anything that changed when I removed this. It's needed because if the markdown sets something as bold and then there's an inner span/p you don't expect it to be overwritten by this css again.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s probably meant to fix an issue when running Web Forms within Central. This reset file was mainly added to reduce overrides from Central's CSS. Hopefully, we can make Web Forms a standalone app soon and avoid these kinds of issues altogether.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thinking! I've had a look in central and it still seems to work. In ControlLabel.vue we have label: { font-weight: 400} which over-rights the bolding from central without the need for the reset. I wonder if the reset was added first, and then we specified the weight again at a later date making the reset unnecessary. An alternative would be for the reset to use inherit instead of normal but I unless I find something broken I'd rather remove it altogether.

Copy link
Collaborator

@latin-panda latin-panda Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm testing this branch in Central and the Select's options are bold now because Central's bootstrap is overriding the styles.

Before:

After:

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good find! I'm going to do more extensive testing inside Central when I deploy to the test server. In the meantime I've fixed this by using inherit which overrides the Central bootstrap but not the weight when nested inside a <strong>.

span,
label {
	font-weight: inherit;
}

@garethbowen
Copy link
Collaborator Author

@latin-panda Review please! It's a fairly big change and much more complex than I'd like - feedback welcome!

@garethbowen
Copy link
Collaborator Author

garethbowen commented Oct 8, 2025

My todo list...

  • make sure the styling works when used in central
  • go through the issue again and make sure I haven't missed anything
  • get this through qa

@latin-panda
Copy link
Collaborator

Great! I plan to review and test this later today and will look for additional forms to test.

Copy link
Collaborator

@latin-panda latin-panda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clear and easy to follow ✨
I’m still testing, but I’m adding some comments in the meantime.

span,
label {
font-weight: normal;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s probably meant to fix an issue when running Web Forms within Central. This reset file was mainly added to reduce overrides from Central's CSS. Hopefully, we can make Web Forms a standalone app soon and avoid these kinds of issues altogether.

},
} as AnyInputNode;

describe('ControlLabel', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this test has lost a lot of its value. It’s probably better to just have the E2E test asserting the labels and maintain only that one instead (and remove this). What do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just have the E2E test asserting the labels

Does this test already exist? Or are you thinking once the select one from map work is merged I write an e2e test to take a screenshot which checks for both markdown formatting and the required styling?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are tests already asserting that the label text is displayed, but not for "required" or other visual elements; we would need to create those tests.

if (!entries.length) {
return;
}
return Object.fromEntries(entries) as StyleProperty;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, the object could be built within the earlier loops to avoid that extra bit of processing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my initial attempt but I couldn't get the TS compiler to allow it. Because StyleProperty has readonly properties I couldn't set them one at a time in the loop. The way the engine usually does it would be to hand off all the parsing to the constructor which can set the properties before init completes, but that's a bit too much OO for me. There's probably some clever pattern I'm missing that's more idiomatic - any ideas?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I can see the issue, as long as performance is good, this should be fine and still clear.

@latin-panda
Copy link
Collaborator

latin-panda commented Oct 10, 2025

I reviewed the code again, and it looks good! I also tested it with some forms but didn't do a full regression. I found a few issues:

1.. Central's bootstrap is overriding select's options

2.. The text's line breaks aren't effective anymore; this is something reported by the forum before that we fixed here

In `main` branch Screenshot 2025-10-11 at 1 21 21 AM
In this branch Screenshot 2025-10-11 at 1 20 57 AM

3.. Is this <qrcode> element part of this work, and what should it render? If that's for later, please create a ticket.

In `main` branch Screenshot 2025-10-11 at 1 27 50 AM
In this branch Screenshot 2025-10-11 at 1 28 00 AM

4.. This form displays more styles in Collect than this branch deployed in Central

styletest.xlsx

In Collect
In this branch

5.. The image isn't displayed in the label

formstyling.xlsx

Note this form combines markdown titles with emojis to increase the size of the emoji, to test this forum post: https://forum.getodk.org/t/large-single-character-emoji-text-display/56875

In Collect
In this branch

@garethbowen
Copy link
Collaborator Author

garethbowen commented Oct 12, 2025

Central's bootstrap is overriding

Thanks. Fixed (more details in PR).

The text's line breaks aren't effective anymore; this is something reported by the forum before that we fixed

Ooh yes. So the previous fix was to use white-space: pre-wrap; which meant line breaks and spaces were respected, but with markdown formatting these are stripped out and replaced with <p> tags so the pre-wrap doesn't do anything any more. Unfortunately every bit of text was getting marked down with <p> tags (eg: option labels) which adds a whole lot of extra padding to things we don't want to be padded. To get around this I had just swallowed the p tags which is why you're seeing it all on one line. My latest version leaves the <p> tags everywhere, but changes the margin - if a p has a following sibling then the margin bottom is 1rem, if not, it's 0. This overrides the Central styling and looks identical in everything I've tested but may have regressed somewhere else.

Also I've edited the "all question types" xml so the "version" information is in an html list rather than attempting to manually indent it.

Is this element part of this work, and what should it render?

It's not part of this work, and I'm not sure what it should render. The current rendering is consistent with Enketo. Issue raised: #520

This form displays more styles in Collect than this branch deployed in Central

I had configured the DOMPurify to be highly restrictive, and only allow for elements which weren't already covered by markdown, ie: for a heading use # not <h1>. I see now that we should allow both so I've switched DOMPurify back to the default which is much more permissive but still safe against injection. Now this branch renders all of those (and even gets the nested list elements correct which Collect isn't!).

The image isn't displayed in the label

This is also a problem in main. I've fixed this by including the code from the TextMedia template in the ControlLabel template.

Note this form combines markdown titles with emojis to increase the size of the emoji

This is working in this branch, but because it's a h5 the emoji also has top and bottom margin. I think this is acceptable for now, but would be worth looking at a better solution for this which didn't involve headings. I've added support for the font-size style property to be passed through which will get some way towards a solution for this.

@latin-panda
Copy link
Collaborator

Is this element part of this work, and what should it render? If that's for later, please create a ticket.

It's not part of this work, and I'm not sure what it should render. The current rendering is consistent with Enketo. Issue raised: #520

Interesting! The print appearance for text inputs displays the value as a read-only note. I guess if someone uses markdown, it should style it accordingly.

When you click the print button, it generates a QR code.

@latin-panda latin-panda mentioned this pull request Oct 13, 2025
8 tasks
Copy link
Collaborator

@latin-panda latin-panda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I added a few questions and some observations from my tests in the Safari browser.
Have you tested on Android or iPhone? I reached my free quota of requests and couldn't test it. I need to find another backup service.

You'll need to add the tunnel address in package/web-forms/vite.config.ts:

<template>
<div v-if="question.currentState.hint" class="hint">
{{ question.currentState.hint?.asString }}
<MarkdownBlock v-for="(elem, index) in question.currentState.hint?.formatted" :key="index" :elem="elem" />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Web Forms stand-alone is good. But I noticed that Central is making the text cut in a weird place. Can you add something here, use all the space available? Or in the reset file

Image

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose we leave this as is - the Central team have decided that anything wider than 77ch is not readable. In this case it doesn't actually break web-forms and if they want to change it it can be reset on their side. If we start including overrides for everything any customer may have set in the parent context we'll end up with the world's biggest reset file (eg: we'd also have to do something about the following line p:lang(ja), p:lang(zh) { max-width: 54ch; }). What do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, in WF standalone, the text is fully expanded. This only affects WF in Central.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might get some issues reported, in that case, we'll coordinate with Central team to fix those css conflicts.

span,
label {
font-weight: normal;
font-weight: inherit;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still see the issue in Central, Safari browser. The font-weight with 400 works in p element (L51). The inherit value didn't work.

Image

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes 😭

So the reset wasn't really working - even for the question labels we had specific weight override in ControlLabel.vue. I've added the same in select-options.scss.


label,
.hint {
p {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The p with text is there but has no color

Image

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oof. I'm not sure why this changed - I would expect it to be broken in main too. Bottom line is Central defines a .label { color: white } and we don't override it either in main or this branch. Fixed by overriding.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I wonder if it's some new code on their affecting Web Forms.

export interface StyleProperty {
readonly color: string | undefined;
readonly 'font-family': string | undefined;
readonly 'text-align': 'center' | 'left' | 'right' | undefined;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Collect, each option is centered aligned when using text-align.

In Central + this branch:

Image

This is the styletest form I shared the other day.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is caused by the max-width: 77ch as above. The reason it doesn't line up with the label is the font-size in the label is bigger and therefore the max-width is wider. As above, I propose leaving this for Central to define whatever max-width they want.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI @srujner

<span v-else-if="elem.role === 'html'" v-html="purify(elem)" />

<!-- link -->
<a v-else-if="elem.role === 'parent' && elem.elementName === 'a'" :href="getUrl(elem)" target="_blank">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This link is opening in the same tab. I guess this elem.role is HTML instead of a (so rendering L44)

Image

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes precisely. I wanted to avoid actually parsing the HTML and just sanitizing it which means we're limited about what changes we can make to it. In this case, the person building the form would have to specify the target attribute. Is that ok?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay 👍 that's reasonable

<span>{{ props.question.currentState.label?.asString }}</span>
<label :for="question.nodeId" :class="{ required: question.currentState.required }">
<MarkdownBlock v-for="(elem, index) in text" :key="index" :elem="elem" />
<div v-if="image || video || audio" class="media-content">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this Label's image should be left-aligned - @alyblenkin is that correct?
Image

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be good to test loading a small image and a big image. I wonder if we need to add max-width

For example, the image question type has a box for small images, and has a max-width for big images.

Image

This could be getting out of scope, and we can address it in another ticket.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to put this out of scope, because it may require design input separate testing, etc. Should I remove the image handling code that I added pending dev on the separate issue, or do you think I leave it in there because it's better than nothing?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to leave this code as is, showing an image is preferable to showing nothing. Can you please create a ticket to style it and tag Aly for input? :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue raised: #525

@garethbowen
Copy link
Collaborator Author

garethbowen commented Oct 15, 2025

Have you tested on Android or iPhone?

I've tested Android. Unfortunately I don't have access to an iPhone.

@latin-panda latin-panda self-requested a review October 15, 2025 03:52
Copy link
Collaborator

@latin-panda latin-panda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exciting to have this ready. Thanks! 🤩

@garethbowen garethbowen mentioned this pull request Oct 16, 2025
8 tasks
@garethbowen
Copy link
Collaborator Author

@latin-panda Sorry to bother you again, but Szymon sent through the first of the QA forms and I found another issue, which led me down a rabbit hole of more markdown in more places! Would you mind reviewing the last 3 commits as well?

Copy link
Collaborator

@latin-panda latin-panda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to support this in a map's properties pop-up? I don't think so, but I'm asking to double-check.

I left some other comments below

panelClass += ' no-buttons';
}
const getSelectedLabels = () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

@garethbowen garethbowen Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Thanks. I need to read up more about vue...

});
});
const getSelectedLabel = () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same feedback here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@change="$emit('change')"
/>
>
<template #option="slotProps">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The search is no longer working. It is case-insensitive, matches based on the label, and finds matches anywhere within the text.

Before:

In this branch:

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for finding this. It turns out it needs a string to work. I've fixed this by passing the asString value in as the option-label.

<MarkdownBlock v-for="(elem, index) in slotProps.option.label.formatted" :key="index" :elem="elem" />
</template>
<template #value>
<MarkdownBlock v-for="(elem, index) in getSelectedLabel()" :key="index" :elem="elem" />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not showing the comma between elements, so now it's difficult to read the selection.

Before:

In this branch:

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh, yeah. I've updated the #value template so it adds the comma in between. With options that don't use markdown it looks identical to before. With markdown it's got a bit more padding just because of the whitespace between HTML tags. I think it's ok, and difficult to manage without knowing what HTML they've included...

Screenshot From 2025-10-17 08-42-18 Screenshot From 2025-10-17 08-42-11

@srujner
Copy link

srujner commented Oct 16, 2025

Which one is the correct behavior?

WebForms
Screenshot(340)

Enketo
Screenshot(339)

@latin-panda
Copy link
Collaborator

@srujner Collect displays selections as a comma-separated list, which better conveys what was selected rather than how many items were chosen.

@garethbowen
Copy link
Collaborator Author

Do we need to support this in a map's properties pop-up?

I think not. Let's leave it for now and revisit it once we have a use case. Because you can include structured elements it could break out of the list structure really easily...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Design: Markdown in label/hint (and TextRange engine/client API generally) Apply Markdown and HTML styling

3 participants