@pushwoosh/dumb-components 2.3.1 → 2.5.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/Banner/Banner.d.ts +3 -0
- package/Banner/Banner.js +12 -3
- package/Banner/__tests__/Banner.test.d.ts +1 -0
- package/Banner/__tests__/Banner.test.js +57 -0
- package/Banner/styles.d.ts +1 -1
- package/Banner/styles.js +2 -2
- package/Banner/types.d.ts +13 -4
- package/Tabs/components/TabBar/TabBar.d.ts +3 -1
- package/Tabs/components/TabBar/TabBar.js +7 -3
- package/Tabs/components/TabBar/styles.d.ts +1 -0
- package/Tabs/components/TabBar/styles.js +5 -1
- package/Tabs/components/TabBar/types.d.ts +12 -0
- package/Tabs/components/TabBarScrollableFrame/TabBarScrollableFrame.js +6 -2
- package/Tabs/components/TabBarScrollableFrame/styles.d.ts +4 -1
- package/Tabs/components/TabBarScrollableFrame/styles.js +6 -1
- package/Tabs/index.d.ts +1 -0
- package/index.d.ts +1 -1
- package/package.json +1 -1
package/Banner/Banner.d.ts
CHANGED
|
@@ -3,8 +3,11 @@ import type { BannerPropsWithButton, RequiredBannerProps } from './types';
|
|
|
3
3
|
/**
|
|
4
4
|
* Prominent page-level message with severity styling, an optional action button,
|
|
5
5
|
* a loading state and a dismiss button. Hides itself once closed.
|
|
6
|
+
* Needs a non-empty `title`, body content (`children`), or both; with neither it renders nothing.
|
|
6
7
|
*
|
|
7
8
|
* @example
|
|
8
9
|
* <Banner type="success" title="Saved" canBeClosed action={{ buttonText: 'Undo', onClick: undo }} />
|
|
10
|
+
* @example
|
|
11
|
+
* <Banner type="warning" view="ghost">Only a description, no title</Banner>
|
|
9
12
|
*/
|
|
10
13
|
export declare function Banner(props: RequiredBannerProps | BannerPropsWithButton): ReactElement | null;
|
package/Banner/Banner.js
CHANGED
|
@@ -9,9 +9,12 @@ import { ActionButton, BannerContent, BannerSpinner, CloseButton, IconContainer
|
|
|
9
9
|
/**
|
|
10
10
|
* Prominent page-level message with severity styling, an optional action button,
|
|
11
11
|
* a loading state and a dismiss button. Hides itself once closed.
|
|
12
|
+
* Needs a non-empty `title`, body content (`children`), or both; with neither it renders nothing.
|
|
12
13
|
*
|
|
13
14
|
* @example
|
|
14
15
|
* <Banner type="success" title="Saved" canBeClosed action={{ buttonText: 'Undo', onClick: undo }} />
|
|
16
|
+
* @example
|
|
17
|
+
* <Banner type="warning" view="ghost">Only a description, no title</Banner>
|
|
15
18
|
*/
|
|
16
19
|
export function Banner(props) {
|
|
17
20
|
const [isOpen, setIsOpen] = useState(true);
|
|
@@ -48,6 +51,12 @@ export function Banner(props) {
|
|
|
48
51
|
console.error(`[Banner]: required prop type "${type}" is not set`);
|
|
49
52
|
return null;
|
|
50
53
|
}
|
|
54
|
+
const hasTitle = title !== undefined && title !== '';
|
|
55
|
+
const hasChildren = children !== undefined && children !== null && typeof children !== 'boolean';
|
|
56
|
+
if (!hasTitle && !hasChildren) {
|
|
57
|
+
console.error('[Banner]: neither title nor children is set');
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
51
60
|
const BannerBox = BannerPropsToBannerBoxMap[view];
|
|
52
61
|
if (!BannerBox) {
|
|
53
62
|
console.error(`[Banner]: cannot render with type "${type}" and view "${view}"`);
|
|
@@ -71,10 +80,10 @@ export function Banner(props) {
|
|
|
71
80
|
view: "filled"
|
|
72
81
|
})
|
|
73
82
|
}), _jsxs(BannerContent, {
|
|
74
|
-
"$
|
|
75
|
-
children: [_jsx(H4, {
|
|
83
|
+
"$hasTitleAndChildren": hasTitle && hasChildren,
|
|
84
|
+
children: [hasTitle && _jsx(H4, {
|
|
76
85
|
children: title
|
|
77
|
-
}),
|
|
86
|
+
}), hasChildren && _jsx("div", {
|
|
78
87
|
children: children
|
|
79
88
|
})]
|
|
80
89
|
}), action && _jsx(Vertical, {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { Banner } from '../Banner';
|
|
4
|
+
describe('Banner', () => {
|
|
5
|
+
let consoleError;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
consoleError.mockRestore();
|
|
11
|
+
});
|
|
12
|
+
it('renders a title without children', () => {
|
|
13
|
+
render(_jsx(Banner, {
|
|
14
|
+
type: "info",
|
|
15
|
+
title: "Heading"
|
|
16
|
+
}));
|
|
17
|
+
expect(screen.getByText('Heading')).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
it('renders children without a title', () => {
|
|
20
|
+
const {
|
|
21
|
+
container
|
|
22
|
+
} = render(_jsx(Banner, {
|
|
23
|
+
type: "info",
|
|
24
|
+
children: "Description"
|
|
25
|
+
}));
|
|
26
|
+
expect(screen.getByText('Description')).toBeInTheDocument();
|
|
27
|
+
expect(container.querySelector('h4')).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
it('renders numeric zero children', () => {
|
|
30
|
+
render(_jsx(Banner, {
|
|
31
|
+
type: "info",
|
|
32
|
+
children: 0
|
|
33
|
+
}));
|
|
34
|
+
expect(screen.getByText('0')).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
it('skips an empty title but keeps the children', () => {
|
|
37
|
+
const {
|
|
38
|
+
container
|
|
39
|
+
} = render(_jsx(Banner, {
|
|
40
|
+
type: "info",
|
|
41
|
+
title: "",
|
|
42
|
+
children: "Description"
|
|
43
|
+
}));
|
|
44
|
+
expect(screen.getByText('Description')).toBeInTheDocument();
|
|
45
|
+
expect(container.querySelector('h4')).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
it('renders nothing and reports when there is neither title nor children', () => {
|
|
48
|
+
const {
|
|
49
|
+
container
|
|
50
|
+
} = render(_jsx(Banner, {
|
|
51
|
+
type: "info",
|
|
52
|
+
title: ""
|
|
53
|
+
}));
|
|
54
|
+
expect(container).toBeEmptyDOMElement();
|
|
55
|
+
expect(consoleError).toHaveBeenCalledWith('[Banner]: neither title nor children is set');
|
|
56
|
+
});
|
|
57
|
+
});
|
package/Banner/styles.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export declare const BannerBox: import("styled-components").StyledComponent<"div
|
|
|
12
12
|
$isLoading: boolean;
|
|
13
13
|
}, never>;
|
|
14
14
|
export declare const BannerContent: import("styled-components").StyledComponent<"div", any, {
|
|
15
|
-
$
|
|
15
|
+
$hasTitleAndChildren?: boolean;
|
|
16
16
|
}, never>;
|
|
17
17
|
export declare const IconContainer: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
18
18
|
export declare const BannerShape: import("styled-components").StyledComponent<"div", any, import("react").ClassAttributes<HTMLDivElement> & import("react").HTMLAttributes<HTMLDivElement> & {
|
package/Banner/styles.js
CHANGED
|
@@ -28,8 +28,8 @@ export const BannerContent = styled.div.withConfig({
|
|
|
28
28
|
displayName: "BannerContent",
|
|
29
29
|
componentId: "sc-9uu4ap-4"
|
|
30
30
|
})(["flex-grow:1;padding:", ";color:", ";"], ({
|
|
31
|
-
$
|
|
32
|
-
}) => $
|
|
31
|
+
$hasTitleAndChildren
|
|
32
|
+
}) => $hasTitleAndChildren ? '16px' : '8px 16px', colors.error.shape.base.color);
|
|
33
33
|
export const IconContainer = styled.div.withConfig({
|
|
34
34
|
displayName: "IconContainer",
|
|
35
35
|
componentId: "sc-9uu4ap-5"
|
package/Banner/types.d.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import type { ReactElement, ReactNode } from 'react';
|
|
2
2
|
/** Severity of a {@link Banner}, driving its icon and colors. */
|
|
3
3
|
export type BannerType = 'error' | 'warning' | 'info' | 'success';
|
|
4
|
-
|
|
5
|
-
export type RequiredBannerProps = {
|
|
4
|
+
type BannerBaseProps = {
|
|
6
5
|
/** Ref to the banner root element. */
|
|
7
6
|
readonly boxRef?: any;
|
|
8
7
|
/** Severity — sets the default icon and colors. */
|
|
9
8
|
readonly type: BannerType;
|
|
10
|
-
/** Banner heading. */
|
|
11
|
-
readonly title: string;
|
|
12
9
|
/** Surface style: `shape` (filled, default) or `ghost`. */
|
|
13
10
|
readonly view?: 'shape' | 'ghost';
|
|
14
11
|
/** Custom icon; falls back to the type's default icon. */
|
|
@@ -21,9 +18,20 @@ export type RequiredBannerProps = {
|
|
|
21
18
|
readonly canBeClosed?: boolean;
|
|
22
19
|
/** Fires when the close button is clicked. */
|
|
23
20
|
readonly onClose?: () => void;
|
|
21
|
+
};
|
|
22
|
+
type BannerContentProps = {
|
|
23
|
+
/** Banner heading; an empty string counts as no title. */
|
|
24
|
+
readonly title: string;
|
|
24
25
|
/** Body content under the title. */
|
|
25
26
|
readonly children?: ReactNode;
|
|
27
|
+
} | {
|
|
28
|
+
/** Banner heading; may be omitted when the body content is set. */
|
|
29
|
+
readonly title?: string;
|
|
30
|
+
/** Body content, shown alone when there is no title. */
|
|
31
|
+
readonly children: Exclude<ReactNode, null | undefined | boolean>;
|
|
26
32
|
};
|
|
33
|
+
/** Base props of {@link Banner}: a title, body content, or both. */
|
|
34
|
+
export type RequiredBannerProps = BannerBaseProps & BannerContentProps;
|
|
27
35
|
/** {@link Banner} props with an optional action button. */
|
|
28
36
|
export type BannerPropsWithButton = RequiredBannerProps & {
|
|
29
37
|
/** Action button shown on the right. */
|
|
@@ -34,3 +42,4 @@ export type BannerPropsWithButton = RequiredBannerProps & {
|
|
|
34
42
|
readonly buttonText: string;
|
|
35
43
|
};
|
|
36
44
|
};
|
|
45
|
+
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Horizontal
|
|
2
|
+
* Horizontal row of {@link Tab}s with an optional title. Scrolls when the tabs
|
|
3
|
+
* overflow, or stretches them to equal widths with `layout="stretch"`.
|
|
3
4
|
*/
|
|
4
5
|
export declare const TabBar: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & Readonly<{
|
|
5
6
|
title?: import("react").ReactNode;
|
|
7
|
+
layout?: import("./types").TabBarLayout;
|
|
6
8
|
}> & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from 'react';
|
|
3
3
|
import { H4 } from '@pushwoosh/kit-typography';
|
|
4
|
-
import { TabBarTitleBox, TabBarContainer } from './styles';
|
|
4
|
+
import { TabBarTitleBox, TabBarContainer, TabBarStretchedContent } from './styles';
|
|
5
5
|
import { TabBarScrollableFrame } from '../TabBarScrollableFrame';
|
|
6
6
|
/**
|
|
7
|
-
* Horizontal
|
|
7
|
+
* Horizontal row of {@link Tab}s with an optional title. Scrolls when the tabs
|
|
8
|
+
* overflow, or stretches them to equal widths with `layout="stretch"`.
|
|
8
9
|
*/
|
|
9
10
|
export const TabBar = forwardRef((props, ref) => {
|
|
10
11
|
const {
|
|
11
12
|
children,
|
|
12
13
|
title,
|
|
14
|
+
layout = 'scroll',
|
|
13
15
|
...rest
|
|
14
16
|
} = props;
|
|
15
17
|
return _jsxs(TabBarContainer, {
|
|
@@ -20,7 +22,9 @@ export const TabBar = forwardRef((props, ref) => {
|
|
|
20
22
|
children: _jsx(H4, {
|
|
21
23
|
children: title
|
|
22
24
|
})
|
|
23
|
-
}), _jsx(
|
|
25
|
+
}), layout === 'stretch' ? _jsx(TabBarStretchedContent, {
|
|
26
|
+
children: children
|
|
27
|
+
}) : _jsx(TabBarScrollableFrame, {
|
|
24
28
|
children: children
|
|
25
29
|
})]
|
|
26
30
|
});
|
|
@@ -2,3 +2,4 @@ export declare const TabBarContainer: import("styled-components").StyledComponen
|
|
|
2
2
|
hasTitle: boolean;
|
|
3
3
|
}, never>;
|
|
4
4
|
export declare const TabBarTitleBox: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
5
|
+
export declare const TabBarStretchedContent: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
@@ -9,4 +9,8 @@ export const TabBarContainer = styled.div.withConfig({
|
|
|
9
9
|
export const TabBarTitleBox = styled.div.withConfig({
|
|
10
10
|
displayName: "TabBarTitleBox",
|
|
11
11
|
componentId: "sc-pu8yhh-1"
|
|
12
|
-
})(["display:flex;align-items:center;position:relative;padding:0 12px 0 16px;"]);
|
|
12
|
+
})(["display:flex;align-items:center;position:relative;padding:0 12px 0 16px;"]);
|
|
13
|
+
export const TabBarStretchedContent = styled.div.withConfig({
|
|
14
|
+
displayName: "TabBarStretchedContent",
|
|
15
|
+
componentId: "sc-pu8yhh-2"
|
|
16
|
+
})(["display:flex;align-items:center;gap:4px;width:100%;height:32px;& > *{flex:1 1 0;min-width:0;}"]);
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
+
/** How a {@link TabBar} lays out its tabs. */
|
|
3
|
+
export type TabBarLayout =
|
|
4
|
+
/** Tabs take their content width; overflowing tabs scroll behind arrows. */
|
|
5
|
+
'scroll'
|
|
6
|
+
/**
|
|
7
|
+
* Tabs share the bar width equally. Meant for narrow panels (drawers, sidebars,
|
|
8
|
+
* cards around 350px wide), not for page-level tabs. Nothing scrolls in this
|
|
9
|
+
* layout, so keep it to 3, at most 4, tabs with short labels.
|
|
10
|
+
*/
|
|
11
|
+
| 'stretch';
|
|
2
12
|
/** Props of {@link TabBar} — all `<div>` attributes plus an optional title. */
|
|
3
13
|
export type TabBarProps = Omit<JSX.IntrinsicElements['div'], 'ref'> & Readonly<{
|
|
4
14
|
/** Optional heading shown before the tabs. */
|
|
5
15
|
title?: ReactNode;
|
|
16
|
+
/** Tab layout, `'scroll'` by default; see {@link TabBarLayout} for when to stretch. */
|
|
17
|
+
layout?: TabBarLayout;
|
|
6
18
|
}>;
|
|
@@ -15,6 +15,8 @@ export const TabBarScrollableFrame = forwardRef(({
|
|
|
15
15
|
const [scrollLeft, setScrollLeft] = useState(0);
|
|
16
16
|
const scrollable = contentWidth > frameWidth;
|
|
17
17
|
const minLeft = frameWidth - contentWidth;
|
|
18
|
+
const canScrollLeft = scrollable && scrollLeft < 0;
|
|
19
|
+
const canScrollRight = scrollable && scrollLeft > minLeft;
|
|
18
20
|
const onClick = useCallback(event => {
|
|
19
21
|
const {
|
|
20
22
|
currentTarget
|
|
@@ -49,20 +51,22 @@ export const TabBarScrollableFrame = forwardRef(({
|
|
|
49
51
|
...props,
|
|
50
52
|
ref: ref,
|
|
51
53
|
children: [_jsx(TabBarWindow, {
|
|
54
|
+
"$clipLeft": canScrollLeft,
|
|
55
|
+
"$clipRight": canScrollRight,
|
|
52
56
|
ref: windowRef,
|
|
53
57
|
children: _jsx(TabBarContent, {
|
|
54
58
|
left: scrollLeft,
|
|
55
59
|
ref: contentRef,
|
|
56
60
|
children: children
|
|
57
61
|
})
|
|
58
|
-
}),
|
|
62
|
+
}), canScrollLeft && _jsx(TabBarArrowContainer, {
|
|
59
63
|
"data-left": true,
|
|
60
64
|
onClick: onClick,
|
|
61
65
|
children: _jsx(ChevronIcon, {
|
|
62
66
|
direction: "left",
|
|
63
67
|
size: "small"
|
|
64
68
|
})
|
|
65
|
-
}),
|
|
69
|
+
}), canScrollRight && _jsx(TabBarArrowContainer, {
|
|
66
70
|
"$right": true,
|
|
67
71
|
onClick: onClick,
|
|
68
72
|
children: _jsx(ChevronIcon, {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export declare const TabBarArrowContainer: import("styled-components").StyledComponent<"div", any, {
|
|
2
2
|
$right?: boolean;
|
|
3
3
|
}, never>;
|
|
4
|
-
export declare const TabBarWindow: import("styled-components").StyledComponent<"div", any, {
|
|
4
|
+
export declare const TabBarWindow: import("styled-components").StyledComponent<"div", any, {
|
|
5
|
+
$clipLeft: boolean;
|
|
6
|
+
$clipRight: boolean;
|
|
7
|
+
}, never>;
|
|
5
8
|
export declare const TabsListContentBase: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
6
9
|
export declare const TabBarContent: import("styled-components").StyledComponent<"div", any, {
|
|
7
10
|
left: number;
|
|
@@ -6,10 +6,15 @@ export const TabBarArrowContainer = styled.div.withConfig({
|
|
|
6
6
|
})(["position:absolute;top:0;", ":0;display:flex;align-items:center;height:32px;cursor:pointer;color:", ";background-color:", ";box-shadow:", ";border-radius:", ";padding:8px 4px;&:hover{color:", ";}"], ({
|
|
7
7
|
$right
|
|
8
8
|
}) => $right ? 'right' : 'left', Color.MAIN, Color.CLEAR, Shadow.SMALL, ShapeRadius.CONTROL, Color.PRIMARY_HOVER);
|
|
9
|
+
const SHADOW_BLEED = '-4px';
|
|
9
10
|
export const TabBarWindow = styled.div.withConfig({
|
|
10
11
|
displayName: "TabBarWindow",
|
|
11
12
|
componentId: "sc-1sonok9-1"
|
|
12
|
-
})(["position:relative;width:100%;height:32px;
|
|
13
|
+
})(["position:relative;width:100%;height:32px;clip-path:inset( ", " ", " ", " ", " );"], SHADOW_BLEED, ({
|
|
14
|
+
$clipRight
|
|
15
|
+
}) => $clipRight ? 0 : SHADOW_BLEED, SHADOW_BLEED, ({
|
|
16
|
+
$clipLeft
|
|
17
|
+
}) => $clipLeft ? 0 : SHADOW_BLEED);
|
|
13
18
|
export const TabsListContentBase = styled.div.withConfig({
|
|
14
19
|
displayName: "TabsListContentBase",
|
|
15
20
|
componentId: "sc-1sonok9-2"
|
package/Tabs/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { Tab } from './components/Tab';
|
|
2
2
|
export { TabBar } from './components/TabBar';
|
|
3
|
+
export type { TabBarLayout } from './components/TabBar/types';
|
|
3
4
|
export { TabPanel } from './components/TabPanel';
|
|
4
5
|
export { Tabs } from './components/Tabs';
|
|
5
6
|
export type { TabCode } from './types';
|
package/index.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export { ComboboxMulti, type ComboboxMultiProps } from './ComboboxMulti';
|
|
|
29
29
|
export { Switch } from './Switch';
|
|
30
30
|
export { TextSkeleton, RectangularSkeleton, CircularSkeleton } from './Skeleton';
|
|
31
31
|
export { Tooltip, type TooltipPosition, type TooltipColor, TooltipTrigger } from './Tooltip';
|
|
32
|
-
export { Tab, TabBar, TabPanel, Tabs, type TabCode } from './Tabs';
|
|
32
|
+
export { Tab, TabBar, TabPanel, Tabs, type TabCode, type TabBarLayout } from './Tabs';
|
|
33
33
|
export { Link, type LinkPropsWithoutAs, type LinkPropsWithAsTag, type LinkPropsWithAsComponent, } from './Link';
|
|
34
34
|
export { StatisticCard } from './StatisticCard';
|
|
35
35
|
export { EmailTemplatePreview, NoEmailTemplatePreview, replaceLocalization, replacePlaceholders, matchClickMapItems, decorateClickMap, CLICK_MAP_BADGE_ATTRIBUTE, type EmailClickMapItem, type EmailClickMapPlacement, type EmailClickMapProps, type PreviewIframeProps, } from './EmailTemplatePreview';
|