Skip to content

Commit 6f1674c

Browse files
committed
Add dataset options to better link
1 parent 569864d commit 6f1674c

3 files changed

Lines changed: 100 additions & 10 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/** @jest-environment @happy-dom/jest-environment */
2+
3+
import * as JestGlobals from '@jest/globals';
4+
import { scenario } from '@testduet/given-when-then';
5+
import { micromark } from 'micromark';
6+
import betterLinkDocumentMod from './betterLinkDocumentMod';
7+
import parseDocumentFragmentFromString from './parseDocumentFragmentFromString';
8+
import serializeDocumentFragmentIntoString from './serializeDocumentFragmentIntoString';
9+
10+
scenario(
11+
'"dataset" option',
12+
bdd => {
13+
bdd
14+
.given('the document fragment', () => ({
15+
documentFragment: parseDocumentFragmentFromString(
16+
micromark('[Example](https://example.com)', { allowDangerousHtml: true })
17+
)
18+
}))
19+
.when('passing "attributes"', ({ documentFragment }) =>
20+
betterLinkDocumentMod(documentFragment, () => ({
21+
dataset: { hello: 'World!' }
22+
}))
23+
)
24+
.then('should match snapshot', (_, actual) => {
25+
expect(serializeDocumentFragmentIntoString(actual)).toBe(
26+
'<p xmlns="http://www.w3.org/1999/xhtml"><a href="https://example.com" data-hello="World!">Example</a></p>'
27+
);
28+
});
29+
},
30+
JestGlobals
31+
);
32+
33+
scenario(
34+
'"dataset" option with "asButton: true"',
35+
bdd => {
36+
bdd
37+
.given('the document fragment', () => ({
38+
documentFragment: parseDocumentFragmentFromString(
39+
micromark('[Example](https://example.com)', { allowDangerousHtml: true })
40+
)
41+
}))
42+
.when('passing "attributes"', ({ documentFragment }) =>
43+
betterLinkDocumentMod(documentFragment, () => ({
44+
asButton: true,
45+
dataset: { hello: 'World!' }
46+
}))
47+
)
48+
.then('should match snapshot', (_, actual) => {
49+
expect(serializeDocumentFragmentIntoString(actual)).toBe(
50+
'<p xmlns="http://www.w3.org/1999/xhtml"><button type="button" value="https://example.com" data-hello="World!">Example</button></p>'
51+
);
52+
});
53+
},
54+
JestGlobals
55+
);

packages/component-better-link/src/betterLinkDocumentMod.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ type BetterLinkDocumentModDecoration = {
1212
/** Value of "class" attribute of the link. If set to `false`, remove existing attribute. */
1313
className?: BetterLinkDocumentModAttributeSetter;
1414

15+
/** Value of "data-*" attributes to be added. */
16+
dataset?: DOMStringMap | undefined;
17+
1518
/** Alternate text of the image icon appended to the link. */
1619
iconAlt?: string;
1720

@@ -65,7 +68,7 @@ function betterLinkDocumentMod<T extends Document | DocumentFragment>(
6568
continue;
6669
}
6770

68-
const { ariaLabel, asButton, className, iconAlt, iconClassName, rel, target, title, wrapZeroWidthSpace } =
71+
const { ariaLabel, asButton, className, dataset, iconAlt, iconClassName, rel, target, title, wrapZeroWidthSpace } =
6972
decoration;
7073

7174
setOrRemoveAttribute(anchor, 'aria-label', ariaLabel ?? false);
@@ -83,8 +86,10 @@ function betterLinkDocumentMod<T extends Document | DocumentFragment>(
8386
anchor.insertAdjacentElement('beforeend', image);
8487
}
8588

89+
let datasetTarget: HTMLAnchorElement | HTMLButtonElement = anchor;
90+
8691
if (asButton) {
87-
const button = document.createElement('button');
92+
const button = (datasetTarget = document.createElement('button'));
8893

8994
copyAttribute(anchor, button, 'aria-label');
9095
copyAttribute(anchor, button, 'class');
@@ -110,6 +115,18 @@ function betterLinkDocumentMod<T extends Document | DocumentFragment>(
110115
anchor.insertAdjacentText('afterend', ZERO_WIDTH_SPACE);
111116
}
112117
}
118+
119+
if (dataset) {
120+
for (const [key, value] of Object.entries(dataset)) {
121+
if (key === 'constructor' || key === 'proto' || key === 'prototype') {
122+
continue;
123+
}
124+
125+
// Filtered out dangerous property names.
126+
// eslint-disable-next-line security/detect-object-injection
127+
datasetTarget.dataset[key] = value;
128+
}
129+
}
113130
}
114131

115132
return documentFragment;

packages/component/src/Utils/InlineMarkdown.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,31 @@ import { validateProps } from '@msinternal/botframework-webchat-react-valibot';
1010
import { useStyleOptions } from 'botframework-webchat-api/hook.js';
1111
import { micromark } from 'micromark';
1212
import React, { useCallback, useMemo } from 'react';
13-
import { array, function_, object, optional, pipe, readonly, string, type InferInput } from 'valibot';
13+
import {
14+
args,
15+
array,
16+
function_,
17+
instance,
18+
object,
19+
optional,
20+
parse,
21+
pipe,
22+
readonly,
23+
string,
24+
tuple,
25+
union,
26+
type InferInput
27+
} from 'valibot';
1428

1529
import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject';
1630
import createCustomEvent from './createCustomEvent';
1731

32+
const referenceEventSchema = union([instance(Event), object({ data: string() })]);
33+
1834
const inlineMarkdownPropsSchema = pipe(
1935
object({
2036
markdown: string(),
21-
onReference: function_(),
37+
onReference: pipe(function_(), args(tuple([referenceEventSchema]))),
2238
references: optional(array(string()))
2339
}),
2440
readonly()
@@ -71,16 +87,13 @@ const InlineMarkdown = (props: InlineMarkdownProps) => {
7187

7288
const documentFragment = parseDocumentFragmentFromString(micromark(markdownWithLinkReferenceDefinitions));
7389

74-
// Turn "<a href="#retry">Retry</a>" into "<button type="button" value="retry">Retry</button>"
90+
// Turn "<a href="#retry">Retry</a>" into "<button type="button" data-markdown-ref="#retry">Retry</button>"
7591
betterLinkDocumentMod(documentFragment, href => {
7692
if (href.startsWith('#')) {
77-
return { asButton: true };
93+
return { asButton: true, dataset: { markdownHref: href } };
7894
}
7995
});
8096

81-
// TODO: [P*] Instead of <button value="">, it need to be <button data-markdown-href="a1b2c"> to trigger the CSS.
82-
// TODO: [P*] Remove trailing <p></p>.
83-
8497
return { __html: stripParagraphContainer(serializeDocumentFragmentIntoString(documentFragment)) };
8598
}, [markdown, references]);
8699

@@ -90,7 +103,12 @@ const InlineMarkdown = (props: InlineMarkdownProps) => {
90103

91104
const href = event.target.getAttribute('value') ?? undefined;
92105

93-
href && onReference?.(createCustomEvent('reference', { data: href }));
106+
if (href) {
107+
const event = createCustomEvent('reference', { data: href });
108+
109+
parse(referenceEventSchema, event);
110+
onReference?.(event);
111+
}
94112
},
95113
[onReference]
96114
);

0 commit comments

Comments
 (0)