@pushwoosh/dumb-components 4.0.0 → 4.2.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/Button/Button.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChevronIcon } from '@pushwoosh/kit-icons';
2
3
  import { Spinner } from '../Spinner';
3
4
  import { geometryBySizeAndView, spinnerSizeBySize } from './constants';
4
- import { ButtonBox, ButtonIcon, ButtonSpinner, ButtonContent } from './styles';
5
+ import { ButtonBox, ButtonChevron, ButtonIcon, ButtonSpinner, ButtonContent } from './styles';
5
6
  export function Button(props) {
6
7
  const {
7
8
  type = 'button',
@@ -14,6 +15,7 @@ export function Button(props) {
14
15
  isOpen,
15
16
  icon,
16
17
  rightIcon,
18
+ hasChevron,
17
19
  children,
18
20
  onClick,
19
21
  ...rest
@@ -26,6 +28,10 @@ export function Button(props) {
26
28
  console.error(`[Button]: cannot render with color "${color}" and size "${size}" and view "${view}"`);
27
29
  return null;
28
30
  }
31
+ if (process.env.NODE_ENV === 'development' && hasChevron && rightIcon) {
32
+ console.warn('[Button]: rightIcon is ignored when hasChevron is set');
33
+ }
34
+ const isShowRightIcon = !!rightIcon && !hasChevron;
29
35
  const isShowOverlaySpinner = !!children && !icon && view !== 'ghost' && isLoading;
30
36
  const isDisabledNonButton = !!isDisabled && !!rest.as && rest.as !== 'button';
31
37
  const handleClick = isDisabledNonButton ? event => {
@@ -47,7 +53,8 @@ export function Button(props) {
47
53
  "$isOpen": isOpen,
48
54
  "$hideContent": isShowOverlaySpinner,
49
55
  "$hasLeftIcon": !!icon,
50
- "$hasRightIcon": !!rightIcon,
56
+ "$hasRightIcon": isShowRightIcon,
57
+ "$hasChevron": hasChevron,
51
58
  "$hasContent": !!children,
52
59
  ref: boxRef,
53
60
  children: [icon && _jsx(ButtonIcon, {
@@ -58,8 +65,15 @@ export function Button(props) {
58
65
  children: _jsx("div", {
59
66
  children: children
60
67
  })
61
- }), rightIcon && _jsx(ButtonIcon, {
68
+ }), isShowRightIcon && _jsx(ButtonIcon, {
62
69
  children: rightIcon
70
+ }), hasChevron && _jsx(ButtonChevron, {
71
+ "$isOpen": isOpen,
72
+ "aria-hidden": true,
73
+ children: _jsx(ChevronIcon, {
74
+ size: "small",
75
+ direction: "down"
76
+ })
63
77
  }), isShowOverlaySpinner && _jsx(ButtonSpinner, {
64
78
  children: _jsx(Spinner, {
65
79
  color: "danger",
@@ -7,6 +7,9 @@ export type ButtonGeometry = {
7
7
  readonly labelPadding: string;
8
8
  readonly gap: string;
9
9
  readonly iconSlot: string;
10
+ /** Label→chevron distance; defaults to `gap`. */
11
+ readonly chevronGap?: string;
12
+ readonly chevronEdge: string;
10
13
  readonly fontSize: string;
11
14
  readonly lineHeight: number;
12
15
  readonly fontWeight: number;
@@ -1,4 +1,5 @@
1
1
  import { FontSize, FontWeight, LineHeight, UnitSize } from '@pushwoosh/kit-constants';
2
+ const rhythm = '6px';
2
3
  const fieldTypography = {
3
4
  fontSize: FontSize.REGULAR,
4
5
  lineHeight: LineHeight.REGULAR,
@@ -13,15 +14,17 @@ const compactTypography = {
13
14
  };
14
15
  const boxed = {
15
16
  border: '1px',
16
- padding: '6px',
17
- labelPadding: '6px',
18
- gap: '0px'
17
+ padding: rhythm,
18
+ labelPadding: rhythm,
19
+ gap: '0px',
20
+ chevronGap: '4px'
19
21
  };
20
22
  const fieldBox = {
21
23
  ...fieldTypography,
22
24
  ...boxed,
23
25
  height: UnitSize.FIELD_HEIGHT,
24
- iconSlot: UnitSize.ICON_MEDIUM
26
+ iconSlot: UnitSize.ICON_MEDIUM,
27
+ chevronEdge: '8px'
25
28
  };
26
29
  /** Geometry per `size` × `view`; colors live in `colors.ts`. */
27
30
  export const geometryBySizeAndView = {
@@ -34,7 +37,8 @@ export const geometryBySizeAndView = {
34
37
  ...compactTypography,
35
38
  ...boxed,
36
39
  height: UnitSize.COMPACT_HEIGHT,
37
- iconSlot: UnitSize.ICON_SMALL
40
+ iconSlot: UnitSize.ICON_SMALL,
41
+ chevronEdge: rhythm
38
42
  },
39
43
  ghost: {
40
44
  ...compactTypography,
@@ -42,8 +46,9 @@ export const geometryBySizeAndView = {
42
46
  border: '0px',
43
47
  padding: '0px',
44
48
  labelPadding: '0px',
45
- gap: '6px',
46
- iconSlot: UnitSize.ICON_SMALL
49
+ gap: rhythm,
50
+ iconSlot: UnitSize.ICON_SMALL,
51
+ chevronEdge: '0px'
47
52
  }
48
53
  }
49
54
  };
@@ -5,6 +5,11 @@ export declare const ButtonIcon: import("styled-components/dist/types").IStyledC
5
5
  export declare const ButtonContent: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "style"> & {
6
6
  style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
7
7
  }> & string;
8
+ export declare const ButtonChevron: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Merged<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "style"> & {
9
+ style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
10
+ }, {
11
+ $isOpen?: boolean;
12
+ }>> & string;
8
13
  export declare const ButtonSpinner: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "style"> & {
9
14
  style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
10
15
  }> & string;
@@ -18,6 +23,7 @@ type ButtonBoxProps = {
18
23
  $hideContent?: boolean;
19
24
  $hasLeftIcon?: boolean;
20
25
  $hasRightIcon?: boolean;
26
+ $hasChevron?: boolean;
21
27
  $hasContent?: boolean;
22
28
  };
23
29
  export declare const ButtonBox: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Merged<Omit<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "style"> & {
package/Button/styles.js CHANGED
@@ -10,24 +10,32 @@ export const ButtonContent = styled.div.withConfig({
10
10
  displayName: "ButtonContent",
11
11
  componentId: "sc-qltvdl-1"
12
12
  })(["display:grid;justify-content:start;padding:0 var(--btn-label-padding);& > div{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}&:first-child{justify-content:center;}"]);
13
+ export const ButtonChevron = styled.span.withConfig({
14
+ displayName: "ButtonChevron",
15
+ componentId: "sc-qltvdl-2"
16
+ })(["display:flex;margin-right:calc(var(--btn-chevron-edge) - var(--btn-padding));transform:", ";transition:transform 150ms;@media (prefers-reduced-motion:reduce){transition:none;}"], ({
17
+ $isOpen
18
+ }) => $isOpen ? 'rotate(180deg)' : 'none');
13
19
  export const ButtonSpinner = styled.div.withConfig({
14
20
  displayName: "ButtonSpinner",
15
- componentId: "sc-qltvdl-2"
21
+ componentId: "sc-qltvdl-3"
16
22
  })(["position:absolute;top:0;left:0;right:0;bottom:0;display:flex;flex-wrap:nowrap;align-items:center;justify-content:center;"]);
17
23
  function geometryVars(geometry) {
18
- return css(["--btn-height:", ";--btn-border:", ";--btn-padding:", ";--btn-label-padding:", ";--btn-gap:", ";--btn-icon-slot:", ";font-size:", ";line-height:", ";font-weight:", ";text-transform:", ";"], geometry.height, geometry.border, geometry.padding, geometry.labelPadding, geometry.gap, geometry.iconSlot, geometry.fontSize, geometry.lineHeight, geometry.fontWeight, geometry.textTransform);
24
+ return css(["--btn-height:", ";--btn-border:", ";--btn-padding:", ";--btn-label-padding:", ";--btn-gap:", ";--btn-icon-slot:", ";--btn-chevron-gap:", ";--btn-chevron-edge:", ";font-size:", ";line-height:", ";font-weight:", ";text-transform:", ";"], geometry.height, geometry.border, geometry.padding, geometry.labelPadding, geometry.gap, geometry.iconSlot, geometry.chevronGap ?? geometry.gap, geometry.chevronEdge, geometry.fontSize, geometry.lineHeight, geometry.fontWeight, geometry.textTransform);
19
25
  }
20
26
  const ghostCompactHitArea = css(["&::before{content:'';position:absolute;inset:calc((var(--btn-height) - ", ") / 2) 0;}"], UnitSize.COMPACT_HEIGHT);
21
27
  export const ButtonBox = styled.button.withConfig({
22
28
  displayName: "ButtonBox",
23
- componentId: "sc-qltvdl-3"
24
- })(["", " position:relative;display:inline-grid;align-items:center;grid-template-columns:", ";gap:var(--btn-gap);box-sizing:border-box;height:var(--btn-height);padding:0 calc(var(--btn-padding) - var(--btn-border));appearance:none !important;border-style:solid;border-width:var(--btn-border) var(--btn-border-right,var(--btn-border)) var(--btn-border) var(--btn-border-left,var(--btn-border));border-radius:var(--btn-radius-left,", ") var(--btn-radius-right,", ") var(--btn-radius-right,", ") var(--btn-radius-left,", ");text-decoration:none;cursor:", ";user-select:none;&:hover{text-decoration:none;}&:focus{outline:none;}&:active{pointer-events:", ";}", " & ", "{opacity:", ";}", ""], ({
29
+ componentId: "sc-qltvdl-4"
30
+ })(["", " position:relative;display:inline-grid;align-items:center;grid-template-columns:", ";gap:var(--btn-gap);box-sizing:border-box;height:var(--btn-height);padding:0 calc(var(--btn-padding) - var(--btn-border));appearance:none !important;border-style:solid;border-width:var(--btn-border) var(--btn-border-right,var(--btn-border)) var(--btn-border) var(--btn-border-left,var(--btn-border));border-radius:var(--btn-radius-left,", ") var(--btn-radius-right,", ") var(--btn-radius-right,", ") var(--btn-radius-left,", ");text-decoration:none;cursor:", ";user-select:none;&:hover{text-decoration:none;}&:focus{outline:none;}&:active{pointer-events:", ";}", " & ", "{margin-left:", ";}& ", ",& ", "{opacity:", ";}", ""], ({
25
31
  $size,
26
32
  $view
27
- }) => geometryVars(geometryBySizeAndView[$size][$view]), props => [props.$hasLeftIcon && 'auto', props.$hasContent && 'minmax(0, 1fr)', props.$hasRightIcon && 'auto'].filter(Boolean).join(' '), ShapeRadius.CONTROL, ShapeRadius.CONTROL, ShapeRadius.CONTROL, ShapeRadius.CONTROL, props => props.$isDisabled || props.$isLoading ? 'not-allowed!important' : 'pointer', props => props.$isDisabled || props.$isLoading ? 'none' : 'auto', ({
33
+ }) => geometryVars(geometryBySizeAndView[$size][$view]), props => [props.$hasLeftIcon && 'auto', props.$hasContent && 'minmax(0, 1fr)', (props.$hasRightIcon || props.$hasChevron) && 'auto'].filter(Boolean).join(' '), ShapeRadius.CONTROL, ShapeRadius.CONTROL, ShapeRadius.CONTROL, ShapeRadius.CONTROL, props => props.$isDisabled || props.$isLoading ? 'not-allowed!important' : 'pointer', props => props.$isDisabled || props.$isLoading ? 'none' : 'auto', ({
28
34
  $size,
29
35
  $view
30
- }) => $size === 'compact' && $view === 'ghost' && ghostCompactHitArea, /* sc-selector */ButtonContent, ({
36
+ }) => $size === 'compact' && $view === 'ghost' && ghostCompactHitArea, /* sc-selector */ButtonChevron, ({
37
+ $hasContent
38
+ }) => $hasContent ? 'calc(var(--btn-chevron-gap) - var(--btn-label-padding) - var(--btn-gap))' : 'calc(var(--btn-chevron-gap) - var(--btn-gap))', /* sc-selector */ButtonContent, /* sc-selector */ButtonChevron, ({
31
39
  $hideContent,
32
40
  $isLoading
33
41
  }) => {
@@ -42,10 +50,11 @@ export const ButtonBox = styled.button.withConfig({
42
50
  }) => colorStyles[$color][$view][$size]);
43
51
  export const RemoveButtonBox = styled.button.withConfig({
44
52
  displayName: "RemoveButtonBox",
45
- componentId: "sc-qltvdl-4"
53
+ componentId: "sc-qltvdl-5"
46
54
  })(["width:", ";height:", ";padding:0;appearance:none !important;border:none;background:transparent;cursor:pointer;color:", ";&:hover{color:", ";}&:active{color:", ";}"], UnitSize.ICON_SMALL, UnitSize.ICON_SMALL, Color.PHANTOM, Color.DANGER_HOVER, Color.DANGER_PRESSED);
47
55
  ButtonBox.displayName = 'ButtonBox';
48
56
  ButtonIcon.displayName = 'ButtonIcon';
49
57
  ButtonContent.displayName = 'ButtonContent';
58
+ ButtonChevron.displayName = 'ButtonChevron';
50
59
  ButtonSpinner.displayName = 'ButtonSpinner';
51
60
  RemoveButtonBox.displayName = 'RemoveButtonBox';
package/Button/types.d.ts CHANGED
@@ -25,8 +25,13 @@ export type RequiredButtonProps = {
25
25
  readonly isOpen?: boolean;
26
26
  /** Icon rendered before the label. */
27
27
  readonly icon?: ReactNode;
28
- /** Icon rendered after the label. */
28
+ /**
29
+ * Icon rendered after the label.
30
+ * @deprecated Use `hasChevron` for the dropdown chevron; ignored when `hasChevron` is set.
31
+ */
29
32
  readonly rightIcon?: ReactNode;
33
+ /** Render a chevron after the label that turns over while `isOpen`. Decoration only: it does not make the button a menu trigger. */
34
+ readonly hasChevron?: boolean;
30
35
  /** Button label. */
31
36
  readonly children?: ReactNode;
32
37
  };
@@ -27,10 +27,12 @@ export const CalendarSuperForm = props => {
27
27
  gap,
28
28
  firstDayOfWeek,
29
29
  initialVisibleMonth,
30
- footerContent
30
+ footerContent,
31
+ limitHint
31
32
  } = props;
32
33
  const isRangePicker = mode === 'range-date' || mode === 'range-datetime';
33
34
  const withTime = mode === 'single-datetime' || mode === 'range-datetime';
35
+ const notice = limitHint ?? (maxDays && maxDays > 0 ? `Max length: ${maxDays} days` : undefined);
34
36
  return _jsx(Vertical, {
35
37
  "$gap": 8,
36
38
  children: _jsx(CalendarRangePicker, {
@@ -87,17 +89,17 @@ export const CalendarSuperForm = props => {
87
89
  visibleCount: visibleCount !== undefined ? visibleCount : isRangePicker ? 2 : 1,
88
90
  monthWidth: monthWidth,
89
91
  gap: gap,
90
- renderAfter: withTime || maxDays && maxDays > 0 || footerContent ? () => {
92
+ renderAfter: withTime || notice || footerContent ? () => {
91
93
  return _jsxs(Vertical, {
92
94
  "$gap": 8,
93
95
  "$margin": {
94
96
  top: 16
95
97
  },
96
98
  children: [_jsxs(Vertical, {
97
- children: [maxDays && maxDays > 0 && _jsx(Text, {
99
+ children: [notice && _jsx(Text, {
98
100
  "$variant": "footnote",
99
101
  "$color": Color.LOCKED,
100
- children: `Max length: ${maxDays} days`
102
+ children: notice
101
103
  }), footerContent]
102
104
  }), withTime && timezone && onChangeTimezone && _jsx(CalendarTimeAndTimezoneSection, {
103
105
  isRangePicker: isRangePicker,
@@ -18,6 +18,8 @@ interface BaseProps {
18
18
  initialVisibleMonth?: DateStruct;
19
19
  /** Custom footer node rendered below the calendar and the min/max-days notice. */
20
20
  footerContent?: ReactNode;
21
+ /** Replaces the default `Max length: N days` notice, e.g. `Limitation: last 12 months | Max length: 3 months`. */
22
+ limitHint?: ReactNode;
21
23
  }
22
24
  /** Selection mode of {@link CalendarSuperForm}: single vs range, date-only vs date-time. */
23
25
  export type CalendarMode = 'single-date' | 'single-datetime' | 'range-date' | 'range-datetime';
@@ -38,6 +38,8 @@ export const DropdownMenuButton = ({
38
38
  boxRef: ref,
39
39
  onClick: onClick,
40
40
  isOpen: isOpen,
41
+ "aria-expanded": isOpen,
42
+ "aria-haspopup": "menu",
41
43
  isDisabled: isDisabled,
42
44
  isLoading: isLoading,
43
45
  children: label
@@ -29,8 +29,13 @@ export type DropdownMenuButtonProps = {
29
29
  className?: string;
30
30
  /** Icon before the label. */
31
31
  icon?: RequiredButtonProps['icon'];
32
- /** Icon after the label. */
32
+ /**
33
+ * Icon after the label.
34
+ * @deprecated Use `hasChevron`; ignored when `hasChevron` is set.
35
+ */
33
36
  rightIcon?: RequiredButtonProps['rightIcon'];
37
+ /** Render a chevron after the label that turns over while the menu is open. Off by default. */
38
+ hasChevron?: RequiredButtonProps['hasChevron'];
34
39
  /** Menu contents — nodes, or a function returning nodes. */
35
40
  children: (() => ReactNode) | ReactNode;
36
41
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/dumb-components",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "description": "React components to build Pushwoosh products",
5
5
  "main": "index.js",
6
6
  "module": "index.js",