@transferwise/components 0.0.0-experimental-5f3c456 → 0.0.0-experimental-b2dc1ea

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.
Files changed (62) hide show
  1. package/build/header/Header.js +89 -34
  2. package/build/header/Header.js.map +1 -1
  3. package/build/header/Header.mjs +85 -31
  4. package/build/header/Header.mjs.map +1 -1
  5. package/build/index.js +1 -1
  6. package/build/index.mjs +1 -1
  7. package/build/link/Link.js +5 -3
  8. package/build/link/Link.js.map +1 -1
  9. package/build/link/Link.mjs +5 -3
  10. package/build/link/Link.mjs.map +1 -1
  11. package/build/main.css +3 -0
  12. package/build/selectOption/SelectOption.js +1 -1
  13. package/build/selectOption/SelectOption.js.map +1 -1
  14. package/build/selectOption/SelectOption.mjs +1 -1
  15. package/build/styles/header/Header.css +3 -0
  16. package/build/styles/main.css +3 -0
  17. package/build/title/Title.js +6 -3
  18. package/build/title/Title.js.map +1 -1
  19. package/build/title/Title.mjs +6 -3
  20. package/build/title/Title.mjs.map +1 -1
  21. package/build/typeahead/Typeahead.js +1 -1
  22. package/build/typeahead/Typeahead.js.map +1 -1
  23. package/build/typeahead/Typeahead.mjs +1 -1
  24. package/build/typeahead/Typeahead.mjs.map +1 -1
  25. package/build/typeahead/typeaheadOption/TypeaheadOption.js +4 -6
  26. package/build/typeahead/typeaheadOption/TypeaheadOption.js.map +1 -1
  27. package/build/typeahead/typeaheadOption/TypeaheadOption.mjs +5 -7
  28. package/build/typeahead/typeaheadOption/TypeaheadOption.mjs.map +1 -1
  29. package/build/typeahead/util/highlight.js +3 -8
  30. package/build/typeahead/util/highlight.js.map +1 -1
  31. package/build/typeahead/util/highlight.mjs +4 -9
  32. package/build/typeahead/util/highlight.mjs.map +1 -1
  33. package/build/types/header/Header.d.ts +34 -10
  34. package/build/types/header/Header.d.ts.map +1 -1
  35. package/build/types/header/index.d.ts +1 -0
  36. package/build/types/header/index.d.ts.map +1 -1
  37. package/build/types/index.d.ts +1 -0
  38. package/build/types/index.d.ts.map +1 -1
  39. package/build/types/link/Link.d.ts +3 -1
  40. package/build/types/link/Link.d.ts.map +1 -1
  41. package/build/types/title/Title.d.ts +3 -4
  42. package/build/types/title/Title.d.ts.map +1 -1
  43. package/build/types/typeahead/typeaheadOption/TypeaheadOption.d.ts.map +1 -1
  44. package/build/types/typeahead/util/highlight.d.ts +1 -5
  45. package/build/types/typeahead/util/highlight.d.ts.map +1 -1
  46. package/package.json +3 -3
  47. package/src/header/Header.css +3 -0
  48. package/src/header/Header.less +4 -0
  49. package/src/header/Header.spec.tsx +33 -0
  50. package/src/header/Header.story.tsx +54 -40
  51. package/src/header/Header.tsx +126 -60
  52. package/src/header/index.ts +1 -0
  53. package/src/index.ts +1 -0
  54. package/src/link/Link.tsx +29 -27
  55. package/src/main.css +3 -0
  56. package/src/title/Title.tsx +25 -11
  57. package/src/typeahead/Typeahead.rtl.spec.tsx +0 -26
  58. package/src/typeahead/Typeahead.tsx +2 -2
  59. package/src/typeahead/typeaheadOption/TypeaheadOption.spec.js +4 -5
  60. package/src/typeahead/typeaheadOption/TypeaheadOption.tsx +3 -7
  61. package/src/typeahead/util/highlight.spec.js +5 -5
  62. package/src/typeahead/util/highlight.tsx +3 -11
@@ -1,92 +1,158 @@
1
1
  import { clsx } from 'clsx';
2
2
 
3
3
  import { ActionButtonProps } from '../actionButton/ActionButton';
