+
+ The content in this container doesn't have a max width
+
+
The content in this container won't exceed the extra large width.
diff --git a/src/Container/index.jsx b/src/Container/index.jsx
deleted file mode 100644
index a2f38de7bb..0000000000
--- a/src/Container/index.jsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React, { forwardRef } from 'react';
-import classNames from 'classnames';
-import RBContainer from 'react-bootstrap/Container';
-import PropTypes from 'prop-types';
-
-const SIZE_CLASS_NAMES = {
- xs: 'container-mw-xs',
- sm: 'container-mw-sm',
- md: 'container-mw-md',
- lg: 'container-mw-lg',
- xl: 'container-mw-xl',
-};
-
-const Container = forwardRef(({ size, children, ...props }, ref) => (
-
- {children}
-
-));
-
-Container.propTypes = {
- ...RBContainer.propTypes,
- /** Override the base element */
- as: PropTypes.elementType,
- /** Specifies the contents of the container */
- children: PropTypes.node,
- /** Fill all available space at any breakpoint */
- fluid: PropTypes.bool,
- /** Set the maximum width for the container */
- size: PropTypes.oneOf(Object.keys(SIZE_CLASS_NAMES)),
- /** Overrides underlying component base CSS class name */
- bsPrefix: PropTypes.string,
-};
-
-Container.defaultProps = {
- as: 'div',
- children: undefined,
- fluid: true,
- size: undefined,
- bsPrefix: 'container',
-};
-
-export default Container;
diff --git a/src/Container/index.tsx b/src/Container/index.tsx
new file mode 100644
index 0000000000..ea95d58867
--- /dev/null
+++ b/src/Container/index.tsx
@@ -0,0 +1,64 @@
+/* eslint-disable react/require-default-props */
+import React from 'react';
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import RBContainer, { type ContainerProps as RBContainerProps } from 'react-bootstrap/Container';
+
+import type { ComponentWithAsProp } from '../utils/types/bootstrap';
+
+enum ContainerSizeClass {
+ xs = 'container-mw-xs',
+ sm = 'container-mw-sm',
+ md = 'container-mw-md',
+ lg = 'container-mw-lg',
+ xl = 'container-mw-xl',
+}
+
+export type ContainerSize = keyof typeof ContainerSizeClass;
+
+interface ContainerProps extends RBContainerProps {
+ size?: ContainerSize;
+}
+
+type ContainerType = ComponentWithAsProp<'div', ContainerProps>;
+
+const Container: ContainerType = React.forwardRef
(({
+ size,
+ children,
+ ...props
+}, ref) => (
+
+ {children}
+
+));
+
+Container.propTypes = {
+ ...RBContainer.propTypes,
+ /** Override the base element */
+ as: PropTypes.elementType,
+ /** Specifies the contents of the container */
+ children: PropTypes.node,
+ /** Fill all available space at any breakpoint */
+ fluid: PropTypes.bool,
+ /** Set the maximum width for the container. Omiting the prop will remove the max-width */
+ size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
+ /** Overrides underlying component base CSS class name */
+ bsPrefix: PropTypes.string,
+};
+
+Container.defaultProps = {
+ as: 'div',
+ children: undefined,
+ fluid: true,
+ size: undefined,
+ bsPrefix: 'container',
+};
+
+export default Container;
diff --git a/src/DataTable/selection/tests/ControlledSelectHeader.test.jsx b/src/DataTable/selection/tests/ControlledSelectHeader.test.jsx
index 732c3cdb43..2af2b283ba 100644
--- a/src/DataTable/selection/tests/ControlledSelectHeader.test.jsx
+++ b/src/DataTable/selection/tests/ControlledSelectHeader.test.jsx
@@ -8,6 +8,13 @@ import DataTableContext from '../../DataTableContext';
import * as selectActions from '../data/actions';
import { getRowIds } from '../data/helpers';
+function DataTableContextChild() {
+ const contextValue = useContext(DataTableContext);
+ return (
+
+ );
+}
+
// eslint-disable-next-line react/prop-types
function ControlledSelectHeaderWrapper({ tableProps, selectProps, ...rest }) {
return (
@@ -18,13 +25,6 @@ function ControlledSelectHeaderWrapper({ tableProps, selectProps, ...rest }) {
);
}
-function DataTableContextChild() {
- const contextValue = useContext(DataTableContext);
- return (
-
- );
-}
-
const mockToggleAllPageRowsSelectedProps = jest.fn();
const rows = [{ id: 1 }, { id: 2 }];
const tableProps = {
diff --git a/src/Hyperlink/Hyperlink.test.jsx b/src/Hyperlink/Hyperlink.test.tsx
similarity index 74%
rename from src/Hyperlink/Hyperlink.test.jsx
rename to src/Hyperlink/Hyperlink.test.tsx
index 2d5ffd3c5e..3982cc6fa6 100644
--- a/src/Hyperlink/Hyperlink.test.jsx
+++ b/src/Hyperlink/Hyperlink.test.tsx
@@ -4,30 +4,34 @@ import userEvent from '@testing-library/user-event';
import Hyperlink from '.';
-const content = 'content';
const destination = 'destination';
+const content = 'content';
const onClick = jest.fn();
const props = {
- content,
destination,
onClick,
};
const externalLinkAlternativeText = 'externalLinkAlternativeText';
const externalLinkTitle = 'externalLinkTitle';
const externalLinkProps = {
- target: '_blank',
+ target: '_blank' as const,
externalLinkAlternativeText,
externalLinkTitle,
...props,
};
describe('correct rendering', () => {
+ beforeEach(() => {
+ onClick.mockClear();
+ });
+
it('renders Hyperlink', async () => {
- const { getByRole } = render();
+ const { getByRole } = render({content});
const wrapper = getByRole('link');
expect(wrapper).toBeInTheDocument();
expect(wrapper).toHaveClass('pgn__hyperlink');
+ expect(wrapper).toHaveClass('standalone-link');
expect(wrapper).toHaveTextContent(content);
expect(wrapper).toHaveAttribute('href', destination);
expect(wrapper).toHaveAttribute('target', '_self');
@@ -36,8 +40,17 @@ describe('correct rendering', () => {
expect(onClick).toHaveBeenCalledTimes(1);
});
+ it('renders an underlined Hyperlink', async () => {
+ const { getByRole } = render({content});
+ const wrapper = getByRole('link');
+ expect(wrapper).toBeInTheDocument();
+ expect(wrapper).toHaveClass('pgn__hyperlink');
+ expect(wrapper).not.toHaveClass('standalone-link');
+ expect(wrapper).toHaveClass('inline-link');
+ });
+
it('renders external Hyperlink', () => {
- const { getByRole, getByTestId } = render();
+ const { getByRole, getByTestId } = render({content});
const wrapper = getByRole('link');
const icon = getByTestId('hyperlink-icon');
const iconSvg = icon.querySelector('svg');
@@ -53,18 +66,16 @@ describe('correct rendering', () => {
describe('security', () => {
it('prevents reverse tabnabbing for links with target="_blank"', () => {
- const { getByRole } = render();
+ const { getByRole } = render({content});
const wrapper = getByRole('link');
expect(wrapper).toHaveAttribute('rel', 'noopener noreferrer');
});
});
describe('event handlers are triggered correctly', () => {
- let spy;
- beforeEach(() => { spy = jest.fn(); });
-
it('should fire onClick', async () => {
- const { getByRole } = render();
+ const spy = jest.fn();
+ const { getByRole } = render({content});
const wrapper = getByRole('link');
expect(spy).toHaveBeenCalledTimes(0);
await userEvent.click(wrapper);
diff --git a/src/Hyperlink/index.jsx b/src/Hyperlink/index.tsx
similarity index 63%
rename from src/Hyperlink/index.jsx
rename to src/Hyperlink/index.tsx
index 7c4a61f882..5229f73f8f 100644
--- a/src/Hyperlink/index.jsx
+++ b/src/Hyperlink/index.tsx
@@ -1,29 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import isRequiredIf from 'react-proptype-conditional-require';
import { Launch } from '../../icons';
import Icon from '../Icon';
-import withDeprecatedProps, { DeprTypes } from '../withDeprecatedProps';
-
export const HYPER_LINK_EXTERNAL_LINK_ALT_TEXT = 'in a new tab';
export const HYPER_LINK_EXTERNAL_LINK_TITLE = 'Opens in a new tab';
-const Hyperlink = React.forwardRef((props, ref) => {
- const {
- className,
- destination,
- children,
- target,
- onClick,
- externalLinkAlternativeText,
- externalLinkTitle,
- variant,
- isInline,
- showLaunchIcon,
- ...attrs
- } = props;
+interface Props extends Omit, 'href' | 'target'> {
+ /** specifies the URL */
+ destination: string;
+ /** Content of the hyperlink */
+ children: React.ReactNode;
+ /** Custom class names for the hyperlink */
+ className?: string;
+ /** Alt text for the icon indicating that this link opens in a new tab, if target="_blank". e.g. _("in a new tab") */
+ externalLinkAlternativeText?: string;
+ /** Tooltip text for the "opens in new tab" icon, if target="_blank". e.g. _("Opens in a new tab"). */
+ externalLinkTitle?: string;
+ /** type of hyperlink */
+ variant?: 'default' | 'muted' | 'brand';
+ /** Display the link with an underline. By default, it is only underlined on hover. */
+ isInline?: boolean;
+ /** specify if we need to show launch Icon. By default, it will be visible. */
+ showLaunchIcon?: boolean;
+ target?: '_blank' | '_self';
+}
+
+const Hyperlink = React.forwardRef(({
+ className,
+ destination,
+ children,
+ target,
+ onClick,
+ externalLinkAlternativeText,
+ externalLinkTitle,
+ variant,
+ isInline,
+ showLaunchIcon,
+ ...attrs
+}, ref) => {
let externalLinkIcon;
if (target === '_blank') {
@@ -105,32 +121,20 @@ Hyperlink.propTypes = {
* loaded into the same browsing context as the current one.
* If the target is `_blank` (opening a new window) `rel='noopener'` will be added to the anchor tag to prevent
* any potential [reverse tabnabbing attack](https://www.owasp.org/index.php/Reverse_Tabnabbing).
- */
- target: PropTypes.string,
+ */
+ target: PropTypes.oneOf(['_blank', '_self']),
/** specifies the callback function when the link is clicked */
onClick: PropTypes.func,
- /** specifies the text for links with a `_blank` target (which loads the URL in a new browsing context). */
- externalLinkAlternativeText: isRequiredIf(
- PropTypes.string,
- props => props.target === '_blank',
- ),
- /** specifies the title for links with a `_blank` target (which loads the URL in a new browsing context). */
- externalLinkTitle: isRequiredIf(
- PropTypes.string,
- props => props.target === '_blank',
- ),
+ /** Alt text for the icon indicating that this link opens in a new tab, if target="_blank". e.g. _("in a new tab") */
+ externalLinkAlternativeText: PropTypes.string,
+ /** Tooltip text for the "opens in new tab" icon, if target="_blank". e.g. _("Opens in a new tab"). */
+ externalLinkTitle: PropTypes.string,
/** type of hyperlink */
variant: PropTypes.oneOf(['default', 'muted', 'brand']),
- /** specify the link style. By default, it will be underlined. */
+ /** Display the link with an underline. By default, it is only underlined on hover. */
isInline: PropTypes.bool,
/** specify if we need to show launch Icon. By default, it will be visible. */
showLaunchIcon: PropTypes.bool,
};
-export default withDeprecatedProps(Hyperlink, 'Hyperlink', {
- /** specifies the text or element that a URL should be associated with */
- content: {
- deprType: DeprTypes.MOVED,
- newName: 'children',
- },
-});
+export default Hyperlink;
diff --git a/src/Icon/index.d.ts b/src/Icon/index.d.ts
index 45505bb49a..b9d6f5d746 100644
--- a/src/Icon/index.d.ts
+++ b/src/Icon/index.d.ts
@@ -1,13 +1,15 @@
import React from 'react';
export interface IconProps extends React.ComponentPropsWithoutRef<'span'> {
- src?: React.ReactElement | Function;
+ // Note: React.ComponentType is what we want here. React.ElementType would allow some element type strings like "div",
+ // but we only want to allow components like 'Add' (a specific icon component function/class)
+ src?: React.ComponentType;
svgAttrs?: {
'aria-label'?: string;
'aria-labelledby'?: string;
};
id?: string | null;
- size?: 'xs' | 'sm' | 'md' | 'lg';
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'inline';
className?: string | string[];
hidden?: boolean;
screenReaderText?: React.ReactNode;
diff --git a/src/Icon/index.jsx b/src/Icon/index.jsx
index 6f0a7a3cf3..89403430f2 100644
--- a/src/Icon/index.jsx
+++ b/src/Icon/index.jsx
@@ -74,7 +74,7 @@ Icon.propTypes = {
* An icon component to render.
* Example import of a Paragon icon component: `import { Check } from '@openedx/paragon/icons';`
*/
- src: PropTypes.oneOfType([PropTypes.element, PropTypes.elementType]),
+ src: PropTypes.elementType,
/** HTML element attributes to pass through to the underlying svg element */
svgAttrs: PropTypes.shape({
'aria-label': PropTypes.string,
diff --git a/src/IconButton/IconButton.test.jsx b/src/IconButton/IconButton.test.tsx
similarity index 82%
rename from src/IconButton/IconButton.test.jsx
rename to src/IconButton/IconButton.test.tsx
index 9f098002ea..8e4b2e72c4 100644
--- a/src/IconButton/IconButton.test.jsx
+++ b/src/IconButton/IconButton.test.tsx
@@ -11,21 +11,27 @@ describe('', () => {
const alt = 'alternative';
const iconAs = Icon;
const src = InfoOutline;
- const variant = 'secondary';
+ const variant = 'secondary' as const;
const props = {
alt,
src,
iconAs,
variant,
};
- const iconParams = {
+ const deprecatedFontAwesomeExample = {
prefix: 'pgn',
iconName: 'InfoOutlineIcon',
icon: [InfoOutline],
};
it('renders with required props', () => {
const tree = renderer.create((
-
+
+ )).toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+ it('renders with deprecated props', () => {
+ const tree = renderer.create((
+
)).toJSON();
expect(tree).toMatchSnapshot();
});
@@ -94,4 +100,19 @@ describe('', () => {
expect(spy2).toHaveBeenCalledTimes(1);
});
});
+
+ describe('', () => {
+ it('renders with required props', () => {
+ const tree = renderer.create((
+
+ )).toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+ });
});
diff --git a/src/IconButton/__snapshots__/IconButton.test.jsx.snap b/src/IconButton/__snapshots__/IconButton.test.jsx.snap
deleted file mode 100644
index b30da46240..0000000000
--- a/src/IconButton/__snapshots__/IconButton.test.jsx.snap
+++ /dev/null
@@ -1,43 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[` renders with required props 1`] = `
-
-`;
diff --git a/src/IconButton/__snapshots__/IconButton.test.tsx.snap b/src/IconButton/__snapshots__/IconButton.test.tsx.snap
new file mode 100644
index 0000000000..dd82977c10
--- /dev/null
+++ b/src/IconButton/__snapshots__/IconButton.test.tsx.snap
@@ -0,0 +1,112 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` renders with required props 1`] = `
+
+`;
+
+exports[` renders with deprecated props 1`] = `
+
+`;
+
+exports[` renders with required props 1`] = `
+
+`;
diff --git a/src/IconButton/index.jsx b/src/IconButton/index.tsx
similarity index 59%
rename from src/IconButton/index.jsx
rename to src/IconButton/index.tsx
index bf25709577..5cc805c8a2 100644
--- a/src/IconButton/index.jsx
+++ b/src/IconButton/index.tsx
@@ -1,12 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { type Placement } from 'react-bootstrap/Overlay';
+
import { OverlayTrigger } from '../Overlay';
import Tooltip from '../Tooltip';
+import Icon from '../Icon';
-const IconButton = React.forwardRef(({
+interface Props extends React.HTMLAttributes {
+ iconAs?: typeof Icon | typeof FontAwesomeIcon,
+ /** Additional CSS class[es] to apply to this button */
+ className?: string;
+ /** Alt text for your icon. For best practice, avoid using alt text to describe
+ * the image in the `IconButton`. Instead, we recommend describing the function
+ * of the button. */
+ alt: string;
+ /** Changes icon styles for dark background */
+ invertColors?: boolean;
+ /** An icon component to render. Example import of a Paragon icon component:
+ * `import { Check } from '@openedx/paragon/icons';`
+ * */
+ // Note: React.ComponentType is what we want here. React.ElementType would allow some element type strings like "div",
+ // but we only want to allow components like 'Add' (a specific icon component function/class)
+ src?: React.ComponentType;
+ /** Extra class names that will be added to the icon */
+ iconClassNames?: string;
+ /** Click handler for the button */
+ onClick?: React.MouseEventHandler;
+ /** whether to show the `IconButton` in an active state, whose styling is distinct from default state */
+ isActive?: boolean;
+ /** @deprecated Using FontAwesome icons is deprecated. Instead, pass iconAs={Icon} src={...} */
+ icon?: { prefix?: string; iconName?: string, icon?: any[] },
+ /** Type of button (uses Bootstrap options) */
+ variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'light' | 'dark' | 'black' | 'brand';
+ /** size of button to render */
+ size?: 'sm' | 'md' | 'inline';
+ /** no children */
+ children?: never;
+}
+
+const IconButton = React.forwardRef(({
className,
alt,
invertColors,
@@ -18,6 +52,7 @@ const IconButton = React.forwardRef(({
variant,
iconAs,
isActive,
+ children, // unused, just here because we don't want it to be part of 'attrs'
...attrs
}, ref) => {
const invert = invertColors ? 'inverse-' : '';
@@ -50,7 +85,7 @@ const IconButton = React.forwardRef(({
@@ -60,7 +95,7 @@ const IconButton = React.forwardRef(({
IconButton.defaultProps = {
iconAs: undefined,
- src: null,
+ src: undefined,
icon: undefined,
iconClassNames: undefined,
className: undefined,
@@ -69,6 +104,7 @@ IconButton.defaultProps = {
size: 'md',
onClick: () => {},
isActive: false,
+ children: undefined,
};
IconButton.propTypes = {
@@ -76,11 +112,11 @@ IconButton.propTypes = {
className: PropTypes.string,
/** Component that renders the icon, currently defaults to `FontAwesomeIcon`,
* but is going to be deprecated soon, please use Paragon's icons instead. */
- iconAs: PropTypes.elementType,
+ iconAs: PropTypes.elementType as any,
/** An icon component to render. Example import of a Paragon icon component:
- * `import { Check } from '@openedx/paragon/dist/icon';`
+ * `import { Check } from '@openedx/paragon/icons';`
* */
- src: PropTypes.oneOfType([PropTypes.element, PropTypes.elementType]),
+ src: PropTypes.elementType as any,
/** Alt text for your icon. For best practice, avoid using alt text to describe
* the image in the `IconButton`. Instead, we recommend describing the function
* of the button. */
@@ -93,7 +129,7 @@ IconButton.propTypes = {
iconName: PropTypes.string,
// eslint-disable-next-line react/forbid-prop-types
icon: PropTypes.array,
- }),
+ }) as any,
/** Extra class names that will be added to the icon */
iconClassNames: PropTypes.string,
/** Click handler for the button */
@@ -106,38 +142,40 @@ IconButton.propTypes = {
isActive: PropTypes.bool,
};
+interface PropsWithTooltip extends Props {
+ /** choose from https://popper.js.org/docs/v2/constructors/#options */
+ tooltipPlacement: Placement,
+ /** any content to pass to tooltip content area */
+ tooltipContent: React.ReactNode,
+}
+
/**
- *
- * @param { object } args Arguments
- * @param { string } args.tooltipPlacement choose from https://popper.js.org/docs/v2/constructors/#options
- * @param { React.Component } args.tooltipContent any content to pass to tooltip content area
- * @returns { IconButton } a button wrapped in overlaytrigger
+ * An icon button wrapped in overlaytrigger to display a tooltip.
*/
function IconButtonWithTooltip({
- tooltipPlacement, tooltipContent, variant, invertColors, ...props
-}) {
- const invert = invertColors ? 'inverse-' : '';
+ tooltipPlacement, tooltipContent, ...props
+}: PropsWithTooltip) {
+ const invert = props.invertColors ? 'inverse-' : '';
return (
{tooltipContent}
)}
>
-
+
);
}
IconButtonWithTooltip.defaultProps = {
+ ...IconButton.defaultProps,
tooltipPlacement: 'top',
- variant: 'primary',
- invertColors: false,
};
IconButtonWithTooltip.propTypes = {
@@ -151,7 +189,9 @@ IconButtonWithTooltip.propTypes = {
invertColors: PropTypes.bool,
};
-IconButton.IconButtonWithTooltip = IconButtonWithTooltip;
+(IconButton as any).IconButtonWithTooltip = IconButtonWithTooltip;
-export default IconButton;
+export default IconButton as typeof IconButton & {
+ IconButtonWithTooltip: typeof IconButtonWithTooltip,
+};
export { IconButtonWithTooltip };
diff --git a/src/Menu/SelectMenu.jsx b/src/Menu/SelectMenu.jsx
index 5bee47ba7b..0e6feb9506 100644
--- a/src/Menu/SelectMenu.jsx
+++ b/src/Menu/SelectMenu.jsx
@@ -15,6 +15,7 @@ function SelectMenu({
children,
className,
variant,
+ disabled,
...props
}) {
const [triggerTarget, setTriggerTarget] = useState(null);
@@ -89,6 +90,7 @@ function SelectMenu({
variant={variant}
iconAfter={ExpandMore}
onClick={open}
+ disabled={disabled}
>
{selected !== undefined && children[selected] ? children[selected].props.children : defaultMessage}
@@ -131,12 +133,15 @@ SelectMenu.propTypes = {
className: PropTypes.string,
/** Specifies variant to use. */
variant: PropTypes.string,
+ /** Specifies if the `SelectMenu` is disabled. */
+ disabled: PropTypes.bool,
};
SelectMenu.defaultProps = {
defaultMessage: SELECT_MENU_DEFAULT_MESSAGE,
className: undefined,
variant: 'outline-primary',
+ disabled: false,
};
const SelectMenuWithDeprecatedProp = withDeprecatedProps(SelectMenu, 'SelectMenu', {
diff --git a/src/Menu/SelectMenu.test.jsx b/src/Menu/SelectMenu.test.jsx
index 9d542da4e2..49d0f544a5 100644
--- a/src/Menu/SelectMenu.test.jsx
+++ b/src/Menu/SelectMenu.test.jsx
@@ -58,6 +58,12 @@ describe('correct rendering', () => {
const button = screen.getByRole('button');
expect(button).toHaveClass('btn-brand');
});
+
+ it('renders as disabled', () => {
+ render(DefaultSelectMenu({ disabled: true }));
+ const button = screen.getByRole('button');
+ expect(button).toBeDisabled();
+ });
});
describe('mouse behavior & keyboard behavior', () => {
diff --git a/src/Menu/select-menu.md b/src/Menu/select-menu.md
index 0aa7ad33fc..e11a6116f2 100644
--- a/src/Menu/select-menu.md
+++ b/src/Menu/select-menu.md
@@ -56,3 +56,11 @@ The ``Modal`` brings focus to the first menu element upon the click of the trigg
```
+
+## Disabled
+
+```jsx live
+
+
+
+```
diff --git a/src/Modal/ModalContext.jsx b/src/Modal/ModalContext.tsx
similarity index 50%
rename from src/Modal/ModalContext.jsx
rename to src/Modal/ModalContext.tsx
index f374c3ee6f..a9bdd3b702 100644
--- a/src/Modal/ModalContext.jsx
+++ b/src/Modal/ModalContext.tsx
@@ -1,14 +1,29 @@
import React, { useMemo } from 'react';
-import PropTypes from 'prop-types';
-const ModalContext = React.createContext({
+interface ContextData {
+ onClose: () => void;
+ isOpen: boolean;
+ isBlocking: boolean;
+}
+
+const ModalContext = React.createContext({
onClose: () => {},
+ isOpen: false,
+ isBlocking: false,
});
function ModalContextProvider({
- onClose, isOpen, isBlocking, children,
+ onClose,
+ isOpen,
+ isBlocking = false,
+ children = null,
+}: {
+ onClose: () => void;
+ isOpen: boolean;
+ isBlocking?: boolean;
+ children?: React.ReactNode;
}) {
- const modalContextValue = useMemo(
+ const modalContextValue = useMemo(
() => ({ onClose, isOpen, isBlocking }),
[onClose, isOpen, isBlocking],
);
@@ -20,17 +35,5 @@ function ModalContextProvider({
);
}
-ModalContextProvider.propTypes = {
- children: PropTypes.node,
- onClose: PropTypes.func.isRequired,
- isBlocking: PropTypes.bool,
- isOpen: PropTypes.bool.isRequired,
-};
-
-ModalContextProvider.defaultProps = {
- children: null,
- isBlocking: false,
-};
-
export { ModalContextProvider };
export default ModalContext;
diff --git a/src/Modal/ModalDialog.jsx b/src/Modal/ModalDialog.tsx
similarity index 64%
rename from src/Modal/ModalDialog.jsx
rename to src/Modal/ModalDialog.tsx
index 6814b3c22e..6ad659ca06 100644
--- a/src/Modal/ModalDialog.jsx
+++ b/src/Modal/ModalDialog.tsx
@@ -3,11 +3,16 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useMediaQuery } from 'react-responsive';
import ModalLayer from './ModalLayer';
+// @ts-ignore for now - this needs to be converted to TypeScript
import ModalCloseButton from './ModalCloseButton';
import ModalDialogHeader from './ModalDialogHeader';
+// @ts-ignore for now - this needs to be converted to TypeScript
import ModalDialogTitle from './ModalDialogTitle';
+// @ts-ignore for now - this needs to be converted to TypeScript
import ModalDialogFooter from './ModalDialogFooter';
+// @ts-ignore for now - this needs to be converted to TypeScript
import ModalDialogBody from './ModalDialogBody';
+// @ts-ignore for now - this needs to be converted to TypeScript
import ModalDialogHero from './ModalDialogHero';
import Icon from '../Icon';
@@ -16,22 +21,57 @@ import { Close } from '../../icons';
export const MODAL_DIALOG_CLOSE_LABEL = 'Close';
+interface Props {
+ /** Specifies the content of the dialog */
+ children: React.ReactNode;
+ /** The aria-label of the dialog */
+ title: string;
+ /** A callback to close the modal dialog, e.g. when Escape is pressed */
+ onClose: () => void;
+ /** Is the modal dialog open or closed? */
+ isOpen?: boolean;
+ /** The close 'x' icon button in the top right of the dialog box */
+ hasCloseButton?: boolean;
+ /** Size determines the maximum width of the dialog box */
+ size?: 'sm' | 'md' | 'lg' | 'xl' | 'fullscreen';
+ /** The visual style of the dialog box */
+ variant?: 'default' | 'warning' | 'danger' | 'success' | 'dark';
+ /** The label supplied to the close icon button if one is rendered */
+ closeLabel?: string;
+ /** Specifies class name to append to the base element */
+ className?: string;
+ /**
+ * Determines where a scrollbar should appear if a modal is too large for the
+ * viewport. When false, the ``ModalDialog``. Body receives a scrollbar, when true
+ * the browser window itself receives the scrollbar.
+ */
+ isFullscreenScroll?: boolean;
+ /** To show full screen view on mobile screens */
+ isFullscreenOnMobile?: boolean;
+ /** Prevent clicking on the backdrop or pressing Esc to close the modal */
+ isBlocking?: boolean;
+ /** Specifies the z-index of the modal */
+ zIndex?: number;
+ /** Specifies whether overflow is visible in the modal */
+ isOverflowVisible?: boolean;
+}
+
function ModalDialog({
children,
title,
- isOpen,
+ isOpen = false,
onClose,
- size,
- variant,
- hasCloseButton,
- closeLabel,
- isFullscreenScroll,
+ size = 'md',
+ variant = 'default',
+ hasCloseButton = true,
+ closeLabel = MODAL_DIALOG_CLOSE_LABEL,
+ isFullscreenScroll = false,
className,
- isFullscreenOnMobile,
- isBlocking,
+ isFullscreenOnMobile = false,
+ isBlocking = false,
zIndex,
- isOverflowVisible,
-}) {
+ isOverflowVisible = true,
+}: Props) {
const isMobile = useMediaQuery({ query: '(max-width: 767.98px)' });
const showFullScreen = (isFullscreenOnMobile && isMobile);
return (
@@ -126,20 +166,6 @@ ModalDialog.propTypes = {
isOverflowVisible: PropTypes.bool,
};
-ModalDialog.defaultProps = {
- isOpen: false,
- hasCloseButton: true,
- size: 'md',
- variant: 'default',
- closeLabel: MODAL_DIALOG_CLOSE_LABEL,
- className: undefined,
- isFullscreenScroll: false,
- isFullscreenOnMobile: false,
- isBlocking: false,
- zIndex: undefined,
- isOverflowVisible: true,
-};
-
ModalDialog.Header = ModalDialogHeader;
ModalDialog.Title = ModalDialogTitle;
ModalDialog.Footer = ModalDialogFooter;
diff --git a/src/Modal/ModalDialogHeader.jsx b/src/Modal/ModalDialogHeader.tsx
similarity index 57%
rename from src/Modal/ModalDialogHeader.jsx
rename to src/Modal/ModalDialogHeader.tsx
index 0a0ff4ca9a..9299db8295 100644
--- a/src/Modal/ModalDialogHeader.jsx
+++ b/src/Modal/ModalDialogHeader.tsx
@@ -1,21 +1,32 @@
+/* eslint-disable react/require-default-props */
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
+import type { ComponentWithAsProp } from '../utils/types/bootstrap';
-function ModalDialogHeader({
- as,
+export interface Props {
+ as?: string;
+ children: React.ReactNode;
+ className?: string;
+}
+
+type HeaderType = ComponentWithAsProp<'div', Props>;
+
+const ModalDialogHeader: HeaderType = React.forwardRef(({
+ as = 'div',
children,
...props
-}) {
- return React.createElement(
+}, ref) => (
+ React.createElement(
as,
{
...props,
+ ref,
className: classNames('pgn__modal-header', props.className),
},
children,
- );
-}
+ )
+));
ModalDialogHeader.propTypes = {
/** Specifies the base element */
@@ -26,9 +37,4 @@ ModalDialogHeader.propTypes = {
className: PropTypes.string,
};
-ModalDialogHeader.defaultProps = {
- as: 'div',
- className: undefined,
-};
-
export default ModalDialogHeader;
diff --git a/src/Modal/ModalLayer.jsx b/src/Modal/ModalLayer.tsx
similarity index 79%
rename from src/Modal/ModalLayer.jsx
rename to src/Modal/ModalLayer.tsx
index 1cc38cf0f4..dc74923403 100644
--- a/src/Modal/ModalLayer.jsx
+++ b/src/Modal/ModalLayer.tsx
@@ -6,7 +6,7 @@ import Portal from './Portal';
import { ModalContextProvider } from './ModalContext';
// istanbul ignore next
-function ModalBackdrop({ onClick }) {
+function ModalBackdrop({ onClick }: { onClick?: () => void }) {
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
{children}
;
}
@@ -35,9 +31,18 @@ ModalContentContainer.propTypes = {
children: PropTypes.node,
};
-ModalContentContainer.defaultProps = {
- children: null,
-};
+interface Props {
+ /** Specifies the contents of the modal */
+ children: React.ReactNode;
+ /** A callback function for when the modal is dismissed */
+ onClose: () => void;
+ /** Is the modal dialog open or closed */
+ isOpen: boolean;
+ /** Prevent clicking on the backdrop or pressing Esc to close the modal */
+ isBlocking?: boolean;
+ /** Specifies the z-index of the modal */
+ zIndex?: number;
+}
/**
* The ModalLayer should be used for any component that wishes to engage the user
@@ -46,8 +51,8 @@ ModalContentContainer.defaultProps = {
* component is that if a modal object is visible then it is "enabled"
*/
function ModalLayer({
- children, onClose, isOpen, isBlocking, zIndex,
-}) {
+ children, onClose, isOpen, isBlocking = false, zIndex,
+}: Props) {
useEffect(() => {
if (isOpen) {
document.body.classList.add('pgn__hidden-scroll-padding-right');
@@ -63,7 +68,7 @@ function ModalLayer({
return null;
}
- const handleClose = isBlocking ? null : onClose;
+ const handleClose = isBlocking ? undefined : onClose;
return (
@@ -102,10 +107,5 @@ ModalLayer.propTypes = {
zIndex: PropTypes.number,
};
-ModalLayer.defaultProps = {
- isBlocking: false,
- zIndex: undefined,
-};
-
export { ModalBackdrop, ModalContentContainer };
export default ModalLayer;
diff --git a/src/Modal/ModalPopup.jsx b/src/Modal/ModalPopup.jsx
index 52bc3adb04..e6c36fd45e 100644
--- a/src/Modal/ModalPopup.jsx
+++ b/src/Modal/ModalPopup.jsx
@@ -34,6 +34,14 @@ function ModalPopup({
},
];
+ const handleOnClickOutside = (e) => {
+ if (e.type === 'touchstart') {
+ return;
+ }
+
+ onClose();
+ };
+
return (
@@ -47,7 +55,7 @@ function ModalPopup({
scrollLock={false}
enabled={isOpen}
onEscapeKey={onClose}
- onClickOutside={onClose}
+ onClickOutside={handleOnClickOutside}
>
{isOpen && (
diff --git a/src/Modal/Portal.jsx b/src/Modal/Portal.tsx
similarity index 80%
rename from src/Modal/Portal.jsx
rename to src/Modal/Portal.tsx
index cf30fa1c30..d06dc2aa5e 100644
--- a/src/Modal/Portal.jsx
+++ b/src/Modal/Portal.tsx
@@ -1,9 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom';
-import PropTypes from 'prop-types';
-class Portal extends React.Component {
- constructor(props) {
+interface Props {
+ children: React.ReactNode;
+}
+
+class Portal extends React.Component
{
+ private rootName: string;
+
+ private rootElement: HTMLElement | null;
+
+ constructor(props: Props) {
super(props);
this.rootName = 'paragon-portal-root';
// istanbul ignore if
@@ -31,8 +38,4 @@ class Portal extends React.Component {
}
}
-Portal.propTypes = {
- children: PropTypes.node.isRequired,
-};
-
export default Portal;
diff --git a/src/Modal/_ModalDialog.scss b/src/Modal/_ModalDialog.scss
index 65ea4c519b..31ab661e2a 100644
--- a/src/Modal/_ModalDialog.scss
+++ b/src/Modal/_ModalDialog.scss
@@ -76,6 +76,10 @@
border-top: solid 1px $light;
padding-top: $modal-footer-padding-y;
}
+
+ .pgn__modal-header {
+ border-radius: 0;
+ }
}
// Made specific due to a selector in Modal.scss
diff --git a/src/Modal/tests/ModalDialog.test.jsx b/src/Modal/tests/ModalDialog.test.tsx
similarity index 80%
rename from src/Modal/tests/ModalDialog.test.jsx
rename to src/Modal/tests/ModalDialog.test.tsx
index 6ca06dbdcc..93759521eb 100644
--- a/src/Modal/tests/ModalDialog.test.jsx
+++ b/src/Modal/tests/ModalDialog.test.tsx
@@ -3,16 +3,6 @@ import { render, screen } from '@testing-library/react';
import ModalDialog from '../ModalDialog';
-jest.mock('../ModalLayer', () => function ModalLayerMock(props) {
- // eslint-disable-next-line react/prop-types
- const { children, ...otherProps } = props;
- return (
-
- {children}
-
- );
-});
-
describe('ModalDialog', () => {
it('renders a dialog with aria-label and content', () => {
const onClose = jest.fn();
@@ -45,6 +35,22 @@ describe('ModalDialog', () => {
expect(dialogNode).toHaveAttribute('aria-label', 'My dialog');
expect(screen.getByText('The content')).toBeInTheDocument();
});
+
+ it('is hidden by default', () => {
+ const onClose = jest.fn();
+ render(
+
+ The title
+ The hidden content
+ Cancel
+ ,
+ );
+
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+ });
});
describe('ModalDialog with Hero', () => {
diff --git a/src/Modal/tests/ModalLayer.test.jsx b/src/Modal/tests/ModalLayer.test.tsx
similarity index 90%
rename from src/Modal/tests/ModalLayer.test.jsx
rename to src/Modal/tests/ModalLayer.test.tsx
index bc93b66013..f316086cea 100644
--- a/src/Modal/tests/ModalLayer.test.jsx
+++ b/src/Modal/tests/ModalLayer.test.tsx
@@ -6,12 +6,11 @@ import userEvent from '@testing-library/user-event';
import ModalLayer from '../ModalLayer';
/* eslint-disable react/prop-types */
-jest.mock('../Portal', () => function PortalMock(props) {
+jest.mock('../Portal', () => function PortalMock(props: any) {
const { children, ...otherProps } = props;
return (
-
- {children}
-
+ // @ts-ignore this fake element. (Property 'paragon-portal' does not exist on type 'JSX.IntrinsicElements')
+ {children}
);
});
@@ -19,6 +18,7 @@ jest.mock('react-focus-on', () => ({
FocusOn: jest.fn().mockImplementation((props) => {
const { children, ...otherProps } = props;
return (
+ // @ts-ignore this fake element. (Property 'focus-on' does not exist on type 'JSX.IntrinsicElements')
{children}
);
}),
@@ -117,7 +117,7 @@ describe('', () => {
);
expect(FocusOn).toHaveBeenCalledWith(
expect.objectContaining({
- onEscapeKey: null,
+ onEscapeKey: undefined,
}),
// note: this 2nd function argument represents the
// `refOrContext` (in this case, the context value
diff --git a/src/Modal/tests/ModalPopupNoMock.test.jsx b/src/Modal/tests/ModalPopupNoMock.test.jsx
new file mode 100644
index 0000000000..ac7ea3a4e8
--- /dev/null
+++ b/src/Modal/tests/ModalPopupNoMock.test.jsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { fireEvent, render } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import ModalPopup from '../ModalPopup';
+
+describe('', () => {
+ const mockPositionRef = React.createRef();
+
+ describe('when isOpen', () => {
+ const isOpen = true;
+ const closeFn = jest.fn();
+
+ it('calls close on click events but not touchstart events', async () => {
+ render(
+
+ Modal Contents
+ ,
+ );
+ await fireEvent.touchStart(document.body);
+ expect(closeFn).not.toHaveBeenCalled();
+ await userEvent.click(document.body);
+ expect(closeFn).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/Modal/tests/Portal.test.jsx b/src/Modal/tests/Portal.test.tsx
similarity index 83%
rename from src/Modal/tests/Portal.test.jsx
rename to src/Modal/tests/Portal.test.tsx
index 0f3ec32d70..7cda3ac9e0 100644
--- a/src/Modal/tests/Portal.test.jsx
+++ b/src/Modal/tests/Portal.test.tsx
@@ -21,7 +21,7 @@ describe('', () => {
const portalRoot = getPortalRoot();
expect(portalRoot).not.toBeNull();
- expect(portalRoot.children[0].id).toBe('portal-content-a');
+ expect(portalRoot!.children[0].id).toBe('portal-content-a');
});
it('renders both contents in a single #paragon-portal-root div', () => {
@@ -38,7 +38,7 @@ describe('', () => {
const portalRoot = getPortalRoot();
expect(portalRoot).not.toBeNull();
- expect(portalRoot.children[0].id).toBe('portal-content-a');
- expect(portalRoot.children[1].id).toBe('portal-content-b');
+ expect(portalRoot!.children[0].id).toBe('portal-content-a');
+ expect(portalRoot!.children[1].id).toBe('portal-content-b');
});
});
diff --git a/src/Overlay/index.jsx b/src/Overlay/index.tsx
similarity index 88%
rename from src/Overlay/index.jsx
rename to src/Overlay/index.tsx
index 6e3fb7c83d..6c640f7239 100644
--- a/src/Overlay/index.jsx
+++ b/src/Overlay/index.tsx
@@ -1,10 +1,14 @@
import React from 'react';
-import BaseOverlay from 'react-bootstrap/Overlay';
-import BaseOverlayTrigger from 'react-bootstrap/OverlayTrigger';
+import BaseOverlay, { type OverlayProps, type Placement } from 'react-bootstrap/Overlay';
+import BaseOverlayTrigger, { type OverlayTriggerProps, type OverlayTriggerType } from 'react-bootstrap/OverlayTrigger';
import Fade from 'react-bootstrap/Fade';
import PropTypes from 'prop-types';
-const PLACEMENT_VARIANTS = [
+// Note: The only thing this file adds to the base component is propTypes validation.
+// As more Paragon consumers adopt TypeScript, we could consider removing almost all of this code
+// and just re-export the Overlay and OverlayTrigger components from react-bootstrap unmodified.
+
+const PLACEMENT_VARIANTS: Placement[] = [
'auto-start',
'auto',
'auto-end',
@@ -22,16 +26,16 @@ const PLACEMENT_VARIANTS = [
'left-start',
];
-const TRIGGER_VARIANTS = [
+const TRIGGER_VARIANTS: OverlayTriggerType[] = [
'hover',
'click',
'focus',
];
-function Overlay(props) {
+function Overlay(props: OverlayProps) {
return ;
}
-function OverlayTrigger(props) {
+function OverlayTrigger(props: OverlayTriggerProps) {
return (
{props.children}
diff --git a/src/Tooltip/Tooltip.test.jsx b/src/Tooltip/Tooltip.test.tsx
similarity index 100%
rename from src/Tooltip/Tooltip.test.jsx
rename to src/Tooltip/Tooltip.test.tsx
diff --git a/src/Tooltip/index.jsx b/src/Tooltip/index.tsx
similarity index 83%
rename from src/Tooltip/index.jsx
rename to src/Tooltip/index.tsx
index 9b3733112e..9d9131b459 100644
--- a/src/Tooltip/index.jsx
+++ b/src/Tooltip/index.tsx
@@ -1,9 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import BaseTooltip from 'react-bootstrap/Tooltip';
+import BaseTooltip, { type TooltipProps as BaseTooltipProps } from 'react-bootstrap/Tooltip';
+import { type Placement } from 'react-bootstrap/Overlay';
+import type { ComponentWithAsProp } from '../utils/types/bootstrap';
-const PLACEMENT_VARIANTS = [
+interface TooltipProps extends BaseTooltipProps {
+ variant?: 'light';
+}
+
+const PLACEMENT_VARIANTS: Placement[] = [
'auto-start',
'auto',
'auto-end',
@@ -21,7 +27,7 @@ const PLACEMENT_VARIANTS = [
'left-start',
];
-const Tooltip = React.forwardRef(({
+const Tooltip: ComponentWithAsProp<'div', TooltipProps> = React.forwardRef(({
children,
variant,
...props
diff --git a/src/Truncate/Truncate.test.js b/src/Truncate/utils.test.js
similarity index 100%
rename from src/Truncate/Truncate.test.js
rename to src/Truncate/utils.test.js
diff --git a/src/hooks/useArrowKeyNavigation.jsx b/src/hooks/useArrowKeyNavigation.jsx
index 69b3bb6cbc..bdfe258b2f 100644
--- a/src/hooks/useArrowKeyNavigation.jsx
+++ b/src/hooks/useArrowKeyNavigation.jsx
@@ -32,8 +32,7 @@ function handleArrowKey({ event, currentIndex, availableElements }) {
[nextElement] = availableElements;
}
- // eslint-disable-next-line no-unused-expressions
- nextElement && nextElement.focus();
+ nextElement?.focus();
event.preventDefault();
}
diff --git a/src/index.d.ts b/src/index.d.ts
index 9d71f85477..217ee12a89 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -5,9 +5,19 @@
// Things that have types
// // // // // // // // // // // // // // // // // // // // // // // // // // //
export { default as Bubble } from './Bubble';
+export { default as Button, ButtonGroup, ButtonToolbar } from './Button';
export { default as Chip, CHIP_PGN_CLASS } from './Chip';
export { default as ChipCarousel } from './ChipCarousel';
+export { default as Container, ContainerSize } from './Container';
+export { default as Hyperlink, HYPER_LINK_EXTERNAL_LINK_ALT_TEXT, HYPER_LINK_EXTERNAL_LINK_TITLE } from './Hyperlink';
export { default as Icon } from './Icon';
+export { default as IconButton, IconButtonWithTooltip } from './IconButton';
+export { default as ModalContext } from './Modal/ModalContext';
+export { default as ModalDialog, MODAL_DIALOG_CLOSE_LABEL } from './Modal/ModalDialog';
+export { default as ModalLayer } from './Modal/ModalLayer';
+export { default as Overlay, OverlayTrigger } from './Overlay';
+export { default as Portal } from './Modal/Portal';
+export { default as Tooltip } from './Tooltip';
// // // // // // // // // // // // // // // // // // // // // // // // // // //
// Things that don't have types
@@ -20,7 +30,6 @@ export const Avatar: any; // from './Avatar';
export const AvatarButton: any; // from './AvatarButton';
export const Badge: any; // from './Badge';
export const Breadcrumb: any; // from './Breadcrumb';
-export const Button: any, ButtonGroup: any, ButtonToolbar: any; // from './Button';
export const
Card: any,
CardColumns: any,
@@ -34,10 +43,11 @@ export const
export const
Carousel: any, CarouselItem: any, CAROUSEL_NEXT_LABEL_TEXT: any, CAROUSEL_PREV_LABEL_TEXT: any;
// from './Carousel';
+/** @deprecated Replaced by `Form.Checkbox`. */
export const CheckBox: any; // from './CheckBox';
+/** @deprecated Replaced by `Form.Checkbox` and `Form.CheckboxSet`. */
export const CheckBoxGroup: any; // from './CheckBoxGroup';
export const CloseButton: any; // from './CloseButton';
-export const Container: any; // from './Container';
export const Layout: any, Col: any, Row: any; // from './Layout';
export const Collapse: any; // from './Collapse';
export const Collapsible: any; // from './Collapsible';
@@ -49,6 +59,7 @@ export const
SplitButton: any;
// from './Dropdown';
export const Fade: any; // from './Fade';
+/** @deprecated */
export const Fieldset: any; // from './Fieldset';
export const
Form: any,
@@ -72,31 +83,31 @@ export const
FormAutosuggestOption: any,
InputGroup: any;
// from './Form';
-export const Hyperlink: any, HYPER_LINK_EXTERNAL_LINK_ALT_TEXT: string, HYPER_LINK_EXTERNAL_LINK_TITLE: string; // from './Hyperlink';
-export const IconButton: any, IconButtonWithTooltip: any; // from './IconButton';
export const IconButtonToggle: any; // from './IconButtonToggle';
+/** @deprecated Replaced by `Form.Control`. */
export const Input: any; // from './Input';
+/** @deprecated Replaced by `Form.Control`. */
export const InputSelect: any; // from './InputSelect';
+/** @deprecated Replaced by `Form.Control`. */
export const InputText: any; // from './InputText';
export const Image: any, Figure; // from './Image';
+/** @deprecated */
export const ListBox: any; // from './ListBox';
+/** @deprecated */
export const ListBoxOption: any; // from './ListBoxOption';
export const MailtoLink: any, MAIL_TO_LINK_EXTERNAL_LINK_ALTERNATIVE_TEXT: string, MAIL_TO_LINK_EXTERNAL_LINK_TITLE: string; // from './MailtoLink';
export const Media: any; // from './Media';
export const Menu: any; // from './Menu';
export const MenuItem: any; // from './Menu/MenuItem';
export const SelectMenu: any, SELECT_MENU_DEFAULT_MESSAGE: string; // from './Menu/SelectMenu';
+/** @deprecated Use `ModalDialog` instead. */
export const Modal: any; // from './Modal';
export const ModalCloseButton: any; // from './Modal/ModalCloseButton';
export const FullscreenModal: any, FULLSCREEN_MODAL_CLOSE_LABEL: string; // from './Modal/FullscreenModal';
export const MarketingModal: any; // from './Modal/MarketingModal';
export const StandardModal: any, STANDARD_MODAL_CLOSE_LABEL: string; // from './Modal/StandardModal';
export const AlertModal: any; // from './Modal/AlertModal';
-export const ModalLayer: any; // from './Modal/ModalLayer';
-export const ModalDialog: any, MODAL_DIALOG_CLOSE_LABEL: string; // from './Modal/ModalDialog';
export const ModalPopup: any; // from './Modal/ModalPopup';
-export const ModalContext: any; // from './Modal/ModalContext';
-export const Portal: any; // from './Modal/Portal';
export const PopperElement: any; // from './Modal/PopperElement';
export const
@@ -106,7 +117,6 @@ export const
NavLink: any;
// from './Nav';
export const Navbar: any, NavbarBrand: any, NAVBAR_LABEL: string; // from './Navbar';
-export const Overlay: any, OverlayTrigger: any; // from './Overlay';
export const PageBanner: any, PAGE_BANNER_DISMISS_ALT_TEXT: string; // from './PageBanner';
export const
Pagination: any,
@@ -121,6 +131,7 @@ export const
export const Popover: any, PopoverTitle: any, PopoverContent: any; // from './Popover';
export const ProgressBar: any; // from './ProgressBar';
export const ProductTour: any; // from './ProductTour';
+/** @deprecated Replaced by `Form.Radio` and `Form.RadioSet`. */
export const RadioButtonGroup: any, RadioButton: any; // from './RadioButtonGroup';
export const ResponsiveEmbed: any; // from './ResponsiveEmbed';
export const
@@ -134,7 +145,9 @@ export const Sheet: any; // from './Sheet';
export const Spinner: any; // from './Spinner';
export const Stepper: any; // from './Stepper';
export const StatefulButton: any; // from './StatefulButton';
+/** @deprecated Replaced by `Alert`. */
export const StatusAlert: any; // from './StatusAlert';
+/** @deprecated Replaced by `DataTable`. */
export const Table: any; // from './Table';
export const
Tabs: any,
@@ -143,9 +156,10 @@ export const
TabContent: any,
TabPane: any;
// from './Tabs';
+/** @deprecated Replaced by `Form.Control`. */
export const TextArea: any; // from './TextArea';
export const Toast: any, TOAST_CLOSE_LABEL_TEXT: string, TOAST_DELAY: number; // from './Toast';
-export const Tooltip: any; // from './Tooltip';
+/** @deprecated Replaced by `Form.Group`. */
export const ValidationFormGroup: any; // from './ValidationFormGroup';
export const TransitionReplace: any; // from './TransitionReplace';
export const ValidationMessage: any; // from './ValidationMessage';
diff --git a/src/index.js b/src/index.js
index 6e8b9294c5..59b0b28cd2 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,13 +1,23 @@
-// To keep this file in sync with the .d.ts file, it's in the same order
-// and each line number is the same
+// Keep this file in sync with the .d.ts file (manually). It's in the same order
+// and each line number is the same, to make it easier.
// // // // // // // // // // // // // // // // // // // // // // // // // // //
// Things that have types
// // // // // // // // // // // // // // // // // // // // // // // // // // //
export { default as Bubble } from './Bubble';
+export { default as Button, ButtonGroup, ButtonToolbar } from './Button';
export { default as Chip, CHIP_PGN_CLASS } from './Chip';
export { default as ChipCarousel } from './ChipCarousel';
+export { default as Container } from './Container';
+export { default as Hyperlink, HYPER_LINK_EXTERNAL_LINK_ALT_TEXT, HYPER_LINK_EXTERNAL_LINK_TITLE } from './Hyperlink';
export { default as Icon } from './Icon';
+export { default as IconButton, IconButtonWithTooltip } from './IconButton';
+export { default as ModalContext } from './Modal/ModalContext';
+export { default as ModalDialog, MODAL_DIALOG_CLOSE_LABEL } from './Modal/ModalDialog';
+export { default as ModalLayer } from './Modal/ModalLayer';
+export { default as Overlay, OverlayTrigger } from './Overlay';
+export { default as Portal } from './Modal/Portal';
+export { default as Tooltip } from './Tooltip';
// // // // // // // // // // // // // // // // // // // // // // // // // // //
// Things that don't have types
@@ -20,7 +30,6 @@ export { default as Avatar } from './Avatar';
export { default as AvatarButton } from './AvatarButton';
export { default as Badge } from './Badge';
export { default as Breadcrumb } from './Breadcrumb';
-export { default as Button, ButtonGroup, ButtonToolbar } from './Button';
export {
default as Card,
CardColumns,
@@ -34,10 +43,11 @@ export {
export {
default as Carousel, CarouselItem, CAROUSEL_NEXT_LABEL_TEXT, CAROUSEL_PREV_LABEL_TEXT,
} from './Carousel';
+/** @deprecated Replaced by `Form.Checkbox`. */
export { default as CheckBox } from './CheckBox';
+/** @deprecated Replaced by `Form.Checkbox` and `Form.CheckboxSet`. */
export { default as CheckBoxGroup } from './CheckBoxGroup';
export { default as CloseButton } from './CloseButton';
-export { default as Container } from './Container';
export { default as Layout, Col, Row } from './Layout';
export { default as Collapse } from './Collapse';
export { default as Collapsible } from './Collapsible';
@@ -49,6 +59,7 @@ export {
SplitButton,
} from './Dropdown';
export { default as Fade } from './Fade';
+/** @deprecated */
export { default as Fieldset } from './Fieldset';
export {
default as Form,
@@ -72,31 +83,31 @@ export {
FormAutosuggestOption,
InputGroup,
} from './Form';
-export { default as Hyperlink, HYPER_LINK_EXTERNAL_LINK_ALT_TEXT, HYPER_LINK_EXTERNAL_LINK_TITLE } from './Hyperlink';
-export { default as IconButton, IconButtonWithTooltip } from './IconButton';
export { default as IconButtonToggle } from './IconButtonToggle';
+/** @deprecated Replaced by `Form.Control`. */
export { default as Input } from './Input';
+/** @deprecated Replaced by `Form.Control`. */
export { default as InputSelect } from './InputSelect';
+/** @deprecated Replaced by `Form.Control`. */
export { default as InputText } from './InputText';
export { default as Image, Figure } from './Image';
+/** @deprecated */
export { default as ListBox } from './ListBox';
+/** @deprecated */
export { default as ListBoxOption } from './ListBoxOption';
export { default as MailtoLink, MAIL_TO_LINK_EXTERNAL_LINK_ALTERNATIVE_TEXT, MAIL_TO_LINK_EXTERNAL_LINK_TITLE } from './MailtoLink';
export { default as Media } from './Media';
export { default as Menu } from './Menu';
export { default as MenuItem } from './Menu/MenuItem';
export { default as SelectMenu, SELECT_MENU_DEFAULT_MESSAGE } from './Menu/SelectMenu';
+/** @deprecated Use `ModalDialog` instead. */
export { default as Modal } from './Modal';
export { default as ModalCloseButton } from './Modal/ModalCloseButton';
export { default as FullscreenModal, FULLSCREEN_MODAL_CLOSE_LABEL } from './Modal/FullscreenModal';
export { default as MarketingModal } from './Modal/MarketingModal';
export { default as StandardModal, STANDARD_MODAL_CLOSE_LABEL } from './Modal/StandardModal';
export { default as AlertModal } from './Modal/AlertModal';
-export { default as ModalLayer } from './Modal/ModalLayer';
-export { default as ModalDialog, MODAL_DIALOG_CLOSE_LABEL } from './Modal/ModalDialog';
export { default as ModalPopup } from './Modal/ModalPopup';
-export { default as ModalContext } from './Modal/ModalContext';
-export { default as Portal } from './Modal/Portal';
export { default as PopperElement } from './Modal/PopperElement';
export {
@@ -106,7 +117,6 @@ export {
NavLink,
} from './Nav';
export { default as Navbar, NavbarBrand, NAVBAR_LABEL } from './Navbar';
-export { default as Overlay, OverlayTrigger } from './Overlay';
export { default as PageBanner, PAGE_BANNER_DISMISS_ALT_TEXT } from './PageBanner';
export {
default as Pagination,
@@ -121,6 +131,7 @@ export {
export { default as Popover, PopoverTitle, PopoverContent } from './Popover';
export { default as ProgressBar } from './ProgressBar';
export { default as ProductTour } from './ProductTour';
+/** @deprecated Replaced by `Form.Radio` and `Form.RadioSet`. */
export { default as RadioButtonGroup, RadioButton } from './RadioButtonGroup';
export { default as ResponsiveEmbed } from './ResponsiveEmbed';
export {
@@ -134,7 +145,9 @@ export { default as Sheet } from './Sheet';
export { default as Spinner } from './Spinner';
export { default as Stepper } from './Stepper';
export { default as StatefulButton } from './StatefulButton';
+/** @deprecated Replaced by `Alert`. */
export { default as StatusAlert } from './StatusAlert';
+/** @deprecated Replaced by `DataTable`. */
export { default as Table } from './Table';
export {
default as Tabs,
@@ -143,9 +156,10 @@ export {
TabContent,
TabPane,
} from './Tabs';
+/** @deprecated Replaced by `Form.Control`. */
export { default as TextArea } from './TextArea';
export { default as Toast, TOAST_CLOSE_LABEL_TEXT, TOAST_DELAY } from './Toast';
-export { default as Tooltip } from './Tooltip';
+/** @deprecated Replaced by `Form.Group`. */
export { default as ValidationFormGroup } from './ValidationFormGroup';
export { default as TransitionReplace } from './TransitionReplace';
export { default as ValidationMessage } from './ValidationMessage';
diff --git a/src/setupTest.js b/src/setupTest.ts
similarity index 66%
rename from src/setupTest.js
rename to src/setupTest.ts
index 525b689e39..2a528b828c 100644
--- a/src/setupTest.js
+++ b/src/setupTest.ts
@@ -1,3 +1,4 @@
+/* eslint-disable import/no-extraneous-dependencies */
import 'regenerator-runtime/runtime';
import '@testing-library/jest-dom';
@@ -20,6 +21,6 @@ class ResizeObserver {
window.ResizeObserver = ResizeObserver;
-window.crypto = {
- getRandomValues: arr => crypto.randomBytes(arr.length),
+(window as any).crypto = {
+ getRandomValues: (arr: any) => crypto.randomBytes(arr.length),
};
diff --git a/src/utils/types/bootstrap.test.tsx b/src/utils/types/bootstrap.test.tsx
new file mode 100644
index 0000000000..0346c5d2b4
--- /dev/null
+++ b/src/utils/types/bootstrap.test.tsx
@@ -0,0 +1,86 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import React from 'react';
+import type { BsPropsWithAs, ComponentWithAsProp } from './bootstrap';
+
+// Note: these are type-only tests. They don't actually do much at runtime; the important checks are at transpile time.
+
+describe('BsPropsWithAs', () => {
+ interface Props extends BsPropsWithAs {
+ otherProp?: number;
+ }
+
+ it('defines optional bsPrefix, className, and as but no other props', () => {
+ const checkProps = (_props: Props) => {};
+ // These are all valid props per the prop definition:
+ checkProps({ });
+ checkProps({ bsPrefix: 'bs' });
+ checkProps({ className: 'foo bar' });
+ checkProps({ as: 'tr' });
+ checkProps({ className: 'foo bar', as: 'button', otherProp: 15 });
+ // But these are all invalid:
+ // @ts-expect-error
+ checkProps({ newProp: 10 });
+ // @ts-expect-error
+ checkProps({ onClick: () => {} });
+ // @ts-expect-error
+ checkProps({ id: 'id' });
+ // @ts-expect-error
+ checkProps({ children:
});
+ });
+});
+
+describe('ComponentWithAsProp', () => {
+ interface MyProps extends BsPropsWithAs {
+ customProp?: string;
+ }
+ const MyComponent: ComponentWithAsProp<'div', MyProps> = (
+ React.forwardRef(
+ ({ as: Inner = 'div', ...props }, ref) => ,
+ )
+ );
+
+ // eslint-disable-next-line react/function-component-definition
+ const CustomComponent: React.FC<{ requiredProp: string }> = () => ;
+
+ it('is defined to wrap a by default, and accepts related props', () => {
+ // This is valid - by default it is a DIV so accepts props and ref related to DIV:
+ const divClick: React.MouseEventHandler
= () => {};
+ const divRef: React.RefObject = { current: null };
+ const valid = ;
+ });
+
+ it('is defined to wrap a by default, and rejects unrelated props', () => {
+ const btnRef: React.RefObject
= { current: null };
+ // @ts-expect-error because the ref is to a