Skip to content

Commit

Permalink
feat: working typings for Paragon, better types for <Icon> component (#…
Browse files Browse the repository at this point in the history
…3016)

* feat: better types for <Icon> component

* fix: TypeScript rootDir now that it's checking '../src' files too

* chore: fix eslint 'import/order' & 'import/no-unresolved' issues in www

* fix: build wasn't including types properly

* fix: <Icon/> types still weren't correct

* fix: explicitly define other exports as having 'any' type

* fix: changing www/tsconfig.json is no longer needed

* fix: warning seen when gatsby parses index.d.ts during 'npm run start'
  • Loading branch information
bradenmacdonald committed Mar 25, 2024
1 parent 98e6313 commit 0983219
Show file tree
Hide file tree
Showing 27 changed files with 2,702 additions and 73 deletions.
5 changes: 3 additions & 2 deletions Makefile
@@ -1,7 +1,8 @@
build:
rm -rf ./dist
tsc --emitDeclarationOnly
./node_modules/.bin/babel src --config-file ./babel.config.json --out-dir dist --source-maps --ignore **/*.test.jsx,**/*.test.tsx,**/__mocks__,**/__snapshots__,**/setupTest.js --copy-files --extensions ".tsx,.jsx"
tsc --project tsconfig.build.json
rm icons/es5/index.d.ts # We don't need this; not sure how to tell tsc not to generate it
./node_modules/.bin/babel src --config-file ./babel.config.json --out-dir dist --source-maps --ignore **/*.d.ts,**/*.test.jsx,**/*.test.tsx,**/__mocks__,**/__snapshots__,**/setupTest.js --copy-files --extensions ".ts,.tsx,.jsx"
# --copy-files will bring in everything else that wasn't processed by babel. Remove what we don't want.
find ./dist -name "tests" -type d -prune -exec rm -rf "{}" \; # delete tests directories
find ./dist -name "*.test.*" -delete # delete other tests files that weren't in tests directories
Expand Down
2,308 changes: 2,308 additions & 0 deletions icons/es5/index.ts

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions icons/index.d.ts
@@ -0,0 +1,5 @@
export * from './es5';

import * as allIcons from './es5';

export type IconName = keyof typeof allIcons;
1 change: 1 addition & 0 deletions icons/index.mjs
@@ -0,0 +1 @@
export * from './es5';
8 changes: 7 additions & 1 deletion icons/package.json
Expand Up @@ -5,7 +5,7 @@
"main": "index.js",
"sideEffects": false,
"scripts": {
"build": "node copy-mui-icons.js && node copy-brand-icons.js && svgr svg --out-dir jsx && babel jsx -d es5",
"build": "node copy-mui-icons.js && node copy-brand-icons.js && svgr svg --out-dir jsx && babel jsx -d es5 && echo '// @ts-nocheck' > es5/index.ts && cat es5/index.js >> es5/index.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
Expand All @@ -22,5 +22,11 @@
},
"peerDependencies": {
"react": "^16.8.6 || ^17.0.2"
},
"exports": {
".": {
"import": "./index.mjs",
"types": "./index.d.ts"
}
}
}
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -4,6 +4,7 @@
"description": "Accessible, responsive UI component library based on Bootstrap.",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public"
Expand Down Expand Up @@ -107,6 +108,8 @@
"@types/jest": "^29.5.10",
"@types/react": "17.0.0",
"@types/react-dom": "17.0.11",
"@types/react-responsive": "^8.0.8",
"@types/react-table": "^7.7.19",
"@types/react-test-renderer": "^18.0.0",
"@types/uuid": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^5.22.0",
Expand Down
44 changes: 44 additions & 0 deletions src/Icon/Icon.test.jsx → src/Icon/Icon.test.tsx
@@ -1,5 +1,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import * as ParagonIcons from '../../icons';
import { type IconName } from '../../icons';

import Icon from './index';

Expand All @@ -14,7 +16,41 @@ function BlankSrc() {
return <div />;
}

/** A compile time check. Whatever React elements this wraps won't run at runtime. */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function CompileCheck(_props: { children: React.ReactNode }) { return null; }

describe('IconName type', () => {
it('has correct typing', () => {
/* eslint-disable @typescript-eslint/no-unused-vars */

const realName: IconName = 'ArrowCircleDown';
// @ts-expect-error This should be a compile-time error, as 'FooBarIcon' doesn't exist.
const wrongName: IconName = 'FooBarIcon';

/* eslint-enable @typescript-eslint/no-unused-vars */
});
});