4
- import Button from '../button';
5
4
  import { AriaLabelProperty, CommonProps, Heading, LinkProps, Typography } from '../common';
6
5
  import Link from '../link';
6
+ import Button from '../button';
7
7
  import Title from '../title';
8
+ import React, { useEffect, useRef, FunctionComponent } from 'react';
8
9
 
9
10
  type ActionProps = AriaLabelProperty & {
10
11
  text: string;
11
12
  };
12
13
 
13
14
  type ButtonActionProps = ActionProps & ActionButtonProps;
14
-
15
15
  type LinkActionProps = ActionProps & LinkProps;
16
16
 
17
- export type HeaderProps = CommonProps & {
17
+ export interface HeaderProps extends CommonProps {
18
18
  /**
19
- * When the `href` property is provided to the `action`, we will render a `Link` instead of a `ActionButton`.
19
+ * Optional prop to define the action for the header. If the `href` property
20
+ * is provided, a `Link` will be rendered instead of an `ActionButton`.
20
21
  */
21
22
  action?: ButtonActionProps | LinkActionProps;
22
- /**
23
- * Override the heading element rendered for the title, useful to specify the semantics of your header.
24
- *
25
- * @default "h5"
26
- */
23
+
24
+ /** Option prop to specify DOM render element of the title */
27
25
  as?: Heading | 'legend';
26
+
27
+ /** Optional prop to specify classNames onto the Header */
28
+ className?: string;
29
+
30
+ /** Optional prop to specify the ID used for testing */
31
+ testId?: string;
32
+
33
+ /** Required prop to set the title of the Header. */
28
34
  title: string;
29
- };
30
35
 
31
- const HeaderAction = ({ action }: { action: ButtonActionProps | LinkActionProps }) => {
32
- const props = {
33
- 'aria-label': action['aria-label'],
34
- };
36
+ /** Optional prop to specify the variant of the Header */
37
+ variant?: 'section' | 'group';
38
+ }
39
+
40
+ /**
41
+ * Renders a header action which can be either a button or a link.
42
+ *
43
+ * @param {Object} props - The properties object.
44
+ * @param {ButtonActionProps | LinkActionProps} props.action - The action object which can be either a button or a link.
45
+ * @returns {JSX.Element} The rendered header action component.
46
+ */
47
+ const HeaderAction = React.forwardRef(
48
+ (
49
+ { action }: { action: ButtonActionProps | LinkActionProps },
50
+ ref: React.Ref<HTMLButtonElement | HTMLAnchorElement>,
51
+ ) => {
52
+ const { 'aria-label': ariaLabel, text, onClick } = action;
53
+
54
+ if ('href' in action) {
55
+ const { href, target, onClick: linkOnClick } = action;
56
+ return (
57
+ <Link
58
+ ref={ref as React.Ref<HTMLAnchorElement>}
59
+ href={href}
60
+ target={target}
61
+ aria-label={ariaLabel}
62
+ onClick={linkOnClick}
63
+ >
64
+ {text}
65
+ </Link>
66
+ );
67
+ }
35
68
 
36
- if ('href' in action) {
37
69
  return (
38
- <Link href={action.href} target={action.target} onClick={action.onClick} {...props}>
39
- {action.text}
40
- </Link>
70
+ <Button
71
+ ref={ref as React.Ref<HTMLButtonElement>}
72
+ className="np-header__button"
73
+ priority="tertiary"
74
+ size="sm"
75
+ aria-label={ariaLabel}
76
+ onClick={onClick}
77
+ >
78
+ {text}
79
+ </Button>
41
80
  );
42
- }
43
-
44
- return (
45
- <Button
46
- className="np-header__button"
47
- priority="tertiary"
48
- size="sm"
49
- onClick={action.onClick}
50
- {...props}
51
- >
52
- {action.text}
53
- </Button>
54
- );
55
- };
81
+ },
82
+ );
56
83
 
