@openedx/paragon 22.6.0 → 22.7.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.
Files changed (46) hide show
  1. package/dist/Chip/ChipIcon.d.ts +11 -6
  2. package/dist/Chip/ChipIcon.js +0 -2
  3. package/dist/Chip/ChipIcon.js.map +1 -1
  4. package/dist/Chip/constants.d.ts +4 -0
  5. package/dist/Chip/constants.js +3 -2
  6. package/dist/Chip/constants.js.map +1 -0
  7. package/dist/Chip/index.d.ts +2 -1
  8. package/dist/Chip/index.js +0 -2
  9. package/dist/Chip/index.js.map +1 -1
  10. package/dist/ChipCarousel/index.js +0 -2
  11. package/dist/ChipCarousel/index.js.map +1 -1
  12. package/dist/IconButton/index.d.ts +344 -0
  13. package/dist/IconButton/index.js +17 -25
  14. package/dist/IconButton/index.js.map +1 -1
  15. package/dist/Menu/SelectMenu.js +9 -4
  16. package/dist/Menu/SelectMenu.js.map +1 -1
  17. package/dist/Modal/ModalPopup.js +7 -1
  18. package/dist/Modal/ModalPopup.js.map +1 -1
  19. package/dist/Overlay/index.d.ts +128 -0
  20. package/dist/Overlay/index.js +5 -0
  21. package/dist/Overlay/index.js.map +1 -1
  22. package/dist/Tooltip/index.d.ts +7 -0
  23. package/dist/Tooltip/index.js.map +1 -1
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.js +3 -3
  26. package/package.json +1 -1
  27. package/src/Chip/{Chip.test.jsx → Chip.test.tsx} +5 -7
  28. package/src/Chip/ChipIcon.tsx +7 -7
  29. package/src/Chip/{constants.js → constants.ts} +1 -1
  30. package/src/Chip/index.tsx +1 -3
  31. package/src/ChipCarousel/index.tsx +0 -2
  32. package/src/IconButton/{IconButton.test.jsx → IconButton.test.tsx} +24 -3
  33. package/src/IconButton/__snapshots__/IconButton.test.tsx.snap +112 -0
  34. package/src/IconButton/{index.jsx → index.tsx} +62 -22
  35. package/src/Menu/SelectMenu.jsx +5 -0
  36. package/src/Menu/SelectMenu.test.jsx +6 -0
  37. package/src/Menu/select-menu.md +8 -0
  38. package/src/Modal/ModalPopup.jsx +9 -1
  39. package/src/Modal/tests/ModalPopupNoMock.test.jsx +29 -0
  40. package/src/Overlay/{index.jsx → index.tsx} +10 -6
  41. package/src/Tooltip/{index.jsx → index.tsx} +9 -3
  42. package/src/index.d.ts +3 -3
  43. package/src/index.js +3 -3
  44. package/src/IconButton/__snapshots__/IconButton.test.jsx.snap +0 -43
  45. /package/src/Chip/__snapshots__/{Chip.test.jsx.snap → Chip.test.tsx.snap} +0 -0
  46. /package/src/Tooltip/{Tooltip.test.jsx → Tooltip.test.tsx} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openedx/paragon",
3
- "version": "22.6.0",
3
+ "version": "22.7.0",
4
4
  "description": "Accessible, responsive UI component library based on Bootstrap.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -7,7 +7,7 @@ import { Close } from '../../icons';
7
7
  import { STYLE_VARIANTS } from './constants';
8
8
  import Chip from '.';
9
9
 