describe('<Icon />', () => {
it('has correct typing', () => {
<CompileCheck>
{/* Correct usage */}
<Icon src={ParagonIcons.ArrowCircleDown} id="icon123" size="sm" />
{/* An empty <Icon /> is allowed; if not, the checks below would need to be modified. */}
<Icon />

{/* @ts-expect-error Using a non-existent icon from @openedx/paragon/icons is a type error */}
<Icon src={ParagonIcons.FooBarIcon} />
{/* @ts-expect-error The 'src' prop cannot be a string. */}
<Icon src="string" />
{/* @ts-expect-error Random props cannot be added */}
<Icon foo="bar" />
{/* @ts-expect-error This is not a valid size property */}
<Icon size="big" />
</CompileCheck>;
});

describe('props received correctly', () => {
it('receives required props', () => {
const { container } = render(<Icon className={classNames} />);
Expand Down Expand Up @@ -66,5 +102,13 @@ describe('<Icon />', () => {

expect(iconSpan.classList.contains('pgn__icon__xs')).toEqual(true);
});

it('receives style or other arbitrary HTML properties correctly', () => {
const { container } = render(<Icon src={BlankSrc} style={{ color: 'red' }} size="xs" />);
const iconSpans = container.querySelectorAll('span');
const iconSpan = iconSpans[0];

expect(iconSpan.style.color).toEqual('red');
});
});
});
6 changes: 3 additions & 3 deletions src/Icon/index.d.ts
@@ -1,14 +1,14 @@
import React from 'react';

export interface IconProps {
export interface IconProps extends React.ComponentPropsWithoutRef<'span'> {
src?: React.ReactElement | Function;
svgAttrs?: {
'aria-label'?: string;
'aria-labelledby'?: string;
};
id?: string;
id?: string | null;
size?: 'xs' | 'sm' | 'md' | 'lg';
className?: string;
className?: string | string[];
hidden?: boolean;
screenReaderText?: React.ReactNode;
}
Expand Down
216 changes: 216 additions & 0 deletions src/index.d.ts
@@ -0,0 +1,216 @@
/* eslint-disable max-len, one-var, one-var-declaration-per-line */
// each line in this file corresponds with the line in index.js

// // // // // // // // // // // // // // // // // // // // // // // // // // //
// Things that have types
// // // // // // // // // // // // // // // // // // // // // // // // // // //
export { default as Bubble } from './Bubble';
export { default as Chip, CHIP_PGN_CLASS } from './Chip';
export { default as ChipCarousel } from './ChipCarousel';
export { default as Icon } from './Icon';

// // // // // // // // // // // // // // // // // // // // // // // // // // //
// Things that don't have types
// // // // // // // // // // // // // // // // // // // // // // // // // // //
export const asInput: any; // from './asInput';
export const ActionRow: any; // from './ActionRow';
export const Alert: any, ALERT_CLOSE_LABEL_TEXT: string; // from './Alert';
export const Annotation: any; // from './Annotation';
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,
CardDeck: any,
CardImg: any,
CardGroup: any,
CardGrid: any,
CardCarousel: any,
CARD_VARIANTS: any;
// from './Card';
export const
Carousel: any, CarouselItem: any, CAROUSEL_NEXT_LABEL_TEXT: any, CAROUSEL_PREV_LABEL_TEXT: any;
// from './Carousel';
export const CheckBox: any; // from './CheckBox';
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';
export const Scrollable: any; // from './Scrollable';
export const
Dropdown: any,
DropdownToggle: any,
DropdownButton: any,
SplitButton: any;
// from './Dropdown';
export const Fade: any; // from './Fade';
export const Fieldset: any; // from './Fieldset';
export const
Form: any,
RadioControl: any,
CheckboxControl: any,
SwitchControl: any,
FormSwitchSet: any,
FormControl: any,
FormControlDecoratorGroup: any,
FormControlFeedback: any,
FormCheck: any,
FormFile: any,
FormRadio: any,
FormRadioSet: any,
FormRadioSetContext: any,
FormGroup: any,
FormLabel: any,
useCheckboxSetValues: any,
FormText: any,
FormAutosuggest: any,
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';
export const Input: any; // from './Input';
export const InputSelect: any; // from './InputSelect';
export const InputText: any; // from './InputText';
export const Image: any, Figure; // from './Image';
export const ListBox: any; // from './ListBox';
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';
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
Nav: any,
NavDropdown: any,
NavItem: any,
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,
PAGINATION_BUTTON_LABEL_PREV: string,
PAGINATION_BUTTON_ICON_BUTTON_NEXT_ALT: string,
PAGINATION_BUTTON_ICON_BUTTON_PREV_ALT: string,
PAGINATION_BUTTON_LABEL_PAGE_OF_COUNT: string,
PAGINATION_BUTTON_LABEL_CURRENT_PAGE: string,
PAGINATION_BUTTON_LABEL_NEXT: string,
PAGINATION_BUTTON_LABEL_PAGE: string;
// from './Pagination';
export const Popover: any, PopoverTitle: any, PopoverContent: any; // from './Popover';
export const ProgressBar: any; // from './ProgressBar';
export const ProductTour: any; // from './ProductTour';
export const RadioButtonGroup: any, RadioButton: any; // from './RadioButtonGroup';
export const ResponsiveEmbed: any; // from './ResponsiveEmbed';
export const
SearchField: any,
SEARCH_FIELD_SCREEN_READER_TEXT_LABEL: string,
SEARCH_FIELD_SCREEN_READER_TEXT_CLEAR_BUTTON: string,
SEARCH_FIELD_SCREEN_READER_TEXT_SUBMIT_BUTTON: string,
SEARCH_FIELD_BUTTON_TEXT: string;
// from './SearchField';
export const Sheet: any; // from './Sheet';
export const Spinner: any; // from './Spinner';
export const Stepper: any; // from './Stepper';
export const StatefulButton: any; // from './StatefulButton';
export const StatusAlert: any; // from './StatusAlert';
export const Table: any; // from './Table';
export const
Tabs: any,
Tab: any,
TabContainer: any,
TabContent: any,
TabPane: any;
// 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';
export const DataTable: any; // from './DataTable';
export const TextFilter: any; // from './DataTable/filters/TextFilter';
export const CheckboxFilter: any; // from './DataTable/filters/CheckboxFilter';
export const DropdownFilter: any; // from './DataTable/filters/DropdownFilter';
export const MultiSelectDropdownFilter: any; // from './DataTable/filters/MultiSelectDropdownFilter';
export const TableHeaderCell: any; // from './DataTable/TableHeaderCell';
export const TableCell: any; // from './DataTable/TableCell';
export const TableFilters: any, TABLE_FILTERS_BUTTON_TEXT: string; // from './DataTable/TableFilters';
export const TableHeader: any; // from './DataTable/TableHeaderRow';
export const TableRow: any; // from './DataTable/TableRow';
export const TablePagination: any; // from './DataTable/TablePagination';
export const TablePaginationMinimal: any; // from './DataTable/TablePaginationMinimal';
export const DataTableContext: any; // from './DataTable/DataTableContext';
export const BulkActions: any; // from './DataTable/BulkActions';
export const TableControlBar: any; // from './DataTable/TableControlBar';
export const TableFooter: any; // from './DataTable/TableFooter';
export const CardView: any; // from './DataTable/CardView';
export const Skeleton: any, SkeletonTheme: any; // from './Skeleton/index';
export const Stack: any; // from './Stack';
export const ToggleButton: any, ToggleButtonGroup: any; // from './ToggleButton';
export const Sticky: any; // from './Sticky';
export const SelectableBox: any; // from './SelectableBox';
export const breakpoints: any; // from './utils/breakpoints';
export const Variant: any; // from './utils/constants';
export const useWindowSize: any; // from './hooks/useWindowSize';
export const useToggle: any; // from './hooks/useToggle';
export const useArrowKeyNavigation: any; // from './hooks/useArrowKeyNavigation';
export const useIndexOfLastVisibleChild: any; // from './hooks/useIndexOfLastVisibleChild';
export const useIsVisible: any; // from './hooks/useIsVisible';
export const
OverflowScrollContext: any,
OverflowScroll: any,
useOverflowScroll: any,
useOverflowScrollItems: any;
// from './OverflowScroll';
export const Dropzone: any; // from './Dropzone';
export const messages: any; // from './i18n';
export const Truncate: any; // from './Truncate';
export const ColorPicker: any; // from './ColorPicker';

// Pass through any needed whole third-party library functionality
// useTable for example is needed to use the DataTable component seamlessly
// rather than setting a peer dependency in this project, we opt to tightly
// couple these dependencies by passing through needed functionality.
export {
default as MediaQuery,
useMediaQuery,
Context as ResponsiveContext,
} from 'react-responsive';
export {
useTable,
useFilters,
useGlobalFilter,
useSortBy,
useGroupBy,
useExpanded,
usePagination,
useRowSelect,
useRowState,
useColumnOrder,
useResizeColumns,
useBlockLayout,
useAbsoluteLayout,
useFlexLayout,
} from 'react-table';

0 comments on commit 0983219

Please sign in to comment.