diff --git a/build/config.js b/build/config.js index f9333a91d..f90771550 100644 --- a/build/config.js +++ b/build/config.js @@ -1,5 +1,5 @@ module.exports = { dev: { - componentName: 'tree', // dev components + componentName: 'affix', // dev components }, }; diff --git a/components/_util/__tests__/easings.test.js b/components/_util/__tests__/easings.test.js new file mode 100644 index 000000000..d32e0dd7f --- /dev/null +++ b/components/_util/__tests__/easings.test.js @@ -0,0 +1,13 @@ +import { easeInOutCubic } from '../easings'; + +describe('Test easings', () => { + it('easeInOutCubic return value', () => { + const nums = []; + // eslint-disable-next-line no-plusplus + for (let index = 0; index < 5; index++) { + nums.push(easeInOutCubic(index, 1, 5, 4)); + } + + expect(nums).toEqual([1, 1.25, 3, 4.75, 5]); + }); +}); diff --git a/components/_util/__tests__/scrollTo.test.js b/components/_util/__tests__/scrollTo.test.js new file mode 100644 index 000000000..3e41a60ee --- /dev/null +++ b/components/_util/__tests__/scrollTo.test.js @@ -0,0 +1,56 @@ +import scrollTo from '../scrollTo'; + +describe('Test ScrollTo function', () => { + let dateNowMock; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + dateNowMock = jest + .spyOn(Date, 'now') + .mockImplementationOnce(() => 0) + .mockImplementationOnce(() => 1000); + }); + + afterEach(() => { + dateNowMock.mockRestore(); + }); + + it('test scrollTo', async () => { + const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((x, y) => { + window.scrollY = y; + window.pageYOffset = y; + }); + + scrollTo(1000); + + jest.runAllTimers(); + expect(window.pageYOffset).toBe(1000); + + scrollToSpy.mockRestore(); + }); + + it('test callback - option', async () => { + const cbMock = jest.fn(); + scrollTo(1000, { + callback: cbMock, + }); + jest.runAllTimers(); + expect(cbMock).toHaveBeenCalledTimes(1); + }); + + it('test getContainer - option', async () => { + const div = document.createElement('div'); + scrollTo(1000, { + getContainer: () => div, + }); + jest.runAllTimers(); + expect(div.scrollTop).toBe(1000); + }); +}); diff --git a/components/_util/colors.js b/components/_util/colors.js new file mode 100644 index 000000000..5b0d35bd3 --- /dev/null +++ b/components/_util/colors.js @@ -0,0 +1,17 @@ +import { tuple } from './type'; + +export const PresetColorTypes = tuple( + 'pink', + 'red', + 'yellow', + 'orange', + 'cyan', + 'green', + 'blue', + 'purple', + 'geekblue', + 'magenta', + 'volcano', + 'gold', + 'lime', +); diff --git a/components/_util/easings.js b/components/_util/easings.js new file mode 100644 index 000000000..e11ab904d --- /dev/null +++ b/components/_util/easings.js @@ -0,0 +1,8 @@ +export function easeInOutCubic(t, b, c, d) { + const cc = c - b; + t /= d / 2; + if (t < 1) { + return (cc / 2) * t * t * t + b; + } + return (cc / 2) * ((t -= 2) * t * t + 2) + b; +} diff --git a/components/_util/openAnimation.js b/components/_util/openAnimation.js index 9d63df31c..2c81887ee 100644 --- a/components/_util/openAnimation.js +++ b/components/_util/openAnimation.js @@ -6,7 +6,7 @@ function animate(node, show, done) { let height; let requestAnimationFrameId; let appearRequestAnimationFrameId; - return cssAnimation(node, 'ant-motion-collapse', { + return cssAnimation(node, 'ant-motion-collapse-legacy', { start() { if (appearRequestAnimationFrameId) { raf.cancel(appearRequestAnimationFrameId); diff --git a/components/_util/raf.js b/components/_util/raf.js index 6e8c9b083..e4c4d28a5 100644 --- a/components/_util/raf.js +++ b/components/_util/raf.js @@ -13,18 +13,20 @@ export default function wrapperRaf(callback, delayFrames = 1) { if (restFrames <= 0) { callback(); - delete ids[id]; + delete ids[myId]; } else { - ids[id] = raf(internalCallback); + ids[myId] = raf(internalCallback); } } - ids[id] = raf(internalCallback); + ids[myId] = raf(internalCallback); return myId; } wrapperRaf.cancel = function(pid) { + if (pid === undefined) return; raf.cancel(ids[pid]); delete ids[pid]; }; +wrapperRaf.ids = ids; // export this for test usage diff --git a/components/_util/scrollTo.js b/components/_util/scrollTo.js new file mode 100644 index 000000000..efdb25200 --- /dev/null +++ b/components/_util/scrollTo.js @@ -0,0 +1,37 @@ +import raf from 'raf'; +import getScroll from './getScroll'; +import { easeInOutCubic } from './easings'; + +// interface ScrollToOptions { +// /** Scroll container, default as window */ +// getContainer?: () => HTMLElement | Window; +// /** Scroll end callback */ +// callback?: () => any; +// /** Animation duration, default as 450 */ +// duration?: number; +// } + +export default function scrollTo(y, options = {}) { + const { getContainer = () => window, callback, duration = 450 } = options; + + const container = getContainer(); + const scrollTop = getScroll(container, true); + const startTime = Date.now(); + + const frameFunc = () => { + const timestamp = Date.now(); + const time = timestamp - startTime; + const nextScrollTop = easeInOutCubic(time > duration ? duration : time, scrollTop, y, duration); + if (container === window) { + window.scrollTo(window.pageXOffset, nextScrollTop); + } else { + container.scrollTop = nextScrollTop; + } + if (time < duration) { + raf(frameFunc); + } else if (typeof callback === 'function') { + callback(); + } + }; + raf(frameFunc); +} diff --git a/components/_util/styleChecker.js b/components/_util/styleChecker.js index f54c1f7a9..967f0422f 100644 --- a/components/_util/styleChecker.js +++ b/components/_util/styleChecker.js @@ -1,4 +1,4 @@ -function isStyleSupport(styleName) { +const isStyleSupport = styleName => { if (typeof window !== 'undefined' && window.document && window.document.documentElement) { const styleNameList = Array.isArray(styleName) ? styleName : [styleName]; const { documentElement } = window.document; @@ -6,7 +6,7 @@ function isStyleSupport(styleName) { return styleNameList.some(name => name in documentElement.style); } return false; -} +}; export const isFlexSupported = isStyleSupport(['flex', 'webkitFlex', 'Flex', 'msFlex']); diff --git a/components/_util/type.js b/components/_util/type.js new file mode 100644 index 000000000..8d3e0d7cb --- /dev/null +++ b/components/_util/type.js @@ -0,0 +1,4 @@ +// https://stackoverflow.com/questions/46176165/ways-to-get-string-literal-type-of-array-values-without-enum-overhead +export const tuple = (...args) => args; + +export const tupleNum = (...args) => args; diff --git a/components/_util/warning.js b/components/_util/warning.js index b76fce037..cfacc52fd 100644 --- a/components/_util/warning.js +++ b/components/_util/warning.js @@ -1,38 +1,7 @@ -/* eslint-disable no-console */ -let warned = {}; +import warning, { resetWarned } from '../vc-util/warning'; -export function warning(valid, message) { - // Support uglify - if (process.env.NODE_ENV !== 'production' && !valid && console !== undefined) { - console.error(`Warning: ${message}`); - } -} +export { resetWarned }; -export function note(valid, message) { - // Support uglify - if (process.env.NODE_ENV !== 'production' && !valid && console !== undefined) { - console.warn(`Note: ${message}`); - } -} - -export function resetWarned() { - warned = {}; -} - -export function call(method, valid, message) { - if (!valid && !warned[message]) { - method(false, message); - warned[message] = true; - } -} - -export function warningOnce(valid, message) { - call(warning, valid, message); -} - -export function noteOnce(valid, message) { - call(note, valid, message); -} - -export default warningOnce; -/* eslint-enable */ +export default (valid, component, message) => { + warning(valid, `[antdv: ${component}] ${message}`); +}; diff --git a/components/_util/wave.jsx b/components/_util/wave.jsx index 61cd2ed9e..bc073d67b 100644 --- a/components/_util/wave.jsx +++ b/components/_util/wave.jsx @@ -1,5 +1,6 @@ import TransitionEvents from './css-animation/Event'; -import raf from '../_util/raf'; +import raf from './raf'; +import { ConfigConsumerProps } from '../config-provider'; let styleForPesudo; // Where el is the DOM element you'd like to test for visibility @@ -9,7 +10,14 @@ function isHidden(element) { } return !element || element.offsetParent === null; } - +function isNotGrey(color) { + // eslint-disable-next-line no-useless-escape + const match = (color || '').match(/rgba?\((\d*), (\d*), (\d*)(, [\.\d]*)?\)/); + if (match && match[1] && match[2] && match[3]) { + return !(match[1] === match[2] && match[2] === match[3]); + } + return true; +} export default { name: 'Wave', props: ['insertExtraNode'], @@ -22,7 +30,9 @@ export default { this.instance = this.bindAnimationEvent(node); }); }, - + inject: { + configProvider: { default: () => ConfigConsumerProps }, + }, beforeDestroy() { if (this.instance) { this.instance.cancel(); @@ -33,19 +43,10 @@ export default { this.destroy = true; }, methods: { - isNotGrey(color) { - const match = (color || '').match(/rgba?\((\d*), (\d*), (\d*)(, [\.\d]*)?\)/); - if (match && match[1] && match[2] && match[3]) { - return !(match[1] === match[2] && match[2] === match[3]); - } - return true; - }, - onClick(node, waveColor) { if (!node || isHidden(node) || node.className.indexOf('-leave') >= 0) { return; } - this.removeExtraStyleNode(); const { insertExtraNode } = this.$props; this.extraNode = document.createElement('div'); const extraNode = this.extraNode; @@ -59,13 +60,19 @@ export default { waveColor && waveColor !== '#ffffff' && waveColor !== 'rgb(255, 255, 255)' && - this.isNotGrey(waveColor) && + isNotGrey(waveColor) && !/rgba\(\d*, \d*, \d*, 0\)/.test(waveColor) && // any transparent rgba color waveColor !== 'transparent' ) { + // Add nonce if CSP exist + if (this.csp && this.csp.nonce) { + styleForPesudo.nonce = this.csp.nonce; + } extraNode.style.borderColor = waveColor; - - styleForPesudo.innerHTML = `[ant-click-animating-without-extra-node]:after { border-color: ${waveColor}; }`; + styleForPesudo.innerHTML = ` + [ant-click-animating-without-extra-node='true']::after, .ant-click-animating-node { + --antd-wave-shadow-color: ${waveColor}; + }`; if (!document.body.contains(styleForPesudo)) { document.body.appendChild(styleForPesudo); } @@ -76,7 +83,28 @@ export default { TransitionEvents.addStartEventListener(node, this.onTransitionStart); TransitionEvents.addEndEventListener(node, this.onTransitionEnd); }, + onTransitionStart(e) { + if (this.destroy) return; + const node = this.$el; + if (!e || e.target !== node) { + return; + } + + if (!this.animationStart) { + this.resetEffect(node); + } + }, + onTransitionEnd(e) { + if (!e || e.animationName !== 'fadeEffect') { + return; + } + this.resetEffect(e.target); + }, + getAttributeName() { + const { insertExtraNode } = this.$props; + return insertExtraNode ? 'ant-click-animating' : 'ant-click-animating-without-extra-node'; + }, bindAnimationEvent(node) { if ( !node || @@ -113,10 +141,6 @@ export default { }, }; }, - getAttributeName() { - const { insertExtraNode } = this.$props; - return insertExtraNode ? 'ant-click-animating' : 'ant-click-animating-without-extra-node'; - }, resetEffect(node) { if (!node || node === this.extraNode || !(node instanceof Element)) { @@ -124,40 +148,22 @@ export default { } const { insertExtraNode } = this.$props; const attributeName = this.getAttributeName(); - node.removeAttribute(attributeName); - this.removeExtraStyleNode(); + node.setAttribute(attributeName, 'false'); // edge has bug on `removeAttribute` #14466 + if (styleForPesudo) { + styleForPesudo.innerHTML = ''; + } if (insertExtraNode && this.extraNode && node.contains(this.extraNode)) { node.removeChild(this.extraNode); } TransitionEvents.removeStartEventListener(node, this.onTransitionStart); TransitionEvents.removeEndEventListener(node, this.onTransitionEnd); }, - onTransitionStart(e) { - if (this.destroy) return; - - const node = this.$el; - if (!e || e.target !== node) { - return; - } - - if (!this.animationStart) { - this.resetEffect(node); - } - }, - onTransitionEnd(e) { - if (!e || e.animationName !== 'fadeEffect') { - return; - } - this.resetEffect(e.target); - }, - removeExtraStyleNode() { - if (styleForPesudo) { - styleForPesudo.innerHTML = ''; - } - }, }, render() { + if (this.configProvider.csp) { + this.csp = csp; + } return this.$slots.default && this.$slots.default[0]; }, }; diff --git a/components/vc-util/warning.js b/components/vc-util/warning.js new file mode 100644 index 000000000..b76fce037 --- /dev/null +++ b/components/vc-util/warning.js @@ -0,0 +1,38 @@ +/* eslint-disable no-console */ +let warned = {}; + +export function warning(valid, message) { + // Support uglify + if (process.env.NODE_ENV !== 'production' && !valid && console !== undefined) { + console.error(`Warning: ${message}`); + } +} + +export function note(valid, message) { + // Support uglify + if (process.env.NODE_ENV !== 'production' && !valid && console !== undefined) { + console.warn(`Note: ${message}`); + } +} + +export function resetWarned() { + warned = {}; +} + +export function call(method, valid, message) { + if (!valid && !warned[message]) { + method(false, message); + warned[message] = true; + } +} + +export function warningOnce(valid, message) { + call(warning, valid, message); +} + +export function noteOnce(valid, message) { + call(note, valid, message); +} + +export default warningOnce; +/* eslint-enable */