@transferwise/components 46.31.0 → 46.33.0
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/index.js +764 -474
- package/build/index.js.map +1 -1
- package/build/index.mjs +763 -474
- package/build/index.mjs.map +1 -1
- package/build/main.css +135 -0
- package/build/styles/carousel/Carousel.css +135 -0
- package/build/styles/main.css +135 -0
- package/build/types/carousel/Carousel.d.ts +26 -0
- package/build/types/carousel/Carousel.d.ts.map +1 -0
- package/build/types/carousel/index.d.ts +3 -0
- package/build/types/carousel/index.d.ts.map +1 -0
- package/build/types/common/card/Card.d.ts +2 -2
- package/build/types/common/card/Card.d.ts.map +1 -1
- package/build/types/common/domHelpers/documentIosClick.d.ts +0 -1
- package/build/types/common/domHelpers/documentIosClick.d.ts.map +1 -1
- package/build/types/common/domHelpers/index.d.ts +1 -1
- package/build/types/common/domHelpers/index.d.ts.map +1 -1
- package/build/types/dateLookup/DateLookup.d.ts.map +1 -1
- package/build/types/index.d.ts +2 -0
- package/build/types/index.d.ts.map +1 -1
- package/build/types/moneyInput/MoneyInput.d.ts +4 -2
- package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
- package/build/types/phoneNumberInput/PhoneNumberInput.d.ts +1 -1
- package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
- package/build/types/promoCard/PromoCard.d.ts +16 -5
- package/build/types/promoCard/PromoCard.d.ts.map +1 -1
- package/build/types/select/Select.d.ts +7 -7
- package/build/types/select/Select.d.ts.map +1 -1
- package/build/types/typeahead/Typeahead.d.ts +4 -55
- package/build/types/typeahead/Typeahead.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/carousel/Carousel.css +135 -0
- package/src/carousel/Carousel.less +133 -0
- package/src/carousel/Carousel.spec.tsx +221 -0
- package/src/carousel/Carousel.story.tsx +63 -0
- package/src/carousel/Carousel.tsx +345 -0
- package/src/carousel/index.ts +3 -0
- package/src/common/card/Card.tsx +51 -43
- package/src/common/domHelpers/documentIosClick.ts +0 -5
- package/src/common/domHelpers/index.ts +0 -1
- package/src/dateLookup/DateLookup.rtl.spec.tsx +2 -3
- package/src/dateLookup/DateLookup.tsx +1 -3
- package/src/index.ts +2 -0
- package/src/inputs/SelectInput.spec.tsx +1 -1
- package/src/main.css +135 -0
- package/src/main.less +1 -0
- package/src/moneyInput/MoneyInput.rtl.spec.tsx +10 -0
- package/src/moneyInput/MoneyInput.spec.js +10 -5
- package/src/moneyInput/MoneyInput.tsx +21 -14
- package/src/phoneNumberInput/PhoneNumberInput.rtl.spec.tsx +10 -0
- package/src/phoneNumberInput/PhoneNumberInput.tsx +11 -2
- package/src/promoCard/PromoCard.story.tsx +2 -2
- package/src/promoCard/PromoCard.tsx +30 -9
- package/src/select/Select.js +18 -15
- package/src/select/Select.rtl.spec.tsx +17 -0
- package/src/select/Select.spec.js +2 -7
- package/src/typeahead/Typeahead.rtl.spec.tsx +16 -0
- package/src/typeahead/Typeahead.tsx +21 -7
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { StoryFn } from '@storybook/react';
|
|
2
|
+
import { Illustration } from '@wise/art';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import Display from '../display';
|
|
6
|
+
|
|
7
|
+
import Carousel, { type CarouselCard } from './Carousel';
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
component: Carousel,
|
|
11
|
+
title: 'Navigation/Carousel',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const carouselCards: CarouselCard[] = [
|
|
15
|
+
{
|
|
16
|
+
id: 'YOUR_INVOICE',
|
|
17
|
+
type: 'button',
|
|
18
|
+
onClick: () => {
|
|
19
|
+
console.log('Clicked on button');
|
|
20
|
+
},
|
|
21
|
+
content: (
|
|
22
|
+
<div className="d-flex p-a-1 p-t-0 flex-column align-items-center">
|
|
23
|
+
<div style={{ position: 'absolute' }}>
|
|
24
|
+
<Illustration disablePadding name="globe" size="small" alt="" />
|
|
25
|
+
</div>
|
|
26
|
+
<div style={{ position: 'absolute', zIndex: '2', bottom: '8px', left: '16px' }}>
|
|
27
|
+
<Display type="display-small">Here there, everywhere</Display>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'YOUR_WEBSITE',
|
|
34
|
+
type: 'promo',
|
|
35
|
+
onClick: () => {
|
|
36
|
+
console.log('hi');
|
|
37
|
+
},
|
|
38
|
+
href: 'http://wise.com',
|
|
39
|
+
title: 'Wise',
|
|
40
|
+
description: 'The best way to move money internationally',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'SHOP_OR_STALL',
|
|
44
|
+
type: 'anchor',
|
|
45
|
+
href: 'https://wise.com',
|
|
46
|
+
onClick: () => {},
|
|
47
|
+
styles: {
|
|
48
|
+
backgroundImage:
|
|
49
|
+
'url(https://wise.com/public-resources/assets/team-page/tapestry/team_member_tapestry_01.jpg)',
|
|
50
|
+
backgroundSize: 'cover',
|
|
51
|
+
},
|
|
52
|
+
content: <Display type="display-small">Get Quick Pay</Display>,
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const Template: StoryFn = (args) => {
|
|
57
|
+
return <Carousel header="Pretty nifty stuff" cards={carouselCards} {...args} />;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const CarouselDefault = {
|
|
61
|
+
render: Template,
|
|
62
|
+
args: {},
|
|
63
|
+
};
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { ChevronLeft, ChevronRight } from '@transferwise/icons';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { type CSSProperties, type ReactNode, useEffect, useRef, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import ActionButton from '../actionButton';
|
|
6
|
+
import Title from '../title';
|
|
7
|
+
import type { PromoCardLinkProps } from '../promoCard/PromoCard';
|
|
8
|
+
import PromoCard from '../promoCard/PromoCard';
|
|
9
|
+
|
|
10
|
+
export type CarouselCardCommon = {
|
|
11
|
+
id: string;
|
|
12
|
+
href?: string;
|
|
13
|
+
onClick?: () => void;
|
|
14
|
+
className?: string;
|
|
15
|
+
styles?: CSSProperties;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type CarouselDefaultCard = CarouselCardCommon & {
|
|
19
|
+
type: 'anchor' | 'button';
|
|
20
|
+
content: ReactNode;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type CarouselPromoCard = CarouselCardCommon & {
|
|
24
|
+
type: 'promo';
|
|
25
|
+
} & Omit<PromoCardLinkProps, 'type'>;
|
|
26
|
+
|
|
27
|
+
export type CarouselCard = CarouselDefaultCard | CarouselPromoCard;
|
|
28
|
+
export interface CarouselProps {
|
|
29
|
+
header: string | ReactNode;
|
|
30
|
+
className?: string;
|
|
31
|
+
cards: CarouselCard[];
|
|
32
|
+
onClick?: (cardId: string) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type CardsReference = {
|
|
36
|
+
type: 'promo' | 'default';
|
|
37
|
+
cardElement: HTMLDivElement | HTMLAnchorElement;
|
|
38
|
+
anchorElement?: HTMLAnchorElement;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const LEFT_SCROLL_OFFSET = 8;
|
|
42
|
+
|
|
43
|
+
const Carousel: React.FC<CarouselProps> = ({ header, className, cards, onClick }) => {
|
|
44
|
+
const [scrollPosition, setScrollPosition] = useState(0);
|
|
45
|
+
const [previousScrollPosition, setPreviousScrollPosition] = useState(0);
|
|
46
|
+
const [scrollIsAtEnd, setScrollIsAtEnd] = useState(false);
|
|
47
|
+
const [visibleCardOnMobileView, setVisibleCardOnMobileView] = useState<string>('');
|
|
48
|
+
const carouselElementRef = useRef<HTMLDivElement>(null);
|
|
49
|
+
const carouselCardsRef = useRef<CardsReference[]>([]);
|
|
50
|
+
|
|
51
|
+
const isLeftActionButtonEnabled = scrollPosition > LEFT_SCROLL_OFFSET;
|
|
52
|
+
|
|
53
|
+
const areActionButtonsEnabled = isLeftActionButtonEnabled || !scrollIsAtEnd;
|
|
54
|
+
|
|
55
|
+
const [focusedCard, setFocusedCard] = useState(cards?.[0]?.id);
|
|
56
|
+
|
|
57
|
+
const updateScrollButtonsState = () => {
|
|
58
|
+
if (carouselElementRef.current) {
|
|
59
|
+
const { scrollWidth, offsetWidth } = carouselElementRef.current;
|
|
60
|
+
|
|
61
|
+
const scrollAtEnd = scrollWidth - offsetWidth <= scrollPosition + LEFT_SCROLL_OFFSET;
|
|
62
|
+
setScrollIsAtEnd(scrollAtEnd);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const scrollDirecton = scrollPosition > previousScrollPosition ? 'right' : 'left';
|
|
66
|
+
|
|
67
|
+
const cardsInFullViewIds: string[] = [];
|
|
68
|
+
carouselCardsRef.current.forEach((card) => {
|
|
69
|
+
if (isVisible(carouselElementRef.current as HTMLElement, card.cardElement as HTMLElement)) {
|
|
70
|
+
// eslint-disable-next-line functional/immutable-data
|
|
71
|
+
cardsInFullViewIds.push(card.cardElement.getAttribute('id') ?? '');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (cardsInFullViewIds.length >= 1) {
|
|
76
|
+
const visibleCardIndex = scrollDirecton === 'right' ? cardsInFullViewIds.length - 1 : 0;
|
|
77
|
+
const visibleCardId = cardsInFullViewIds[visibleCardIndex];
|
|
78
|
+
setVisibleCardOnMobileView(visibleCardId);
|
|
79
|
+
setFocusedCard(visibleCardId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
setPreviousScrollPosition(scrollPosition);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const scrollCarousel = (direction: 'left' | 'right' = 'right') => {
|
|
86
|
+
if (carouselElementRef.current) {
|
|
87
|
+
const { scrollWidth } = carouselElementRef.current;
|
|
88
|
+
|
|
89
|
+
const cardWidth = scrollWidth / carouselCardsRef.current.length;
|
|
90
|
+
|
|
91
|
+
const res = Math.floor(cardWidth - cardWidth * 0.05);
|
|
92
|
+
|
|
93
|
+
carouselElementRef.current.scrollBy({
|
|
94
|
+
left: direction === 'right' ? res : -res,
|
|
95
|
+
behavior: 'smooth',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleOnKeyDown = (
|
|
101
|
+
event: React.KeyboardEvent<HTMLAnchorElement | HTMLDivElement>,
|
|
102
|
+
index: number,
|
|
103
|
+
) => {
|
|
104
|
+
if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
|
|
105
|
+
const nextIndex = event.key === 'ArrowRight' ? index + 1 : index - 1;
|
|
106
|
+
const nextCard = cards[nextIndex];
|
|
107
|
+
if (nextCard) {
|
|
108
|
+
const ref = carouselCardsRef.current[nextIndex];
|
|
109
|
+
if (ref.type === 'promo') {
|
|
110
|
+
ref.anchorElement?.focus();
|
|
111
|
+
} else {
|
|
112
|
+
ref.cardElement?.focus();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
scrollCardIntoView(carouselCardsRef.current[nextIndex].cardElement, nextCard);
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
121
|
+
event.currentTarget.click();
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const scrollCardIntoView = (element: HTMLElement, card: CarouselCard) => {
|
|
126
|
+
element.scrollIntoView({
|
|
127
|
+
behavior: 'smooth',
|
|
128
|
+
block: 'nearest',
|
|
129
|
+
inline: 'center',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
setFocusedCard(card.id);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
updateScrollButtonsState();
|
|
137
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
138
|
+
}, [scrollPosition]);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
window.addEventListener('resize', updateScrollButtonsState);
|
|
142
|
+
|
|
143
|
+
return () => {
|
|
144
|
+
window.removeEventListener('resize', updateScrollButtonsState);
|
|
145
|
+
};
|
|
146
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
147
|
+
}, []);
|
|
148
|
+
|
|
149
|
+
const addElementToCardsRefArray = (index: number, ref: Partial<CardsReference>) => {
|
|
150
|
+
if (ref) {
|
|
151
|
+
// eslint-disable-next-line functional/immutable-data
|
|
152
|
+
carouselCardsRef.current[index] = {
|
|
153
|
+
type: ref.type ?? carouselCardsRef.current?.[index]?.type,
|
|
154
|
+
cardElement: ref.cardElement ?? carouselCardsRef.current?.[index]?.cardElement,
|
|
155
|
+
anchorElement: ref.anchorElement ?? carouselCardsRef.current?.[index]?.anchorElement,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div className={classNames('carousel-wrapper', className)}>
|
|
162
|
+
<div className="d-flex justify-content-between carousel__header">
|
|
163
|
+
{typeof header === 'string' ? (
|
|
164
|
+
<Title as="span" type="title-body">
|
|
165
|
+
{header}
|
|
166
|
+
</Title>
|
|
167
|
+
) : (
|
|
168
|
+
header
|
|
169
|
+
)}
|
|
170
|
+
{areActionButtonsEnabled ? (
|
|
171
|
+
<div className="hidden-xs">
|
|
172
|
+
<ActionButton
|
|
173
|
+
className="carousel__scroll-button"
|
|
174
|
+
tabIndex={-1}
|
|
175
|
+
priority="secondary"
|
|
176
|
+
disabled={!isLeftActionButtonEnabled}
|
|
177
|
+
aria-hidden="true"
|
|
178
|
+
data-testid="scroll-carousel-left"
|
|
179
|
+
onClick={() => scrollCarousel('left')}
|
|
180
|
+
>
|
|
181
|
+
<ChevronLeft />
|
|
182
|
+
</ActionButton>
|
|
183
|
+
<ActionButton
|
|
184
|
+
tabIndex={-1}
|
|
185
|
+
className="carousel__scroll-button m-l-1"
|
|
186
|
+
priority="secondary"
|
|
187
|
+
aria-hidden="true"
|
|
188
|
+
data-testid="scroll-carousel-right"
|
|
189
|
+
disabled={scrollIsAtEnd}
|
|
190
|
+
onClick={() => scrollCarousel()}
|
|
191
|
+
>
|
|
192
|
+
<ChevronRight />
|
|
193
|
+
</ActionButton>
|
|
194
|
+
</div>
|
|
195
|
+
) : null}
|
|
196
|
+
</div>
|
|
197
|
+
<div
|
|
198
|
+
ref={carouselElementRef}
|
|
199
|
+
tabIndex={-1}
|
|
200
|
+
role="list"
|
|
201
|
+
className="carousel"
|
|
202
|
+
onScroll={(event) => {
|
|
203
|
+
const target = event.target as HTMLElement;
|
|
204
|
+
setScrollPosition(target.scrollLeft);
|
|
205
|
+
}}
|
|
206
|
+
>
|
|
207
|
+
{cards?.map((card, index) => {
|
|
208
|
+
const sharedProps = {
|
|
209
|
+
id: card.id,
|
|
210
|
+
className: classNames('carousel__card', {
|
|
211
|
+
'carousel__card--focused': card.id === focusedCard,
|
|
212
|
+
}),
|
|
213
|
+
onClick: () => {
|
|
214
|
+
card.onClick?.();
|
|
215
|
+
onClick?.(card.id);
|
|
216
|
+
},
|
|
217
|
+
onFocus: (event: React.FocusEvent<HTMLAnchorElement | HTMLDivElement>) => {
|
|
218
|
+
scrollCardIntoView(event.currentTarget, card);
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const cardContent =
|
|
223
|
+
card.type !== 'promo' ? (
|
|
224
|
+
<div
|
|
225
|
+
id={`${card.id}-content`}
|
|
226
|
+
className={classNames('carousel__card-content', {
|
|
227
|
+
[card.className ?? '']: !!card.className,
|
|
228
|
+
})}
|
|
229
|
+
// eslint-disable-next-line react/forbid-dom-props
|
|
230
|
+
style={card.styles}
|
|
231
|
+
>
|
|
232
|
+
{card.content}
|
|
233
|
+
</div>
|
|
234
|
+
) : null;
|
|
235
|
+
|
|
236
|
+
if (card.type === 'button') {
|
|
237
|
+
return (
|
|
238
|
+
<div key={card.id} aria-labelledby={`${card.id}-content`} role="listitem">
|
|
239
|
+
<div
|
|
240
|
+
{...sharedProps}
|
|
241
|
+
ref={(el) => {
|
|
242
|
+
if (el) {
|
|
243
|
+
// eslint-disable-next-line functional/immutable-data
|
|
244
|
+
carouselCardsRef.current[index] = {
|
|
245
|
+
type: 'default',
|
|
246
|
+
cardElement: el,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}}
|
|
250
|
+
role="button"
|
|
251
|
+
tabIndex={0}
|
|
252
|
+
onKeyDown={(event) => handleOnKeyDown(event, index)}
|
|
253
|
+
>
|
|
254
|
+
{cardContent}
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (card.type === 'promo') {
|
|
261
|
+
return (
|
|
262
|
+
<div key={card.id} id={card.id} role="listitem" aria-labelledby={`${card.id}-anchor`}>
|
|
263
|
+
<PromoCard
|
|
264
|
+
{...{ ...card, type: undefined }}
|
|
265
|
+
{...{ ...sharedProps }}
|
|
266
|
+
ref={(el: HTMLDivElement | null) => {
|
|
267
|
+
if (el) {
|
|
268
|
+
addElementToCardsRefArray(index, {
|
|
269
|
+
type: 'promo',
|
|
270
|
+
cardElement: el,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}}
|
|
274
|
+
anchorRef={(el: HTMLAnchorElement) => {
|
|
275
|
+
if (el) {
|
|
276
|
+
addElementToCardsRefArray(index, {
|
|
277
|
+
type: 'promo',
|
|
278
|
+
anchorElement: el,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}}
|
|
282
|
+
anchorId={`${card.id}-anchor`}
|
|
283
|
+
onKeyDown={(event) => handleOnKeyDown(event, index)}
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<div key={card.id} aria-labelledby={`${card.id}-content`} role="listitem">
|
|
291
|
+
<a
|
|
292
|
+
{...sharedProps}
|
|
293
|
+
ref={(el) => {
|
|
294
|
+
if (el) {
|
|
295
|
+
// eslint-disable-next-line functional/immutable-data
|
|
296
|
+
carouselCardsRef.current[index] = {
|
|
297
|
+
type: 'default',
|
|
298
|
+
cardElement: el,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}}
|
|
302
|
+
href={card.href}
|
|
303
|
+
rel="noreferrer"
|
|
304
|
+
onKeyDown={(event) => handleOnKeyDown(event, index)}
|
|
305
|
+
>
|
|
306
|
+
{cardContent}
|
|
307
|
+
</a>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
})}
|
|
311
|
+
</div>
|
|
312
|
+
<div className="visible-xs">
|
|
313
|
+
<div className="carousel__indicators">
|
|
314
|
+
{cards?.map((card, index) => (
|
|
315
|
+
<button
|
|
316
|
+
key={`${card.id}-indicator`}
|
|
317
|
+
data-testid={`${card.id}-indicator`}
|
|
318
|
+
tabIndex={-1}
|
|
319
|
+
aria-hidden
|
|
320
|
+
type="button"
|
|
321
|
+
className={classNames('carousel__indicator', {
|
|
322
|
+
'carousel__indicator--selected': card.id === visibleCardOnMobileView,
|
|
323
|
+
})}
|
|
324
|
+
onClick={() => {
|
|
325
|
+
scrollCardIntoView(carouselCardsRef.current[index].cardElement, card);
|
|
326
|
+
}}
|
|
327
|
+
/>
|
|
328
|
+
))}
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const isVisible = (container: HTMLElement, el: HTMLElement) => {
|
|
336
|
+
const cWidth = container.offsetWidth;
|
|
337
|
+
const cScrollOffset = container.scrollLeft;
|
|
338
|
+
|
|
339
|
+
const elemLeft = el.offsetLeft - container.offsetLeft;
|
|
340
|
+
const elemRight = elemLeft + el.offsetWidth;
|
|
341
|
+
|
|
342
|
+
return elemLeft >= cScrollOffset && elemRight <= cScrollOffset + cWidth;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
export default Carousel;
|
package/src/common/card/Card.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
|
-
import { MouseEvent, ReactNode, useRef } from 'react';
|
|
2
|
+
import { MouseEvent, type ReactNode, forwardRef, useRef } from 'react';
|
|
3
3
|
|
|
4
4
|
import { CloseButton } from '../closeButton';
|
|
5
5
|
import { stopPropagation } from '../domHelpers';
|
|
@@ -48,48 +48,56 @@ export interface CardProps {
|
|
|
48
48
|
* <p>Hello World!</p>
|
|
49
49
|
* </Card>
|
|
50
50
|
*/
|
|
51
|
-
const Card
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
const Card = forwardRef<HTMLDivElement, CardProps>(
|
|
52
|
+
(
|
|
53
|
+
{
|
|
54
|
+
className,
|
|
55
|
+
children = null,
|
|
56
|
+
id,
|
|
57
|
+
isDisabled = false,
|
|
58
|
+
isSmall = false,
|
|
59
|
+
onDismiss,
|
|
60
|
+
testId,
|
|
61
|
+
...props
|
|
62
|
+
},
|
|
63
|
+
ref,
|
|
64
|
+
) => {
|
|
65
|
+
const closeButtonReference = useRef(null);
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
ref={ref}
|
|
70
|
+
className={classNames(
|
|
71
|
+
'np-Card',
|
|
72
|
+
{
|
|
73
|
+
'np-Card--small': !!isSmall,
|
|
74
|
+
'is-disabled': !!isDisabled,
|
|
75
|
+
},
|
|
76
|
+
className,
|
|
77
|
+
)}
|
|
78
|
+
id={id}
|
|
79
|
+
data-testid={testId}
|
|
80
|
+
{...props}
|
|
81
|
+
>
|
|
82
|
+
{onDismiss && (
|
|
83
|
+
<CloseButton
|
|
84
|
+
ref={closeButtonReference}
|
|
85
|
+
className="np-Card-closeButton"
|
|
86
|
+
size={isSmall ? 'sm' : 'md'}
|
|
87
|
+
isDisabled={isDisabled}
|
|
88
|
+
testId="close-button"
|
|
89
|
+
onClick={(e) => {
|
|
90
|
+
stopPropagation(e);
|
|
91
|
+
onDismiss();
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
)}
|
|
95
|
+
{children}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
Card.displayName = 'Card';
|
|
94
102
|
|
|
95
103
|
export default Card;
|
|
@@ -35,8 +35,3 @@ export function stopPropagation(event: SyntheticEvent) {
|
|
|
35
35
|
event.nativeEvent.stopImmediatePropagation();
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
export function getSimpleRandomId(prefix: string): string {
|
|
40
|
-
const random = Math.ceil(Math.random() * 999999);
|
|
41
|
-
return `${prefix}${random}`;
|
|
42
|
-
}
|
|
@@ -14,8 +14,7 @@ describe('DateLookup', () => {
|
|
|
14
14
|
<DateLookup value={now} onChange={() => {}} />
|
|
15
15
|
</Field>,
|
|
16
16
|
);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
);
|
|
17
|
+
// TODO: Replace with `.toHaveAttribute('aria-haspopup')`
|
|
18
|
+
expect(screen.getByLabelText('Date of birth')).toHaveTextContent(now.getFullYear().toString());
|
|
20
19
|
});
|
|
21
20
|
});
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type { ActionOptionProps } from './actionOption';
|
|
|
6
6
|
export type { AlertAction, AlertProps, AlertType } from './alert';
|
|
7
7
|
export type { AvatarProps } from './avatar';
|
|
8
8
|
export type { BadgeProps } from './badge';
|
|
9
|
+
export type { CarouselProps } from './carousel';
|
|
9
10
|
export type { CircularButtonProps } from './circularButton';
|
|
10
11
|
export type {
|
|
11
12
|
BodyTypes,
|
|
@@ -84,6 +85,7 @@ export { default as AvatarWrapper } from './avatarWrapper';
|
|
|
84
85
|
export { default as Badge } from './badge';
|
|
85
86
|
export { default as Body } from './body';
|
|
86
87
|
export { default as Button } from './button';
|
|
88
|
+
export { default as Carousel } from './carousel';
|
|
87
89
|
export { default as Card } from './card';
|
|
88
90
|
export { default as Checkbox } from './checkbox';
|
|
89
91
|
export { default as CheckboxButton } from './checkboxButton';
|
|
@@ -235,6 +235,6 @@ describe('SelectInput', () => {
|
|
|
235
235
|
<SelectInput items={[{ type: 'option', value: 'USD' }]} value="USD" />
|
|
236
236
|
</Field>,
|
|
237
237
|
);
|
|
238
|
-
expect(screen.getByLabelText('Currency')).
|
|
238
|
+
expect(screen.getByLabelText('Currency')).toHaveAttribute('aria-haspopup');
|
|
239
239
|
});
|
|
240
240
|
});
|