57
84
  /**
85
+ * Header component
86
+ *
87
+ * The header component is used to render a section header with an optional action button or link.
88
+ * The header component can be rendered as a `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, or `legend` element.
58
89
  *
59
- * Neptune Web: https://transferwise.github.io/neptune-web/components/content/Header
90
+ * @component
91
+ * @param {ButtonActionProps | LinkActionProps} [action] - Optional prop to specify the action button or link.
92
+ * @param {Heading | 'legend'} [as='h5'] - Optional prop to override the heading element rendered for the title.
93
+ * @param {string} title - Required prop to set the title of the section header.
94
+ * @param {'group' | 'section'} [variant='group'] - Optional prop to specify the variant of the section header.
95
+ * @param {string} [className] - Optional prop to specify classNames onto the Header.
96
+ * @param {string} [testId] - Optional prop to specify the ID used for testing.
60
97
  *
98
+ * @example
99
+ * // Example usage:
100
+ * import Header from './Header';
101
+ *
102
+ * function App() {
103
+ * return (
104
+ * <Header title="Header" />
105
+ * );
106
+ * }
61
107
  */
62
- export const Header = ({ action, as = 'h5', title, className }: HeaderProps) => {
63
- if (!action) {
64
- return (
65
- <Title
66
- as={as}
67
- type={Typography.TITLE_GROUP}
68
- className={clsx('np-header', 'np-header__title', className)}
69
- >
70
- {title}
71
- </Title>
72
- );
73
- }
108
+ const Header: FunctionComponent<HeaderProps> = React.forwardRef(
109
+ (
110
+ { as = 'h5', action, className, testId, title, variant = 'group', ...props },
111
+ ref: React.Ref<HTMLDivElement | HTMLHeadingElement | HTMLLegendElement>,
112
+ ) => {
113
+ const internalRef = useRef<HTMLLegendElement>(null);
114
+ const variantTypography =
115
+ variant === 'section' ? Typography.TITLE_SUBSECTION : Typography.TITLE_GROUP;
116
+ const headerClasses = clsx('np-header', className, {
117
+ 'np-header--section': variant === 'section',
118
+ 'np-header__title': !action || as === 'legend',
119
+ });
120
+
121
+ const commonProps = {
122
+ className: headerClasses,
123
+ 'data-testid': testId,
124
+ };
125
+
126
+ useEffect(() => {
127
+ if (as === 'legend' && internalRef.current) {
128
+ const { parentElement } = internalRef.current;
129
+ if (!parentElement || parentElement.tagName.toLowerCase() !== 'fieldset') {
130
+ console.warn(
131
+ 'Legends should be the first child in a fieldset, and this is not possible when including an action',
132
+ );
133
+ }
134
+ }
135
+ }, [as]);
136
+
137
+ if (!action || as === 'legend') {
138
+ return (
139
+ <Title ref={internalRef} as={as} type={variantTypography} {...commonProps} {...props}>
140
+ {title}
141
+ </Title>
142
+ );
143
+ }
74
144
 
75
- if (as === 'legend') {
76
- // eslint-disable-next-line no-console
77
- console.warn(
78
- 'Legends should be the first child in a fieldset, and this is not possible when including an action',
145
+ const actionRef = React.createRef<HTMLButtonElement | HTMLAnchorElement>();
146
+
147
+ return (
148
+ <div {...commonProps} {...props} ref={ref as React.Ref<HTMLDivElement>}>
149
+ <Title as={as} type={variantTypography} className="np-header__title">
150
+ {title}
151
+ </Title>
152
+ <HeaderAction ref={actionRef} action={action} />
153
+ </div>
79
154
  );
80
- }
81
-
82
- return (
83
- <div className={clsx('np-header', className)}>
84
- <Title as={as} type={Typography.TITLE_GROUP} className="np-header__title">
85
- {title}
86
- </Title>
87
- <HeaderAction action={action} />
88
- </div>
89
- );
90
- };
155
+ },
156
+ );
91
157
 
92
158
  export default Header;
@@ -1 +1,2 @@
1
1
  export { default } from './Header';
2
+ export type { HeaderProps } from './Header';
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ export type { DecisionProps } from './decision/Decision';
28
28
  export type { DefinitionListProps, DefinitionListDefinition } from './definitionList';
29
29
  export type { DimmerProps } from './dimmer';
30
30
  export type { DrawerProps } from './drawer';
31
+ export type { HeaderProps } from './header';
31
32
  export type { EmphasisProps } from './emphasis';
32
33
  export type { FieldProps } from './field/Field';
33
34
  export type { InfoProps } from './info';
package/src/link/Link.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import { NavigateAway as NavigateAwayIcon } from '@transferwise/icons';
2
2
  import { clsx } from 'clsx';
3
- import { AnchorHTMLAttributes } from 'react';
3
+ import { AnchorHTMLAttributes, forwardRef } from 'react';
4
4
  import { useIntl } from 'react-intl';
5
5
 
6
6
  import { LinkLarge, LinkDefault } from '../common';
@@ -14,33 +14,35 @@ export type Props = AnchorHTMLAttributes<HTMLAnchorElement> & { type?: LinkLarge
14
14
  *
15
15
  * Documentation: https://transferwise.github.io/neptune-web/components/content/Link
16
16
  */
17
- const Link = ({
18
- className,
19
- children,
20
- href,
21
- target,
22
- type,
23
- 'aria-label': ariaLabel,
24
- onClick,
25
- ...props
26
- }: Props) => {
27
- const isBlank = target === '_blank';
17
+ const Link = forwardRef<HTMLAnchorElement, Props>(
18
+ (
19
+ { className, children, href, target, type, 'aria-label': ariaLabel, onClick, ...props }: Props,
20
+ ref,
21
+ ) => {
22
+ const isBlank = target === '_blank';
28
23
 
29
- const { formatMessage } = useIntl();
24
+ const { formatMessage } = useIntl();
30
25
 
31
- return (
32
- <a
33
- href={href}
34
- target={target}
35
- className={clsx('np-link', type ? `np-text-${type}` : undefined, 'd-inline-flex', className)}
36
- aria-label={ariaLabel}
37
- rel={isBlank ? 'noreferrer' : undefined}
38
- onClick={onClick}
39
- {...props}
40
- >
41
- {children} {isBlank && <NavigateAwayIcon title={formatMessage(messages.opensInNewTab)} />}
42
- </a>
43
- );
44
- };
26
+ return (
27
+ <a
28
+ ref={ref}
29
+ href={href}
30
+ target={target}
31
+ className={clsx(
32
+ 'np-link',
33
+ type ? `np-text-${type}` : undefined,
34
+ 'd-inline-flex',
35
+ className,
36
+ )}
37
+ aria-label={ariaLabel}
38
+ rel={isBlank ? 'noreferrer' : undefined}
39
+ onClick={onClick}
40
+ {...props}
41
+ >
42
+ {children} {isBlank && <NavigateAwayIcon title={formatMessage(messages.opensInNewTab)} />}
43
+ </a>
44
+ );
45
+ },
46
+ );
45
47
 
46
48
  export default Link;
package/src/main.css CHANGED
@@ -2242,6 +2242,9 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
2242
2242
  -moz-column-gap: var(--size-24);
2243
2243
  column-gap: var(--size-24);
2244
2244
  }
2245
+ .np-header--section {
2246
+ border-bottom: none;
2247
+ }
2245
2248
  .np-header__title {
2246
2249
  color: #5d7079;
2247
2250
  color: var(--color-content-secondary);
@@ -1,5 +1,5 @@
1
1
  import { clsx } from 'clsx';
2
- import { LabelHTMLAttributes, LiHTMLAttributes, ReactHTML } from 'react';
2
+ import { forwardRef, LabelHTMLAttributes, LiHTMLAttributes } from 'react';
3
3
 
4
4
  import { TitleTypes, Typography, Heading } from '../common';
5
5
 
@@ -25,15 +25,29 @@ type Props = LabelHTMLAttributes<HTMLHeadingElement | HTMLSpanElement | HTMLLabe
25
25
  type?: TitleTypes;
26
26
  };
27
27
 
28
- function Title({ as, type = DEFAULT_TYPE, className, ...props }: Props) {
29
- const mapping = titleTypeMapping[type];
30
- const isTypeSupported = mapping !== undefined;
31
- if (isTypeSupported) {
32
- const HeaderTag = as ?? mapping;
33
- return <HeaderTag {...props} className={clsx(`np-text-${type}`, className)} />;
34
- }
35
- const HeaderTag = as ?? titleTypeMapping[DEFAULT_TYPE];
36
- return <HeaderTag {...props} className={clsx(`np-text-${DEFAULT_TYPE}`, className)} />;
37
- }
28
+ const Title = forwardRef<HTMLHeadingElement | HTMLSpanElement | HTMLLabelElement, Props>(
29
+ ({ as, type = DEFAULT_TYPE, className, ...props }, ref) => {
30
+ const mapping = titleTypeMapping[type as keyof typeof titleTypeMapping];
31
+ const isTypeSupported = mapping !== undefined;
32
+ if (isTypeSupported) {
33
+ const HeaderTag = as ?? mapping;
34
+ return (
35
+ <HeaderTag
36
+ ref={ref as React.Ref<any>}
37
+ {...props}
38
+ className={clsx(`np-text-${type}`, className)}
39
+ />
40
+ );
41
+ }
42
+ const HeaderTag = as ?? titleTypeMapping[DEFAULT_TYPE];
43
+ return (
44
+ <HeaderTag
45
+ ref={ref as React.Ref<any>}
46
+ {...props}
47
+ className={clsx(`np-text-${DEFAULT_TYPE}`, className)}
48
+ />
49
+ );
50
+ },
51
+ );
38
52
 
39
53
  export default Title;
@@ -25,30 +25,4 @@ describe('Typeahead', () => {
25
25
  );
26
26
  expect(screen.getAllByRole('group')[0]).toHaveAccessibleName(/^Tags/);
27
27
  });
