@transferwise/components 0.0.0-experimental-b2dc1ea → 0.0.0-experimental-1fd4714
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/build/header/Header.js +34 -89
- package/build/header/Header.js.map +1 -1
- package/build/header/Header.mjs +31 -85
- package/build/header/Header.mjs.map +1 -1
- package/build/index.js +1 -1
- package/build/index.mjs +1 -1
- package/build/link/Link.js +3 -5
- package/build/link/Link.js.map +1 -1
- package/build/link/Link.mjs +3 -5
- package/build/link/Link.mjs.map +1 -1
- package/build/main.css +0 -3
- package/build/selectOption/SelectOption.js +1 -1
- package/build/selectOption/SelectOption.js.map +1 -1
- package/build/selectOption/SelectOption.mjs +1 -1
- package/build/styles/header/Header.css +0 -3
- package/build/styles/main.css +0 -3
- package/build/title/Title.js +3 -6
- package/build/title/Title.js.map +1 -1
- package/build/title/Title.mjs +3 -6
- package/build/title/Title.mjs.map +1 -1
- package/build/typeahead/Typeahead.js +1 -1
- package/build/typeahead/Typeahead.js.map +1 -1
- package/build/typeahead/Typeahead.mjs +1 -1
- package/build/typeahead/Typeahead.mjs.map +1 -1
- package/build/typeahead/typeaheadOption/TypeaheadOption.js +6 -4
- package/build/typeahead/typeaheadOption/TypeaheadOption.js.map +1 -1
- package/build/typeahead/typeaheadOption/TypeaheadOption.mjs +7 -5
- package/build/typeahead/typeaheadOption/TypeaheadOption.mjs.map +1 -1
- package/build/typeahead/util/highlight.js +8 -3
- package/build/typeahead/util/highlight.js.map +1 -1
- package/build/typeahead/util/highlight.mjs +9 -4
- package/build/typeahead/util/highlight.mjs.map +1 -1
- package/build/types/header/Header.d.ts +10 -34
- package/build/types/header/Header.d.ts.map +1 -1
- package/build/types/header/index.d.ts +0 -1
- package/build/types/header/index.d.ts.map +1 -1
- package/build/types/index.d.ts +0 -1
- package/build/types/index.d.ts.map +1 -1
- package/build/types/link/Link.d.ts +1 -3
- package/build/types/link/Link.d.ts.map +1 -1
- package/build/types/title/Title.d.ts +4 -3
- package/build/types/title/Title.d.ts.map +1 -1
- package/build/types/typeahead/typeaheadOption/TypeaheadOption.d.ts.map +1 -1
- package/build/types/typeahead/util/highlight.d.ts +5 -1
- package/build/types/typeahead/util/highlight.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/header/Header.css +0 -3
- package/src/header/Header.less +0 -4
- package/src/header/Header.spec.tsx +0 -33
- package/src/header/Header.story.tsx +40 -54
- package/src/header/Header.tsx +60 -126
- package/src/header/index.ts +0 -1
- package/src/index.ts +0 -1
- package/src/link/Link.tsx +27 -29
- package/src/main.css +0 -3
- package/src/title/Title.tsx +11 -25
- package/src/typeahead/Typeahead.rtl.spec.tsx +26 -0
- package/src/typeahead/Typeahead.tsx +2 -2
- package/src/typeahead/typeaheadOption/TypeaheadOption.spec.js +5 -4
- package/src/typeahead/typeaheadOption/TypeaheadOption.tsx +7 -3
- package/src/typeahead/util/highlight.spec.js +5 -5
- package/src/typeahead/util/highlight.tsx +11 -3
package/src/header/Header.tsx
CHANGED
|
@@ -1,158 +1,92 @@
|
|
|
1
1
|
import { clsx } from 'clsx';
|
|
2
2
|
|
|
3
3
|
import { ActionButtonProps } from '../actionButton/ActionButton';
|
|
4
|
+
import Button from '../button';
|
|
4
5
|
import { AriaLabelProperty, CommonProps, Heading, LinkProps, Typography } from '../common';
|
|
5
6
|
import Link from '../link';
|
|
6
|
-
import Button from '../button';
|
|
7
7
|
import Title from '../title';
|
|
8
|
-
import React, { useEffect, useRef, FunctionComponent } from 'react';
|
|
9
8
|
|
|
10
9
|
type ActionProps = AriaLabelProperty & {
|
|
11
10
|
text: string;
|
|
12
11
|
};
|
|
13
12
|
|
|
14
13
|
type ButtonActionProps = ActionProps & ActionButtonProps;
|
|
14
|
+
|
|
15
15
|
type LinkActionProps = ActionProps & LinkProps;
|
|
16
16
|
|
|
17
|
-
export
|
|
17
|
+
export type HeaderProps = CommonProps & {
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
* is provided, a `Link` will be rendered instead of an `ActionButton`.
|
|
19
|
+
* When the `href` property is provided to the `action`, we will render a `Link` instead of a `ActionButton`.
|
|
21
20
|
*/
|
|
22
21
|
action?: ButtonActionProps | LinkActionProps;
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Override the heading element rendered for the title, useful to specify the semantics of your header.
|
|
24
|
+
*
|
|
25
|
+
* @default "h5"
|
|
26
|
+
*/
|
|
25
27
|
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. */
|
|
34
28
|
title: string;
|
|
29
|
+
};
|
|
35
30
|
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
}
|
|
31
|
+
const HeaderAction = ({ action }: { action: ButtonActionProps | LinkActionProps }) => {
|
|
32
|
+
const props = {
|
|
33
|
+
'aria-label': action['aria-label'],
|
|
34
|
+
};
|
|
68
35
|
|
|
36
|
+
if ('href' in action) {
|
|
69
37
|
return (
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
priority="tertiary"
|
|
74
|
-
size="sm"
|
|
75
|
-
aria-label={ariaLabel}
|
|
76
|
-
onClick={onClick}
|
|
77
|
-
>
|
|
78
|
-
{text}
|
|
79
|
-
</Button>
|
|
38
|
+
<Link href={action.href} target={action.target} onClick={action.onClick} {...props}>
|
|
39
|
+
{action.text}
|
|
40
|
+
</Link>
|
|
80
41
|
);
|
|
81
|
-
}
|
|
82
|
-
|
|
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
|
+
};
|
|
83
56
|
|
|
84
57
|
/**
|
|
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.
|
|
89
58
|
*
|
|
90
|
-
*
|
|
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.
|
|
59
|
+
* Neptune Web: https://transferwise.github.io/neptune-web/components/content/Header
|
|
97
60
|
*
|
|
98
|
-
* @example
|
|
99
|
-
* // Example usage:
|
|
100
|
-
* import Header from './Header';
|
|
101
|
-
*
|
|
102
|
-
* function App() {
|
|
103
|
-
* return (
|
|
104
|
-
* <Header title="Header" />
|
|
105
|
-
* );
|
|
106
|
-
* }
|
|
107
61
|
*/
|
|
108
|
-
const Header:
|
|
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
|
-
}
|
|
144
|
-
|
|
145
|
-
const actionRef = React.createRef<HTMLButtonElement | HTMLAnchorElement>();
|
|
146
|
-
|
|
62
|
+
export const Header = ({ action, as = 'h5', title, className }: HeaderProps) => {
|
|
63
|
+
if (!action) {
|
|
147
64
|
return (
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
65
|
+
<Title
|
|
66
|
+
as={as}
|
|
67
|
+
type={Typography.TITLE_GROUP}
|
|
68
|
+
className={clsx('np-header', 'np-header__title', className)}
|
|
69
|
+
>
|
|
70
|
+
{title}
|
|
71
|
+
</Title>
|
|
154
72
|
);
|
|
155
|
-
}
|
|
156
|
-
|
|
73
|
+
}
|
|
74
|
+
|
|
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',
|
|
79
|
+
);
|
|
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
|
+
};
|
|
157
91
|
|
|
158
92
|
export default Header;
|
package/src/header/index.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -28,7 +28,6 @@ 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';
|
|
32
31
|
export type { EmphasisProps } from './emphasis';
|
|
33
32
|
export type { FieldProps } from './field/Field';
|
|
34
33
|
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
|
|
3
|
+
import { AnchorHTMLAttributes } from 'react';
|
|
4
4
|
import { useIntl } from 'react-intl';
|
|
5
5
|
|
|
6
6
|
import { LinkLarge, LinkDefault } from '../common';
|
|
@@ -14,35 +14,33 @@ 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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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';
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
const { formatMessage } = useIntl();
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
{...props}
|
|
41
|
-
>
|
|
42
|
-
{children} {isBlank && <NavigateAwayIcon title={formatMessage(messages.opensInNewTab)} />}
|
|
43
|
-
</a>
|
|
44
|
-
);
|
|
45
|
-
},
|
|
46
|
-
);
|
|
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
|
+
};
|
|
47
45
|
|
|
48
46
|
export default Link;
|
package/src/main.css
CHANGED
|
@@ -2242,9 +2242,6 @@ 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
|
-
}
|
|
2248
2245
|
.np-header__title {
|
|
2249
2246
|
color: #5d7079;
|
|
2250
2247
|
color: var(--color-content-secondary);
|
package/src/title/Title.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { clsx } from 'clsx';
|
|
2
|
-
import {
|
|
2
|
+
import { LabelHTMLAttributes, LiHTMLAttributes, ReactHTML } from 'react';
|
|
3
3
|
|
|
4
4
|
import { TitleTypes, Typography, Heading } from '../common';
|
|
5
5
|
|
|
@@ -25,29 +25,15 @@ type Props = LabelHTMLAttributes<HTMLHeadingElement | HTMLSpanElement | HTMLLabe
|
|
|
25
25
|
type?: TitleTypes;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
);
|
|
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
|
+
}
|
|
52
38
|
|
|
53
39
|
export default Title;
|
|
@@ -25,4 +25,30 @@ 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
|
+
});
|
|
28
54
|
});
|
|
@@ -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 && (
|
|
409
|
+
{(!!optionsToRender.length || footer) && (
|
|
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,14 +3,15 @@ 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';
|
|
6
7
|
|
|
7
8
|
describe('Typeahead Option', () => {
|
|
8
9
|
let props;
|
|
9
10
|
let component;
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
+
const labelHighlight = () => component.find(Highlight);
|
|
12
13
|
const noteSpan = () => component.find('.np-text-body-default.m-l-1');
|
|
13
|
-
const
|
|
14
|
+
const secondaryTextHighlight = () => component.find('.np-text-body-default.text-ellipsis');
|
|
14
15
|
const dropdownItem = () => component.find('.dropdown-item');
|
|
15
16
|
|
|
16
17
|
beforeEach(() => {
|
|
@@ -26,7 +27,7 @@ describe('Typeahead Option', () => {
|
|
|
26
27
|
it('renders a label', () => {
|
|
27
28
|
const label = 'test';
|
|
28
29
|
component.setProps({ option: { label } });
|
|
29
|
-
expect(
|
|
30
|
+
expect(labelHighlight().dive().text()).toStrictEqual(label);
|
|
30
31
|
});
|
|
31
32
|
|
|
32
33
|
it('renders a note', () => {
|
|
@@ -40,7 +41,7 @@ describe('Typeahead Option', () => {
|
|
|
40
41
|
const label = 'test';
|
|
41
42
|
const secondary = 'test note';
|
|
42
43
|
component.setProps({ option: { label, secondary } });
|
|
43
|
-
expect(
|
|
44
|
+
expect(secondaryTextHighlight().dive().text()).toStrictEqual(secondary);
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
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
|
|
5
|
+
import Highlight from '../util/highlight';
|
|
6
6
|
|
|
7
7
|
export type TypeaheadOptionProps<T> = {
|
|
8
8
|
option: TypeaheadOption<T>;
|
|
@@ -27,10 +27,14 @@ const Option = forwardRef<HTMLLIElement, TypeaheadOptionProps<any>>((props, ref)
|
|
|
27
27
|
})}
|
|
28
28
|
>
|
|
29
29
|
<a className="dropdown-item" href="#" tabIndex={0} onClick={onClick}>
|
|
30
|
-
<
|
|
30
|
+
<Highlight value={label} query={query} />
|
|
31
31
|
{note && <span className="np-text-body-default m-l-1">{note}</span>}
|
|
32
32
|
{secondary && (
|
|
33
|
-
<
|
|
33
|
+
<Highlight
|
|
34
|
+
className="np-text-body-default text-ellipsis"
|
|
35
|
+
value={secondary}
|
|
36
|
+
query={query}
|
|
37
|
+
/>
|
|
34
38
|
)}
|
|
35
39
|
</a>
|
|
36
40
|
</li>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mount } from 'enzyme';
|
|
2
2
|
|
|
3
|
-
import
|
|
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(
|
|
12
|
+
const result = mount(<Highlight value={label} query={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 =
|
|
22
|
+
const result = mount(<Highlight value={label} query={query} />);
|
|
23
23
|
|
|
24
|
-
expect(result).toBe(label);
|
|
24
|
+
expect(result.text()).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(
|
|
30
|
+
const result = mount(<Highlight value={label} query={query} />);
|
|
31
31
|
|
|
32
32
|
expect(highlighted(result).text()).toStrictEqual(query);
|
|
33
33
|
});
|
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
export default function
|
|
1
|
+
export default function Highlight({
|
|
2
|
+
className,
|
|
3
|
+
value,
|
|
4
|
+
query,
|
|
5
|
+
}: {
|
|
6
|
+
className?: string;
|
|
7
|
+
value: string;
|
|
8
|
+
query: string;
|
|
9
|
+
}) {
|
|
2
10
|
if (value && query) {
|
|
3
11
|
const highlightStart = value.toUpperCase().indexOf(query.trim().toUpperCase());
|
|
4
12
|
const highlightEnd = highlightStart + query.trim().length;
|
|
5
13
|
if (highlightStart !== -1) {
|
|
6
14
|
return (
|
|
7
|
-
|
|
15
|
+
<span className={className}>
|
|
8
16
|
{value.slice(0, Math.max(0, highlightStart))}
|
|
9
17
|
<strong>{value.slice(highlightStart, highlightEnd)}</strong>
|
|
10
18
|
{value.slice(Math.max(0, highlightEnd))}
|
|
11
|
-
|
|
19
|
+
</span>
|
|
12
20
|
);
|
|
13
21
|
}
|
|
14
22
|
}
|