@transferwise/components 0.0.0-experimental-a7b7094 → 0.0.0-experimental-580125e
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/field/Field.js +32 -63
- package/build/field/Field.js.map +1 -1
- package/build/field/Field.mjs +34 -65
- package/build/field/Field.mjs.map +1 -1
- package/build/i18n/en.json +0 -1
- package/build/i18n/en.json.js +0 -1
- package/build/i18n/en.json.js.map +1 -1
- package/build/i18n/en.json.mjs +0 -1
- package/build/i18n/en.json.mjs.map +1 -1
- package/build/inputs/TextArea.js +0 -25
- package/build/inputs/TextArea.js.map +1 -1
- package/build/inputs/TextArea.mjs +2 -27
- package/build/inputs/TextArea.mjs.map +1 -1
- package/build/inputs/contexts.js +0 -16
- package/build/inputs/contexts.js.map +1 -1
- package/build/inputs/contexts.mjs +2 -16
- package/build/inputs/contexts.mjs.map +1 -1
- package/build/main.css +3 -21
- package/build/popover/Popover.js +9 -12
- package/build/popover/Popover.js.map +1 -1
- package/build/popover/Popover.mjs +9 -12
- package/build/popover/Popover.mjs.map +1 -1
- package/build/styles/field/Field.css +3 -19
- package/build/styles/main.css +3 -21
- package/build/types/field/Field.d.ts.map +1 -1
- package/build/types/inputs/TextArea.d.ts.map +1 -1
- package/build/types/inputs/contexts.d.ts +0 -6
- package/build/types/inputs/contexts.d.ts.map +1 -1
- package/build/types/popover/Popover.d.ts.map +1 -1
- package/build/types/test-utils/index.d.ts +0 -2
- package/build/types/test-utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/field/Field.css +3 -19
- package/src/field/Field.less +3 -17
- package/src/field/Field.story.tsx +1 -5
- package/src/field/Field.test.tsx +0 -90
- package/src/field/Field.tsx +37 -84
- package/src/i18n/en.json +0 -1
- package/src/inputs/TextArea.tsx +3 -34
- package/src/inputs/contexts.tsx +1 -18
- package/src/main.css +3 -21
- package/src/popover/Popover.story.tsx +45 -0
- package/src/popover/Popover.test.story.tsx +63 -0
- package/src/popover/Popover.test.tsx +56 -0
- package/src/popover/Popover.tsx +10 -12
- package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.story.tsx +0 -1
- package/build/field/Field.messages.js +0 -14
- package/build/field/Field.messages.js.map +0 -1
- package/build/field/Field.messages.mjs +0 -10
- package/build/field/Field.messages.mjs.map +0 -1
- package/build/types/field/Field.messages.d.ts +0 -8
- package/build/types/field/Field.messages.d.ts.map +0 -1
- package/src/field/Field.messages.ts +0 -8
- package/src/inputs/TextArea.story.tsx +0 -56
package/src/field/Field.tsx
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { clsx } from 'clsx';
|
|
2
|
-
import {
|
|
3
|
-
import { useIntl } from 'react-intl';
|
|
2
|
+
import { useId, useRef } from 'react';
|
|
4
3
|
|
|
5
|
-
import Body from '../body';
|
|
6
4
|
import { Sentiment } from '../common';
|
|
7
|
-
import messages from './Field.messages';
|
|
8
5
|
import { InlinePrompt, type InlinePromptProps } from '../prompt';
|
|
9
6
|
import {
|
|
10
|
-
TextareaCharacterCountProvider,
|
|
11
|
-
type TextareaCharacterCountState,
|
|
12
7
|
FieldLabelContextProvider,
|
|
13
8
|
InputDescribedByProvider,
|
|
14
9
|
InputIdContextProvider,
|
|
@@ -59,7 +54,6 @@ export const Field = ({
|
|
|
59
54
|
children,
|
|
60
55
|
...props
|
|
61
56
|
}: FieldProps) => {
|
|
62
|
-
const { formatMessage } = useIntl();
|
|
63
57
|
const labelRef = useRef<HTMLLabelElement>(null);
|
|
64
58
|
const sentiment = props.error ? Sentiment.NEGATIVE : propType;
|
|
65
59
|
const message = propMessage || props.error;
|
|
@@ -72,18 +66,6 @@ export const Field = ({
|
|
|
72
66
|
|
|
73
67
|
const messageId = useId();
|
|
74
68
|
const descriptionId = useId();
|
|
75
|
-
const textareaCharCounterId = useId();
|
|
76
|
-
|
|
77
|
-
const [textareaCharacterCount, setTextareaCharacterCount] =
|
|
78
|
-
useState<TextareaCharacterCountState>(null);
|
|
79
|
-
const handleTextareaCharacterCount = useCallback(
|
|
80
|
-
(state: TextareaCharacterCountState) => setTextareaCharacterCount(state),
|
|
81
|
-
[],
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
const isNearCharLimit =
|
|
85
|
-
textareaCharacterCount != null &&
|
|
86
|
-
textareaCharacterCount.current >= textareaCharacterCount.max * 0.8;
|
|
87
69
|
|
|
88
70
|
/**
|
|
89
71
|
* form control can have multiple messages to describe it,
|
|
@@ -97,9 +79,6 @@ export const Field = ({
|
|
|
97
79
|
if (message) {
|
|
98
80
|
messageIds.push(messageId);
|
|
99
81
|
}
|
|
100
|
-
if (textareaCharacterCount) {
|
|
101
|
-
messageIds.push(textareaCharCounterId);
|
|
102
|
-
}
|
|
103
82
|
return messageIds.length > 0 ? messageIds.join(' ') : undefined;
|
|
104
83
|
}
|
|
105
84
|
|
|
@@ -108,69 +87,43 @@ export const Field = ({
|
|
|
108
87
|
<InputIdContextProvider value={inputId}>
|
|
109
88
|
<InputDescribedByProvider value={ariaDescribedbyByIds()}>
|
|
110
89
|
<InputInvalidProvider value={hasError}>
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
)}
|
|
90
|
+
<div
|
|
91
|
+
className={clsx(
|
|
92
|
+
'np-field form-group d-block',
|
|
93
|
+
{
|
|
94
|
+
'has-success': sentiment === Sentiment.POSITIVE,
|
|
95
|
+
'has-warning': sentiment === Sentiment.WARNING,
|
|
96
|
+
'has-error': hasError,
|
|
97
|
+
'has-info': sentiment === Sentiment.NEUTRAL,
|
|
98
|
+
},
|
|
99
|
+
className,
|
|
100
|
+
)}
|
|
101
|
+
>
|
|
102
|
+
{label != null ? (
|
|
103
|
+
<>
|
|
104
|
+
<Label ref={labelRef} id={labelId} htmlFor={inputId}>
|
|
105
|
+
{required ? label : <Label.Optional>{label}</Label.Optional>}
|
|
106
|
+
</Label>
|
|
107
|
+
<Label.Description id={descriptionId}>{description}</Label.Description>
|
|
108
|
+
<div className="np-field-control">{children}</div>
|
|
109
|
+
</>
|
|
110
|
+
) : (
|
|
111
|
+
children
|
|
112
|
+
)}
|
|
135
113
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
)}
|
|
150
|
-
{textareaCharacterCount && (
|
|
151
|
-
<Body
|
|
152
|
-
as="span"
|
|
153
|
-
id={textareaCharCounterId}
|
|
154
|
-
{...(isNearCharLimit
|
|
155
|
-
? {
|
|
156
|
-
role: 'status' as const,
|
|
157
|
-
'aria-live': 'polite' as const,
|
|
158
|
-
'aria-atomic': 'true' as const,
|
|
159
|
-
}
|
|
160
|
-
: {})}
|
|
161
|
-
aria-label={formatMessage(messages.characterCount, {
|
|
162
|
-
current: textareaCharacterCount.current,
|
|
163
|
-
max: textareaCharacterCount.max,
|
|
164
|
-
})}
|
|
165
|
-
className="np-field-textarea-char-counter"
|
|
166
|
-
>
|
|
167
|
-
{textareaCharacterCount.current}/{textareaCharacterCount.max}
|
|
168
|
-
</Body>
|
|
169
|
-
)}
|
|
170
|
-
</div>
|
|
171
|
-
)}
|
|
172
|
-
</div>
|
|
173
|
-
</TextareaCharacterCountProvider>
|
|
114
|
+
{message && (
|
|
115
|
+
<InlinePrompt
|
|
116
|
+
sentiment={sentiment}
|
|
117
|
+
id={messageId}
|
|
118
|
+
mediaLabel={messageIconLabel}
|
|
119
|
+
className="np-field__prompt"
|
|
120
|
+
loading={messageLoading}
|
|
121
|
+
width="full"
|
|
122
|
+
>
|
|
123
|
+
{message}
|
|
124
|
+
</InlinePrompt>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
174
127
|
</InputInvalidProvider>
|
|
175
128
|
</InputDescribedByProvider>
|
|
176
129
|
</InputIdContextProvider>
|
package/src/i18n/en.json
CHANGED
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
"neptune.Expander.expandAriaLabel": "Expand",
|
|
21
21
|
"neptune.ExpressiveMoneyInput.currency.search.placeholder": "Type a currency / country",
|
|
22
22
|
"neptune.ExpressiveMoneyInput.currency.select.currency": "Select currency",
|
|
23
|
-
"neptune.Field.characterCount": "{current} of {max} characters used",
|
|
24
23
|
"neptune.FlowNavigation.back": "back to previous step",
|
|
25
24
|
"neptune.Info.ariaLabel": "More information",
|
|
26
25
|
"neptune.Label.optional": "(Optional)",
|
package/src/inputs/TextArea.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { clsx } from 'clsx';
|
|
2
|
-
import { forwardRef
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
3
|
|
|
4
4
|
import { Merge } from '../utils';
|
|
5
5
|
import { inputClassNameBase } from './_common';
|
|
6
|
-
import {
|
|
6
|
+
import { useInputAttributes } from './contexts';
|
|
7
7
|
|
|
8
8
|
export interface TextAreaProps extends Merge<
|
|
9
9
|
React.ComponentPropsWithRef<'textarea'>,
|
|
@@ -13,48 +13,17 @@ export interface TextAreaProps extends Merge<
|
|
|
13
13
|
> {}
|
|
14
14
|
|
|
15
15
|
export const TextArea = forwardRef(function TextArea(
|
|
16
|
-
{ className,
|
|
16
|
+
{ className, ...restProps }: TextAreaProps,
|
|
17
17
|
reference: React.ForwardedRef<HTMLTextAreaElement | null>,
|
|
18
18
|
) {
|
|
19
19
|
const inputAttributes = useInputAttributes();
|
|
20
|
-
const value = restProps.value ?? restProps.defaultValue ?? '';
|
|
21
|
-
const currentLength = getGraphemeLength(typeof value === 'string' ? value : String(value));
|
|
22
|
-
|
|
23
|
-
useTextareaCharacterCount(currentLength, maxLength);
|
|
24
|
-
|
|
25
|
-
const handleChange = useCallback(
|
|
26
|
-
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
27
|
-
if (maxLength != null && getGraphemeLength(e.target.value) > maxLength) {
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
onChange?.(e);
|
|
31
|
-
},
|
|
32
|
-
[maxLength, onChange],
|
|
33
|
-
);
|
|
34
20
|
|
|
35
21
|
return (
|
|
36
22
|
<textarea
|
|
37
23
|
ref={reference}
|
|
38
24
|
className={clsx(className, inputClassNameBase(), 'np-text-area')}
|
|
39
|
-
onChange={handleChange}
|
|
40
25
|
{...inputAttributes}
|
|
41
26
|
{...restProps}
|
|
42
27
|
/>
|
|
43
28
|
);
|
|
44
29
|
});
|
|
45
|
-
|
|
46
|
-
const segmenter =
|
|
47
|
-
typeof Intl.Segmenter === 'function'
|
|
48
|
-
? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
|
49
|
-
: undefined;
|
|
50
|
-
|
|
51
|
-
function getGraphemeLength(str: string): number {
|
|
52
|
-
if (segmenter) {
|
|
53
|
-
let count = 0;
|
|
54
|
-
for (const _ of segmenter.segment(str)) {
|
|
55
|
-
count += 1;
|
|
56
|
-
}
|
|
57
|
-
return count;
|
|
58
|
-
}
|
|
59
|
-
return Array.from(str).length;
|
|
60
|
-
}
|
package/src/inputs/contexts.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, useContext
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
2
|
|
|
3
3
|
type FieldLabelContextType = {
|
|
4
4
|
id?: string;
|
|
@@ -36,23 +36,6 @@ export function useFieldLabelRef() {
|
|
|
36
36
|
return useContext(FieldLabelContext)?.ref;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export type TextareaCharacterCountState = { current: number; max: number } | null;
|
|
40
|
-
|
|
41
|
-
const TextareaCharacterCountContext = createContext<
|
|
42
|
-
((state: TextareaCharacterCountState) => void) | undefined
|
|
43
|
-
>(undefined);
|
|
44
|
-
export const TextareaCharacterCountProvider = TextareaCharacterCountContext.Provider;
|
|
45
|
-
|
|
46
|
-
export function useTextareaCharacterCount(current: number, max: number | undefined) {
|
|
47
|
-
const setCharacterCount = useContext(TextareaCharacterCountContext);
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
if (setCharacterCount && max != null) {
|
|
50
|
-
setCharacterCount({ current, max });
|
|
51
|
-
return () => setCharacterCount(null);
|
|
52
|
-
}
|
|
53
|
-
}, [setCharacterCount, current, max]);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
39
|
export interface WithInputAttributesProps {
|
|
57
40
|
inputAttributes: ReturnType<typeof useInputAttributes>;
|
|
58
41
|
}
|
package/src/main.css
CHANGED
|
@@ -30194,36 +30194,18 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
|
|
|
30194
30194
|
stroke-dasharray: var(--wds-list-item-spotlight-strokeDashSize) var(--wds-list-item-spotlight-strokeDashSize);
|
|
30195
30195
|
}
|
|
30196
30196
|
|
|
30197
|
-
.np-field-control
|
|
30197
|
+
.np-field-control,
|
|
30198
|
+
.np-field__prompt {
|
|
30198
30199
|
margin-top: 4px;
|
|
30199
30200
|
margin-top: var(--size-4);
|
|
30200
30201
|
}
|
|
30201
30202
|
|
|
30202
|
-
.np-field-validation {
|
|
30203
|
-
display: flex;
|
|
30204
|
-
align-items: flex-start;
|
|
30205
|
-
margin-top: 4px;
|
|
30206
|
-
margin-top: var(--size-4);
|
|
30207
|
-
gap: 8px;
|
|
30208
|
-
gap: var(--size-8);
|
|
30209
|
-
}
|
|
30210
|
-
|
|
30211
|
-
.np-field-textarea-char-counter {
|
|
30212
|
-
min-width: 55px;
|
|
30213
|
-
text-align: right;
|
|
30214
|
-
margin-left: auto;
|
|
30215
|
-
padding: 4px 0;
|
|
30216
|
-
padding: var(--size-4) 0;
|
|
30217
|
-
color: #768e9c;
|
|
30218
|
-
color: var(--color-content-tertiary);
|
|
30219
|
-
}
|
|
30220
|
-
|
|
30221
30203
|
.np-field .form-group--typeahead[class],
|
|
30222
30204
|
.np-field .np-checkbox-label[class] {
|
|
30223
30205
|
margin-bottom: 0;
|
|
30224
30206
|
}
|
|
30225
30207
|
|
|
30226
|
-
.np-field:has(.wds-radio-group) .np-
|
|
30208
|
+
.np-field:has(.wds-radio-group) .np-field__prompt {
|
|
30227
30209
|
margin-top: 12px;
|
|
30228
30210
|
margin-top: var(--size-12);
|
|
30229
30211
|
}
|
|
@@ -71,3 +71,48 @@ export const WithoutVisibleTitle: Story = {
|
|
|
71
71
|
),
|
|
72
72
|
},
|
|
73
73
|
} satisfies Meta<typeof Popover>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The `preferredPlacement` prop indicates where the Popover should appear
|
|
77
|
+
* relative to its trigger element. However, it is a preference, not a guarantee.
|
|
78
|
+
*
|
|
79
|
+
* If there isn't enough viewport space in the preferred direction, the Popover
|
|
80
|
+
* automatically flips to an alternative placement. The fallback order is:
|
|
81
|
+
* - `top` → tries `bottom`, then `right`, then `left`
|
|
82
|
+
* - `bottom` → tries `top`, then `right`, then `left`
|
|
83
|
+
* - `left` → tries `right`, then `top`, then `bottom`
|
|
84
|
+
* - `right` → tries `left`, then `top`, then `bottom`
|
|
85
|
+
*
|
|
86
|
+
* For example, if you set `preferredPlacement="top"` but the trigger is near
|
|
87
|
+
* the top of the viewport, the Popover will flip to `bottom` instead.
|
|
88
|
+
*
|
|
89
|
+
* Try scrolling this story so triggers are near viewport edges to see the
|
|
90
|
+
* flip behaviour in action.
|
|
91
|
+
*/
|
|
92
|
+
export const PreferredPlacement: Story = {
|
|
93
|
+
render: () => (
|
|
94
|
+
<div
|
|
95
|
+
style={{
|
|
96
|
+
display: 'flex',
|
|
97
|
+
flexWrap: 'wrap',
|
|
98
|
+
gap: '48px',
|
|
99
|
+
justifyContent: 'center',
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
padding: '120px 80px',
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
{([Position.TOP, Position.RIGHT, Position.BOTTOM, Position.LEFT] as const).map(
|
|
105
|
+
(placement) => (
|
|
106
|
+
<Popover
|
|
107
|
+
key={placement}
|
|
108
|
+
preferredPlacement={placement}
|
|
109
|
+
title="Guaranteed rate"
|
|
110
|
+
content={<Content />}
|
|
111
|
+
>
|
|
112
|
+
<Button v2>{placement}</Button>
|
|
113
|
+
</Popover>
|
|
114
|
+
),
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
),
|
|
118
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
2
|
+
import { userEvent, within } from 'storybook/test';
|
|
3
|
+
|
|
4
|
+
import Button from '../button';
|
|
5
|
+
import { Position } from '../common';
|
|
6
|
+
|
|
7
|
+
import Popover, { PopoverPreferredPlacement } from './Popover';
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
component: Popover,
|
|
11
|
+
title: 'Dialogs/Popover/Tests',
|
|
12
|
+
tags: ['!autodocs', '!manifest'],
|
|
13
|
+
} satisfies Meta<typeof Popover>;
|
|
14
|
+
|
|
15
|
+
type Story = StoryObj<typeof Popover>;
|
|
16
|
+
|
|
17
|
+
const createPlacementStory = (
|
|
18
|
+
placement: PopoverPreferredPlacement,
|
|
19
|
+
containerStyle?: React.CSSProperties,
|
|
20
|
+
): Story => ({
|
|
21
|
+
parameters: {
|
|
22
|
+
chromatic: {
|
|
23
|
+
delay: 1000,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
render: () => (
|
|
27
|
+
<div style={containerStyle ?? { display: 'flex', justifyContent: 'center', padding: '150px' }}>
|
|
28
|
+
<Popover preferredPlacement={placement} title="Info" content="Details">
|
|
29
|
+
<Button v2>{placement}</Button>
|
|
30
|
+
</Popover>
|
|
31
|
+
</div>
|
|
32
|
+
),
|
|
33
|
+
play: async ({ canvasElement }) => {
|
|
34
|
+
const canvas = within(canvasElement);
|
|
35
|
+
await userEvent.click(canvas.getByRole('button'));
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const PlacementTop: Story = createPlacementStory(Position.TOP);
|
|
40
|
+
export const PlacementRight: Story = createPlacementStory(Position.RIGHT);
|
|
41
|
+
export const PlacementBottom: Story = createPlacementStory(Position.BOTTOM);
|
|
42
|
+
export const PlacementLeft: Story = createPlacementStory(Position.LEFT);
|
|
43
|
+
|
|
44
|
+
export const FlipTop: Story = createPlacementStory(Position.TOP, {
|
|
45
|
+
position: 'absolute',
|
|
46
|
+
top: 0,
|
|
47
|
+
left: '50%',
|
|
48
|
+
});
|
|
49
|
+
export const FlipRight: Story = createPlacementStory(Position.RIGHT, {
|
|
50
|
+
position: 'absolute',
|
|
51
|
+
top: '50%',
|
|
52
|
+
right: 0,
|
|
53
|
+
});
|
|
54
|
+
export const FlipBottom: Story = createPlacementStory(Position.BOTTOM, {
|
|
55
|
+
position: 'absolute',
|
|
56
|
+
bottom: 0,
|
|
57
|
+
left: '50%',
|
|
58
|
+
});
|
|
59
|
+
export const FlipLeft: Story = createPlacementStory(Position.LEFT, {
|
|
60
|
+
position: 'absolute',
|
|
61
|
+
top: '50%',
|
|
62
|
+
left: 0,
|
|
63
|
+
});
|
|
@@ -142,6 +142,62 @@ describe('Popover', () => {
|
|
|
142
142
|
expect(handleOnClose).toHaveBeenCalledTimes(1);
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
+
describe('className', () => {
|
|
146
|
+
describe('on desktop', () => {
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
global.innerWidth = Breakpoint.SMALL;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('applies className to the panel container', async () => {
|
|
152
|
+
render(
|
|
153
|
+
<Popover {...props} className="custom-popover-class">
|
|
154
|
+
<button type="button">Open</button>
|
|
155
|
+
</Popover>,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
await userEvent.click(screen.getByText('Open'));
|
|
159
|
+
await waitForPanel();
|
|
160
|
+
|
|
161
|
+
const panel = getPanel();
|
|
162
|
+
expect(panel).toHaveClass('np-popover__container');
|
|
163
|
+
expect(panel).toHaveClass('custom-popover-class');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('on mobile', () => {
|
|
168
|
+
beforeEach(() => {
|
|
169
|
+
global.innerWidth = Breakpoint.SMALL - 1;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('applies className to the bottom sheet container', async () => {
|
|
173
|
+
render(
|
|
174
|
+
<Popover {...props} className="custom-popover-class">
|
|
175
|
+
<button type="button">Open</button>
|
|
176
|
+
</Popover>,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
await userEvent.click(screen.getByText('Open'));
|
|
180
|
+
|
|
181
|
+
const bottomSheet = getBottomSheet();
|
|
182
|
+
expect(bottomSheet).toHaveClass('np-popover__container');
|
|
183
|
+
expect(bottomSheet).toHaveClass('custom-popover-class');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('does not apply className to the trigger wrapper', () => {
|
|
188
|
+
const { container } = render(
|
|
189
|
+
<Popover {...props} className="custom-popover-class">
|
|
190
|
+
<button type="button">Open</button>
|
|
191
|
+
</Popover>,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const wrapper = container.firstChild;
|
|
195
|
+
expect(wrapper).toHaveClass('np-popover');
|
|
196
|
+
expect(wrapper).toHaveClass('d-inline-block');
|
|
197
|
+
expect(wrapper).not.toHaveClass('custom-popover-class');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
145
201
|
const waitForPanel = async () => screen.findByText('content');
|
|
146
202
|
const getDimmer = () => document.querySelector('.dimmer');
|
|
147
203
|
const getPanel = () => document.querySelector('.np-panel');
|
package/src/popover/Popover.tsx
CHANGED
|
@@ -75,17 +75,15 @@ export default function Popover({
|
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
return (
|
|
78
|
-
<span className=
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
onClick
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
: children}
|
|
88
|
-
</span>
|
|
78
|
+
<span ref={anchorReference} className="np-popover d-inline-block">
|
|
79
|
+
{isValidElement<{ onClick?: () => void }>(children)
|
|
80
|
+
? cloneElement(children, {
|
|
81
|
+
onClick: () => {
|
|
82
|
+
children.props.onClick?.();
|
|
83
|
+
setOpen((prevOpen) => !prevOpen);
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
: children}
|
|
89
87
|
<ResponsivePanel
|
|
90
88
|
aria-label={ariaLabel}
|
|
91
89
|
aria-labelledby={title && !ariaLabel ? titleId : undefined}
|
|
@@ -93,7 +91,7 @@ export default function Popover({
|
|
|
93
91
|
anchorRef={anchorReference}
|
|
94
92
|
position={resolvedPlacement}
|
|
95
93
|
arrow
|
|
96
|
-
className=
|
|
94
|
+
className={clsx('np-popover__container', className)}
|
|
97
95
|
onClose={handleOnClose}
|
|
98
96
|
>
|
|
99
97
|
<div className="np-popover__content np-text-default-body">
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
-
|
|
5
|
-
var reactIntl = require('react-intl');
|
|
6
|
-
|
|
7
|
-
var messages = reactIntl.defineMessages({
|
|
8
|
-
characterCount: {
|
|
9
|
-
id: "neptune.Field.characterCount"
|
|
10
|
-
}
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
exports.default = messages;
|
|
14
|
-
//# sourceMappingURL=Field.messages.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Field.messages.js","sources":["../../src/field/Field.messages.ts"],"sourcesContent":["import { defineMessages } from 'react-intl';\n\nexport default defineMessages({\n characterCount: {\n id: 'neptune.Field.characterCount',\n defaultMessage: '{current} of {max} characters used',\n },\n});\n"],"names":["defineMessages","characterCount","id"],"mappings":";;;;;;AAEA,eAAeA,wBAAc,CAAC;AAC5BC,EAAAA,cAAc,EAAE;IACdC,EAAE,EAAA;AAEH;AACF,CAAA,CAAC;;;;"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Field.messages.mjs","sources":["../../src/field/Field.messages.ts"],"sourcesContent":["import { defineMessages } from 'react-intl';\n\nexport default defineMessages({\n characterCount: {\n id: 'neptune.Field.characterCount',\n defaultMessage: '{current} of {max} characters used',\n },\n});\n"],"names":["defineMessages","characterCount","id"],"mappings":";;AAEA,eAAeA,cAAc,CAAC;AAC5BC,EAAAA,cAAc,EAAE;IACdC,EAAE,EAAA;AAEH;AACF,CAAA,CAAC;;;;"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Field.messages.d.ts","sourceRoot":"","sources":["../../../src/field/Field.messages.ts"],"names":[],"mappings":";;;;;;AAEA,wBAKG"}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
3
|
-
|
|
4
|
-
import { Field } from '../field/Field';
|
|
5
|
-
import { Sentiment } from '../common';
|
|
6
|
-
import { TextArea } from './TextArea';
|
|
7
|
-
|
|
8
|
-
const meta: Meta<typeof TextArea> = {
|
|
9
|
-
title: 'Forms/TextArea',
|
|
10
|
-
component: TextArea,
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export default meta;
|
|
14
|
-
type Story = StoryObj<typeof TextArea>;
|
|
15
|
-
|
|
16
|
-
export const Basic: Story = {
|
|
17
|
-
render: () => {
|
|
18
|
-
const [value, setValue] = useState('');
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<Field label="Message" required={false}>
|
|
22
|
-
<TextArea maxLength={10} value={value} onChange={({ target }) => setValue(target.value)} />
|
|
23
|
-
</Field>
|
|
24
|
-
);
|
|
25
|
-
},
|
|
26
|
-
parameters: {
|
|
27
|
-
docs: {
|
|
28
|
-
source: {
|
|
29
|
-
code: `<Field label="Message" required={false}>
|
|
30
|
-
<TextArea
|
|
31
|
-
maxLength={200}
|
|
32
|
-
value={value}
|
|
33
|
-
onChange={({ target }) => setValue(target.value)}
|
|
34
|
-
/>
|
|
35
|
-
</Field>`,
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export const WithError: Story = {
|
|
42
|
-
render: () => {
|
|
43
|
-
const [value, setValue] = useState('');
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<Field
|
|
47
|
-
label="Message"
|
|
48
|
-
required={false}
|
|
49
|
-
sentiment={Sentiment.NEGATIVE}
|
|
50
|
-
message="You have exceeded the character limit"
|
|
51
|
-
>
|
|
52
|
-
<TextArea maxLength={200} value={value} onChange={({ target }) => setValue(target.value)} />
|
|
53
|
-
</Field>
|
|
54
|
-
);
|
|
55
|
-
},
|
|
56
|
-
};
|