28
-
29
- describe('when no options are provided', () => {
30
- it('does not render a dropdown when no options and no footer are provided', () => {
31
- render(
32
- <Field id="test" label="Tags">
33
- <Typeahead id="test" name="test" options={[]} intl={intl} onChange={() => {}} />
34
- </Field>,
35
- );
36
- expect(screen.queryByRole('menu')).not.toBeInTheDocument();
37
- });
38
- it('does render a dropdown when only a footer is provided', () => {
39
- render(
40
- <Field id="test" label="Tags">
41
- <Typeahead
42
- id="test"
43
- name="test"
44
- options={[]}
45
- intl={intl}
46
- footer={<p>hello</p>}
47
- onChange={() => {}}
48
- />
49
- </Field>,
50
- );
51
- expect(screen.getByRole('menu')).toBeInTheDocument();
52
- });
53
- });
54
28
  });
@@ -406,7 +406,7 @@ class Typeahead<T> extends Component<TypeaheadPropsWithInputAttributes<T>, Typea
406
406
  className={clsx('dropdown btn-group btn-block', { open: dropdownOpen })}
407
407
  id={`menu-${id}`}
408
408
  >
409
- {(!!optionsToRender.length || footer) && (
409
+ {!!optionsToRender.length && (
410
410
  <ul className="dropdown-menu" role="menu">
411
411
  {optionsToRender.map((option, idx) => {
412
412
  const ref = React.createRef<HTMLLIElement>();
@@ -423,7 +423,7 @@ class Typeahead<T> extends Component<TypeaheadPropsWithInputAttributes<T>, Typea
423
423
  this.onOptionSelected(event, option);
424
424
  }}
425
425
  />
426
- );
426
+ )
427
427
  })}
