From e35bc4933e9ff6cdeca9fc11f2f5db32340ce970 Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Mon, 10 Dec 2018 20:31:57 +0000 Subject: [PATCH 01/11] Convert pager service to TS --- src/services/paging/{index.js => index.ts} | 0 .../paging/{pager.test.js => pager.test.ts} | 9 ++++- src/services/paging/{pager.js => pager.ts} | 40 ++++++++++++------- 3 files changed, 33 insertions(+), 16 deletions(-) rename src/services/paging/{index.js => index.ts} (100%) rename src/services/paging/{pager.test.js => pager.test.ts} (94%) rename src/services/paging/{pager.js => pager.ts} (71%) diff --git a/src/services/paging/index.js b/src/services/paging/index.ts similarity index 100% rename from src/services/paging/index.js rename to src/services/paging/index.ts diff --git a/src/services/paging/pager.test.js b/src/services/paging/pager.test.ts similarity index 94% rename from src/services/paging/pager.test.js rename to src/services/paging/pager.test.ts index aab3b143819..d0371fb6a27 100644 --- a/src/services/paging/pager.test.js +++ b/src/services/paging/pager.test.ts @@ -5,14 +5,17 @@ import { describe('Pager', () => { describe('constructor', () => { test('throws error if missing totalItems', () => { + // @ts-ignore expect(() => new Pager()).toThrow(); }); test('throws error if missing itemsPerPage', () => { + // @ts-ignore expect(() => new Pager(10)).toThrow(); }); test('throws error if non-number initialPageIndex', () => { + // @ts-ignore expect(() => new Pager(10, 3, 'invalid argument')).toThrow(); }); }); @@ -21,7 +24,8 @@ describe('Pager', () => { const totalItems = 10; const itemsPerPage = 3; const initialPageIndex = 1; - let pager; + // Initialising this to a Pager straight away keep TS happy. + let pager = new Pager(totalItems, itemsPerPage, initialPageIndex); beforeEach(() => { pager = new Pager(totalItems, itemsPerPage, initialPageIndex); @@ -152,7 +156,7 @@ describe('Pager', () => { expect(pager.getLastItemIndex()).toBe(3); }); - test(`doesn't update current page`, () => { + test("doesn't update current page", () => { pager.setItemsPerPage(2); expect(pager.getCurrentPageIndex()).toBe(initialPageIndex); }); @@ -161,6 +165,7 @@ describe('Pager', () => { describe('behavior', () => { describe('when there are no items', () => { + // TODO }); }); }); diff --git a/src/services/paging/pager.js b/src/services/paging/pager.ts similarity index 71% rename from src/services/paging/pager.js rename to src/services/paging/pager.ts index c9c5de8e14c..f84034ddbfd 100644 --- a/src/services/paging/pager.js +++ b/src/services/paging/pager.ts @@ -1,33 +1,45 @@ +import { isNumber } from '../predicate'; + export class Pager { - constructor(totalItems, itemsPerPage, initialPageIndex = 0) { - if (isNaN(parseInt(totalItems, 10))) { + currentPageIndex: number; + firstItemIndex: number; + itemsPerPage: number; + lastItemIndex: number; + totalItems: number; + totalPages: number; + + constructor(totalItems: number, itemsPerPage: number, initialPageIndex: number = 0) { + if (!isNumber(totalItems) || isNaN(totalItems)) { throw new Error('Please provide a number of totalItems'); } - if (isNaN(parseInt(itemsPerPage, 10))) { + if (!isNumber(itemsPerPage) || isNaN(itemsPerPage)) { throw new Error('Please provide a number of itemsPerPage'); } - if (isNaN(parseInt(initialPageIndex, 10))) { + if (!isNumber(initialPageIndex) || isNaN(initialPageIndex)) { throw new Error('Please provide a number of initialPageIndex'); } - this.totalItems = totalItems; - this.itemsPerPage = itemsPerPage; this.currentPageIndex = initialPageIndex; + this.firstItemIndex = -1; + this.itemsPerPage = itemsPerPage; + this.lastItemIndex = -1; + this.totalItems = totalItems; + this.totalPages = 0; this.update(); } - setTotalItems = (totalItems) => { + setTotalItems = (totalItems: number) => { this.totalItems = totalItems; this.update(); - }; + } - setItemsPerPage = (itemsPerPage) => { + setItemsPerPage = (itemsPerPage: number) => { this.itemsPerPage = itemsPerPage; this.update(); - }; + } isPageable = () => this.firstItemIndex !== -1; @@ -45,13 +57,13 @@ export class Pager { goToNextPage = () => { this.goToPageIndex(this.currentPageIndex + 1); - }; + } goToPreviousPage = () => { this.goToPageIndex(this.currentPageIndex - 1); - }; + } - goToPageIndex = (pageIndex) => { + goToPageIndex = (pageIndex: number) => { this.currentPageIndex = pageIndex; this.update(); } @@ -73,5 +85,5 @@ export class Pager { // Find the range of visible items on the current page. this.firstItemIndex = this.currentPageIndex * this.itemsPerPage; this.lastItemIndex = Math.min(this.firstItemIndex + this.itemsPerPage, this.totalItems) - 1; - }; + } } From d9d8aa07a50a48f7a054be63f4736312b4b97b25 Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Tue, 18 Dec 2018 21:49:02 +0000 Subject: [PATCH 02/11] Convert popover code to TS. Yikes --- .../babel/proptypes-from-ts-props/index.js | 24 +- src/index.d.ts | 1 - src/services/index.d.ts | 16 -- ...ition.js => calculate_popover_position.ts} | 59 +++- src/services/popover/index.d.ts | 54 ---- src/services/popover/{index.js => index.ts} | 0 ...ng.test.js => popover_positioning.test.ts} | 118 ++++---- ..._positioning.js => popover_positioning.ts} | 271 ++++++++++++------ src/services/popover/types.ts | 1 + 9 files changed, 303 insertions(+), 241 deletions(-) delete mode 100644 src/services/index.d.ts rename src/services/popover/{calculate_popover_position.js => calculate_popover_position.ts} (66%) delete mode 100644 src/services/popover/index.d.ts rename src/services/popover/{index.js => index.ts} (100%) rename src/services/popover/{popover_positioning.test.js => popover_positioning.test.ts} (91%) rename src/services/popover/{popover_positioning.js => popover_positioning.ts} (74%) create mode 100644 src/services/popover/types.ts diff --git a/scripts/babel/proptypes-from-ts-props/index.js b/scripts/babel/proptypes-from-ts-props/index.js index 54f38f89d04..a35cace5684 100644 --- a/scripts/babel/proptypes-from-ts-props/index.js +++ b/scripts/babel/proptypes-from-ts-props/index.js @@ -290,16 +290,20 @@ function getPropTypesForNode(node, optional, state) { ), [ types.objectExpression( - node.body.map(property => { - const objectProperty = types.objectProperty( - types.identifier(property.key.name || `"${property.key.value}"`), - getPropTypesForNode(property.typeAnnotation, property.optional, state) - ); - if (property.leadingComments != null) { - objectProperty.leadingComments = property.leadingComments.map(({ type, value }) => ({ type, value })); - } - return objectProperty; - }) + node.body + // This helps filter out index signatures from interfaces, + // which don't translate to prop types. + .filter(property => property.key != null) + .map(property => { + const objectProperty = types.objectProperty( + types.identifier(property.key.name || `"${property.key.value}"`), + getPropTypesForNode(property.typeAnnotation, property.optional, state) + ); + if (property.leadingComments != null) { + objectProperty.leadingComments = property.leadingComments.map(({ type, value }) => ({ type, value })); + } + return objectProperty; + }) ) ] ); diff --git a/src/index.d.ts b/src/index.d.ts index f5225f47aaa..d0f8c86bfa8 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,4 +1,3 @@ -/// /// /// /// diff --git a/src/services/index.d.ts b/src/services/index.d.ts deleted file mode 100644 index f016a527aa4..00000000000 --- a/src/services/index.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/// - -declare module '@elastic/eui' { - // @ts-ignore - export * from '@elastic/eui/src/services/alignment'; - // @ts-ignore - export * from '@elastic/eui/src/services/copy_to_clipboard'; - // @ts-ignore - export * from '@elastic/eui/src/services/key_codes'; - // @ts-ignore - export * from '@elastic/eui/src/services/objects'; - // @ts-ignore - export * from '@elastic/eui/src/services/random'; - // @ts-ignore - export * from '@elastic/eui/src/services/utils'; -} diff --git a/src/services/popover/calculate_popover_position.js b/src/services/popover/calculate_popover_position.ts similarity index 66% rename from src/services/popover/calculate_popover_position.js rename to src/services/popover/calculate_popover_position.ts index 6b97011ab76..679df5fc3a2 100644 --- a/src/services/popover/calculate_popover_position.js +++ b/src/services/popover/calculate_popover_position.ts @@ -1,4 +1,27 @@ -const getVisibleArea = (bounds, windowWidth, windowHeight) => { +import { EuiPopoverPosition } from './types'; + +interface EuiPopoverBoundingBox { + top: number; + left: number; + width: number; + height: number; +} + +interface EuiPopoverAnchorRect extends EuiPopoverBoundingBox { + right: number; + bottom: number; +} + +interface EuiPopoverDimensions { + width: number; + height: number; +} + +interface EuiPopoverPositionedBox extends EuiPopoverBoundingBox { + position: EuiPopoverPosition; +} + +const getVisibleArea = (bounds: EuiPopoverBoundingBox, windowWidth: number, windowHeight: number): number => { const { left, top, width, height } = bounds; // This is a common algorithm for finding the intersected area among two rectangles. const dx = Math.min(left + width, windowWidth) - Math.max(left, 0); @@ -6,35 +29,42 @@ const getVisibleArea = (bounds, windowWidth, windowHeight) => { return dx * dy; }; -const positionAtTop = (anchorBounds, width, height, buffer) => { +type Positioner = ( + bounds: EuiPopoverAnchorRect, + width: number, + height: number, + buffer: number +) => EuiPopoverBoundingBox; + +const positionAtTop: Positioner = (anchorBounds, width, height, buffer) => { const widthDifference = width - anchorBounds.width; const left = anchorBounds.left - widthDifference * 0.5; const top = anchorBounds.top - height - buffer; return { left, top, width, height }; }; -const positionAtRight = (anchorBounds, width, height, buffer) => { +const positionAtRight: Positioner = (anchorBounds, width, height, buffer) => { const left = anchorBounds.right + buffer; const heightDifference = height - anchorBounds.height; const top = anchorBounds.top - heightDifference * 0.5; return { left, top, width, height }; }; -const positionAtBottom = (anchorBounds, width, height, buffer) => { +const positionAtBottom: Positioner = (anchorBounds, width, height, buffer) => { const widthDifference = width - anchorBounds.width; const left = anchorBounds.left - widthDifference * 0.5; const top = anchorBounds.bottom + buffer; return { left, top, width, height }; }; -const positionAtLeft = (anchorBounds, width, height, buffer) => { +const positionAtLeft: Positioner = (anchorBounds, width, height, buffer) => { const left = anchorBounds.left - width - buffer; const heightDifference = height - anchorBounds.height; const top = anchorBounds.top - heightDifference * 0.5; return { left, top, width, height }; }; -const positionToPositionerMap = { +const positionToPositionerMap: { [position: string]: Positioner } = { top: positionAtTop, right: positionAtRight, bottom: positionAtBottom, @@ -47,13 +77,20 @@ const positionToPositionerMap = { * @param {Object} anchorBounds - getBoundingClientRect() of the node the popover is tethered to (e.g. a button). * @param {Object} popoverBounds - getBoundingClientRect() of the popover node (e.g. the tooltip). * @param {string} requestedPosition - Position the user wants. One of ["top", "right", "bottom", "left"] - * @param {number} buffer - The space between the wrapper and the popover. Also the minimum space between the popover and the window. + * @param {number} buffer - The space between the wrapper and the popover. Also the minimum space between the + * popover and the window. * @param {Array} positions - List of acceptable positions. Defaults to ["top", "right", "bottom", "left"]. * * @returns {Object} With properties position (one of ["top", "right", "bottom", "left"]), left, top, width, and height. */ -export function calculatePopoverPosition(anchorBounds, popoverBounds, requestedPosition, - buffer = 16, positions = ['top', 'right', 'bottom', 'left']) { +export function calculatePopoverPosition( + anchorBounds: EuiPopoverAnchorRect, + popoverBounds: EuiPopoverDimensions, + requestedPosition: EuiPopoverPosition, + buffer: number = 16, + positions: EuiPopoverPosition[] = ['top', 'right', 'bottom', 'left'] +): EuiPopoverPositionedBox { + if (typeof buffer !== 'number') { throw new Error(`calculatePopoverPosition received a buffer argument of ${buffer}' but expected a number`); } @@ -62,8 +99,8 @@ export function calculatePopoverPosition(anchorBounds, popoverBounds, requestedP const windowHeight = window.innerHeight; const { width: popoverWidth, height: popoverHeight } = popoverBounds; - const positionToBoundsMap = {}; - const positionToVisibleAreaMap = {}; + const positionToBoundsMap: { [position: string]: EuiPopoverBoundingBox } = {}; + const positionToVisibleAreaMap: { [positon: string]: number } = {}; positions.forEach(position => { const bounds = positionToPositionerMap[position](anchorBounds, popoverWidth, popoverHeight, buffer); diff --git a/src/services/popover/index.d.ts b/src/services/popover/index.d.ts deleted file mode 100644 index 8b542294264..00000000000 --- a/src/services/popover/index.d.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { EuiPopoverPosition } from "@elastic/eui"; - -declare module '@elastic/eui' { - type EuiToolTipPosition = 'top' | 'right' | 'bottom' | 'left'; - - type EuiPopoverAnchorRect = { - top: number; - left: number; - width: number; - height: number; - }; - - type EuiPopoverDimensions = { - width: number; - height: number; - }; - - export const calculatePopoverPosition: ( - anchorBounds: EuiPopoverAnchorRect, - popoverBounds: EuiPopoverDimensions, - requestedPosition: EuiToolTipPosition, - buffer?: number, - positions?: EuiToolTipPosition[] - ) => { - top: number; - left: number; - width: number; - height: number; - position: EuiToolTipPosition; - }; - - type EuiPopoverPosition = - 'topLeft' | 'topCenter' | 'topRight' | - 'rightTop' | 'rightCenter' | 'rightBottom' | - 'bottomLeft' | 'bottomCenter' | 'bottomRight' | - 'leftTop' | 'leftCenter' | 'leftBottom'; - - type FindPopoverPositionArgs = { - anchor: HTMLElement | JSX.Element, - popover: HTMLElement | JSX.Element, - position: string, - buffer?: number, - offset?: number, - container?: HTMLElement - } - export const findPopoverPosition: ( - args: FindPopoverPositionArgs - ) => { - position: EuiToolTipPosition, - relativePosition: EuiPopoverPosition, - top: number, - left: number - }; -} diff --git a/src/services/popover/index.js b/src/services/popover/index.ts similarity index 100% rename from src/services/popover/index.js rename to src/services/popover/index.ts diff --git a/src/services/popover/popover_positioning.test.js b/src/services/popover/popover_positioning.test.ts similarity index 91% rename from src/services/popover/popover_positioning.test.js rename to src/services/popover/popover_positioning.test.ts index 093021cbd91..5922386281e 100644 --- a/src/services/popover/popover_positioning.test.js +++ b/src/services/popover/popover_positioning.test.ts @@ -3,26 +3,28 @@ import { getAvailableSpace, getElementBoundingBox, getPopoverScreenCoordinates, - getVisibleFit + getVisibleFit, + EuiClientRect, + POSITIONS, } from './popover_positioning'; -function makeBB(top, right, bottom, left) { +function makeBB(top: number, right: number, bottom: number, left: number): EuiClientRect { return { top, right, bottom, left, width: right - left, - height: bottom - top + height: bottom - top, }; } describe('popover_positioning', () => { describe('getElementBoundingBox', () => { const clientRect = { top: 5, right: 20, left: 5, bottom: 50, width: 15, height: 45 }; - const _getBoundingClientRect = HTMLElement.prototype.getBoundingClientRect; + const origGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect; beforeEach(() => HTMLElement.prototype.getBoundingClientRect = () => clientRect); - afterEach(() => HTMLElement.prototype.getBoundingClientRect = _getBoundingClientRect); + afterEach(() => HTMLElement.prototype.getBoundingClientRect = origGetBoundingClientRect); it('returns a new JavaScript object with correct values', () => { // `getBoundingClientRect` in the browser returns a `DOMRect` @@ -51,14 +53,14 @@ describe('popover_positioning', () => { }; it('returns the distance from each side of the anchor to each side of the container', () => { - ['top', 'right', 'bottom', 'left'].forEach(side => { + POSITIONS.forEach(side => { expect(getAvailableSpace(anchorBoundingBox, containerBoundingBox, 0, 0, side)).toEqual( expectedAvailableSpace); }); }); it('subtracts the buffer amount from the returned distances', () => { - ['top', 'right', 'bottom', 'left'].forEach(side => { + POSITIONS.forEach(side => { expect(getAvailableSpace(anchorBoundingBox, containerBoundingBox, 5, 0, side)).toEqual({ top: expectedAvailableSpace.top - 5, right: expectedAvailableSpace.right - 5, @@ -69,7 +71,7 @@ describe('popover_positioning', () => { }); it('subtracts the offset from the specified offsetSide', () => { - ['top', 'right', 'bottom', 'left'].forEach(side => { + POSITIONS.forEach(side => { expect(getAvailableSpace(anchorBoundingBox, containerBoundingBox, 0, 5, side)).toEqual({ ...expectedAvailableSpace, [side]: expectedAvailableSpace[side] - 5, @@ -78,7 +80,7 @@ describe('popover_positioning', () => { }); it('subtracts the buffer and the offset from the specified offsetSide', () => { - ['top', 'right', 'bottom', 'left'].forEach(side => { + POSITIONS.forEach(side => { expect(getAvailableSpace(anchorBoundingBox, containerBoundingBox, 3, 1, side)).toEqual({ // apply buffer space top: expectedAvailableSpace.top - 3, @@ -131,7 +133,7 @@ describe('popover_positioning', () => { })).toEqual({ fit: 0.2, top: -40, - left: 450 + left: 450, }); }); @@ -146,7 +148,7 @@ describe('popover_positioning', () => { })).toEqual({ fit: 0.2, top: -40, - left: 430 + left: 430, }); }); @@ -161,7 +163,7 @@ describe('popover_positioning', () => { })).toEqual({ fit: 0.2, top: -40, - left: 450 + left: 450, }); }); @@ -173,12 +175,10 @@ describe('popover_positioning', () => { popoverBoundingBox: makeBB(0, 100, 50, 0), windowBoundingBox: makeBB(0, 520, 768, 430), containerBoundingBox: makeBB(0, 1024, 768, 0), - availableWindowSpace: { top: 200, right: 200, bottom: 200, left: 200 }, - availableContainerSpace: { top: 50, right: 20, bottom: 200, left: 20 } })).toEqual({ fit: 0.18, top: -40, - left: 430 + left: 430, }); }); }); @@ -193,11 +193,11 @@ describe('popover_positioning', () => { popoverBoundingBox: makeBB(0, 50, 50, 0), windowBoundingBox: makeBB(0, 1024, 768, 0), containerBoundingBox: makeBB(0, 1024, 768, 0), - offset: 20 + offset: 20, })).toEqual({ fit: 1, top: 285, - left: 220 + left: 220, }); }); @@ -210,11 +210,11 @@ describe('popover_positioning', () => { popoverBoundingBox: makeBB(0, 50, 50, 0), windowBoundingBox: makeBB(0, 1024, 330, 0), containerBoundingBox: makeBB(0, 1024, 768, 0), - offset: 20 + offset: 20, })).toEqual({ fit: 1, top: 280, - left: 220 + left: 220, }); // top space is limited, should shift down to make the difference @@ -224,11 +224,11 @@ describe('popover_positioning', () => { popoverBoundingBox: makeBB(0, 50, 50, 0), windowBoundingBox: makeBB(300, 1024, 768, 0), containerBoundingBox: makeBB(0, 1024, 768, 0), - offset: 20 + offset: 20, })).toEqual({ fit: 1, top: 300, - left: 220 + left: 220, }); }); }); @@ -242,11 +242,11 @@ describe('popover_positioning', () => { popoverBoundingBox: makeBB(0, 50, 50, 0), windowBoundingBox: makeBB(0, 1024, 768, 0), containerBoundingBox: makeBB(0, 1024, 768, 90), - offset: 35 + offset: 35, })).toEqual({ fit: 1, top: 435, - left: 90 + left: 90, }); // right space is limited, should shift left to make the difference @@ -256,11 +256,11 @@ describe('popover_positioning', () => { popoverBoundingBox: makeBB(0, 50, 50, 0), windowBoundingBox: makeBB(0, 1024, 768, 0), containerBoundingBox: makeBB(0, 125, 768, 0), - offset: 35 + offset: 35, })).toEqual({ fit: 1, top: 215, - left: 75 + left: 75, }); }); }); @@ -275,15 +275,15 @@ describe('popover_positioning', () => { windowBoundingBox: makeBB(0, 1024, 768, 0), containerBoundingBox: makeBB(0, 1024, 768, 0), offset: 10, - arrowConfig: { arrowWidth: 5, arrowBuffer: 0 } + arrowConfig: { arrowWidth: 5, arrowBuffer: 0 }, })).toEqual({ fit: 1, top: 40, left: 25, arrow: { top: 50, - left: 22.5 - } + left: 22.5, + }, }); }); @@ -295,15 +295,15 @@ describe('popover_positioning', () => { windowBoundingBox: makeBB(0, 1024, 768, 0), containerBoundingBox: makeBB(0, 1024, 768, 40), offset: 10, - arrowConfig: { arrowWidth: 5, arrowBuffer: 10 } + arrowConfig: { arrowWidth: 5, arrowBuffer: 10 }, })).toEqual({ fit: 0.63, top: -15, left: 35, arrow: { top: 50, - left: 12.5 - } + left: 12.5, + }, }); }); }); @@ -317,15 +317,15 @@ describe('popover_positioning', () => { popoverBoundingBox: makeBB(0, 100, 50, 0), windowBoundingBox: makeBB(0, 1024, 768, 0), containerBoundingBox: makeBB(0, 1024, 768, 0), - arrowConfig: { arrowWidth: 6, arrowBuffer: 10 } + arrowConfig: { arrowWidth: 6, arrowBuffer: 10 }, })).toEqual({ fit: 1, top: 50, left: 75, arrow: { top: 50, - left: 22 - } + left: 22, + }, }); expect(getPopoverScreenCoordinates({ @@ -335,15 +335,15 @@ describe('popover_positioning', () => { popoverBoundingBox: makeBB(0, 100, 50, 0), windowBoundingBox: makeBB(0, 1024, 768, 0), containerBoundingBox: makeBB(0, 1024, 768, 0), - arrowConfig: { arrowWidth: 6, arrowBuffer: 20 } + arrowConfig: { arrowWidth: 6, arrowBuffer: 20 }, })).toEqual({ fit: 1, top: 110, left: 25, arrow: { top: 0, - left: 72 - } + left: 72, + }, }); }); @@ -355,15 +355,15 @@ describe('popover_positioning', () => { popoverBoundingBox: makeBB(0, 100, 200, 0), windowBoundingBox: makeBB(-200, 1024, 768, 0), containerBoundingBox: makeBB(-200, 1024, 768, 0), - arrowConfig: { arrowWidth: 6, arrowBuffer: 10 } + arrowConfig: { arrowWidth: 6, arrowBuffer: 10 }, })).toEqual({ fit: 1, top: -82, left: 125, arrow: { top: 184, - left: 0 - } + left: 0, + }, }); }); }); @@ -372,7 +372,9 @@ describe('popover_positioning', () => { describe('findPopoverPosition', () => { beforeEach(() => { // reset any scrolling before each test + // @ts-ignore window.pageXOffset = 0; + // @ts-ignore window.pageYOffset = 0; }); @@ -392,12 +394,12 @@ describe('popover_positioning', () => { anchor, popover, container, - offset: 7 + offset: 7, })).toEqual({ fit: 1, position: 'top', top: 43, - left: 85 + left: 85, }); }); }); @@ -419,12 +421,12 @@ describe('popover_positioning', () => { anchor, popover, container, - offset: 5 + offset: 5, })).toEqual({ fit: 1, position: 'right', top: 85, - left: 155 + left: 155, }); }); }); @@ -446,12 +448,12 @@ describe('popover_positioning', () => { anchor, popover, container, - offset: 5 + offset: 5, })).toEqual({ fit: 1, position: 'top', top: 45, - left: 85 + left: 85, }); }); @@ -468,16 +470,16 @@ describe('popover_positioning', () => { expect(findPopoverPosition({ position: 'right', - align: 'down', + align: 'bottom', anchor, popover, container, - offset: 5 + offset: 5, })).toEqual({ fit: 1, position: 'top', top: 45, - left: 85 + left: 85, }); }); @@ -498,12 +500,12 @@ describe('popover_positioning', () => { anchor, popover, container, - offset: 5 + offset: 5, })).toEqual({ fit: 0, position: 'right', top: 85, - left: 155 + left: 155, }); }); }); @@ -525,19 +527,21 @@ describe('popover_positioning', () => { anchor, popover, container, - offset: 5 + offset: 5, })).toEqual({ fit: 1, position: 'bottom', top: 125, - left: 85 + left: 85, }); }); }); describe('scrolling', () => { it('adds body scroll position to position values', () => { + // @ts-ignore window.pageYOffset = 100; + // @ts-ignore window.pageXOffset = 15; const anchor = document.createElement('div'); @@ -554,12 +558,12 @@ describe('popover_positioning', () => { anchor, popover, container, - offset: 7 + offset: 7, })).toEqual({ fit: 1, position: 'top', top: 143, - left: 100 + left: 100, }); }); }); @@ -580,12 +584,12 @@ describe('popover_positioning', () => { anchor, popover, container, - allowCrossAxis: false + allowCrossAxis: false, })).toEqual({ fit: 0.34, position: 'top', top: 350, - left: 85 + left: 85, }); }); }); diff --git a/src/services/popover/popover_positioning.js b/src/services/popover/popover_positioning.ts similarity index 74% rename from src/services/popover/popover_positioning.js rename to src/services/popover/popover_positioning.ts index 0764db0cd2f..b0b933cd9e9 100644 --- a/src/services/popover/popover_positioning.js +++ b/src/services/popover/popover_positioning.ts @@ -1,31 +1,63 @@ -const relatedDimension = { +import { EuiPopoverPosition } from './types'; + +type Dimension = 'height' | 'width'; + +export const POSITIONS: EuiPopoverPosition[] = ['top', 'right', 'bottom', 'left']; + +interface BoundingBox { + [position: string]: number; + top: number; + right: number; + bottom: number; + left: number; +} + +export interface EuiClientRect extends BoundingBox { + height: number; + width: number; +} + +const relatedDimension: { [position in EuiPopoverPosition]: Dimension } = { top: 'height', right: 'width', bottom: 'height', - left: 'width' + left: 'width', }; -const dimensionPositionAttribute = { +const dimensionPositionAttribute: { [dimension in Dimension]: 'top' | 'left' } = { height: 'top', - width: 'left' + width: 'left', }; -const positionComplements = { +const positionComplements: { [position in EuiPopoverPosition]: EuiPopoverPosition } = { top: 'bottom', right: 'left', bottom: 'top', - left: 'right' + left: 'right', }; // always resolving to top/left is taken advantage of by knowing they are the // minimum edges of the bounding box -const positionSubstitutes = { +const positionSubstitutes: { [position in EuiPopoverPosition]: 'left' | 'top' } = { top: 'left', right: 'top', bottom: 'left', - left: 'top' + left: 'top', }; +interface FindPopoverPositionArgs { + anchor: HTMLElement; + popover: HTMLElement; + align?: EuiPopoverPosition; + position: EuiPopoverPosition; + forcePosition?: boolean; + buffer?: number; + offset?: number; + allowCrossAxis?: boolean; + container?: HTMLElement; + arrowConfig?: { arrowWidth: number; arrowBuffer: number }; +} + /** * Calculates the absolute positioning (relative to document.body) to place a popover element * @@ -38,23 +70,31 @@ const positionSubstitutes = { * @param [offset=0] {number} Distance between the popover and the anchor * @param [allowCrossAxis=true] {boolean} Whether to allow the popover to be positioned on the cross-axis * @param [container] {HTMLElement} Element the popover must be constrained to fit within - * @param [arrowConfig] {{arrowWidth: number, arrowBuffer: number}} If present, describes the size & constraints for an arrow element, and the function return value will include an `arrow` param with position details + * @param [arrowConfig] {{arrowWidth: number, arrowBuffer: number}} If + * present, describes the size & constraints for an arrow element, and the + * function return value will include an `arrow` param with position details * - * @returns {{top: number, left: number, position: string, fit: number, arrow?: {left: number, top: number}}|null} absolute page coordinates for the popover, - * and the placements's relation to the anchor; if there's no room this returns null + * @returns {{ + * top: number, + * left: number, + * position: string, + * fit: number, + * arrow?: {left: number, top: number} + * } | null} absolute page coordinates for the popover, and the + * placements's relation to the anchor; if there's no room this returns null */ export function findPopoverPosition({ anchor, popover, - align, + align = null, position, forcePosition, buffer = 16, offset = 0, allowCrossAxis = true, container, - arrowConfig -}) { + arrowConfig, +}: FindPopoverPositionArgs) { // find the screen-relative bounding boxes of the anchor, popover, and container const anchorBoundingBox = getElementBoundingBox(anchor); const popoverBoundingBox = getElementBoundingBox(popover); @@ -64,13 +104,13 @@ export function findPopoverPosition({ // so prefer the clientWidth/clientHeight of the DOM if available const documentWidth = document.documentElement.clientWidth || window.innerWidth; const documentHeight = document.documentElement.clientHeight || window.innerHeight; - const windowBoundingBox = { + const windowBoundingBox: EuiClientRect = { top: 0, right: documentWidth, bottom: documentHeight, left: 0, height: documentHeight, - width: documentWidth + width: documentWidth, }; // if no container element is given fall back to using the window viewport @@ -93,8 +133,10 @@ export function findPopoverPosition({ * if position = "right" the order is right, left, top, bottom */ - const iterationPositions = [position]; // Try the user-desired position first. - const iterationAlignments = [align]; // keep user-defined alignment in the original positions. + // Try the user-desired position first. + const iterationPositions = [position]; + // keep user-defined alignment in the original positions. + const iterationAlignments: Array = [align]; if (forcePosition !== true) { iterationPositions.push(positionComplements[position]); // Try the complementary position. @@ -111,20 +153,18 @@ export function findPopoverPosition({ // position is forced, if it conficts with the alignment then reset align to `null` // e.g. original placement request for `downLeft` is moved to the `left` side, future calls // will position and align `left`, and `leftLeft` is not a valid placement - if (position === align || position === positionComplements[align]) { + if (position === align || (align != null && position === positionComplements[align])) { iterationAlignments[0] = null; } } - const { - bestPosition, - } = iterationPositions.reduce(({ bestFit, bestPosition }, iterationPosition, idx) => { - // If we've already found the ideal fit, use that position. - if (bestFit === 1) { - return { bestFit, bestPosition }; - } + let bestFit = -Infinity; + let bestPosition = null; + + for (let idx = 0; idx < iterationPositions.length; idx++) { + const iterationPosition = iterationPositions[idx]; - // Otherwise, see if we can find a position with a better fit than we've found so far. + // See if we can find a position with a better fit than we've found so far. const screenCoordinates = getPopoverScreenCoordinates({ position: iterationPosition, align: iterationAlignments[idx], @@ -134,41 +174,49 @@ export function findPopoverPosition({ containerBoundingBox, offset, buffer, - arrowConfig + arrowConfig, }); if (screenCoordinates.fit > bestFit) { - return { - bestFit: screenCoordinates.fit, - bestPosition: { - fit: screenCoordinates.fit, - position: iterationPosition, - top: screenCoordinates.top + window.pageYOffset, - left: screenCoordinates.left + window.pageXOffset, - arrow: screenCoordinates.arrow - }, + bestFit = screenCoordinates.fit; + bestPosition = { + fit: screenCoordinates.fit, + position: iterationPosition, + top: screenCoordinates.top + window.pageYOffset, + left: screenCoordinates.left + window.pageXOffset, + arrow: screenCoordinates.arrow, }; + + // If we've already found the ideal fit, use that position. + if (bestFit === 1) { + break; + } } // If we haven't improved the fit, then continue on and try a new position. - return { - bestFit, - bestPosition, - }; - }, { - bestFit: -Infinity, - bestPosition: null, - }); + } return bestPosition; } +interface GetPopoverScreenCoordinatesArgs { + position: EuiPopoverPosition; + align?: EuiPopoverPosition | null; + anchorBoundingBox: EuiClientRect; + popoverBoundingBox: EuiClientRect; + windowBoundingBox: EuiClientRect; + containerBoundingBox: EuiClientRect; + arrowConfig?: { arrowWidth: number; arrowBuffer: number }; + offset?: number; + buffer?: number; +} + /** * Given a target position and the popover's surrounding context, returns either an * object with {top, left} screen coordinates or `null` if it's not possible to show * content in the target position * @param position {string} the target position, one of ["top", "right", "bottom", "left"] - * @param [align] {string} target alignment on the cross-axis, one of ["top", "right", "bottom", "left"] + * @param align {string} target alignment on the cross-axis, one of ["top", "right", "bottom", "left"] * @param anchorBoundingBox {Object} bounding box of the anchor element * @param popoverBoundingBox {Object} bounding box of the popover element * @param windowBoundingBox {Object} bounding box of the window @@ -177,16 +225,18 @@ export function findPopoverPosition({ * constraints for an arrow element, and the function return value will include an `arrow` param * with position details * @param [offset=0] {number} Distance between the popover and the anchor - * @param [buffer=0] {number} Minimum distance between the popover's placement and the container edge + * @param [buffer=0] {number} Minimum distance between the popover's + * placement and the container edge * - * @returns {{top: number, left: number, relativePlacement: string, fit: number, arrow?: {top: number, left: number}}|null} + * @returns {{top: number, left: number, relativePlacement: string, fit: + * number, arrow?: {top: number, left: number}}|null} * object with top/left coordinates, the popover's relative position to the anchor, and how well the * popover fits in the location (0.0 -> 1.0) oordinates and the popover's relative position, if * there is no room in this placement then null */ export function getPopoverScreenCoordinates({ position, - align, + align = null, anchorBoundingBox, popoverBoundingBox, windowBoundingBox, @@ -194,7 +244,7 @@ export function getPopoverScreenCoordinates({ arrowConfig, offset = 0, buffer = 0, -}) { +}: GetPopoverScreenCoordinatesArgs) { /** * The goal is to find the best way to align the popover content * on the given side of the anchor element. The popover prefers @@ -266,7 +316,7 @@ export function getPopoverScreenCoordinates({ const popoverPlacement = { [crossAxisFirstSide]: crossAxisPosition, - [primaryAxisPositionName]: primaryAxisPosition + [primaryAxisPositionName]: primaryAxisPosition, }; // calculate the fit of the popover in this location @@ -287,13 +337,13 @@ export function getPopoverScreenCoordinates({ bottom: popoverPlacement.top + popoverBoundingBox.height, left: popoverPlacement.left, width: popoverBoundingBox.width, - height: popoverBoundingBox.height + height: popoverBoundingBox.height, }, combinedBoundingBox ); const arrow = arrowConfig ? { - [crossAxisFirstSide]: crossAxisArrowPosition - popoverPlacement[crossAxisFirstSide], + [crossAxisFirstSide]: crossAxisArrowPosition! - popoverPlacement[crossAxisFirstSide], [primaryAxisPositionName]: primaryAxisArrowPosition, } : undefined; @@ -305,6 +355,26 @@ export function getPopoverScreenCoordinates({ }; } +interface GetCrossAxisPositionArgs { + crossAxisFirstSide: EuiPopoverPosition; + crossAxisSecondSide: EuiPopoverPosition; + crossAxisDimension: Dimension; + position: EuiPopoverPosition; + align: EuiPopoverPosition | null; + buffer: number; + offset: number; + windowBoundingBox: EuiClientRect; + containerBoundingBox: EuiClientRect; + popoverBoundingBox: EuiClientRect; + anchorBoundingBox: EuiClientRect; + arrowConfig?: { arrowWidth: number; arrowBuffer: number }; +} + +interface CrossAxisPosition { + crossAxisPosition: number; + crossAxisArrowPosition: number | undefined; +} + function getCrossAxisPosition({ crossAxisFirstSide, crossAxisSecondSide, @@ -318,7 +388,7 @@ function getCrossAxisPosition({ popoverBoundingBox, anchorBoundingBox, arrowConfig, -}) { +}: GetCrossAxisPositionArgs): CrossAxisPosition { // how much of the popover overflows past either side of the anchor if its centered const popoverSizeOnCrossAxis = popoverBoundingBox[crossAxisDimension]; const anchorSizeOnCrossAxis = anchorBoundingBox[crossAxisDimension]; @@ -377,8 +447,6 @@ function getCrossAxisPosition({ let crossAxisArrowPosition; if (arrowConfig) { const { arrowWidth } = arrowConfig; - const anchorSizeOnCrossAxis = anchorBoundingBox[crossAxisDimension]; - const anchorHalfSize = anchorSizeOnCrossAxis / 2; crossAxisArrowPosition = anchorBoundingBox[crossAxisFirstSide] + anchorHalfSize - (arrowWidth / 2); // make sure there's enough buffer around the arrow @@ -402,13 +470,21 @@ function getCrossAxisPosition({ }; } +interface GetPrimaryAxisPositionArgs { + position: EuiPopoverPosition; + offset: number; + popoverBoundingBox: BoundingBox; + anchorBoundingBox: BoundingBox; + arrowConfig?: { arrowWidth: number; arrowBuffer: number }; +} + function getPrimaryAxisPosition({ position, offset, popoverBoundingBox, anchorBoundingBox, arrowConfig, -}) { +}: GetPrimaryAxisPositionArgs) { // if positioning to the top or left, the target position decreases // from the anchor's top or left, otherwise the position adds to the anchor's const isOffsetDecreasing = position === 'top' || position === 'left'; @@ -423,7 +499,7 @@ function getPrimaryAxisPosition({ // find the popover position on the primary axis const anchorSizeOnPrimaryAxis = anchorBoundingBox[primaryAxisDimension]; const primaryAxisOffset = isOffsetDecreasing ? popoverSizeOnPrimaryAxis : anchorSizeOnPrimaryAxis; - const contentOffset = (offset + primaryAxisOffset) * (isOffsetDecreasing ? -1 : 1); + const contentOffset = (offset + primaryAxisOffset!) * (isOffsetDecreasing ? -1 : 1); const primaryAxisPosition = anchorEdgeOrigin + contentOffset; let primaryAxisArrowPosition; @@ -445,7 +521,7 @@ function getPrimaryAxisPosition({ * @param {HTMLElement} element * @returns {{top: number, right: number, bottom: number, left: number, height: number, width: number}} */ -export function getElementBoundingBox(element) { +export function getElementBoundingBox(element: HTMLElement): EuiClientRect { const rect = element.getBoundingClientRect(); return { top: rect.top, @@ -453,7 +529,7 @@ export function getElementBoundingBox(element) { bottom: rect.bottom, left: rect.left, height: rect.height, - width: rect.width + width: rect.width, }; } @@ -464,10 +540,17 @@ export function getElementBoundingBox(element) { * @param {Object} containerBoundingBox Client bounding box of the container element * @param {number} buffer Minimum distance between the popover and the bounding container * @param {number} offset Distance between the popover and the anchor - * @param {string} offsetSide Side the offset needs to be applied to, one of ["top", "right", "bottom", "left"] + * @param {string} offsetSide Side the offset needs to be applied to, one + * of ["top", "right", "bottom", "left"] * @returns {{top: number, right: number, bottom: number, left: number}} */ -export function getAvailableSpace(anchorBoundingBox, containerBoundingBox, buffer, offset, offsetSide) { +export function getAvailableSpace( + anchorBoundingBox: BoundingBox, + containerBoundingBox: BoundingBox, + buffer: number, + offset: number, + offsetSide: EuiPopoverPosition +): BoundingBox { return { top: anchorBoundingBox.top - containerBoundingBox.top - buffer - (offsetSide === 'top' ? offset : 0), right: containerBoundingBox.right - anchorBoundingBox.right - buffer - (offsetSide === 'right' ? offset : 0), @@ -482,7 +565,7 @@ export function getAvailableSpace(anchorBoundingBox, containerBoundingBox, buffe * @param containerBoundingBox bounding box of container * @returns {number} */ -export function getVisibleFit(contentBoundingBox, containerBoundingBox) { +export function getVisibleFit(contentBoundingBox: BoundingBox, containerBoundingBox: BoundingBox): number { const intersection = intersectBoundingBoxes(contentBoundingBox, containerBoundingBox); if (intersection.left > intersection.right || intersection.top > intersection.top) { @@ -491,7 +574,8 @@ export function getVisibleFit(contentBoundingBox, containerBoundingBox) { } const intersectionArea = (intersection.right - intersection.left) * (intersection.bottom - intersection.top); - const contentArea = (contentBoundingBox.right - contentBoundingBox.left) * (contentBoundingBox.bottom - contentBoundingBox.top); + const contentArea = (contentBoundingBox.right - contentBoundingBox.left) * + (contentBoundingBox.bottom - contentBoundingBox.top); return intersectionArea / contentArea; } @@ -501,23 +585,26 @@ export function getVisibleFit(contentBoundingBox, containerBoundingBox) { * * @param firstBox * @param secondBox - * @returns {{top: number, right: number, bottom: number, left: number, height: number, width: number}} + * @returns {EuiClientRect} */ -export function intersectBoundingBoxes(firstBox, secondBox) { - const intersection = { - top: Math.max(firstBox.top, secondBox.top), - right: Math.min(firstBox.right, secondBox.right), - bottom: Math.min(firstBox.bottom, secondBox.bottom), - left: Math.max(firstBox.left, secondBox.left) - }; - - intersection.height = Math.max(intersection.bottom - intersection.top, 0); - intersection.width = Math.max(intersection.right - intersection.left, 0); +export function intersectBoundingBoxes(firstBox: BoundingBox, secondBox: BoundingBox): EuiClientRect { + const top = Math.max(firstBox.top, secondBox.top); + const right = Math.min(firstBox.right, secondBox.right); + const bottom = Math.min(firstBox.bottom, secondBox.bottom); + const left = Math.max(firstBox.left, secondBox.left); + const height = Math.max(bottom - top, 0); + const width = Math.max(right - left, 0); - return intersection; + return { + top, + right, + bottom, + left, + height, + width, + }; } - /** * Returns the top-most defined z-index in the element's ancestor hierarchy * relative to the `target` element; if no z-index is defined, returns "0" @@ -525,7 +612,7 @@ export function intersectBoundingBoxes(firstBox, secondBox) { * @param cousin {HTMLElement} * @returns {string} */ -export function getElementZIndex(element, cousin) { +export function getElementZIndex(element: HTMLElement, cousin: HTMLElement): string { /** * finding the z-index of `element` is not the full story * its the CSS stacking context that is important @@ -547,33 +634,33 @@ export function getElementZIndex(element, cousin) { */ // build the array of the element + its offset parents - const nodesToInspect = []; + const nodesToInspect: HTMLElement[] = []; while (true) { nodesToInspect.push(element); - element = element.offsetParent; + // AFAICT this is a valid cast - the libdefs appear wrong + element = element.offsetParent as HTMLElement; // stop if there is no parent - if (element == null) break; + if (element == null) { break; } // stop if the parent contains the related element // as this is the z-index ancestor - if (element.contains(cousin)) break; + if (element.contains(cousin)) { break; } } // reverse the nodes to walk from top -> element nodesToInspect.reverse(); - return nodesToInspect.reduce( - (foundZIndex, node) => { - if (foundZIndex != null) return foundZIndex; + for (const node of nodesToInspect) { + // get this node's z-index css value + const zIndex = window.document.defaultView.getComputedStyle(node).getPropertyValue('z-index'); - // get this node's z-index css value - const zIndex = window.document.defaultView.getComputedStyle(node).getPropertyValue('z-index'); + // if the z-index is not a number (e.g. "auto") return null, else the value + if (!isNaN(parseInt(zIndex, 10))) { + return zIndex; + } + } - // if the z-index is not a number (e.g. "auto") return null, else the value - return isNaN(zIndex) ? null : zIndex; - }, - null - ) || '0'; + return '0'; } diff --git a/src/services/popover/types.ts b/src/services/popover/types.ts new file mode 100644 index 00000000000..1427e4209ab --- /dev/null +++ b/src/services/popover/types.ts @@ -0,0 +1 @@ +export type EuiPopoverPosition = 'top' | 'right' | 'bottom' | 'left'; From 1ef348c5f076599e9787daaad4bf70f15fea8c8d Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Wed, 19 Dec 2018 10:59:10 +0000 Subject: [PATCH 03/11] Migrate predicate service tests to TS --- .../{common_predicates.test.js => common_predicates.test.ts} | 0 src/services/predicate/common_predicates.ts | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/services/predicate/{common_predicates.test.js => common_predicates.test.ts} (100%) diff --git a/src/services/predicate/common_predicates.test.js b/src/services/predicate/common_predicates.test.ts similarity index 100% rename from src/services/predicate/common_predicates.test.js rename to src/services/predicate/common_predicates.test.ts diff --git a/src/services/predicate/common_predicates.ts b/src/services/predicate/common_predicates.ts index 9efc5f2366a..8101e17ef68 100644 --- a/src/services/predicate/common_predicates.ts +++ b/src/services/predicate/common_predicates.ts @@ -1,8 +1,8 @@ import moment from 'moment'; -export const always = () => true; +export const always = (value?: any) => true; -export const never = () => false; +export const never = (value?: any) => false; export const isUndefined = (value: any) => { return value === undefined; From 4bf7a8d9ae07acd34f230d4e5030ced9814d09a7 Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Wed, 19 Dec 2018 11:23:15 +0000 Subject: [PATCH 04/11] Migrate security service to TS --- ...r_target.test.js => get_secure_rel_for_target.test.ts} | 8 ++++++++ ...ure_rel_for_target.js => get_secure_rel_for_target.ts} | 7 +++---- src/services/security/{index.js => index.ts} | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) rename src/services/security/{get_secure_rel_for_target.test.js => get_secure_rel_for_target.test.ts} (83%) rename src/services/security/{get_secure_rel_for_target.js => get_secure_rel_for_target.ts} (60%) rename src/services/security/{index.js => index.ts} (88%) diff --git a/src/services/security/get_secure_rel_for_target.test.js b/src/services/security/get_secure_rel_for_target.test.ts similarity index 83% rename from src/services/security/get_secure_rel_for_target.test.js rename to src/services/security/get_secure_rel_for_target.test.ts index 9a2415fd1f8..14fe5f3df93 100644 --- a/src/services/security/get_secure_rel_for_target.test.js +++ b/src/services/security/get_secure_rel_for_target.test.ts @@ -4,6 +4,14 @@ import { describe('getSecureRelForTarget', () => { describe('returns rel', () => { + test('when target is not supplied', () => { + expect(getSecureRelForTarget(undefined, 'hello')).toBe('hello'); + }); + + test('when target is empty', () => { + expect(getSecureRelForTarget('', 'hello')).toBe('hello'); + }); + test('when target is not _blank', () => { expect(getSecureRelForTarget('_self', 'hello')).toBe('hello'); }); diff --git a/src/services/security/get_secure_rel_for_target.js b/src/services/security/get_secure_rel_for_target.ts similarity index 60% rename from src/services/security/get_secure_rel_for_target.js rename to src/services/security/get_secure_rel_for_target.ts index e688bcf678d..92f70ef4b6e 100644 --- a/src/services/security/get_secure_rel_for_target.js +++ b/src/services/security/get_secure_rel_for_target.ts @@ -1,8 +1,8 @@ /** - * Secures outbound links. See https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/ - * for more info. + * Secures outbound links. For more info: + * https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/ */ -export const getSecureRelForTarget = (target, rel) => { +export const getSecureRelForTarget = (target?: '_blank' | '_self' | '_parent' | '_top' | string, rel?: string) => { if (!target) { return rel; } @@ -21,7 +21,6 @@ export const getSecureRelForTarget = (target, rel) => { secureRel = `${secureRel} noopener`; } - if (!secureRel.includes('noreferrer')) { secureRel = `${secureRel} noreferrer`; } diff --git a/src/services/security/index.js b/src/services/security/index.ts similarity index 88% rename from src/services/security/index.js rename to src/services/security/index.ts index 1e5c8b8293a..0848ae55384 100644 --- a/src/services/security/index.js +++ b/src/services/security/index.ts @@ -1 +1 @@ -export { getSecureRelForTarget } from './get_secure_rel_for_target.js'; +export { getSecureRelForTarget } from './get_secure_rel_for_target'; From 4737fcfa1047eb265259414b15d2ad8ad36728ae Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Wed, 19 Dec 2018 21:31:52 +0000 Subject: [PATCH 05/11] Migrate sort services to TS --- ...omparators.test.js => comparators.test.ts} | 3 +- .../sort/{comparators.js => comparators.ts} | 20 ++++++----- src/services/sort/{index.js => index.ts} | 0 .../{property_sort.js => property_sort.ts} | 2 +- src/services/sort/sort_direction.js | 14 -------- src/services/sort/sort_direction.ts | 21 ++++++++++++ ...es.test.js => sortable_properties.test.ts} | 21 +++++++----- ...e_properties.js => sortable_properties.ts} | 33 +++++++++++++------ 8 files changed, 71 insertions(+), 43 deletions(-) rename src/services/sort/{comparators.test.js => comparators.test.ts} (96%) rename src/services/sort/{comparators.js => comparators.ts} (72%) rename src/services/sort/{index.js => index.ts} (100%) rename src/services/sort/{property_sort.js => property_sort.ts} (80%) delete mode 100644 src/services/sort/sort_direction.js create mode 100644 src/services/sort/sort_direction.ts rename src/services/sort/{sortable_properties.test.js => sortable_properties.test.ts} (91%) rename src/services/sort/{sortable_properties.js => sortable_properties.ts} (78%) diff --git a/src/services/sort/comparators.test.js b/src/services/sort/comparators.test.ts similarity index 96% rename from src/services/sort/comparators.test.js rename to src/services/sort/comparators.test.ts index 69247f58b80..2d896eac2aa 100644 --- a/src/services/sort/comparators.test.js +++ b/src/services/sort/comparators.test.ts @@ -10,7 +10,7 @@ describe('comparators - default', () => { expect(Comparators.default(SortDirection.DESC)(5, 10)).toBeGreaterThan(0); }); test('asc/desc when the two values equal', () => { - const dir = new Random().oneOf(SortDirection.ASC, SortDirection.DESC); + const dir = new Random().oneOf([SortDirection.ASC, SortDirection.DESC]); expect(Comparators.default(dir)(5, 5)).toBe(0); }); }); @@ -52,4 +52,3 @@ describe('default comparator', () => { expect(sorted).toEqual([3, 5, '7', null, undefined, undefined]); }); }); - diff --git a/src/services/sort/comparators.js b/src/services/sort/comparators.ts similarity index 72% rename from src/services/sort/comparators.js rename to src/services/sort/comparators.ts index b45295db79e..6671fb8e001 100644 --- a/src/services/sort/comparators.js +++ b/src/services/sort/comparators.ts @@ -1,10 +1,14 @@ import { SortDirection } from './sort_direction'; import { get } from '../objects'; +export type Primitive = string | boolean | number | null | undefined; + +type Comparator = (a: T, b: T) => number; + export const Comparators = Object.freeze({ - default: (direction = SortDirection.ASC) => { - return (v1, v2) => { + default: (direction: 'asc' | 'desc' = SortDirection.ASC) => { + return (v1: Primitive, v2: Primitive) => { // JavaScript's comparison of null/undefined (and some others not handled here) values always returns `false` // (https://www.ecma-international.org/ecma-262/#sec-abstract-relational-comparison) // resulting in cases where v1 < v2 and v1 > v2 are both false. @@ -36,25 +40,25 @@ export const Comparators = Object.freeze({ if (v1 === v2) { return 0; } - const result = v1 > v2 ? 1 : -1; + const result = v1! > v2! ? 1 : -1; return SortDirection.isAsc(direction) ? result : -1 * result; }; }, - reverse: (comparator) => { + reverse: (comparator: Comparator): Comparator => { return (v1, v2) => comparator(v2, v1); }, - value(valueCallback, comparator = undefined) { + value(valueCallback: (value: T) => Primitive, comparator?: Comparator): Comparator { if (!comparator) { comparator = this.default(SortDirection.ASC); } - return (o1, o2) => { - return comparator(valueCallback(o1), valueCallback(o2)); + return (o1: T, o2: T) => { + return comparator!(valueCallback(o1), valueCallback(o2)); }; }, - property(prop, comparator = undefined) { + property(prop: string, comparator?: Comparator): Comparator { return this.value(value => get(value, prop), comparator); }, diff --git a/src/services/sort/index.js b/src/services/sort/index.ts similarity index 100% rename from src/services/sort/index.js rename to src/services/sort/index.ts diff --git a/src/services/sort/property_sort.js b/src/services/sort/property_sort.ts similarity index 80% rename from src/services/sort/property_sort.js rename to src/services/sort/property_sort.ts index daa4455b8c2..f0310c95f05 100644 --- a/src/services/sort/property_sort.js +++ b/src/services/sort/property_sort.ts @@ -3,5 +3,5 @@ import { SortDirectionType } from './sort_direction'; export const PropertySortType = PropTypes.shape({ field: PropTypes.string.isRequired, - direction: SortDirectionType.isRequired + direction: SortDirectionType.isRequired, }); diff --git a/src/services/sort/sort_direction.js b/src/services/sort/sort_direction.js deleted file mode 100644 index f4e3ed2a785..00000000000 --- a/src/services/sort/sort_direction.js +++ /dev/null @@ -1,14 +0,0 @@ -import PropTypes from 'prop-types'; - -export const SortDirection = Object.freeze({ - ASC: 'asc', - DESC: 'desc', - isAsc(direction) { - return direction === this.ASC; - }, - reverse(direction) { - return this.isAsc(direction) ? this.DESC : this.ASC; - } -}); - -export const SortDirectionType = PropTypes.oneOf([ SortDirection.ASC, SortDirection.DESC ]); diff --git a/src/services/sort/sort_direction.ts b/src/services/sort/sort_direction.ts new file mode 100644 index 00000000000..446dbcaf5ec --- /dev/null +++ b/src/services/sort/sort_direction.ts @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types'; + +const ASC: 'asc' = 'asc'; +const DESC: 'desc' = 'desc'; + +export type Direction = + | typeof ASC + | typeof DESC; + +export const SortDirection = Object.freeze({ + ASC, + DESC, + isAsc(direction: Direction) { + return direction === ASC; + }, + reverse(direction: Direction) { + return this.isAsc(direction) ? DESC : ASC; + }, +}); + +export const SortDirectionType = PropTypes.oneOf([ ASC, DESC ]); diff --git a/src/services/sort/sortable_properties.test.js b/src/services/sort/sortable_properties.test.ts similarity index 91% rename from src/services/sort/sortable_properties.test.js rename to src/services/sort/sortable_properties.test.ts index e0f4bb2b21b..13b0a0fc3b7 100644 --- a/src/services/sort/sortable_properties.test.js +++ b/src/services/sort/sortable_properties.test.ts @@ -1,27 +1,31 @@ -import { - SortableProperties, -} from './sortable_properties'; +import { SortableProperty, SortableProperties } from './sortable_properties'; + +interface Bird { + name: string; + color: string; + size: number; +} describe('SortProperties', () => { - const name = { + const name: SortableProperty = { name: 'name', getValue: bird => bird.name, isAscending: true, }; - const size = { + const size: SortableProperty = { name: 'size', getValue: bird => bird.size, isAscending: false, }; - const color = { + const color: SortableProperty = { name: 'color', getValue: bird => bird.color, isAscending: true, }; - const birds = [ + const birds: Bird[] = [ { name: 'cardinal', color: 'red', @@ -36,7 +40,7 @@ describe('SortProperties', () => { name: 'chickadee', color: 'black and white', size: 3, - } + }, ]; describe('initialSortProperty', () => { @@ -50,6 +54,7 @@ describe('SortProperties', () => { }); test('throws an error property name is not defined', () => { + // @ts-ignore second param is mandatory expect(() => new SortableProperties([name, size, color])).toThrow(); }); }); diff --git a/src/services/sort/sortable_properties.js b/src/services/sort/sortable_properties.ts similarity index 78% rename from src/services/sort/sortable_properties.js rename to src/services/sort/sortable_properties.ts index a2e97e7aa40..5c70d0dab3c 100644 --- a/src/services/sort/sortable_properties.js +++ b/src/services/sort/sortable_properties.ts @@ -1,4 +1,10 @@ -import { Comparators } from './comparators'; +import { Comparators, Primitive } from './comparators'; + +export interface SortableProperty { + name: string; + getValue: (obj: T) => Primitive; + isAscending: boolean; +} /** * @typedef {Object} SortableProperty @@ -13,21 +19,26 @@ import { Comparators } from './comparators'; * Stores sort information for a set of SortableProperties, including which property is currently being sorted on, as * well as the last sort order for each property. */ -export class SortableProperties { +export class SortableProperties { + sortableProperties: Array>; + currentSortedProperty: SortableProperty; + /** * @param {Array} sortableProperties - a set of sortable properties. * @param {string} initialSortablePropertyName - Which sort property should be sorted on by default. */ - constructor(sortableProperties, initialSortablePropertyName) { + constructor(sortableProperties: Array>, initialSortablePropertyName: string) { this.sortableProperties = sortableProperties; /** * The current property that is being sorted on. * @type {SortableProperty} */ - this.currentSortedProperty = this.getSortablePropertyByName(initialSortablePropertyName); - if (!this.currentSortedProperty) { + const currentSortedProperty = this.getSortablePropertyByName(initialSortablePropertyName); + if (!currentSortedProperty) { throw new Error(`No property with the name ${initialSortablePropertyName}`); } + + this.currentSortedProperty = currentSortedProperty; } /** @@ -42,7 +53,7 @@ export class SortableProperties { * @param items {Array.} * @returns {Array.} sorted array of items, based off the sort properties. */ - sortItems(items) { + sortItems(items: T[]): T[] { const copy = [...items]; let comparator = Comparators.value(this.getSortedProperty().getValue); if (!this.isCurrentSortAscending()) { @@ -57,7 +68,7 @@ export class SortableProperties { * @param {String} propertyName * @returns {SortableProperty|undefined} */ - getSortablePropertyByName(propertyName) { + getSortablePropertyByName(propertyName: string) { return this.sortableProperties.find(property => property.name === propertyName); } @@ -66,8 +77,11 @@ export class SortableProperties { * property was already being sorted. * @param propertyName {String} */ - sortOn(propertyName) { + sortOn(propertyName: string) { const newSortedProperty = this.getSortablePropertyByName(propertyName); + if (!newSortedProperty) { + throw new Error(`No property with the name ${propertyName}`); + } const sortedProperty = this.getSortedProperty(); if (sortedProperty.name === newSortedProperty.name) { this.flipCurrentSortOrder(); @@ -88,7 +102,7 @@ export class SortableProperties { * @param {string} propertyName * @returns {boolean} True if the given sort property is sorted in ascending order. */ - isAscendingByName(propertyName) { + isAscendingByName(propertyName: string) { const sortedProperty = this.getSortablePropertyByName(propertyName); return sortedProperty ? sortedProperty.isAscending : false; } @@ -100,4 +114,3 @@ export class SortableProperties { this.currentSortedProperty.isAscending = !this.currentSortedProperty.isAscending; } } - From 40756d1dd83abc5e0d6876f75601aca93588e4b5 Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Wed, 19 Dec 2018 21:54:24 +0000 Subject: [PATCH 06/11] Migrate timer services to TS --- src/services/time/{index.js => index.ts} | 0 .../time/{timer.test.js => timer.test.ts} | 31 +++++++++---------- src/services/time/{timer.js => timer.ts} | 21 ++++++++----- 3 files changed, 29 insertions(+), 23 deletions(-) rename src/services/time/{index.js => index.ts} (100%) rename src/services/time/{timer.test.js => timer.test.ts} (61%) rename src/services/time/{timer.js => timer.ts} (59%) diff --git a/src/services/time/index.js b/src/services/time/index.ts similarity index 100% rename from src/services/time/index.js rename to src/services/time/index.ts diff --git a/src/services/time/timer.test.js b/src/services/time/timer.test.ts similarity index 61% rename from src/services/time/timer.test.js rename to src/services/time/timer.test.ts index 58a81f6b649..85c8135968a 100644 --- a/src/services/time/timer.test.js +++ b/src/services/time/timer.test.ts @@ -1,5 +1,3 @@ -import sinon from 'sinon'; - import { Timer, } from './timer'; @@ -7,11 +5,12 @@ import { describe('Timer', () => { describe('constructor', () => { test('counts down until time elapses and calls callback', done => { - const callbackSpy = sinon.spy(); - const timer = new Timer(callbackSpy, 5); // eslint-disable-line no-unused-vars + const callbackSpy = jest.fn(); + // tslint:disable-next-line:no-unused-expression + new Timer(callbackSpy, 5); setTimeout(() => { - expect(callbackSpy.called).toBe(true); + expect(callbackSpy).toBeCalled(); done(); }, 8); }); @@ -19,12 +18,12 @@ describe('Timer', () => { describe('pause', () => { test('stops timer', done => { - const callbackSpy = sinon.spy(); + const callbackSpy = jest.fn(); const timer = new Timer(callbackSpy, 5); - timer.pause(0); + timer.pause(); setTimeout(() => { - expect(callbackSpy.called).toBe(false); + expect(callbackSpy).not.toBeCalled(); done(); }, 8); }); @@ -32,13 +31,13 @@ describe('Timer', () => { describe('resume', () => { test('starts timer again', done => { - const callbackSpy = sinon.spy(); + const callbackSpy = jest.fn(); const timer = new Timer(callbackSpy, 5); - timer.pause(0); + timer.pause(); timer.resume(); setTimeout(() => { - expect(callbackSpy.called).toBe(true); + expect(callbackSpy).toBeCalled(); done(); }, 8); }); @@ -46,12 +45,12 @@ describe('Timer', () => { describe('clear', () => { test('prevents timer from being called', done => { - const callbackSpy = sinon.spy(); + const callbackSpy = jest.fn(); const timer = new Timer(callbackSpy, 5); - timer.clear(0); + timer.clear(); setTimeout(() => { - expect(callbackSpy.called).toBe(false); + expect(callbackSpy).not.toBeCalled(); done(); }, 8); }); @@ -59,10 +58,10 @@ describe('Timer', () => { describe('finish', () => { test('calls callback immediately', () => { - const callbackSpy = sinon.spy(); + const callbackSpy = jest.fn(); const timer = new Timer(callbackSpy, 5); timer.finish(); - expect(callbackSpy.called).toBe(true); + expect(callbackSpy).toBeCalled(); }); }); }); diff --git a/src/services/time/timer.js b/src/services/time/timer.ts similarity index 59% rename from src/services/time/timer.js rename to src/services/time/timer.ts index 56fdd5d1501..7be70a3a081 100644 --- a/src/services/time/timer.js +++ b/src/services/time/timer.ts @@ -1,5 +1,12 @@ export class Timer { - constructor(callback, timeMs) { + // In a browser this is a number, but in node it's a NodeJS.Time (a + // class). We don't care about this difference. + id: any; + callback: undefined | (() => void); + finishTime: number | undefined; + timeRemaining: number | undefined; + + constructor(callback: () => void, timeMs: number) { this.id = setTimeout(this.finish, timeMs); this.callback = callback; this.finishTime = Date.now() + timeMs; @@ -9,14 +16,14 @@ export class Timer { pause = () => { clearTimeout(this.id); this.id = undefined; - this.timeRemaining = this.finishTime - Date.now(); - }; + this.timeRemaining = (this.finishTime || 0) - Date.now(); + } resume = () => { this.id = setTimeout(this.finish, this.timeRemaining); - this.finishTime = Date.now() + this.timeRemaining; + this.finishTime = Date.now() + (this.timeRemaining || 0); this.timeRemaining = undefined; - }; + } clear = () => { clearTimeout(this.id); @@ -24,12 +31,12 @@ export class Timer { this.callback = undefined; this.finishTime = undefined; this.timeRemaining = undefined; - }; + } finish = () => { if (this.callback) { this.callback(); } this.clear(); - }; + } } From 0110b0afdbb77747826b2db3a2c75bebc7c8509a Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Thu, 20 Dec 2018 09:58:37 +0000 Subject: [PATCH 07/11] Convert window event service to TS --- src/services/window_event/index.js | 1 - src/services/window_event/index.ts | 1 + ...ow_event.test.js => window_event.test.tsx} | 4 +-- .../{window_event.js => window_event.ts} | 29 +++++++------------ 4 files changed, 13 insertions(+), 22 deletions(-) delete mode 100644 src/services/window_event/index.js create mode 100644 src/services/window_event/index.ts rename src/services/window_event/{window_event.test.js => window_event.test.tsx} (97%) rename src/services/window_event/{window_event.js => window_event.ts} (52%) diff --git a/src/services/window_event/index.js b/src/services/window_event/index.js deleted file mode 100644 index a426a83af0b..00000000000 --- a/src/services/window_event/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as EuiWindowEvent } from './window_event'; \ No newline at end of file diff --git a/src/services/window_event/index.ts b/src/services/window_event/index.ts new file mode 100644 index 00000000000..a4b88b33d02 --- /dev/null +++ b/src/services/window_event/index.ts @@ -0,0 +1 @@ +export { EuiWindowEvent } from './window_event'; diff --git a/src/services/window_event/window_event.test.js b/src/services/window_event/window_event.test.tsx similarity index 97% rename from src/services/window_event/window_event.test.js rename to src/services/window_event/window_event.test.tsx index aa21a725e5a..356d35bc02b 100644 --- a/src/services/window_event/window_event.test.js +++ b/src/services/window_event/window_event.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiWindowEvent } from '.'; +import { EuiWindowEvent } from './window_event'; describe('EuiWindowEvent', () => { @@ -50,4 +50,4 @@ describe('EuiWindowEvent', () => { expect(window.removeEventListener).not.toHaveBeenCalled(); }); -}); \ No newline at end of file +}); diff --git a/src/services/window_event/window_event.js b/src/services/window_event/window_event.ts similarity index 52% rename from src/services/window_event/window_event.js rename to src/services/window_event/window_event.ts index 8bf407863ce..f99567a061b 100644 --- a/src/services/window_event/window_event.js +++ b/src/services/window_event/window_event.ts @@ -1,13 +1,18 @@ import { Component } from 'react'; -import PropTypes from 'prop-types'; -export default class WindowEvent extends Component { +type EventNames = keyof WindowEventMap; +interface Props { + event: Ev; + handler: (this: Window, ev: WindowEventMap[Ev]) => any; +} + +export class EuiWindowEvent extends Component> { componentDidMount() { this.addEvent(this.props); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: Props) { if (prevProps.event !== this.props.event || prevProps.handler !== this.props.handler) { this.removeEvent(prevProps); this.addEvent(this.props); @@ -18,29 +23,15 @@ export default class WindowEvent extends Component { this.removeEvent(this.props); } - addEvent({ event, handler }) { + addEvent({ event, handler }: Props) { window.addEventListener(event, handler); } - removeEvent({ event, handler }) { + removeEvent({ event, handler }: Props) { window.removeEventListener(event, handler); } render() { return null; } - } - -WindowEvent.displayName = 'WindowEvent'; - -WindowEvent.propTypes = { - /** - * Type of valid DOM event - */ - event: PropTypes.string.isRequired, - /** - * Event callback function - */ - handler: PropTypes.func.isRequired -}; From 83e45351fb841c096be86319a592270e2a4a021f Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Thu, 20 Dec 2018 10:25:39 +0000 Subject: [PATCH 08/11] Export Query and ast from the defining component instead of services --- src/components/index.js | 4 +++- src/components/search_bar/index.js | 3 +-- src/components/search_bar/search_bar.js | 2 ++ src/services/{index.js => index.ts} | 6 ------ 4 files changed, 6 insertions(+), 9 deletions(-) rename src/services/{index.js => index.ts} (88%) diff --git a/src/components/index.js b/src/components/index.js index a6d565280f5..e71d9a4f250 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -256,7 +256,9 @@ export { } from './progress'; export { - EuiSearchBar + EuiSearchBar, + Query, + Ast } from './search_bar'; export { diff --git a/src/components/search_bar/index.js b/src/components/search_bar/index.js index a203e6fc36f..9ad496ff6d8 100644 --- a/src/components/search_bar/index.js +++ b/src/components/search_bar/index.js @@ -1,4 +1,3 @@ -export { EuiSearchBar } from './search_bar'; -export { QueryType } from './search_bar'; +export { EuiSearchBar, QueryType, Query, Ast } from './search_bar'; export { SearchBoxConfigPropTypes } from './search_box'; export { SearchFiltersFiltersType } from './search_filters'; diff --git a/src/components/search_bar/search_bar.js b/src/components/search_bar/search_bar.js index 2bda8ae1d34..b1c18ab1f19 100644 --- a/src/components/search_bar/search_bar.js +++ b/src/components/search_bar/search_bar.js @@ -13,6 +13,8 @@ import PropTypes from 'prop-types'; import { Query } from './query'; import { EuiFlexItem } from '../flex/flex_item'; +export { Query, AST as Ast } from './query'; + export const QueryType = PropTypes.oneOfType([ PropTypes.instanceOf(Query), PropTypes.string ]); export const SearchBarPropTypes = { diff --git a/src/services/index.js b/src/services/index.ts similarity index 88% rename from src/services/index.js rename to src/services/index.ts index aabefafa4cd..a07bfc4059b 100644 --- a/src/services/index.js +++ b/src/services/index.ts @@ -43,12 +43,6 @@ export { Pager } from './paging'; -// TODO: Migrate these services into the services directory. -export { - Query, - AST as Ast, -} from '../components/search_bar/query'; - export { Random } from './random'; From 7236ecd8f3826276f0a9d6e1c8c3a84ea02892e6 Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Thu, 20 Dec 2018 10:27:39 +0000 Subject: [PATCH 09/11] Update changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7991ef14db..393f1907d70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ ## [`master`](https://github.com/elastic/eui/tree/master) -No public interface changes since `6.0.1`. +- Convert the other of the services to TypeScript ([#1392](https://github.com/elastic/eui/pull/1392)) -## [`6.0.1`](https://github.com/elastic/eui/tree/v6.0.1) +## [`6.0.1`](https://github.com/elastic/eui/tree/v6.0.1) **Bug fixes** From 49cf16744f8c534f4a2564335c5f58d16ebaa0a2 Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Thu, 20 Dec 2018 10:53:36 +0000 Subject: [PATCH 10/11] Run the build in CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 528900df80b..ac3c8ae97ff 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "scripts": { "preinstall": "node ./preinstall_check", "start": "webpack-dev-server --port 8030 --inline --hot --config=src-docs/webpack.config.js", - "test-docker": "docker pull $npm_package_docker_image && docker run --rm -i -e GIT_COMMITTER_NAME=test -e GIT_COMMITTER_EMAIL=test --user=$(id -u):$(id -g) -e HOME=/tmp -v $(pwd):/app -w /app $npm_package_docker_image bash -c 'npm config set spin false && /opt/yarn*/bin/yarn && npm run test'", + "test-docker": "docker pull $npm_package_docker_image && docker run --rm -i -e GIT_COMMITTER_NAME=test -e GIT_COMMITTER_EMAIL=test --user=$(id -u):$(id -g) -e HOME=/tmp -v $(pwd):/app -w /app $npm_package_docker_image bash -c 'npm config set spin false && /opt/yarn*/bin/yarn && npm run test && npm run build'", "sync-docs": "node ./scripts/docs-sync.js", "build-docs": "webpack --config=src-docs/webpack.config.js", "build": "node ./scripts/compile-clean.js && node ./scripts/compile-eui.js && node ./scripts/compile-scss.js", From 4f598bde59ac55eda2100ed95c341a94e1d91566 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Wed, 2 Jan 2019 11:11:21 -0700 Subject: [PATCH 11/11] Update eui.d.ts generator to properly handle nested index files --- package.json | 4 ++- scripts/dtsgenerator.js | 62 +++++++++++++++++++++++++++++------------ yarn.lock | 18 ++++++++++++ 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index ac3c8ae97ff..5ce846926ec 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "url": "https://github.com/elastic/eui.git" }, "dependencies": { + "@types/lodash": "^4.14.116", "@types/numeral": "^0.0.25", "classnames": "^2.2.5", "core-js": "^2.5.1", @@ -74,7 +75,6 @@ "@types/classnames": "^2.2.6", "@types/enzyme": "^3.1.13", "@types/jest": "^23.3.9", - "@types/lodash": "^4.14.116", "@types/react": "^16.3.0", "@types/react-virtualized": "^9.18.6", "@types/uuid": "^3.4.4", @@ -112,6 +112,7 @@ "eslint-plugin-prettier": "^2.6.0", "eslint-plugin-react": "^7.4.0", "file-loader": "^1.1.11", + "findup": "^0.1.5", "fork-ts-checker-webpack-plugin": "^0.4.4", "geckodriver": "^1.11.0", "glob": "^7.1.2", @@ -142,6 +143,7 @@ "react-test-renderer": "^16.2.0", "redux": "^3.7.2", "redux-thunk": "^2.2.0", + "resolve": "^1.5.0", "rimraf": "^2.6.2", "sass-extract": "^2.1.0", "sass-extract-js": "^0.3.0", diff --git a/scripts/dtsgenerator.js b/scripts/dtsgenerator.js index 50d54b71416..23980916ab5 100644 --- a/scripts/dtsgenerator.js +++ b/scripts/dtsgenerator.js @@ -1,8 +1,27 @@ +const findup = require('findup'); +const resolve = require('resolve'); const fs = require('fs'); const path = require('path'); const dtsGenerator = require('dts-generator').default; const baseDir = path.resolve(__dirname, '..'); +const srcDir = path.resolve(baseDir, 'src'); + +function hasParentIndex(pathToFile) { + const isIndexFile = path.basename(pathToFile, path.extname(pathToFile)) === 'index'; + try { + const fileDirectory = path.dirname(pathToFile); + const parentIndex = findup.sync( + // if this is an index file start looking in its parent directory + isIndexFile ? path.resolve(fileDirectory, '..') : fileDirectory, + 'index.ts' + ); + // ensure the found file is in the project + return parentIndex.startsWith(baseDir); + } catch (e) { + return false; + } +} const generator = dtsGenerator({ name: '@elastic/eui', @@ -10,33 +29,37 @@ const generator = dtsGenerator({ out: 'eui.d.ts', exclude: ['node_modules/**/*.d.ts', 'src/custom_typings/**/*.d.ts'], resolveModuleId(params) { - if (path.basename(params.currentModuleId) === 'index') { + if (path.basename(params.currentModuleId) === 'index' && !hasParentIndex(path.resolve(baseDir, params.currentModuleId))) { // this module is exporting from an `index(.d)?.ts` file, declare its exports straight to @elastic/eui module return '@elastic/eui'; } else { // otherwise export as the module's path relative to the @elastic/eui namespace - return path.join('@elastic/eui', params.currentModuleId); + if (params.currentModuleId.endsWith('/index')) { + return path.join('@elastic/eui', path.dirname(params.currentModuleId)); + } else { + return path.join('@elastic/eui', params.currentModuleId); + } } }, resolveModuleImport(params) { // only intercept relative imports (don't modify node-modules references) - const isRelativeImport = params.importedModuleId[0] === '.'; + const importFromBaseDir = path.resolve(baseDir, path.dirname(params.currentModuleId)); + const isFromEuiSrc = importFromBaseDir.startsWith(srcDir); + const isRelativeImport = isFromEuiSrc && params.importedModuleId[0] === '.'; if (isRelativeImport) { - // if importing an `index` file - let isModuleIndex = false; - if (path.basename(params.importedModuleId) === 'index') { - isModuleIndex = true; - } else { - const basePath = path.resolve(baseDir, path.dirname(params.currentModuleId)); - // check if the imported module resolves to ${importedModuleId}/index.ts - if (!fs.existsSync(path.resolve(basePath, `${params.importedModuleId}.ts`))) { - // not pointing at ${importedModuleId}.ts, check if it's a directory with `index.ts` - if (fs.existsSync(path.resolve(basePath, `${params.importedModuleId}/index.ts`))) { - isModuleIndex = true; - } + // if importing from an `index` file (directly or targeting a directory with an index), + // then if there is no parent index file this should import from @elastic/eui + const importPathTarget = resolve.sync( + params.importedModuleId, + { + basedir: importFromBaseDir, + extensions: ['.ts', '.tsx'], } - } + ); + + const isIndexFile = importPathTarget.endsWith('/index.ts'); + const isModuleIndex = isIndexFile && !hasParentIndex(importPathTarget); if (isModuleIndex) { // importing an `index` file, in `resolveModuleId` above we change those modules to '@elastic/eui' @@ -55,11 +78,14 @@ const generator = dtsGenerator({ }, }); -// strip any `/// { const defsFilePath = path.resolve(baseDir, 'eui.d.ts'); fs.writeFileSync( defsFilePath, - fs.readFileSync(defsFilePath).toString().replace(/\/\/\/\W+