10
- function TestChip(props) {
10
+ function TestChip(props: Omit<React.ComponentProps<typeof Chip>, 'children'>) {
11
11
  return (
12
12
  <Chip {...props}>
13
13
  Test
@@ -42,15 +42,13 @@ describe('<Chip />', () => {
42
42
  iconBeforeAlt="close icon"
43
43
  iconAfter={Close}
44
44
  iconAfterAlt="close icon"
45
- >
46
- Chip
47
- </TestChip>
45
+ />
48
46
  )).toJSON();
49
47
  expect(tree).toMatchSnapshot();
50
48
  });
51
49
  it('renders div with "button" role when onClick is provided', () => {
52
50
  const tree = renderer.create((
53
- <TestChip onClick={jest.fn}>Chip</TestChip>
51
+ <TestChip onClick={jest.fn} />
54
52
  )).toJSON();
55
53
  expect(tree).toMatchSnapshot();
56
54
  });
@@ -104,7 +102,7 @@ describe('<Chip />', () => {
104
102
  />,
105
103
  );
106
104
  const iconAfter = screen.getByLabelText('icon-after');
107
- await userEvent.click(iconAfter, '{enter}', { skipClick: true });
105
+ await userEvent.click(iconAfter);
108
106
  expect(func).toHaveBeenCalledTimes(1);
109
107
  });