428
428
  {footer}
429
429
  </ul>
@@ -3,15 +3,14 @@ import { shallow } from 'enzyme';
3
3
  import { fakeEvent } from '../../common/fakeEvents';
4
4
 
5
5
  import TypeaheadOption from './TypeaheadOption';
6
- import Highlight from '../util/highlight';
7
6
 
8
7
  describe('Typeahead Option', () => {
9
8
  let props;
10
9
  let component;
11
10
 
12
- const labelHighlight = () => component.find(Highlight);
11
+ const labelSpan = () => component.find('span:first-child');
13
12
  const noteSpan = () => component.find('.np-text-body-default.m-l-1');
14
- const secondaryTextHighlight = () => component.find('.np-text-body-default.text-ellipsis');
13
+ const secondaryTextSpan = () => component.find('.np-text-body-default.text-ellipsis');
15
14
  const dropdownItem = () => component.find('.dropdown-item');
16
15
 
17
16
  beforeEach(() => {
@@ -27,7 +26,7 @@ describe('Typeahead Option', () => {
27
26
  it('renders a label', () => {
28
27
  const label = 'test';
29
28
  component.setProps({ option: { label } });
30
- expect(labelHighlight().dive().text()).toStrictEqual(label);
29
+ expect(labelSpan().text()).toStrictEqual(label);
31
30
  });
32
31
 
33
32
  it('renders a note', () => {
@@ -41,7 +40,7 @@ describe('Typeahead Option', () => {
41
40
  const label = 'test';
42
41
  const secondary = 'test note';
43
42
  component.setProps({ option: { label, secondary } });
44
- expect(secondaryTextHighlight().dive().text()).toStrictEqual(secondary);
43
+ expect(secondaryTextSpan().text()).toStrictEqual(secondary);
45
44
  });
46
45
 
47
46
  it('highlights when selected', () => {
@@ -2,7 +2,7 @@ import { clsx } from 'clsx';
2
2
  import { forwardRef } from 'react';
3
3
 
4
4
  import { TypeaheadOption } from '../Typeahead';
5
- import Highlight from '../util/highlight';
5
+ import highlight from '../util/highlight';
6
6
 
7
7
  export type TypeaheadOptionProps<T> = {
8
8
  option: TypeaheadOption<T>;
@@ -27,14 +27,10 @@ const Option = forwardRef<HTMLLIElement, TypeaheadOptionProps<any>>((props, ref)
27
27
  })}
28
28
  >
29
29
  <a className="dropdown-item" href="#" tabIndex={0} onClick={onClick}>
30
- <Highlight value={label} query={query} />
30
+ <span>{highlight(label, query)}</span>
31
31
  {note && <span className="np-text-body-default m-l-1">{note}</span>}
32
32
  {secondary && (
33
- <Highlight
34
- className="np-text-body-default text-ellipsis"
35
- value={secondary}
36
- query={query}
37
- />
33
+ <span className="np-text-body-default text-ellipsis">{highlight(secondary, query)}</span>
38
34
  )}
39
35
  </a>
40
36
  </li>
@@ -1,6 +1,6 @@
1
1
  import { mount } from 'enzyme';
2
2
 
3
- import Highlight from './highlight';
3
+ import highlight from './highlight';
4
4
 
5
5
  describe('Typeahead input', () => {
6
6
  const highlighted = (node) => node.find('strong');
@@ -9,7 +9,7 @@ describe('Typeahead input', () => {
9
9
  it('highlights part of label that matches the query', () => {
10
10
  const query = 'test';
11
11
  const label = `this is a ${query} label`;
12
- const result = mount(<Highlight value={label} query={query} />);
12
+ const result = mount(highlight(label, query));
13
13
 
14
14
  expect(highlighted(result).text()).toStrictEqual(query);
15
15
 
@@ -19,15 +19,15 @@ describe('Typeahead input', () => {
19
19
  it('does not change text if query is not present in it', () => {
20
20
  const query = 'test';
21
21
  const label = `this is a label`;
22
- const result = mount(<Highlight value={label} query={query} />);
22
+ const result = highlight(label, query);
23
23
 
24
- expect(result.text()).toBe(label);
24
+ expect(result).toBe(label);
25
25
  });
26
26
 
27
27
  it('highlights whole label that matches the query', () => {
28
28
  const query = 'test';
29
29
  const label = query;
30
- const result = mount(<Highlight value={label} query={query} />);
30
+ const result = mount(highlight(label, query));
31
31
 
32
32
  expect(highlighted(result).text()).toStrictEqual(query);
33
33
  });
@@ -1,22 +1,14 @@
1
- export default function Highlight({
2
- className,
3
- value,
4
- query,
5
- }: {
6
- className?: string;
7
- value: string;
8
- query: string;
9
- }) {
1
+ export default function highlight(value: string, query: string) {
10
2
  if (value && query) {
11
3
  const highlightStart = value.toUpperCase().indexOf(query.trim().toUpperCase());
12
4
  const highlightEnd = highlightStart + query.trim().length;
13
5
  if (highlightStart !== -1) {
14
6
  return (
15
- <span className={className}>
7
+ <>
16
8
  {value.slice(0, Math.max(0, highlightStart))}
17
9
  <strong>{value.slice(highlightStart, highlightEnd)}</strong>
18
10
  {value.slice(Math.max(0, highlightEnd))}
19
- </span>
11
+ </>
20
12
  );
21
13
  }
22
14
  }