@pushwoosh/dumb-components 2.3.1 → 2.4.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.
@@ -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
- "$hasChildren": !!children,
75
- children: [_jsx(H4, {
83
+ "$hasTitleAndChildren": hasTitle && hasChildren,
84
+ children: [hasTitle && _jsx(H4, {
76
85
  children: title
77
- }), children && _jsx("div", {
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
+ });
@@ -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
- $hasChildren?: boolean;
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
- $hasChildren
32
- }) => $hasChildren ? '16px' : '8px 16px', colors.error.shape.base.color);
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
- /** Base props of {@link Banner}. */
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 {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/dumb-components",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
4
4
  "description": "React components to build Pushwoosh products",
5
5
  "main": "index.js",
6
6
  "module": "index.js",