diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000000..31354ec138 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000000..d2ae35e84b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn lint-staged diff --git a/CHANGELOG.md b/CHANGELOG.md index f592e785bc..2f737cf1d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +# [1.6.0](https://github.com/react-bootstrap/react-bootstrap/compare/v1.5.2...v1.6.0) (2021-05-11) + + +### Bug Fixes + +* **Popover:** fix arrow alignment when running Popper >= 2.84 ([#5774](https://github.com/react-bootstrap/react-bootstrap/issues/5774)) ([da85f30](https://github.com/react-bootstrap/react-bootstrap/commit/da85f30f985870183f09835ac49d0fac075587d6)) +* **types:** fix types for event key ([700e944](https://github.com/react-bootstrap/react-bootstrap/commit/700e944de72072106f1ca50a9b797512dfa95009)) + + +### Features + +* **Nav:** Add navbarScroll prop ([#5620](https://github.com/react-bootstrap/react-bootstrap/issues/5620)) ([d9c1bb7](https://github.com/react-bootstrap/react-bootstrap/commit/d9c1bb78412e42e6f6add1cc19b83cce48f2830e)) + + + + + ## [1.5.2](https://github.com/react-bootstrap/react-bootstrap/compare/v1.5.1...v1.5.2) (2021-03-11) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e853f065f9..d0b8aa9a8f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,15 +46,16 @@ desired change easier. ## Documentation -Please update the docs with any API changes, the code and docs should always be -in sync. +Please update the docs with any API changes, the code and docs should always be in sync. -Component prop documentation is generated automatically from the React components -and their leading comments. Please make sure to provide comments for any `propTypes` you add -or change in a Component. +The main documentation lives in the `www` folder which is a Gatsby project that uses [MDX](https://www.gatsbyjs.com/docs/how-to/routing/mdx/). The long-form documentation for components, including interactive examples and guides, is found within the `pages/components` directory. + +Separately, component prop API documentation is generated automatically from the React components in the `src` directory and their leading comments. This is the documentation that shows up in the tables at the bottom of component doc pages, e.g. [here](https://react-bootstrap.github.io/components/accordion/#api). Please make sure to provide [TSDOC-style](https://tsdoc.org/) comments\* for any `propTypes` you add or change in a component. + +Here's an example of well-documented `propTypes`: ```js -propTypes: { +const propTypes = { /** * Sets the visibility of the Component */ @@ -65,15 +66,15 @@ propTypes: { * @type {func} * @required */ - onHide: myCustomPropType -} + onHide: myCustomPropType, +}; ``` -There are a few caveats to this format that differ from conventional JSDoc comments. +\*Note: there are a few caveats to this format that differ from conventional TSDoc comments: -- Only specific doclets (the @ things) should be used, and only when the data cannot be parsed from the component itself - - `@type`: Override the "type", use the same names as the default React PropTypes: string, func, bool, number, object. You can express enum and oneOfType types, Like `{("optionA"|"optionB")}`. - - `@required`: to mark a prop as required (use the normal React isRequired if possible) +- Only specific doclets (the @ things, a.k.a Block Tags) should be used, and only when the data cannot be parsed from the component itself. + - `@type`: An optional type override. Use the same names as the default React PropTypes: string, func, bool, number, object. This can be helpful to express enums and `oneOfType` types, e.g. `{("optionA"|"optionB")}`. + - `@required`: Mark a prop as required (use the normal React `isRequired` if possible) - `@private`: Will hide the prop in the documentation - All description text should be above the doclets. diff --git a/package.json b/package.json index b8c046bce7..30ad733b7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-bootstrap", - "version": "1.5.2", + "version": "1.6.0", "description": "Bootstrap 4 components built with React", "keywords": [ "react", @@ -32,7 +32,7 @@ "url": "git+https://github.com/react-bootstrap/react-bootstrap.git" }, "scripts": { - "bootstrap": "yarn && yarn --cwd www", + "bootstrap": "yarn --network-timeout 100000 && yarn --cwd www --network-timeout 100000", "build": "node tools/build.js", "build-docs": "yarn --cwd www build", "build-types": "yarn tsc -d --emitDeclarationOnly --outDir types", @@ -46,12 +46,8 @@ "tdd": "karma start", "test": "npm run lint && npm run test-browser && npm run test-node", "test-browser": "cross-env NODE_ENV=test karma start --single-run", - "test-node": "cross-env NODE_ENV=test-server mocha test/server/*Spec.js" - }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } + "test-node": "cross-env NODE_ENV=test-server mocha test/server/*Spec.js", + "prepare": "husky install" }, "lint-staged": { "*.{js,ts,tsx}": "eslint --fix" @@ -81,42 +77,42 @@ "warning": "^4.0.3" }, "devDependencies": { - "@4c/rollout": "^2.2.0", + "@4c/rollout": "^2.2.1", "@4c/tsconfig": "^0.3.1", - "@babel/cli": "^7.13.10", - "@babel/core": "^7.13.10", + "@babel/cli": "^7.13.16", + "@babel/core": "^7.14.0", "@babel/preset-typescript": "^7.13.0", - "@babel/register": "^7.13.8", + "@babel/register": "^7.13.16", "@react-bootstrap/babel-preset": "^1.2.0", "@react-bootstrap/eslint-config": "^1.3.2", - "@typescript-eslint/eslint-plugin": "^4.17.0", - "@typescript-eslint/parser": "^4.17.0", + "@typescript-eslint/eslint-plugin": "^4.22.1", + "@typescript-eslint/parser": "^4.22.1", "babel-eslint": "^10.1.0", "babel-loader": "^8.2.2", "babel-plugin-istanbul": "^6.0.0", - "chai": "^4.3.3", - "chalk": "^4.1.0", + "chai": "^4.3.4", + "chalk": "^4.1.1", "cherry-pick": "^0.5.0", - "codecov": "^3.8.1", + "codecov": "^3.8.2", "conventional-changelog-cli": "^2.1.1", "cpy-cli": "^3.1.1", "cross-env": "^7.0.3", - "dtslint": "^4.0.7", + "dtslint": "^4.0.9", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.6", - "eslint": "^7.21.0", + "eslint": "^7.26.0", "eslint-config-4catalyzer-typescript": "^3.0.3", "eslint-import-resolver-node": "^0.3.4", "eslint-import-resolver-webpack": "^0.13.0", "eslint-plugin-import": "^2.22.1", "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-mocha": "^8.1.0", - "eslint-plugin-prettier": "^3.3.1", - "eslint-plugin-react": "^7.22.0", + "eslint-plugin-prettier": "^3.4.0", + "eslint-plugin-react": "^7.23.2", "execa": "^5.0.0", - "fs-extra": "^9.1.0", - "husky": "^4.3.8", - "karma": "^6.2.0", + "fs-extra": "^10.0.0", + "husky": "^6.0.0", + "karma": "^6.3.2", "karma-chrome-launcher": "^3.1.0", "karma-cli": "^2.0.0", "karma-coverage": "^2.0.3", @@ -126,19 +122,19 @@ "karma-sinon-chai": "^2.0.2", "karma-sourcemap-loader": "^0.3.8", "karma-webpack": "^5.0.0", - "lint-staged": "^10.5.4", + "lint-staged": "^11.0.0", "lodash": "^4.17.21", - "mocha": "^8.3.1", - "prettier": "^2.2.1", + "mocha": "^8.4.0", + "prettier": "^2.3.0", "react": "^16.14.0", "react-dom": "^16.14.0", "react-test-renderer": "^16.14.0", "simulant": "^0.2.2", "sinon": "^9.2.4", - "sinon-chai": "^3.5.0", + "sinon-chai": "^3.6.0", "stream-browserify": "^3.0.0", - "typescript": "^4.2.3", - "webpack": "^5.24.4" + "typescript": "^4.2.4", + "webpack": "^5.36.2" }, "peerDependencies": { "react": ">=16.8.0", diff --git a/src/AbstractNavItem.tsx b/src/AbstractNavItem.tsx index 4bd86a05e7..086c2f3a1b 100644 --- a/src/AbstractNavItem.tsx +++ b/src/AbstractNavItem.tsx @@ -7,6 +7,7 @@ import warning from 'warning'; import NavContext from './NavContext'; import SelectableContext, { makeEventKey } from './SelectableContext'; import { BsPrefixRefForwardingComponent } from './helpers'; +import { EventKey } from './types'; // TODO: check this interface AbstractNavItemProps { @@ -14,7 +15,7 @@ interface AbstractNavItemProps { as: React.ElementType; className?: string; disabled?: boolean; - eventKey?: any; // TODO: especially fix this + eventKey?: EventKey; href?: string; role?: string; id?: string; diff --git a/src/DropdownItem.tsx b/src/DropdownItem.tsx index defc3bd41b..c487b645ca 100644 --- a/src/DropdownItem.tsx +++ b/src/DropdownItem.tsx @@ -12,11 +12,12 @@ import { BsPrefixRefForwardingComponent, SelectCallback, } from './helpers'; +import { EventKey } from './types'; export interface DropdownItemProps extends BsPrefixPropsWithChildren { active?: boolean; disabled?: boolean; - eventKey?: string; + eventKey?: EventKey; href?: string; onClick?: React.MouseEventHandler; onSelect?: SelectCallback; @@ -44,7 +45,7 @@ const propTypes = { /** * Value passed to the `onSelect` handler, useful for identifying the selected menu item. */ - eventKey: PropTypes.any, + eventKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** * HTML `href` attribute corresponding to `a.href`. @@ -95,8 +96,8 @@ const DropdownItem: DropdownItem = React.forwardRef( const navContext = useContext(NavContext); const { activeKey } = navContext || {}; - // TODO: Restrict eventKey to string in v5? - const key = makeEventKey(eventKey as any, href); + + const key = makeEventKey(eventKey, href); const active = propActive == null && key != null diff --git a/src/DropdownToggle.tsx b/src/DropdownToggle.tsx index f152e4f6c1..d975c1f38a 100644 --- a/src/DropdownToggle.tsx +++ b/src/DropdownToggle.tsx @@ -17,7 +17,6 @@ export interface DropdownToggleProps ButtonProps { split?: boolean; childBsPrefix?: string; - eventKey?: any; // TODO: fix this type } type DropdownToggle = BsPrefixRefForwardingComponent< diff --git a/src/ListGroup.tsx b/src/ListGroup.tsx index 690920ff51..2a7a130ea9 100644 --- a/src/ListGroup.tsx +++ b/src/ListGroup.tsx @@ -13,12 +13,13 @@ import { BsPrefixRefForwardingComponent, SelectCallback, } from './helpers'; +import { EventKey } from './types'; export interface ListGroupProps extends BsPrefixProps { variant?: 'flush'; horizontal?: boolean | 'sm' | 'md' | 'lg' | 'xl'; - activeKey?: unknown; - defaultActiveKey?: unknown; + activeKey?: EventKey; + defaultActiveKey?: EventKey; onSelect?: SelectCallback; } diff --git a/src/ListGroupItem.tsx b/src/ListGroupItem.tsx index 9c06b1c4fd..a257f507b8 100644 --- a/src/ListGroupItem.tsx +++ b/src/ListGroupItem.tsx @@ -3,10 +3,9 @@ import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import AbstractNavItem from './AbstractNavItem'; -import { makeEventKey } from './SelectableContext'; import { useBootstrapPrefix } from './ThemeProvider'; import { BsPrefixProps, BsPrefixRefForwardingComponent } from './helpers'; -import { Variant } from './types'; +import { EventKey, Variant } from './types'; export interface ListGroupItemProps extends Omit, 'onSelect'>, @@ -14,7 +13,7 @@ export interface ListGroupItemProps action?: boolean; active?: boolean; disabled?: boolean; - eventKey?: string; + eventKey?: EventKey; href?: string; onClick?: React.MouseEventHandler; variant?: Variant; @@ -48,7 +47,7 @@ const propTypes = { */ disabled: PropTypes.bool, - eventKey: PropTypes.string, + eventKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), onClick: PropTypes.func, @@ -79,7 +78,6 @@ const ListGroupItem: ListGroupItem = React.forwardRef( variant, action, as, - eventKey, onClick, ...props }, @@ -109,8 +107,6 @@ const ListGroupItem: ListGroupItem = React.forwardRef( ; } @@ -59,7 +61,7 @@ const propTypes = { * * @type {string} */ - activeKey: PropTypes.any, + activeKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** * Have all `NavItem`s proportionately fill all available width. @@ -103,6 +105,11 @@ const propTypes = { */ navbar: PropTypes.bool, + /** + * Enable vertical scrolling within the toggleable contents of a collapsed Navbar. + */ + navbarScroll: PropTypes.bool, + as: PropTypes.elementType, /** @private */ @@ -122,6 +129,7 @@ const Nav: Nav = (React.forwardRef((uncontrolledProps: NavProps, ref) => { fill, justify, navbar, + navbarScroll, className, children, activeKey, @@ -152,6 +160,7 @@ const Nav: Nav = (React.forwardRef((uncontrolledProps: NavProps, ref) => { className={classNames(className, { [bsPrefix]: !isNavbar, [`${navbarBsPrefix}-nav`]: isNavbar, + [`${navbarBsPrefix}-nav-scroll`]: isNavbar && navbarScroll, [`${cardHeaderBsPrefix}-${variant}`]: !!cardHeaderBsPrefix, [`${bsPrefix}-${variant}`]: !!variant, [`${bsPrefix}-fill`]: fill, diff --git a/src/NavLink.tsx b/src/NavLink.tsx index 8a9a42b91e..366aa4943c 100644 --- a/src/NavLink.tsx +++ b/src/NavLink.tsx @@ -11,6 +11,7 @@ import { BsPrefixRefForwardingComponent, SelectCallback, } from './helpers'; +import { EventKey } from './types'; export interface NavLinkProps extends BsPrefixPropsWithChildren { active?: boolean; @@ -18,7 +19,7 @@ export interface NavLinkProps extends BsPrefixPropsWithChildren { role?: string; href?: string; onSelect?: SelectCallback; - eventKey?: unknown; + eventKey?: EventKey; } type NavLink = BsPrefixRefForwardingComponent<'a', NavLinkProps>; @@ -60,7 +61,7 @@ const propTypes = { * Uniquely idenifies the `NavItem` amongst its siblings, * used to determine and control the active state of the parent `Nav` */ - eventKey: PropTypes.any, + eventKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** @default 'a' */ as: PropTypes.elementType, diff --git a/src/SelectableContext.tsx b/src/SelectableContext.tsx index a872f4c94f..62f57e5e26 100644 --- a/src/SelectableContext.tsx +++ b/src/SelectableContext.tsx @@ -8,7 +8,7 @@ const SelectableContext = React.createContext( ); export const makeEventKey = ( - eventKey: string | null, + eventKey?: string | number | null, href: string | null = null, ): string | null => { if (eventKey != null) return String(eventKey); diff --git a/src/Tab.tsx b/src/Tab.tsx index c64bce04b7..2b2acf629d 100644 --- a/src/Tab.tsx +++ b/src/Tab.tsx @@ -4,9 +4,10 @@ import React from 'react'; import TabContainer from './TabContainer'; import TabContent from './TabContent'; import TabPane from './TabPane'; +import { EventKey } from './types'; export interface TabProps extends React.ComponentPropsWithRef { - eventKey?: string; + eventKey?: EventKey; title: React.ReactNode; disabled?: boolean; tabClassName?: string; @@ -16,6 +17,7 @@ export interface TabProps extends React.ComponentPropsWithRef { class Tab extends React.Component { static propTypes = { title: PropTypes.node.isRequired, + eventKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), }; public static Container = TabContainer; diff --git a/src/TabContainer.tsx b/src/TabContainer.tsx index ed0c2eba90..2950ef5385 100644 --- a/src/TabContainer.tsx +++ b/src/TabContainer.tsx @@ -5,16 +5,17 @@ import { useUncontrolled } from 'uncontrollable'; import TabContext, { TabContextType } from './TabContext'; import SelectableContext from './SelectableContext'; import { SelectCallback, TransitionType } from './helpers'; +import { EventKey } from './types'; export interface TabContainerProps extends React.PropsWithChildren { id?: string; transition?: TransitionType; mountOnEnter?: boolean; unmountOnExit?: boolean; - generateChildId?: (eventKey: unknown, type: 'tab' | 'pane') => string; + generateChildId?: (eventKey: EventKey, type: 'tab' | 'pane') => string; onSelect?: SelectCallback; - activeKey?: unknown; - defaultActiveKey?: unknown; + activeKey?: EventKey; + defaultActiveKey?: EventKey; } const validateId: Validator = (props, ...args) => { @@ -92,7 +93,7 @@ const propTypes = { * * @controllable onSelect */ - activeKey: PropTypes.any, + activeKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), }; const TabContainer = (props: TabContainerProps) => { @@ -110,7 +111,7 @@ const TabContainer = (props: TabContainerProps) => { const generateChildId = useMemo( () => generateCustomChildId || - ((key, type) => (id ? `${id}-${type}-${key}` : null)), + ((key: EventKey, type: string) => (id ? `${id}-${type}-${key}` : null)), [id, generateCustomChildId], ); @@ -121,8 +122,8 @@ const TabContainer = (props: TabContainerProps) => { transition, mountOnEnter: mountOnEnter || false, unmountOnExit: unmountOnExit || false, - getControlledId: (key) => generateChildId(key, 'tabpane'), - getControllerId: (key) => generateChildId(key, 'tab'), + getControlledId: (key: EventKey) => generateChildId(key, 'tabpane'), + getControllerId: (key: EventKey) => generateChildId(key, 'tab'), }), [ onSelect, diff --git a/src/TabPane.tsx b/src/TabPane.tsx index 0e305e96b4..e9fd41ba85 100644 --- a/src/TabPane.tsx +++ b/src/TabPane.tsx @@ -12,9 +12,10 @@ import { TransitionCallbacks, TransitionType, } from './helpers'; +import { EventKey } from './types'; export interface TabPaneProps extends TransitionCallbacks, BsPrefixProps { - eventKey?: any; + eventKey?: EventKey; active?: boolean; transition?: TransitionType; mountOnEnter?: boolean; @@ -34,7 +35,7 @@ const propTypes = { /** * A key that associates the `TabPane` with it's controlling `NavLink`. */ - eventKey: PropTypes.any, + eventKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** * Toggles the active state of the TabPane, this is generally controlled by a diff --git a/src/Tabs.tsx b/src/Tabs.tsx index 2ac262c71e..617475fee9 100644 --- a/src/Tabs.tsx +++ b/src/Tabs.tsx @@ -13,10 +13,11 @@ import TabPane from './TabPane'; import { forEach, map } from './ElementChildren'; import { SelectCallback, TransitionType } from './helpers'; +import { EventKey } from './types'; export interface TabsProps extends React.PropsWithChildren { - activeKey?: unknown; - defaultActiveKey?: unknown; + activeKey?: EventKey; + defaultActiveKey?: EventKey; onSelect?: SelectCallback; variant?: 'tabs' | 'pills'; transition?: TransitionType; @@ -31,9 +32,9 @@ const propTypes = { * * @controllable onSelect */ - activeKey: PropTypes.any, + activeKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** The default active key that is selected on start */ - defaultActiveKey: PropTypes.any, + defaultActiveKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** * Navigation style diff --git a/src/types.tsx b/src/types.tsx index fe3763da1a..0c9654114f 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -30,3 +30,5 @@ export type Color = | 'light' | 'white' | 'muted'; + +export type EventKey = string | number; diff --git a/src/usePopperMarginModifiers.tsx b/src/usePopperMarginModifiers.tsx index 8913eb5ba5..e20d4f39e7 100644 --- a/src/usePopperMarginModifiers.tsx +++ b/src/usePopperMarginModifiers.tsx @@ -25,6 +25,7 @@ export default function usePopperMarginModifiers(): [ ] { const overlayRef = useRef(null); const margins = useRef(null); + const arrowMargins = useRef(null); const popoverClass = useBootstrapPrefix(undefined, 'popover'); const dropdownMenuClass = useBootstrapPrefix(undefined, 'dropdown-menu'); @@ -72,6 +73,30 @@ export default function usePopperMarginModifiers(): [ }; }, [margins]); + const arrow = useMemo(() => { + return { + name: 'arrow', + options: { + padding: () => { + // The options here are used for Popper 2.8.4 and up. + // For earlier version, padding is handled in popoverArrowMargins below. + if (!arrowMargins.current) { + return 0; + } + + const { top, right } = arrowMargins.current; + const padding = top || right; + return { + top: padding, + left: padding, + right: padding, + bottom: padding, + }; + }, + }, + }; + }, [arrowMargins]); + // Converts popover arrow margin to arrow modifier padding const popoverArrowMargins = useMemo(() => { return { @@ -83,20 +108,28 @@ export default function usePopperMarginModifiers(): [ if ( !overlayRef.current || !state.elements.arrow || - !hasClass(overlayRef.current, popoverClass) || - !state.modifiersData['arrow#persistent'] + !hasClass(overlayRef.current, popoverClass) ) { return undefined; } - const { top, right } = getMargins(state.elements.arrow); - const padding = top || right; - state.modifiersData['arrow#persistent'].padding = { - top: padding, - left: padding, - right: padding, - bottom: padding, - }; + if (state.modifiersData['arrow#persistent']) { + // @popperjs/core <= 2.8.3 uses arrow#persistent to pass padding to arrow modifier. + const { top, right } = getMargins(state.elements.arrow); + const padding = top || right; + state.modifiersData['arrow#persistent'].padding = { + top: padding, + left: padding, + right: padding, + bottom: padding, + }; + } else { + // @popperjs/core >= 2.8.4 gets the padding from the arrow modifier options, + // so we'll get the margins here, and let the arrow modifier above pass + // it to popper. + arrowMargins.current = getMargins(state.elements.arrow); + } + state.elements.arrow.style.margin = '0'; return () => { @@ -106,5 +139,5 @@ export default function usePopperMarginModifiers(): [ }; }, [popoverClass]); - return [callback, [offset, popoverArrowMargins]]; + return [callback, [offset, arrow, popoverArrowMargins]]; } diff --git a/test/NavSpec.js b/test/NavSpec.js index 2f208095d6..8fc48a4611 100644 --- a/test/NavSpec.js +++ b/test/NavSpec.js @@ -74,6 +74,21 @@ describe('