110
108
  it('onIconBeforeClick is triggered', async () => {
@@ -130,7 +128,7 @@ describe('<Chip />', () => {
130
128
  />,
131
129
  );
132
130
  const iconBefore = screen.getByLabelText('icon-before');
133
- await userEvent.click(iconBefore, '{enter}', { skipClick: true });
131
+ await userEvent.click(iconBefore);
134
132
  expect(func).toHaveBeenCalledTimes(1);
135
133
  });
136
134
  it('checks the absence of the `selected` class in the chip', async () => {
@@ -1,19 +1,19 @@
1
1
  import React, { KeyboardEventHandler, MouseEventHandler } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import Icon from '../Icon';
4
- // @ts-ignore
5
4
  import IconButton from '../IconButton';
6
- // @ts-ignore
7
5
  import { STYLE_VARIANTS } from './constants';
8
6
 
9
- export interface ChipIconProps {
7
+ export type ChipIconProps = {
10
8
  className: string,
11
9
  src: React.ComponentType,
12
- onClick?: KeyboardEventHandler & MouseEventHandler,
13
- alt?: string,
14
- variant: string,
10
+ variant: typeof STYLE_VARIANTS[keyof typeof STYLE_VARIANTS],
15
11
  disabled?: boolean,
16
- }
12
+ } & (
13
+ // Either _both_ onClick and alt are provided, or neither is:
14
+ | { onClick: KeyboardEventHandler<HTMLButtonElement> & MouseEventHandler<HTMLButtonElement>, alt: string }
15
+ | { onClick?: undefined, alt?: undefined }
16
+ );
17
17
 
18
18
  function ChipIcon({
19
19
  className, src, onClick, alt, variant, disabled,
@@ -2,4 +2,4 @@
2
2
  export const STYLE_VARIANTS = {
3
3
  DARK: 'dark',
4
4
  LIGHT: 'light',
5
- };
5
+ } as const;
@@ -3,9 +3,7 @@ import PropTypes, { type Requireable } from 'prop-types';
3
3
  import classNames from 'classnames';
4
4
  // @ts-ignore
5
5
  import { requiredWhen } from '../utils/propTypes';
6
- // @ts-ignore
7
6
  import { STYLE_VARIANTS } from './constants';
8
- // @ts-ignore
9
7
  import ChipIcon from './ChipIcon';
10
8
 
11
9
  export const CHIP_PGN_CLASS = 'pgn__chip';
@@ -14,7 +12,7 @@ export interface IChip {
14
12
  children: React.ReactNode,
15
13
  onClick?: KeyboardEventHandler & MouseEventHandler,
16
14
  className?: string,
17
- variant?: string,
15
+ variant?: typeof STYLE_VARIANTS[keyof typeof STYLE_VARIANTS],
18
16
  iconBefore?: React.ComponentType,
19
17
  iconBeforeAlt?: string,
20
18
  iconAfter?: React.ComponentType,
@@ -4,9 +4,7 @@ import { useIntl } from 'react-intl';
4
4
  import classNames from 'classnames';
5
5
  // @ts-ignore
6
6
  import { OverflowScroll, OverflowScrollContext } from '../OverflowScroll';
7
- // @ts-ignore
8
7
  import IconButton from '../IconButton';
9
- // @ts-ignore
10
8
  import Icon from '../Icon';
11
9
  // @ts-ignore
12
10
  import { ArrowForward, ArrowBack } from '../../icons';
@@ -11,21 +11,27 @@ describe('<IconButton />', () => {
11
11
  const alt = 'alternative';
12
12
  const iconAs = Icon;
13
13
  const src = InfoOutline;
14
- const variant = 'secondary';
14
+ const variant = 'secondary' as const;
15
15
  const props = {
16
16
  alt,
17
17
  src,
18
18
  iconAs,
19
19
  variant,
20
20
  };
21
- const iconParams = {
21
+ const deprecatedFontAwesomeExample = {
22
22
  prefix: 'pgn',
23
23
  iconName: 'InfoOutlineIcon',
24
24
  icon: [InfoOutline],
25
25
  };
26
26
  it('renders with required props', () => {
27
27
  const tree = renderer.create((
28
- <IconButton icon={iconParams} alt={alt} />
28
+ <IconButton iconAs={Icon} src={InfoOutline} alt={alt} />
29
+ )).toJSON();
30
+ expect(tree).toMatchSnapshot();
31
+ });
32
+ it('renders with deprecated props', () => {
33
+ const tree = renderer.create((
34
+ <IconButton icon={deprecatedFontAwesomeExample} alt={alt} />
29
35
  )).toJSON();
30
36
  expect(tree).toMatchSnapshot();
31
37
  });
@@ -94,4 +100,19 @@ describe('<IconButton />', () => {
94
100
  expect(spy2).toHaveBeenCalledTimes(1);
95
101
  });
96
102
  });
103
+
104
+ describe('<IconButton.IconButtonWithTooltip>', () => {
105
+ it('renders with required props', () => {
106
+ const tree = renderer.create((
107
+ <IconButton.IconButtonWithTooltip
108
+ iconAs={Icon}
109
+ src={InfoOutline}
110
+ alt={alt}
111
+ tooltipContent="Hello"
112
+ tooltipPlacement="left-end"
113
+ />
114
+ )).toJSON();
115
+ expect(tree).toMatchSnapshot();
116
+ });
117
+ });
97
118
  });
@@ -0,0 +1,112 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<IconButton /> <IconButton.IconButtonWithTooltip> renders with required props 1`] = `
4
+ <button
5
+ aria-label="alternative"
6
+ className="btn-icon btn-icon-primary btn-icon-md"
7
+ onBlur={[Function]}
8
+ onClick={[Function]}
9
+ onFocus={[Function]}
10
+ onMouseOut={[Function]}
11
+ onMouseOver={[Function]}
12
+ type="button"
13
+ >
14
+ <span
15
+ className="btn-icon__icon-container"
16
+ >
17
+ <span
18
+ className="pgn__icon btn-icon__icon"
19
+ >
20
+ <svg
21
+ aria-hidden={true}
22
+ fill="none"
23
+ focusable={false}
24
+ height={24}
25
+ role="img"
26
+ viewBox="0 0 24 24"
27
+ width={24}
28
+ xmlns="http://www.w3.org/2000/svg"
29
+ >
30
+ <path
31
+ d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
32
+ fill="currentColor"
33
+ />
34
+ </svg>
35
+ </span>
36
+ </span>
37
+ </button>
38
+ `;
39
+
40
+ exports[`<IconButton /> renders with deprecated props 1`] = `
41
+ <button
42
+ aria-label="alternative"
43
+ className="btn-icon btn-icon-primary btn-icon-md"
44
+ onClick={[Function]}
45
+ type="button"
46
+ >
47
+ <span
48
+ className="btn-icon__icon-container"
49
+ >
50
+ <svg
51
+ aria-hidden="true"
52
+ className="svg-inline--fa fa-InfoOutlineIcon btn-icon__icon"
53
+ data-icon="InfoOutlineIcon"
54
+ data-prefix="pgn"
55
+ focusable="false"
56
+ role="img"
57
+ style={{}}
58
+ viewBox="0 0 function SvgInfoOutline(props) {
59
+ return /*#__PURE__*/React.createElement("svg", _extends({
60
+ width: 24,
61
+ height: 24,
62
+ viewBox: "0 0 24 24",
63
+ fill: "none",
64
+ xmlns: "http://www.w3.org/2000/svg"
65
+ }, props), /*#__PURE__*/React.createElement("path", {
66
+ d: "M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z",
67
+ fill: "currentColor"
68
+ }));
69
+ } undefined"
70
+ xmlns="http://www.w3.org/2000/svg"
71
+ >
72
+ <path
73
+ fill="currentColor"
74
+ style={{}}
75
+ />
76
+ </svg>
77
+ </span>
78
+ </button>
79
+ `;
80
+
81
+ exports[`<IconButton /> renders with required props 1`] = `
82
+ <button
83
+ aria-label="alternative"
84
+ className="btn-icon btn-icon-primary btn-icon-md"
85
+ onClick={[Function]}
86
+ type="button"
87
+ >
88
+ <span
89
+ className="btn-icon__icon-container"
90
+ >
91
+ <span
92
+ className="pgn__icon btn-icon__icon"
93
+ >
94
+ <svg
95
+ aria-hidden={true}
96
+ fill="none"
97
+ focusable={false}
98
+ height={24}
99
+ role="img"
100
+ viewBox="0 0 24 24"
101
+ width={24}
102
+ xmlns="http://www.w3.org/2000/svg"
103
+ >
104
+ <path
105
+ d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
106
+ fill="currentColor"
107
+ />
108
+ </svg>
109
+ </span>
110
+ </span>
111
+ </button>
112
+ `;
@@ -1,12 +1,46 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import classNames from 'classnames';
4
-
5
4
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5
+ import { type Placement } from 'react-bootstrap/Overlay';
6
+
6
7
  import { OverlayTrigger } from '../Overlay';
7
8
  import Tooltip from '../Tooltip';
9
+ import Icon from '../Icon';
8
10
 
9
- const IconButton = React.forwardRef(({
11
+ interface Props extends React.HTMLAttributes<HTMLButtonElement> {
12
+ iconAs?: typeof Icon | typeof FontAwesomeIcon,
13
+ /** Additional CSS class[es] to apply to this button */
14
+ className?: string;
15
+ /** Alt text for your icon. For best practice, avoid using alt text to describe
16
+ * the image in the `IconButton`. Instead, we recommend describing the function
17
+ * of the button. */
18
+ alt: string;
19
+ /** Changes icon styles for dark background */
20
+ invertColors?: boolean;
21
+ /** An icon component to render. Example import of a Paragon icon component:
22
+ * `import { Check } from '@openedx/paragon/icons';`
23
+ * */
24
+ // Note: React.ComponentType is what we want here. React.ElementType would allow some element type strings like "div",
25
+ // but we only want to allow components like 'Add' (a specific icon component function/class)
26
+ src?: React.ComponentType;
27
+ /** Extra class names that will be added to the icon */
28
+ iconClassNames?: string;
29
+ /** Click handler for the button */
30
+ onClick?: React.MouseEventHandler<HTMLButtonElement>;
31
+ /** whether to show the `IconButton` in an active state, whose styling is distinct from default state */
32
+ isActive?: boolean;
33
+ /** @deprecated Using FontAwesome icons is deprecated. Instead, pass iconAs={Icon} src={...} */
34
+ icon?: { prefix?: string; iconName?: string, icon?: any[] },
35
+ /** Type of button (uses Bootstrap options) */
36
+ variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'light' | 'dark' | 'black' | 'brand';
37
+ /** size of button to render */
38
+ size?: 'sm' | 'md' | 'inline';
39
+ /** no children */
40
+ children?: never;
41
+ }
42
+
43
+ const IconButton = React.forwardRef<HTMLButtonElement, Props>(({
10
44
  className,
11
45
  alt,
12
46
  invertColors,
@@ -18,6 +52,7 @@ const IconButton = React.forwardRef(({
18
52
  variant,
19
53
  iconAs,
20
54
  isActive,
55
+ children, // unused, just here because we don't want it to be part of 'attrs'
21
56
  ...attrs
22
57
  }, ref) => {
23
58
  const invert = invertColors ? 'inverse-' : '';
@@ -50,7 +85,7 @@ const IconButton = React.forwardRef(({
50
85
  <span className="btn-icon__icon-container">
51
86
  <IconComponent
52
87
  className={classNames('btn-icon__icon', iconClassNames)}
53
- icon={icon}
88
+ icon={icon as any}
54
89
  src={src}
55
90
  />
56
91
  </span>
@@ -60,7 +95,7 @@ const IconButton = React.forwardRef(({
60
95
 
61
96
  IconButton.defaultProps = {
62
97
  iconAs: undefined,
63
- src: null,
98
+ src: undefined,
64
99
  icon: undefined,
65
100
  iconClassNames: undefined,
66
101
  className: undefined,
@@ -69,6 +104,7 @@ IconButton.defaultProps = {
69
104
  size: 'md',
70
105
  onClick: () => {},
71
106
  isActive: false,
107
+ children: undefined,
72
108
  };
73
109
 
74
110
  IconButton.propTypes = {
@@ -76,11 +112,11 @@ IconButton.propTypes = {
76
112
  className: PropTypes.string,
77
113
  /** Component that renders the icon, currently defaults to `FontAwesomeIcon`,
78
114
  * but is going to be deprecated soon, please use Paragon's icons instead. */
79
- iconAs: PropTypes.elementType,
115
+ iconAs: PropTypes.elementType as any,
80
116
  /** An icon component to render. Example import of a Paragon icon component:
81
- * `import { Check } from '@openedx/paragon/dist/icon';`
117
+ * `import { Check } from '@openedx/paragon/icons';`
82
118
  * */
83
- src: PropTypes.oneOfType([PropTypes.element, PropTypes.elementType]),
119
+ src: PropTypes.elementType as any,
84
120
  /** Alt text for your icon. For best practice, avoid using alt text to describe
85
121
  * the image in the `IconButton`. Instead, we recommend describing the function
86
122
  * of the button. */
@@ -93,7 +129,7 @@ IconButton.propTypes = {
93
129
  iconName: PropTypes.string,
94
130
  // eslint-disable-next-line react/forbid-prop-types
95
131
  icon: PropTypes.array,
96
- }),
132
+ }) as any,
97
133
  /** Extra class names that will be added to the icon */
98
134
  iconClassNames: PropTypes.string,
99
135
  /** Click handler for the button */
@@ -106,38 +142,40 @@ IconButton.propTypes = {
106
142
  isActive: PropTypes.bool,
107
143
  };
108
144
 
145
+ interface PropsWithTooltip extends Props {
146
+ /** choose from https://popper.js.org/docs/v2/constructors/#options */
147
+ tooltipPlacement: Placement,
148
+ /** any content to pass to tooltip content area */
149
+ tooltipContent: React.ReactNode,
150
+ }
151
+
109
152
  /**
110
- *
111
- * @param { object } args Arguments
112
- * @param { string } args.tooltipPlacement choose from https://popper.js.org/docs/v2/constructors/#options
113
- * @param { React.Component } args.tooltipContent any content to pass to tooltip content area
114
- * @returns { IconButton } a button wrapped in overlaytrigger
153
+ * An icon button wrapped in overlaytrigger to display a tooltip.
115
154
  */
116
155
  function IconButtonWithTooltip({
117
- tooltipPlacement, tooltipContent, variant, invertColors, ...props
118
- }) {
119
- const invert = invertColors ? 'inverse-' : '';
156
+ tooltipPlacement, tooltipContent, ...props
157
+ }: PropsWithTooltip) {
158
+ const invert = props.invertColors ? 'inverse-' : '';
120
159
  return (
121
160
  <OverlayTrigger
122
161
  placement={tooltipPlacement}
123
162
  overlay={(
124
163
  <Tooltip
125
164
  id={`iconbutton-tooltip-${tooltipPlacement}`}
126
- variant={invert ? 'light' : ''}
165
+ variant={invert ? 'light' : undefined}
127
166
  >
128
167
  {tooltipContent}
129
168
  </Tooltip>
130
169
  )}
131
170
  >
132
- <IconButton variant={variant} invertColors={invertColors} {...props} />
171
+ <IconButton {...props} />
133
172
  </OverlayTrigger>
134
173
  );
135
174
  }
136
175
 
137
176
  IconButtonWithTooltip.defaultProps = {
177
+ ...IconButton.defaultProps,
138
178
  tooltipPlacement: 'top',
139
- variant: 'primary',
140
- invertColors: false,
141
179
  };
142
180
 
143
181
  IconButtonWithTooltip.propTypes = {
@@ -151,7 +189,9 @@ IconButtonWithTooltip.propTypes = {
151
189
  invertColors: PropTypes.bool,
152
190
  };
153
191
 
154
- IconButton.IconButtonWithTooltip = IconButtonWithTooltip;
192
+ (IconButton as any).IconButtonWithTooltip = IconButtonWithTooltip;
155
193
 
156
- export default IconButton;
194
+ export default IconButton as typeof IconButton & {
195
+ IconButtonWithTooltip: typeof IconButtonWithTooltip,
196
+ };
157
197
  export { IconButtonWithTooltip };
@@ -15,6 +15,7 @@ function SelectMenu({
15
15
  children,
16
16
  className,
17
17
  variant,
18
+ disabled,
18
19
  ...props
19
20
  }) {
20
21
  const [triggerTarget, setTriggerTarget] = useState(null);
@@ -89,6 +90,7 @@ function SelectMenu({
89
90
  variant={variant}
90
91
  iconAfter={ExpandMore}
91
92
  onClick={open}
93
+ disabled={disabled}
92
94
  >
93
95
  {selected !== undefined && children[selected] ? children[selected].props.children : defaultMessage}
94
96
  </Button>
@@ -131,12 +133,15 @@ SelectMenu.propTypes = {
131
133
  className: PropTypes.string,
132
134
  /** Specifies variant to use. */
133
135
  variant: PropTypes.string,
136
+ /** Specifies if the `SelectMenu` is disabled. */
137
+ disabled: PropTypes.bool,
134
138
  };
135
139
 
136
140
  SelectMenu.defaultProps = {
137
141
  defaultMessage: SELECT_MENU_DEFAULT_MESSAGE,
138
142
  className: undefined,
139
143
  variant: 'outline-primary',
144
+ disabled: false,
140
145
  };
141
146
 
142
147
  const SelectMenuWithDeprecatedProp = withDeprecatedProps(SelectMenu, 'SelectMenu', {
@@ -58,6 +58,12 @@ describe('correct rendering', () => {
58
58
  const button = screen.getByRole('button');
59
59
  expect(button).toHaveClass('btn-brand');
60
60
  });
61
+
62
+ it('renders as disabled', () => {
63
+ render(DefaultSelectMenu({ disabled: true }));
64
+ const button = screen.getByRole('button');
65
+ expect(button).toBeDisabled();
66
+ });
61
67
  });
62
68
 
63
69
  describe('mouse behavior & keyboard behavior', () => {
@@ -56,3 +56,11 @@ The ``Modal`` brings focus to the first menu element upon the click of the trigg
56
56
  <MenuItem>M. Hortens</MenuItem>
57
57
  </SelectMenu>
58
58
  ```
59
+
60
+ ## Disabled
61
+
62
+ ```jsx live
63
+ <SelectMenu disabled>
64
+ <MenuItem>A Menu Item</MenuItem>
65
+ </SelectMenu>
66
+ ```
@@ -34,6 +34,14 @@ function ModalPopup({
34
34
  },
35
35
  ];
36
36
 
37
+ const handleOnClickOutside = (e) => {
38
+ if (e.type === 'touchstart') {
39
+ return;
40
+ }
41
+
42
+ onClose();
43
+ };
44
+
37
45
  return (
38
46
  <ModalContextProvider onClose={onClose} isOpen={isOpen} isBlocking={isBlocking}>
39
47
  <RootComponent>
@@ -47,7 +55,7 @@ function ModalPopup({
47
55
  scrollLock={false}
48
56
  enabled={isOpen}
49
57
  onEscapeKey={onClose}
50
- onClickOutside={onClose}
58
+ onClickOutside={handleOnClickOutside}
51
59
  >
52
60
  {isOpen && (
53
61
  <div className="pgn__modal-popup__tooltip">
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import { fireEvent, render } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import ModalPopup from '../ModalPopup';
5
+
6
+ describe('<ModalPopup />', () => {
7
+ const mockPositionRef = React.createRef();
8
+
9
+ describe('when isOpen', () => {
10
+ const isOpen = true;
11
+ const closeFn = jest.fn();
12
+
13
+ it('calls close on click events but not touchstart events', async () => {
14
+ render(
15
+ <ModalPopup
16
+ positionRef={mockPositionRef}
17
+ isOpen={isOpen}
18
+ onClose={closeFn}
19
+ >
20
+ <div>Modal Contents</div>
21
+ </ModalPopup>,
22
+ );
23
+ await fireEvent.touchStart(document.body);
24
+ expect(closeFn).not.toHaveBeenCalled();
25
+ await userEvent.click(document.body);
26
+ expect(closeFn).toHaveBeenCalled();
27
+ });
28
+ });
29
+ });
@@ -1,10 +1,14 @@
1
1
  import React from 'react';
2
- import BaseOverlay from 'react-bootstrap/Overlay';
3
- import BaseOverlayTrigger from 'react-bootstrap/OverlayTrigger';
2
+ import BaseOverlay, { type OverlayProps, type Placement } from 'react-bootstrap/Overlay';
3
+ import BaseOverlayTrigger, { type OverlayTriggerProps, type OverlayTriggerType } from 'react-bootstrap/OverlayTrigger';
4
4
  import Fade from 'react-bootstrap/Fade';
5
5
  import PropTypes from 'prop-types';
6
6
 
7
- const PLACEMENT_VARIANTS = [
7
+ // Note: The only thing this file adds to the base component is propTypes validation.
8
+ // As more Paragon consumers adopt TypeScript, we could consider removing almost all of this code
9
+ // and just re-export the Overlay and OverlayTrigger components from react-bootstrap unmodified.
10
+
11
+ const PLACEMENT_VARIANTS: Placement[] = [
8
12
  'auto-start',
9
13
  'auto',
10
14
  'auto-end',
@@ -22,16 +26,16 @@ const PLACEMENT_VARIANTS = [
22
26
  'left-start',
23
27
  ];
24
28
 
25
- const TRIGGER_VARIANTS = [
29
+ const TRIGGER_VARIANTS: OverlayTriggerType[] = [
26
30
  'hover',
27
31
  'click',
28
32
  'focus',
29
33
  ];
30
34
 
31
- function Overlay(props) {
35
+ function Overlay(props: OverlayProps) {
32
36
  return <BaseOverlay {...props} />;
33
37
  }
34
- function OverlayTrigger(props) {
38
+ function OverlayTrigger(props: OverlayTriggerProps) {
35
39
  return (
36
40
  <BaseOverlayTrigger {...props}>
37
41
  {props.children}
@@ -1,9 +1,15 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import classNames from 'classnames';
4
- import BaseTooltip from 'react-bootstrap/Tooltip';
4
+ import BaseTooltip, { type TooltipProps as BaseTooltipProps } from 'react-bootstrap/Tooltip';
5
+ import { type Placement } from 'react-bootstrap/Overlay';
6
+ import type { ComponentWithAsProp } from '../utils/types/bootstrap';
5
7
 
6
- const PLACEMENT_VARIANTS = [
8
+ interface TooltipProps extends BaseTooltipProps {
9
+ variant?: 'light';
10
+ }
11
+
12
+ const PLACEMENT_VARIANTS: Placement[] = [
7
13
  'auto-start',
8
14
  'auto',
9
15
  'auto-end',
@@ -21,7 +27,7 @@ const PLACEMENT_VARIANTS = [
21
27
  'left-start',
22
28
  ];
23
29
 
24
- const Tooltip = React.forwardRef(({
30
+ const Tooltip: ComponentWithAsProp<'div', TooltipProps> = React.forwardRef(({
25
31
  children,
26
32
  variant,
27
33
  ...props
package/src/index.d.ts CHANGED
@@ -10,6 +10,9 @@ export { default as Chip, CHIP_PGN_CLASS } from './Chip';
10
10
  export { default as ChipCarousel } from './ChipCarousel';
11
11
  export { default as Hyperlink, HYPER_LINK_EXTERNAL_LINK_ALT_TEXT, HYPER_LINK_EXTERNAL_LINK_TITLE } from './Hyperlink';
12
12
  export { default as Icon } from './Icon';
13
+ export { default as IconButton, IconButtonWithTooltip } from './IconButton';
14
+ export { default as Overlay, OverlayTrigger } from './Overlay';
15
+ export { default as Tooltip } from './Tooltip';
13
16
 
14
17
  // // // // // // // // // // // // // // // // // // // // // // // // // // //
15
18
  // Things that don't have types
@@ -73,7 +76,6 @@ export const
73
76
  FormAutosuggestOption: any,
74
77
  InputGroup: any;
75
78
  // from './Form';
76
- export const IconButton: any, IconButtonWithTooltip: any; // from './IconButton';
77
79
  export const IconButtonToggle: any; // from './IconButtonToggle';
78
80
  export const Input: any; // from './Input';
79
81
  export const InputSelect: any; // from './InputSelect';
@@ -106,7 +108,6 @@ export const
106
108
  NavLink: any;
107
109
  // from './Nav';
108
110
  export const Navbar: any, NavbarBrand: any, NAVBAR_LABEL: string; // from './Navbar';
109
- export const Overlay: any, OverlayTrigger: any; // from './Overlay';
110
111
  export const PageBanner: any, PAGE_BANNER_DISMISS_ALT_TEXT: string; // from './PageBanner';
111
112
  export const
112
113
  Pagination: any,
@@ -145,7 +146,6 @@ export const
145
146
  // from './Tabs';
146
147
  export const TextArea: any; // from './TextArea';
147
148
  export const Toast: any, TOAST_CLOSE_LABEL_TEXT: string, TOAST_DELAY: number; // from './Toast';
148
- export const Tooltip: any; // from './Tooltip';
149
149
  export const ValidationFormGroup: any; // from './ValidationFormGroup';
150
150
  export const TransitionReplace: any; // from './TransitionReplace';
151
151
  export const ValidationMessage: any; // from './ValidationMessage';