From 10c1c77d31a930636fee8db70c2456fc17835808 Mon Sep 17 00:00:00 2001 From: Gilad Gray Date: Thu, 16 Mar 2017 10:14:23 -0700 Subject: [PATCH 01/22] update syntax highlighting (#848) * update highlights + language-typescript * ensure handlebars grammar * improved light syntax theme * constants and properties look more like VSCode theme * move comment last so it overrides others * revert moving `@each` for specificity reasons * color JSX ={} operator * "support" token (console.log), highlight constant properties (Intent.PRIMARY) --- gulp/util/text.js | 2 +- package.json | 4 +-- packages/docs/src/styles/_syntax.scss | 36 ++++++++++++++++++--------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/gulp/util/text.js b/gulp/util/text.js index 12e11e818b4..63627f3770f 100644 --- a/gulp/util/text.js +++ b/gulp/util/text.js @@ -38,7 +38,7 @@ var renderer = new marked.Renderer(); renderer.code = (textContent, language) => { // massage markdown language hint into TM language scope if (language === "html") { - language = "text.html.basic"; + language = "text.html.handlebars"; } else if (language != null && !/^source\./.test(language)) { language = `source.${language}`; } diff --git a/package.json b/package.json index d66e9a2937a..2b718d22827 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "gulp-tslint": "7.0.1", "gulp-typescript": "3.1.4", "gulp-util": "3.0.7", - "highlights": "1.4.1", + "highlights": "3.0.1", "http-server": "0.9.0", "istanbul-instrumenter-loader": "0.2.0", "json-loader": "0.5.4", @@ -71,7 +71,7 @@ "karma-webpack": "1.8.0", "kss": "2.4.0", "language-less": "github:atom/language-less", - "language-typescript": "github:giladgray/language-typescript", + "language-typescript": "github:giladgray/language-typescript#10.1.15", "lerna": "2.0.0-beta.30", "lodash": "4.16.4", "marked": "0.3.6", diff --git a/packages/docs/src/styles/_syntax.scss b/packages/docs/src/styles/_syntax.scss index d32533aee94..c6f04ace029 100644 --- a/packages/docs/src/styles/_syntax.scss +++ b/packages/docs/src/styles/_syntax.scss @@ -4,30 +4,29 @@ $syntax-background-color: $white; $syntax-text-color: $dark-gray1; $syntax-token-colors: ( "brace": $dark-gray3, - "comment": $gray2, - "constant": $turquoise2, + "support": $turquoise3, "entity.attribute-name": $orange3, - "entity.function": $blue2, + "entity.function": $blue3, "entity.id": $gold2, "entity.pseudo-class": $rose2, - "entity.pseudo-element": $forest2, - "entity.tag": $forest2, - "keyword": $violet3, + "entity.pseudo-element": $forest3, + "entity.tag": $forest3, + "keyword": $violet4, "numeric": $rose2, - "operator": $violet2, + "operator": $violet4, "punctuation": $dark-gray3, - "storage": $violet3, - "string": $lime1, + "storage": $violet4, + "string": $lime2, "type": $gold2, "variable": $turquoise2, + "comment": $gray2, ); $dark-syntax-background-color: $dark-gray2; $dark-syntax-text-color: $gray5; $dark-syntax-token-colors: ( "brace": $light-gray5, - "comment": $gray2, - "constant": $turquoise5, + "support": $turquoise5, "entity.attribute-name": $orange4, "entity.function": $blue4, "entity.id": $gold5, @@ -42,12 +41,14 @@ $dark-syntax-token-colors: ( "string": $lime4, "type": $gold4, "variable": $turquoise3, + "comment": $gray2, ); // given a map of TextMate tokens to colors @mixin syntax($colors) { - // this one is a very special case, token-wise: + // this one is a very special case and must come before @each for specificity .entity.name, + .entity.inherited-class, .meta.name, .support.type { color: map-get($colors, "type"); @@ -59,10 +60,21 @@ $dark-syntax-token-colors: ( } } + .variable.property, .support.type.scss, .punctuation.definition.css { color: inherit; } + + .variable.constant, + .variable.language { + color: map-get($colors, "support"); + } + + // JSX ={prop} punctuation + .punctuation.section.embedded { + color: map-get($colors, "operator"); + } } // actually generate the two themes: From a9bc78bc7aa59a336c9a025dc1367cd1f2371006 Mon Sep 17 00:00:00 2001 From: Chris Lewis Date: Thu, 16 Mar 2017 12:13:52 -0700 Subject: [PATCH 02/22] [Table] Pass custom className to Table, Column, ColumnHeaderCell elements (#851) * Add this.props.className to table container * Add unit test * Remove unnecessary name from Column in unit test * Pass custom className to ColumnHeaderCell container * Respond to CR feedback --- packages/table/src/headers/columnHeaderCell.tsx | 4 ++-- packages/table/src/table.tsx | 8 ++++++-- packages/table/test/columnHeaderCellTests.tsx | 7 +++++++ packages/table/test/columnTests.tsx | 11 +++++++++++ packages/table/test/harness.ts | 4 ++++ packages/table/test/tableTests.tsx | 13 +++++++++++++ 6 files changed, 43 insertions(+), 4 deletions(-) diff --git a/packages/table/src/headers/columnHeaderCell.tsx b/packages/table/src/headers/columnHeaderCell.tsx index 55e3b8dac2e..fac22351288 100644 --- a/packages/table/src/headers/columnHeaderCell.tsx +++ b/packages/table/src/headers/columnHeaderCell.tsx @@ -131,12 +131,12 @@ export class ColumnHeaderCell extends React.Component diff --git a/packages/table/src/table.tsx b/packages/table/src/table.tsx index ee5916ea937..fbfca7fde0b 100644 --- a/packages/table/src/table.tsx +++ b/packages/table/src/table.tsx @@ -369,10 +369,14 @@ export class Table extends AbstractComponent { } public render() { - const { isRowHeaderShown } = this.props; + const { className, isRowHeaderShown } = this.props; this.validateGrid(); return ( -
+
{isRowHeaderShown ? this.renderMenu() : undefined} {this.renderColumnHeader()} diff --git a/packages/table/test/columnHeaderCellTests.tsx b/packages/table/test/columnHeaderCellTests.tsx index 40ed8dd2bde..c0bf16f9330 100644 --- a/packages/table/test/columnHeaderCellTests.tsx +++ b/packages/table/test/columnHeaderCellTests.tsx @@ -33,6 +33,13 @@ describe("", () => { expect(text).to.equal("B"); }); + it("renders with custom className if provided", () => { + const CLASS_NAME = "my-custom-class-name"; + const table = harness.mount(); + const hasCustomClass = table.find(`.${Classes.TABLE_HEADER}`, 0).hasClass(CLASS_NAME); + expect(hasCustomClass).to.be.true; + }); + describe("Custom renderer", () => { it("renders custom name", () => { const renderColumnHeader = (columnIndex: number) => { diff --git a/packages/table/test/columnTests.tsx b/packages/table/test/columnTests.tsx index da24631442a..00dbe5a050b 100644 --- a/packages/table/test/columnTests.tsx +++ b/packages/table/test/columnTests.tsx @@ -84,4 +84,15 @@ describe("Column", () => { const col2cells = table.element.queryAll(`.${Classes.columnCellIndexClass(2)}`); col2cells.forEach((cell) => expectCellLoading(cell, CellType.BODY_CELL, false)); }); + + it("passes custom class name to renderer", () => { + const CLASS_NAME = "my-custom-class-name"; + const table = harness.mount( + + +
, + ); + const hasCustomClass = table.find(`.${Classes.TABLE_HEADER}`, 0).hasClass(CLASS_NAME); + expect(hasCustomClass).to.be.true; + }); }); diff --git a/packages/table/test/harness.ts b/packages/table/test/harness.ts index b36ab58c686..07c59f95c31 100644 --- a/packages/table/test/harness.ts +++ b/packages/table/test/harness.ts @@ -60,6 +60,10 @@ export class ElementHarness { return new ElementHarness(this.findElement(query, nth)); } + public hasClass(className: string) { + return this.element.classList.contains(className); + } + public bounds() { return this.element.getBoundingClientRect(); } diff --git a/packages/table/test/tableTests.tsx b/packages/table/test/tableTests.tsx index 355750d6aeb..b55c8ca50f8 100644 --- a/packages/table/test/tableTests.tsx +++ b/packages/table/test/tableTests.tsx @@ -38,6 +38,19 @@ describe("", () => { expect(table.find(`.${Classes.TABLE_COLUMN_NAME_TEXT}`, 1).text()).to.equal("B"); }); + it("Adds custom className to table container", () => { + const CLASS_NAME = "my-custom-class-name"; + const table = harness.mount( +
+ + + +
, + ); + const hasCustomClass = table.find(`.${Classes.TABLE_CONTAINER}`, 0).hasClass(CLASS_NAME); + expect(hasCustomClass).to.be.true; + }); + it("Renders without ghost cells", () => { const table = harness.mount( From d674c73509259480f50cecc8cd37ab2f76f8bbc4 Mon Sep 17 00:00:00 2001 From: Chris Lewis Date: Fri, 17 Mar 2017 10:33:40 -0700 Subject: [PATCH 03/22] [DateRangeInput] Bugfix: don't shift month view on day hover (#857) * Add dateUtils#areRangesEqual plus unit tests * Fix the month-shift-on-day-hover bug and add a test --- packages/datetime/src/common/dateUtils.ts | 14 +++++++ packages/datetime/src/dateRangePicker.tsx | 8 ++-- .../datetime/test/common/dateUtilsTests.tsx | 42 +++++++++++++++++++ .../datetime/test/dateRangePickerTests.tsx | 19 +++++++++ packages/datetime/test/index.ts | 1 + 5 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 packages/datetime/test/common/dateUtilsTests.tsx diff --git a/packages/datetime/src/common/dateUtils.ts b/packages/datetime/src/common/dateUtils.ts index 62947999228..094ec217131 100644 --- a/packages/datetime/src/common/dateUtils.ts +++ b/packages/datetime/src/common/dateUtils.ts @@ -26,6 +26,20 @@ export function areEqual(date1: Date, date2: Date) { } } +export function areRangesEqual(dateRange1: DateRange, dateRange2: DateRange) { + if (dateRange1 == null && dateRange2 == null) { + return true; + } else if (dateRange1 == null || dateRange2 == null) { + return false; + } else { + const [start1, end1] = dateRange1; + const [start2, end2] = dateRange2; + const areStartsEqual = (start1 == null && start2 == null) || areSameDay(start1, start2); + const areEndsEqual = (end1 == null && end2 == null) || areSameDay(end1, end2); + return areStartsEqual && areEndsEqual; + } +} + export function areSameDay(date1: Date, date2: Date) { return date1 != null && date2 != null diff --git a/packages/datetime/src/dateRangePicker.tsx b/packages/datetime/src/dateRangePicker.tsx index 1795474a6d8..5cd351eb663 100644 --- a/packages/datetime/src/dateRangePicker.tsx +++ b/packages/datetime/src/dateRangePicker.tsx @@ -279,9 +279,11 @@ export class DateRangePicker public componentWillReceiveProps(nextProps: IDateRangePickerProps) { super.componentWillReceiveProps(nextProps); - const nextState = getStateChange(this.props.value, nextProps.value, this.state, - nextProps.contiguousCalendarMonths); - this.setState(nextState); + if (!DateUtils.areRangesEqual(this.props.value, nextProps.value)) { + const nextState = getStateChange(this.props.value, nextProps.value, this.state, + nextProps.contiguousCalendarMonths); + this.setState(nextState); + } } protected validateProps(props: IDateRangePickerProps) { diff --git a/packages/datetime/test/common/dateUtilsTests.tsx b/packages/datetime/test/common/dateUtilsTests.tsx new file mode 100644 index 00000000000..3985d1d090b --- /dev/null +++ b/packages/datetime/test/common/dateUtilsTests.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * Licensed under the BSD-3 License as modified (the “License”); you may obtain a copy + * of the license at https://github.com/palantir/blueprint/blob/master/LICENSE + * and https://github.com/palantir/blueprint/blob/master/PATENTS + */ + +import { expect } from "chai"; + +import { DateRange } from "../../src/"; +import * as DateUtils from "../../src/common/dateUtils"; +import { Months } from "../../src/common/months"; + +describe("dateUtils", () => { + describe("areRangesEqual", () => { + const DATE_1 = new Date(2017, Months.JANUARY, 1); + const DATE_2 = new Date(2017, Months.JANUARY, 2); + const DATE_3 = new Date(2017, Months.JANUARY, 3); + const DATE_4 = new Date(2017, Months.JANUARY, 4); + + describe("returns true for", () => { + runTest("null and null", null, null, true); + runTest("[null, null] and [null, null]", [null, null], [null, null], true); + runTest("[DATE_1, DATE_2] and [DATE_1, DATE_2]", [DATE_1, DATE_2], [DATE_1, DATE_2], true); + }); + + describe("returns false for", () => { + runTest("null and [null, null]", null, [null, null], false); + runTest("[DATE_1, null] and [DATE_2, null]", [DATE_1, null], [DATE_2, null], false); + runTest("[DATE_1, null] and [null, null]", [DATE_1, null], [null, null], false); + runTest("[DATE_1, DATE_2] and [DATE_1, DATE_4]", [DATE_1, DATE_2], [DATE_1, DATE_4], false); + runTest("[DATE_1, DATE_4] and [DATE_2, DATE_4]", [DATE_1, DATE_4], [DATE_2, DATE_4], false); + runTest("[DATE_1, DATE_2] and [DATE_3, DATE_4]", [DATE_1, DATE_2], [DATE_3, DATE_4], false); + }); + + function runTest(description: string, dateRange1: DateRange, dateRange2: DateRange, expectedResult: boolean) { + it(description, () => { + expect(DateUtils.areRangesEqual(dateRange1, dateRange2)).to.equal(expectedResult); + }); + } + }); +}); diff --git a/packages/datetime/test/dateRangePickerTests.tsx b/packages/datetime/test/dateRangePickerTests.tsx index 9138fa18a50..76af345aa0a 100644 --- a/packages/datetime/test/dateRangePickerTests.tsx +++ b/packages/datetime/test/dateRangePickerTests.tsx @@ -529,6 +529,25 @@ describe("", () => { assert.isNull(getHoveredRangeEndDayElement()); }); }); + + it("hovering on day in month prior to selected start date's month, should not shift calendar view", () => { + const INITIAL_MONTH = Months.MARCH; + const MONTH_OUT_OF_VIEW = Months.JANUARY; + renderDateRangePicker({ initialMonth: new Date(2017, INITIAL_MONTH, 1) }); + + clickDay(14); + clickDay(18); + + TestUtils.Simulate.change(getMonthSelect(), { target: { value: MONTH_OUT_OF_VIEW } } as any); + + // hover on left month + mouseEnterDay(14); + assert.equal(dateRangePicker.state.leftView.getMonth(), MONTH_OUT_OF_VIEW); + + // hover on right month + mouseEnterDay(14, false); + assert.equal(dateRangePicker.state.leftView.getMonth(), MONTH_OUT_OF_VIEW); + }); }); describe("when controlled", () => { diff --git a/packages/datetime/test/index.ts b/packages/datetime/test/index.ts index deb5df50b50..6c1ffcfe454 100644 --- a/packages/datetime/test/index.ts +++ b/packages/datetime/test/index.ts @@ -4,6 +4,7 @@ import "../src"; +import "./common/dateUtilsTests"; import "./dateInputTests"; import "./datePickerCaptionTests"; import "./datePickerTests"; From e0c69f7f8c1fc08bb397b2c8dac767960e84e319 Mon Sep 17 00:00:00 2001 From: Chris Lewis Date: Fri, 17 Mar 2017 13:18:29 -0700 Subject: [PATCH 04/22] [DateRangeInput] Make on-click behavior more intuitive (#856) * If both dates defined, just deselect other boundary on click * Clicking a selected endpoint deselects it regardless of focused field * Update tests --- packages/datetime/src/dateRangeInput.tsx | 28 +++++++-- packages/datetime/src/dateRangePicker.tsx | 18 +++--- .../datetime/test/dateRangeInputTests.tsx | 63 ++++++++++--------- 3 files changed, 67 insertions(+), 42 deletions(-) diff --git a/packages/datetime/src/dateRangeInput.tsx b/packages/datetime/src/dateRangeInput.tsx index b62b42cc62e..67b34538582 100644 --- a/packages/datetime/src/dateRangeInput.tsx +++ b/packages/datetime/src/dateRangeInput.tsx @@ -25,6 +25,7 @@ import { DateRange, DateRangeBoundary, fromDateRangeToMomentDateRange, + fromDateToMoment, fromMomentToDate, isMomentInRange, isMomentNull, @@ -349,7 +350,7 @@ export class DateRangeInput extends AbstractComponent { + private handleDateRangePickerHoverChange = (hoveredRange: DateRange, day: Date) => { // ignore mouse events in the date-range picker if the popover is animating closed. if (!this.state.isOpen) { return; @@ -380,6 +381,9 @@ export class DateRangeInput extends AbstractComponent !isMomentNull(d)); const isModifyingStartBoundary = boundaryToModify === DateRangeBoundary.START; + const isModifyingEndBoundary = !isModifyingStartBoundary; + + const hoveredDay = fromDateToMoment(day); // pull the existing values from state; we may not overwrite them. let { isStartInputFocused, isEndInputFocused } = this.state; @@ -396,7 +400,11 @@ export class DateRangeInput extends AbstractComponent void; + onHoverChange?: (hoveredDates: DateRange, hoveredDay: Date) => void; /** * Whether shortcuts to quickly select a range of dates are displayed or not. @@ -390,18 +390,18 @@ export class DateRangePicker } const nextHoverValue = this.getNextValue(this.state.value, day); this.setState({ hoverValue: nextHoverValue }); - Utils.safeInvoke(this.props.onHoverChange, nextHoverValue); + Utils.safeInvoke(this.props.onHoverChange, nextHoverValue, day); } private handleDayMouseLeave = - (_e: React.SyntheticEvent, _day: Date, modifiers: IDatePickerDayModifiers) => { + (_e: React.SyntheticEvent, day: Date, modifiers: IDatePickerDayModifiers) => { if (modifiers.disabled) { return; } const nextHoverValue = undefined as DateRange; this.setState({ hoverValue: nextHoverValue }); - Utils.safeInvoke(this.props.onHoverChange, nextHoverValue); + Utils.safeInvoke(this.props.onHoverChange, nextHoverValue, day); } private handleDayClick = (e: React.SyntheticEvent, day: Date, modifiers: IDatePickerDayModifiers) => { @@ -440,8 +440,10 @@ export class DateRangePicker nextValue = this.createRangeForBoundary(nextBoundaryDate, null, boundary); } else if (boundaryDate == null && otherBoundaryDate != null) { if (DateUtils.areSameDay(day, otherBoundaryDate)) { - const nextOtherBoundaryDate = allowSingleDayRange ? otherBoundaryDate : null; - nextValue = this.createRangeForBoundary(day, nextOtherBoundaryDate, boundary); + const [nextBoundaryDate, nextOtherBoundaryDate] = (allowSingleDayRange) + ? [otherBoundaryDate, otherBoundaryDate] + : [null, null]; + nextValue = this.createRangeForBoundary(nextBoundaryDate, nextOtherBoundaryDate, boundary); } else if (this.isDateOverlappingOtherBoundary(day, otherBoundaryDate, boundary)) { nextValue = this.createRangeForBoundary(otherBoundaryDate, day, boundary); } else { @@ -454,8 +456,10 @@ export class DateRangePicker const nextOtherBoundaryDate = isSingleDayRangeSelected ? null : otherBoundaryDate; nextValue = this.createRangeForBoundary(null, nextOtherBoundaryDate, boundary); } else if (DateUtils.areSameDay(day, otherBoundaryDate)) { + // special case: it's more intuitive to deselect the other boundary date than to + // specify a new date for this boundary const nextOtherBoundaryDate = (allowSingleDayRange) ? otherBoundaryDate : null; - nextValue = this.createRangeForBoundary(day, nextOtherBoundaryDate, boundary); + nextValue = this.createRangeForBoundary(boundaryDate, nextOtherBoundaryDate, boundary); } else if (this.isDateOverlappingOtherBoundary(day, otherBoundaryDate, boundary)) { nextValue = this.createRangeForBoundary(day, null, boundary); } else { diff --git a/packages/datetime/test/dateRangeInputTests.tsx b/packages/datetime/test/dateRangeInputTests.tsx index c44964d23ed..03d852e78af 100644 --- a/packages/datetime/test/dateRangeInputTests.tsx +++ b/packages/datetime/test/dateRangeInputTests.tsx @@ -929,12 +929,12 @@ describe("", () => { dayElement.simulate("mouseenter"); }); - it("shows [null, ] in input fields", () => { - assertInputTextsEqual(root, "", DATE_CONFIG.str); + it("shows [null, null] in input fields", () => { + assertInputTextsEqual(root, "", ""); }); - it("keeps focus on end field", () => { - assertEndInputFocused(root); + it("moves focus to start field", () => { + assertStartInputFocused(root); }); describe("on click", () => { @@ -942,8 +942,8 @@ describe("", () => { dayElement.simulate("click"); }); - it("sets selection to [null, ] on click", () => { - assertInputTextsEqual(root, "", DATE_CONFIG.str); + it("sets selection to [null, null] on click", () => { + assertInputTextsEqual(root, "", ""); }); it("leaves focus on start field", () => { @@ -1078,12 +1078,12 @@ describe("", () => { dayElement.simulate("mouseenter"); }); - it("shows [, null] in input fields", () => { - assertInputTextsEqual(root, DATE_CONFIG.str, ""); + it("shows [null, null] in input fields", () => { + assertInputTextsEqual(root, "", ""); }); - it("keeps focus on start field", () => { - assertStartInputFocused(root); + it("moves focus to end field", () => { + assertEndInputFocused(root); }); describe("on click", () => { @@ -1091,12 +1091,12 @@ describe("", () => { dayElement.simulate("click"); }); - it("sets selection to [, null] on click", () => { - assertInputTextsEqual(root, DATE_CONFIG.str, ""); + it("sets selection to [null, null] on click", () => { + assertInputTextsEqual(root, "", ""); }); - it("keeps focus on end field", () => { - assertEndInputFocused(root); + it("moves focus to start field", () => { + assertStartInputFocused(root); }); }); @@ -1458,12 +1458,12 @@ describe("", () => { dayElement.simulate("mouseenter"); }); - it("shows [, null] in input fields", () => { - assertInputTextsEqual(root, DATE_CONFIG.str, ""); + it("shows [, null] in input fields", () => { + assertInputTextsEqual(root, SELECTED_RANGE[0].str, ""); }); - it("keeps focus on start field", () => { - assertStartInputFocused(root); + it("moves focus to end field", () => { + assertEndInputFocused(root); }); describe("on click", () => { @@ -1471,11 +1471,11 @@ describe("", () => { dayElement.simulate("click"); }); - it("sets selection to [, null]", () => { - assertInputTextsEqual(root, DATE_CONFIG.str, ""); + it("sets selection to [, null]", () => { + assertInputTextsEqual(root, SELECTED_RANGE[0].str, ""); }); - it("moves focus to end field", () => { + it("keeps focus on end field", () => { assertEndInputFocused(root); }); }); @@ -1489,7 +1489,7 @@ describe("", () => { assertInputTextsEqual(root, SELECTED_RANGE[0].str, SELECTED_RANGE[1].str); }); - it("keeps focus on start field", () => { + it("moves focus back to start field", () => { assertStartInputFocused(root); }); }); @@ -1644,12 +1644,12 @@ describe("", () => { dayElement.simulate("mouseenter"); }); - it("shows [null, ] in input fields", () => { - assertInputTextsEqual(root, "", DATE_CONFIG.str); + it("shows [null, ] in input fields", () => { + assertInputTextsEqual(root, "", SELECTED_RANGE[1].str); }); - it("keeps focus on end field", () => { - assertEndInputFocused(root); + it("moves focus to start field", () => { + assertStartInputFocused(root); }); describe("on click", () => { @@ -1657,11 +1657,11 @@ describe("", () => { dayElement.simulate("click"); }); - it("sets selection to [null, ]", () => { - assertInputTextsEqual(root, "", DATE_CONFIG.str); + it("sets selection to [null, ]", () => { + assertInputTextsEqual(root, "", SELECTED_RANGE[1].str); }); - it("moves focus to start field", () => { + it("keeps focus on start field", () => { assertStartInputFocused(root); }); }); @@ -1675,7 +1675,7 @@ describe("", () => { assertInputTextsEqual(root, SELECTED_RANGE[0].str, SELECTED_RANGE[1].str); }); - it("keeps focus on end field", () => { + it("moves focus back to end field", () => { assertEndInputFocused(root); }); }); @@ -1998,8 +1998,9 @@ describe("", () => { const value = [START_DATE, null] as DateRange; const { root, getDayElement } = wrap(); - root.setState({ isOpen: true }); + // popover opens on focus + getStartInput(root).simulate("focus"); getDayElement(START_DAY).simulate("click"); assertDateRangesEqual(onChange.getCall(0).args[0], [null, null]); From 810f5d1898023a0742c8cdc58231974179de77e5 Mon Sep 17 00:00:00 2001 From: Chris Lewis Date: Fri, 17 Mar 2017 15:21:31 -0700 Subject: [PATCH 05/22] [DateRangeInput] Add selectAllOnFocus prop (#858) * Implement selectAllOnFocus prop * Add selectAllOnFocus control to DRI example * Write unit tests (some are skipped b/c don't work in Phantom) --- .../examples/dateRangeInputExample.tsx | 9 +++ packages/datetime/src/dateRangeInput.tsx | 72 +++++++++++++------ .../datetime/test/dateRangeInputTests.tsx | 52 +++++++++++++- 3 files changed, 109 insertions(+), 24 deletions(-) diff --git a/packages/datetime/examples/dateRangeInputExample.tsx b/packages/datetime/examples/dateRangeInputExample.tsx index 48e0850459c..7c9fbf6c201 100644 --- a/packages/datetime/examples/dateRangeInputExample.tsx +++ b/packages/datetime/examples/dateRangeInputExample.tsx @@ -16,6 +16,7 @@ export interface IDateRangeInputExampleState { closeOnSelection?: boolean; disabled?: boolean; format?: string; + selectAllOnFocus?: boolean; } export class DateRangeInputExample extends BaseExample { @@ -23,11 +24,13 @@ export class DateRangeInputExample extends BaseExample this.setState({ disabled })); private toggleFormat = handleStringChange((format) => this.setState({ format })); private toggleSelection = handleBooleanChange((closeOnSelection) => this.setState({ closeOnSelection })); + private toggleSelectAllOnFocus = handleBooleanChange((selectAllOnFocus) => this.setState({ selectAllOnFocus })); protected renderExample() { return ; @@ -48,6 +51,12 @@ export class DateRangeInputExample extends BaseExample, + , ], [ ", () => { expect(root.state("isOpen")).to.be.false; }); + describe("selectAllOnFocus", () => { + + it("if false (the default), does not select any text on focus", () => { + const attachTo = document.createElement("div"); + const { root } = wrap(, attachTo); + + const startInput = getStartInput(root); + startInput.simulate("focus"); + + const startInputNode = attachTo.querySelectorAll("input")[0] as HTMLInputElement; + expect(startInputNode.selectionStart).to.equal(startInputNode.selectionEnd); + }); + + // selectionStart/End works in Chrome but not Phantom. disabling to not fail builds. + it.skip("if true, selects all text on focus", () => { + const attachTo = document.createElement("div"); + const { root } = wrap( + , attachTo); + + const startInput = getStartInput(root); + startInput.simulate("focus"); + + const startInputNode = attachTo.querySelectorAll("input")[0] as HTMLInputElement; + expect(startInputNode.selectionStart).to.equal(0); + expect(startInputNode.selectionEnd).to.equal(START_STR.length); + }); + + it.skip("if true, selects all text on day mouseenter in calendar", () => { + const attachTo = document.createElement("div"); + const { root, getDayElement } = wrap( + , attachTo); + + root.setState({ isOpen: true }); + // getDay is 0-indexed, but getDayElement is 1-indexed + getDayElement(START_DATE_2.getDay() + 1).simulate("mouseenter"); + + const startInputNode = attachTo.querySelectorAll("input")[0] as HTMLInputElement; + expect(startInputNode.selectionStart).to.equal(0); + expect(startInputNode.selectionEnd).to.equal(START_STR.length); + }); + }); + describe("when uncontrolled", () => { it("Shows empty fields when defaultValue is [null, null]", () => { const { root } = wrap(); @@ -2114,8 +2162,8 @@ describe("", () => { expect(actualEnd).to.equal(expectedEnd); } - function wrap(dateRangeInput: JSX.Element) { - const wrapper = mount(dateRangeInput); + function wrap(dateRangeInput: JSX.Element, attachTo?: HTMLElement) { + const wrapper = mount(dateRangeInput, { attachTo }); return { getDayElement: (dayNumber = 1, fromLeftMonth = true) => { const monthElement = wrapper.find(".DayPicker-Month").at(fromLeftMonth ? 0 : 1); From 9af4c4206a3eafb3d37ec75eb9927a70863188cc Mon Sep 17 00:00:00 2001 From: Gilad Gray Date: Mon, 20 Mar 2017 10:56:08 -0700 Subject: [PATCH 06/22] [Tabs2] fix NPEs with null children (#860) * isTab checks existence first * check for empty children in initial id, fix tests * use length check, add test for default selectedTabId --- packages/core/src/components/tabs2/tabs2.tsx | 7 +++-- packages/core/test/tabs/tabs2Tests.tsx | 33 ++++++++++++++++++-- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/core/src/components/tabs2/tabs2.tsx b/packages/core/src/components/tabs2/tabs2.tsx index 299307cf6a0..98d0fe39743 100644 --- a/packages/core/src/components/tabs2/tabs2.tsx +++ b/packages/core/src/components/tabs2/tabs2.tsx @@ -157,6 +157,7 @@ export class Tabs2 extends AbstractComponent { } private getInitialSelectedTabId() { + // NOTE: providing an unknown ID will hide the selection const { defaultSelectedTabId, selectedTabId } = this.props; if (selectedTabId !== undefined) { return selectedTabId; @@ -164,8 +165,8 @@ export class Tabs2 extends AbstractComponent { return defaultSelectedTabId; } else { // select first tab in absence of user input - // NOTE: providing an unknown ID will hide the selection - return this.getTabChildren()[0].props.id; + const tabs = this.getTabChildren(); + return tabs.length === 0 ? undefined : tabs[0].props.id; } } @@ -290,5 +291,5 @@ function isEventKeyCode(e: React.KeyboardEvent, ...codes: number[]) } function isTab(child: React.ReactChild): child is TabElement { - return (child as JSX.Element).type === Tab2; + return child != null && (child as JSX.Element).type === Tab2; } diff --git a/packages/core/test/tabs/tabs2Tests.tsx b/packages/core/test/tabs/tabs2Tests.tsx index 21a0354ecd1..3fb28872499 100644 --- a/packages/core/test/tabs/tabs2Tests.tsx +++ b/packages/core/test/tabs/tabs2Tests.tsx @@ -33,6 +33,33 @@ describe("", () => { afterEach(() => testsContainerElement.remove()); + it("gets by without children", () => { + assert.doesNotThrow(() => mount()); + }); + + it("supports non-existent children", () => { + assert.doesNotThrow(() => mount( + + {null} + + {undefined} + + , + )); + }); + + it("default selectedTabId is first non-null Tab id", () => { + const wrapper = mount( + + {null} + {