diff --git a/.storybook/.babelrc b/.storybook/.babelrc index aae6a85dcb..c7ebaf0dab 100644 --- a/.storybook/.babelrc +++ b/.storybook/.babelrc @@ -13,6 +13,6 @@ } ], "@babel/preset-react", - "@babel/preset-stage-1" + ["@babel/preset-stage-1", { "decoratorsLegacy": true }] ] } diff --git a/config/jest/jsTransform.js b/config/jest/jsTransform.js index 28f5a3aea5..a038a6769a 100644 --- a/config/jest/jsTransform.js +++ b/config/jest/jsTransform.js @@ -14,7 +14,7 @@ const babelOptions = { }, }, ], - '@babel/preset-stage-1', + ['@babel/preset-stage-1', { decoratorsLegacy: true }], '@babel/preset-react', ], // Adding in here otherwise Jest complains about no plugin for class diff --git a/package.json b/package.json index 66ec39d14e..3dbf1a7cd8 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "dependencies": { "classnames": "2.2.5", "downshift": "^1.31.14", - "flatpickr": "4.4.1", + "flatpickr": "4.5.0", "invariant": "^2.2.3", "lodash.debounce": "^4.0.8", "lodash.isequal": "^4.5.0", @@ -198,7 +198,7 @@ "presets": [ "./scripts/env", "@babel/preset-react", - "@babel/preset-stage-1" + ["@babel/preset-stage-1", { "decoratorsLegacy": true }] ] }, "prettier": { diff --git a/src/components/DataTable/TableToolbarSearch.js b/src/components/DataTable/TableToolbarSearch.js index 56bd26a8d7..10c72fd0bc 100644 --- a/src/components/DataTable/TableToolbarSearch.js +++ b/src/components/DataTable/TableToolbarSearch.js @@ -5,11 +5,20 @@ import Search from '../Search'; import setupGetInstanceId from './tools/instanceId'; const getInstanceId = setupGetInstanceId(); +const translationKeys = { + 'carbon.table.toolbar.search.label': 'Filter table', + 'carbon.table.toolbar.search.placeholder': 'Search', +}; + +const translateWithId = id => { + return translationKeys[id]; +}; const TableToolbarSearch = ({ className, searchContainerClass, onChange, + translateWithId: t, id = `data-table-search-${getInstanceId()}`, ...rest }) => { @@ -25,8 +34,8 @@ const TableToolbarSearch = ({ {...rest} small id={id} - labelText="Filter table" - placeHolderText="Search" + labelText={t('carbon.table.toolbar.search.label')} + placeHolderText={t('carbon.table.toolbar.search.placeholder')} onChange={onChange} /> @@ -45,12 +54,25 @@ TableToolbarSearch.propTypes = { * Provide an optional id for the search container */ id: PropTypes.string, + + /** + * Provide an optional className for the overal container of the Search + */ searchContainerClasses: PropTypes.string, /** * Provide an optional hook that is called each time the input is updated */ onChange: PropTypes.func, + + /** + * Provide custom text for the component for each translation id + */ + translateWithId: PropTypes.func.isRequired, +}; + +TableToolbarSearch.defaultProps = { + translateWithId, }; export default TableToolbarSearch; diff --git a/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap b/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap index 3c5e2d6830..c1dd9419d9 100644 --- a/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap +++ b/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap @@ -877,6 +877,7 @@ exports[`DataTable should render 1`] = `
) ) + .addWithInfo( + 'range with calendar and min/max dates', + ` + A range Date Picker consists of two input fields and a calendar, and optionally, the minDate and maxDate fields. + `, + () => ( + + + + + ) + ) .addWithInfo( 'fully controlled', ` diff --git a/src/components/DatePicker/DatePicker-test.js b/src/components/DatePicker/DatePicker-test.js index 7a4e4caf48..7040ab534d 100644 --- a/src/components/DatePicker/DatePicker-test.js +++ b/src/components/DatePicker/DatePicker-test.js @@ -223,6 +223,40 @@ describe('DatePicker', () => { expect(datepicker.props().locale).toBe('en'); }); }); + + describe('Date picker with minDate and maxDate', () => { + console.error = jest.genMockFn(); // eslint-disable-line no-console + + const wrapper = mount( + {}} + datePickerType="range" + className="extra-class" + minDate="01/01/2018" + maxDate="01/30/2018"> +
+ +
+
+ +
+
+ ); + + it('has the range date picker with min and max dates', () => { + const datepicker = wrapper.find('DatePicker'); + expect(datepicker.props().minDate).toBe('01/01/2018'); + expect(datepicker.props().maxDate).toBe('01/30/2018'); + }); + + it('should not have "console.error" being created', () => { + expect(console.error).not.toBeCalled(); // eslint-disable-line no-console + }); + }); }); describe('DatePickerSkeleton', () => { diff --git a/src/components/DatePicker/DatePicker.js b/src/components/DatePicker/DatePicker.js index 8b88721ba7..1134c4628f 100644 --- a/src/components/DatePicker/DatePicker.js +++ b/src/components/DatePicker/DatePicker.js @@ -185,6 +185,16 @@ export default class DatePicker extends Component { * The `change` event handler. */ onChange: PropTypes.func, + + /** + * The minimum date that a user can start picking from. + */ + minDate: PropTypes.string, + + /** + * The maximum date that a user can pick to. + */ + maxDate: PropTypes.string, }; static defaultProps = { @@ -217,17 +227,21 @@ export default class DatePicker extends Component { locale, appendTo, onChange, + minDate, + maxDate, } = this.props; if (datePickerType === 'single' || datePickerType === 'range') { const onHook = (electedDates, dateStr, instance) => { this.updateClassNames(instance); }; - this.cal = flatpickr(this.inputField, { + this.cal = new flatpickr(this.inputField, { appendTo, mode: datePickerType, allowInput: true, dateFormat: dateFormat, locale: l10n[locale], + minDate: minDate, + maxDate: maxDate, plugins: datePickerType === 'range' ? [new rangePlugin({ input: this.toInputField })] @@ -370,6 +384,8 @@ export default class DatePicker extends Component { short, light, datePickerType, + minDate, // eslint-disable-line + maxDate, // eslint-disable-line dateFormat, // eslint-disable-line onChange, // eslint-disable-line ...other diff --git a/src/components/OverflowMenu/OverflowMenu.js b/src/components/OverflowMenu/OverflowMenu.js index 737754690e..486616099f 100644 --- a/src/components/OverflowMenu/OverflowMenu.js +++ b/src/components/OverflowMenu/OverflowMenu.js @@ -257,8 +257,10 @@ export default class OverflowMenu extends Component { } }; - handleClickOutside = () => { - this.closeMenu(); + handleClickOutside = evt => { + if (!this._menuBody || !this._menuBody.contains(evt.target)) { + this.closeMenu(); + } }; closeMenu = () => { @@ -275,8 +277,11 @@ export default class OverflowMenu extends Component { * @private */ _bindMenuBody = menuBody => { - if (!menuBody && this._hFocusIn) { - this._hFocusIn = this._hFocusIn.release(); + if (!menuBody) { + this._menuBody = menuBody; + if (this._hFocusIn) { + this._hFocusIn = this._hFocusIn.release(); + } } }; @@ -287,6 +292,7 @@ export default class OverflowMenu extends Component { */ _handlePlace = menuBody => { if (menuBody) { + this._menuBody = menuBody; ( menuBody.querySelector('[data-floating-menu-primary-focus]') || menuBody ).focus(); diff --git a/src/components/Pagination/Pagination-test.js b/src/components/Pagination/Pagination-test.js index 1b539367ff..10cd09fa72 100644 --- a/src/components/Pagination/Pagination-test.js +++ b/src/components/Pagination/Pagination-test.js @@ -173,6 +173,13 @@ describe('Pagination', () => { expect(label.text()).toBe('1 of 10 pages'); }); + it('should render ranges and pages for no items', () => { + const pager = mount(); + const labels = pager.find('.bx--pagination__text'); + expect(labels.at(1).text()).toBe('0-0 of 0 items'); + expect(labels.at(2).text()).toBe('1 of 1 pages'); + }); + it('should have two buttons for navigation', () => { const buttons = right.find('.bx--pagination__button'); expect(buttons.length).toBe(2); diff --git a/src/components/Pagination/Pagination.js b/src/components/Pagination/Pagination.js index b7096e58d1..501b114a72 100644 --- a/src/components/Pagination/Pagination.js +++ b/src/components/Pagination/Pagination.js @@ -106,7 +106,8 @@ export default class Pagination extends Component { // used for case when page # is 0 or empty. For other cases // existing props will be used. page >= 0 && - page <= Math.ceil(this.props.totalItems / this.state.pageSize) + page <= + Math.max(Math.ceil(this.props.totalItems / this.state.pageSize), 1) ) { this.setState({ page }, () => this.pageInputDebouncer(this.state.page)); } @@ -138,7 +139,7 @@ export default class Pagination extends Component { return itemText(pageSize * (page - 1) + 1, page * pageSize); } else if (page > 0) { return itemRangeText( - pageSize * (page - 1) + 1, + Math.min(pageSize * (page - 1) + 1, totalItems), Math.min(page * pageSize, totalItems), totalItems ); @@ -159,7 +160,7 @@ export default class Pagination extends Component { if (pagesUnknown) { return pageText(page); } else if (page > 0) { - return pageRangeText(page, Math.ceil(totalItems / pageSize)); + return pageRangeText(page, Math.max(Math.ceil(totalItems / pageSize), 1)); } return defaultPageText(Math.ceil(totalItems / pageSize)); }; @@ -192,6 +193,7 @@ export default class Pagination extends Component { const statePage = this.state.page; const statePageSize = this.state.pageSize; + const totalPages = Math.max(Math.ceil(totalItems / statePageSize), 1); const classNames = classnames('bx--pagination', className); const inputId = id || this.uniqueId; @@ -240,9 +242,7 @@ export default class Pagination extends Component { className="bx--pagination__button bx--pagination__button--forward" onClick={this.incrementPage} disabled={ - this.props.disabled || - statePage === Math.ceil(totalItems / statePageSize) || - isLastPage + this.props.disabled || statePage === totalPages || isLastPage }> { expect(label.text()).toBe('1 of 10 pages'); }); + it('should render ranges and pages for no items', () => { + const pager = mount( + + ); + const labels = pager.find('.bx--pagination__text'); + expect(labels.at(1).text()).toBe('\u00a0|\u00a0\u00a00-0 of 0 items'); + expect(labels.at(2).text()).toBe('1 of 1 pages'); + }); + it('should have two buttons for navigation', () => { const buttons = right.find('.bx--pagination__button'); expect(buttons.length).toBe(2); diff --git a/src/components/PaginationV2/PaginationV2.js b/src/components/PaginationV2/PaginationV2.js index e34c91e2f7..b7b266dc4c 100644 --- a/src/components/PaginationV2/PaginationV2.js +++ b/src/components/PaginationV2/PaginationV2.js @@ -165,7 +165,8 @@ export default class PaginationV2 extends Component { const page = Number(evt.target.value); if ( page > 0 && - page <= Math.ceil(this.props.totalItems / this.state.pageSize) + page <= + Math.max(Math.ceil(this.props.totalItems / this.state.pageSize), 1) ) { this.setState({ page }); this.props.onChange({ page, pageSize: this.state.pageSize }); @@ -231,7 +232,7 @@ export default class PaginationV2 extends Component { } ); const inputId = id || this.uniqueId; - const totalPages = Math.ceil(totalItems / statePageSize); + const totalPages = Math.max(Math.ceil(totalItems / statePageSize), 1); const selectItems = this.renderSelectItems(totalPages); return ( @@ -260,7 +261,7 @@ export default class PaginationV2 extends Component { statePage * statePageSize ) : itemRangeText( - statePageSize * (statePage - 1) + 1, + Math.min(statePageSize * (statePage - 1) + 1, totalItems), Math.min(statePage * statePageSize, totalItems), totalItems )} @@ -270,7 +271,7 @@ export default class PaginationV2 extends Component { {pagesUnknown ? pageText(statePage) - : pageRangeText(statePage, Math.ceil(totalItems / statePageSize))} + : pageRangeText(statePage, totalPages)} - {children} - {renderButtons && ( - - )} - {renderButtons && ( - - )}
); } diff --git a/src/components/SearchFilterButton/SearchFilterButton.js b/src/components/SearchFilterButton/SearchFilterButton.js index 25c19412d8..f01f757be3 100644 --- a/src/components/SearchFilterButton/SearchFilterButton.js +++ b/src/components/SearchFilterButton/SearchFilterButton.js @@ -5,8 +5,12 @@ import Icon from '../Icon'; /** * The filter button for ``. */ -const SearchFilterButton = ({ labelText }) => ( -