diff --git a/src/Chip/Chip.test.jsx b/src/Chip/Chip.test.tsx similarity index 94% rename from src/Chip/Chip.test.jsx rename to src/Chip/Chip.test.tsx index f5e5367d18..1624e62d36 100644 --- a/src/Chip/Chip.test.jsx +++ b/src/Chip/Chip.test.tsx @@ -7,7 +7,7 @@ import { Close } from '../../icons'; import { STYLE_VARIANTS } from './constants'; import Chip from '.'; -function TestChip(props) { +function TestChip(props: Omit, 'children'>) { return ( Test @@ -42,15 +42,13 @@ describe('', () => { iconBeforeAlt="close icon" iconAfter={Close} iconAfterAlt="close icon" - > - Chip - + /> )).toJSON(); expect(tree).toMatchSnapshot(); }); it('renders div with "button" role when onClick is provided', () => { const tree = renderer.create(( - Chip + )).toJSON(); expect(tree).toMatchSnapshot(); }); @@ -104,7 +102,7 @@ describe('', () => { />, ); const iconAfter = screen.getByLabelText('icon-after'); - await userEvent.click(iconAfter, '{enter}', { skipClick: true }); + await userEvent.click(iconAfter); expect(func).toHaveBeenCalledTimes(1); }); it('onIconBeforeClick is triggered', async () => { @@ -130,7 +128,7 @@ describe('', () => { />, ); const iconBefore = screen.getByLabelText('icon-before'); - await userEvent.click(iconBefore, '{enter}', { skipClick: true }); + await userEvent.click(iconBefore); expect(func).toHaveBeenCalledTimes(1); }); it('checks the absence of the `selected` class in the chip', async () => { diff --git a/src/Chip/ChipIcon.tsx b/src/Chip/ChipIcon.tsx index 87e08aa256..8ef97ef1ee 100644 --- a/src/Chip/ChipIcon.tsx +++ b/src/Chip/ChipIcon.tsx @@ -1,19 +1,19 @@ import React, { KeyboardEventHandler, MouseEventHandler } from 'react'; import PropTypes from 'prop-types'; import Icon from '../Icon'; -// @ts-ignore import IconButton from '../IconButton'; -// @ts-ignore import { STYLE_VARIANTS } from './constants'; -export interface ChipIconProps { +export type ChipIconProps = { className: string, src: React.ComponentType, - onClick?: KeyboardEventHandler & MouseEventHandler, - alt?: string, - variant: string, + variant: typeof STYLE_VARIANTS[keyof typeof STYLE_VARIANTS], disabled?: boolean, -} +} & ( + // Either _both_ onClick and alt are provided, or neither is: + | { onClick: KeyboardEventHandler & MouseEventHandler, alt: string } + | { onClick?: undefined, alt?: undefined } +); function ChipIcon({ className, src, onClick, alt, variant, disabled, diff --git a/src/Chip/__snapshots__/Chip.test.jsx.snap b/src/Chip/__snapshots__/Chip.test.tsx.snap similarity index 100% rename from src/Chip/__snapshots__/Chip.test.jsx.snap rename to src/Chip/__snapshots__/Chip.test.tsx.snap diff --git a/src/Chip/constants.js b/src/Chip/constants.ts similarity index 91% rename from src/Chip/constants.js rename to src/Chip/constants.ts index 6259d0c8dd..dea354757b 100644 --- a/src/Chip/constants.js +++ b/src/Chip/constants.ts @@ -2,4 +2,4 @@ export const STYLE_VARIANTS = { DARK: 'dark', LIGHT: 'light', -}; +} as const; diff --git a/src/Chip/index.tsx b/src/Chip/index.tsx index 0f78ab2059..81be775f78 100644 --- a/src/Chip/index.tsx +++ b/src/Chip/index.tsx @@ -3,9 +3,7 @@ import PropTypes, { type Requireable } from 'prop-types'; import classNames from 'classnames'; // @ts-ignore import { requiredWhen } from '../utils/propTypes'; -// @ts-ignore import { STYLE_VARIANTS } from './constants'; -// @ts-ignore import ChipIcon from './ChipIcon'; export const CHIP_PGN_CLASS = 'pgn__chip'; @@ -14,7 +12,7 @@ export interface IChip { children: React.ReactNode, onClick?: KeyboardEventHandler & MouseEventHandler, className?: string, - variant?: string, + variant?: typeof STYLE_VARIANTS[keyof typeof STYLE_VARIANTS], iconBefore?: React.ComponentType, iconBeforeAlt?: string, iconAfter?: React.ComponentType, diff --git a/src/ChipCarousel/index.tsx b/src/ChipCarousel/index.tsx index acb26ae115..062a21a347 100644 --- a/src/ChipCarousel/index.tsx +++ b/src/ChipCarousel/index.tsx @@ -4,9 +4,7 @@ import { useIntl } from 'react-intl'; import classNames from 'classnames'; // @ts-ignore import { OverflowScroll, OverflowScrollContext } from '../OverflowScroll'; -// @ts-ignore import IconButton from '../IconButton'; -// @ts-ignore import Icon from '../Icon'; // @ts-ignore import { ArrowForward, ArrowBack } from '../../icons'; 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/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/index.d.ts b/src/index.d.ts index 8cd1eaea8b..a680a1ac86 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -10,6 +10,9 @@ export { default as Chip, CHIP_PGN_CLASS } from './Chip'; export { default as ChipCarousel } from './ChipCarousel'; 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 Overlay, OverlayTrigger } from './Overlay'; +export { default as Tooltip } from './Tooltip'; // // // // // // // // // // // // // // // // // // // // // // // // // // // // Things that don't have types @@ -73,7 +76,6 @@ export const FormAutosuggestOption: any, InputGroup: any; // from './Form'; -export const IconButton: any, IconButtonWithTooltip: any; // from './IconButton'; export const IconButtonToggle: any; // from './IconButtonToggle'; export const Input: any; // from './Input'; export const InputSelect: any; // from './InputSelect'; @@ -106,7 +108,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, @@ -145,7 +146,6 @@ export const // from './Tabs'; 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'; 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 2dbc760aba..258b03af3f 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,9 @@ export { default as Chip, CHIP_PGN_CLASS } from './Chip'; export { default as ChipCarousel } from './ChipCarousel'; 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 Overlay, OverlayTrigger } from './Overlay'; +export { default as Tooltip } from './Tooltip'; // // // // // // // // // // // // // // // // // // // // // // // // // // // // Things that don't have types @@ -73,7 +76,6 @@ export { FormAutosuggestOption, InputGroup, } from './Form'; -export { default as IconButton, IconButtonWithTooltip } from './IconButton'; export { default as IconButtonToggle } from './IconButtonToggle'; export { default as Input } from './Input'; export { default as InputSelect } from './InputSelect'; @@ -106,7 +108,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, @@ -145,7 +146,6 @@ export { } from './Tabs'; export { default as TextArea } from './TextArea'; export { default as Toast, TOAST_CLOSE_LABEL_TEXT, TOAST_DELAY } from './Toast'; -export { default as Tooltip } from './Tooltip'; export { default as ValidationFormGroup } from './ValidationFormGroup'; export { default as TransitionReplace } from './TransitionReplace'; export { default as ValidationMessage } from './ValidationMessage'; diff --git a/www/src/components/CodeBlock.tsx b/www/src/components/CodeBlock.tsx index 4d396da6cb..02064d25f3 100644 --- a/www/src/components/CodeBlock.tsx +++ b/www/src/components/CodeBlock.tsx @@ -37,7 +37,7 @@ const { export type CollapsibleLiveEditorTypes = { children: React.ReactNode; - clickToCopy: (arg: string) => void; + clickToCopy: () => void; handleCodeChange: (e: React.ChangeEvent) => void; }; diff --git a/www/src/components/header/Navbar.tsx b/www/src/components/header/Navbar.tsx index a9306dd1bc..ffef619923 100644 --- a/www/src/components/header/Navbar.tsx +++ b/www/src/components/header/Navbar.tsx @@ -17,7 +17,7 @@ export interface INavbar { siteTitle: string, onMenuClick: () => boolean, setTarget: React.Dispatch>, - onSettingsClick: Function | undefined, + onSettingsClick?: () => void, menuIsOpen?: boolean, showMinimizedTitle?: boolean, showSettingsIcon?: boolean, diff --git a/www/src/context/SettingsContext.tsx b/www/src/context/SettingsContext.tsx index d05cef5ede..777c3a73bb 100644 --- a/www/src/context/SettingsContext.tsx +++ b/www/src/context/SettingsContext.tsx @@ -17,8 +17,8 @@ export interface IDefaultValue { theme?: string, handleSettingsChange: Function, showSettings?: React.SyntheticEvent | React.ReactNode, - closeSettings?: React.SyntheticEvent | React.ReactNode, - openSettings?: Function, + closeSettings?: () => void, + openSettings?: () => void, } const defaultValue = {