Merge branch 'preset-vision/0.9.0' of gitlab.alibaba-inc.com:ali-lowcode/ali-lowcode-engine into preset-vision/0.9.0

This commit is contained in:
kangwei 2020-05-08 15:35:52 +08:00
commit 9bf2e3b4ef
1742 changed files with 541349 additions and 1592 deletions

3
.gitignore vendored
View File

@ -100,3 +100,6 @@ typings/
# mac config files
.DS_Store
# codealike
codealike.json

View File

@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.9.3](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-material-parser@0.9.2...@ali/lowcode-material-parser@0.9.3) (2020-05-08)
### Features
* 🎸 support parsing sub components ([70f3e32](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/70f3e325c64bafe6a098e7eb872a81308566e811))
<a name="0.9.2"></a>
## [0.9.2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-material-parser@0.9.1...@ali/lowcode-material-parser@0.9.2) (2020-05-07)

View File

@ -1,6 +1,6 @@
{
"name": "@ali/lowcode-material-parser",
"version": "0.9.2",
"version": "0.9.3",
"description": "material parser for Ali lowCode engine",
"main": "lib/index.js",
"files": [
@ -54,7 +54,8 @@
"lodash": "^4.17.15",
"react-docgen": "^5.3.0",
"semver": "^7.1.3",
"short-uuid": "^3.1.1"
"short-uuid": "^3.1.1",
"typescript": "^3.8.3"
},
"publishConfig": {
"registry": "https://registry.npm.alibaba-inc.com"

View File

@ -45,10 +45,10 @@ export async function genManifest(
npm: {
package: matScanModel.pkgName,
version: matScanModel.pkgVersion,
exportName: matParsedModel.componentName,
exportName: matParsedModel.meta?.exportName || matParsedModel.componentName,
main: matScanModel.mainFilePath,
destructuring: false,
subName: '',
destructuring: matParsedModel.meta?.exportName !== 'default',
subName: matParsedModel.meta?.subName || '',
},
};

View File

@ -0,0 +1,106 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import { namedTypes as t } from 'ast-types';
const {
getMemberValuePath,
isReactComponentClass,
isReactComponentMethod,
resolveToValue,
match,
} = require('react-docgen').utils;
import getMethodDocumentation from '../utils/getMethodDocumentation';
const { traverseShallow } = require('react-docgen/dist/utils/traverse');
/**
* The following values/constructs are considered methods:
*
* - Method declarations in classes (except "constructor" and React lifecycle
* methods
* - Public class fields in classes whose value are a functions
* - Object properties whose values are functions
*/
function isMethod(path: any) {
const isProbablyMethod =
(t.MethodDefinition.check(path.node) && path.node.kind !== 'constructor') ||
((t.ClassProperty.check(path.node) || t.Property.check(path.node)) && t.Function.check(path.get('value').node));
return isProbablyMethod && !isReactComponentMethod(path);
}
function findAssignedMethods(scope: any, idPath: any) {
const results: any[] = [];
if (!t.Identifier.check(idPath.node)) {
return results;
}
const name = idPath.node.name;
const idScope = idPath.scope.lookup(idPath.node.name);
traverseShallow(scope.path, {
visitAssignmentExpression: function(path: any) {
const node = path.node;
if (
match(node.left, {
type: 'MemberExpression',
object: { type: 'Identifier', name },
}) &&
path.scope.lookup(name) === idScope &&
t.Function.check(resolveToValue(path.get('right')).node)
) {
results.push(path);
return false;
}
return this.traverse(path);
},
});
return results;
}
/**
* Extract all flow types for the methods of a react component. Doesn't
* return any react specific lifecycle methods.
*/
export default function componentMethodsHandler(documentation: any, path: any) {
// Extract all methods from the class or object.
let methodPaths = [];
if (isReactComponentClass(path)) {
methodPaths = path.get('body', 'body').filter(isMethod);
} else if (t.ObjectExpression.check(path.node)) {
methodPaths = path.get('properties').filter(isMethod);
// Add the statics object properties.
const statics = getMemberValuePath(path, 'statics');
if (statics) {
statics.get('properties').each((p: any) => {
if (isMethod(p)) {
p.node.static = true;
methodPaths.push(p);
}
});
}
} else if (
t.VariableDeclarator.check(path.parent.node) &&
path.parent.node.init === path.node &&
t.Identifier.check(path.parent.node.id)
) {
methodPaths = findAssignedMethods(path.parent.scope, path.parent.get('id'));
} else if (
t.AssignmentExpression.check(path.parent.node) &&
path.parent.node.right === path.node &&
t.Identifier.check(path.parent.node.left)
) {
methodPaths = findAssignedMethods(path.parent.scope, path.parent.get('left'));
} else if (t.FunctionDeclaration.check(path.node)) {
methodPaths = findAssignedMethods(path.parent.scope, path.get('id'));
}
documentation.set('methods', methodPaths.map(getMethodDocumentation).filter(Boolean));
}

View File

@ -0,0 +1,95 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import { namedTypes as t } from 'ast-types';
import getTSType from '../utils/getTSType';
const { unwrapUtilityType } = require('react-docgen/dist/utils/flowUtilityTypes');
const { getFlowType, getPropertyName, resolveToValue } = require('react-docgen').utils;
const setPropDescription = require('react-docgen/dist/utils/setPropDescription').default;
import getFlowTypeFromReactComponent from '../utils/getFlowTypeFromReactComponent';
import { applyToFlowTypeProperties } from '../utils/getFlowTypeFromReactComponent';
function setPropDescriptor(documentation: any, path: any, typeParams: any) {
if (t.ObjectTypeSpreadProperty.check(path.node)) {
const argument = unwrapUtilityType(path.get('argument'));
if (t.ObjectTypeAnnotation.check(argument.node)) {
applyToFlowTypeProperties(
documentation,
argument,
(propertyPath: any, innerTypeParams: any) => {
setPropDescriptor(documentation, propertyPath, innerTypeParams);
},
typeParams,
);
return;
}
const name = argument.get('id').get('name');
const resolvedPath = resolveToValue(name);
if (resolvedPath && t.TypeAlias.check(resolvedPath.node)) {
const right = resolvedPath.get('right');
applyToFlowTypeProperties(
documentation,
right,
(propertyPath: any, innerTypeParams: any) => {
setPropDescriptor(documentation, propertyPath, innerTypeParams);
},
typeParams,
);
} else {
documentation.addComposes(name.node.name);
}
} else if (t.ObjectTypeProperty.check(path.node)) {
const type = getFlowType(path.get('value'), typeParams);
const propName = getPropertyName(path);
if (!propName) return;
const propDescriptor = documentation.getPropDescriptor(propName);
propDescriptor.required = !path.node.optional;
propDescriptor.flowType = type;
// We are doing this here instead of in a different handler
// to not need to duplicate the logic for checking for
// imported types that are spread in to props.
setPropDescription(documentation, path);
} else if (t.TSPropertySignature.check(path.node)) {
const type = getTSType(path.get('typeAnnotation'), typeParams);
const propName = getPropertyName(path);
if (!propName) return;
const propDescriptor = documentation.getPropDescriptor(propName);
propDescriptor.required = !path.node.optional;
propDescriptor.tsType = type;
// We are doing this here instead of in a different handler
// to not need to duplicate the logic for checking for
// imported types that are spread in to props.
setPropDescription(documentation, path);
}
}
/**
* This handler tries to find flow Type annotated react components and extract
* its types to the documentation. It also extracts docblock comments which are
* inlined in the type definition.
*/
export default function flowTypeHandler(documentation: any, path: any) {
const flowTypesPath = getFlowTypeFromReactComponent(path);
if (!flowTypesPath) {
return;
}
applyToFlowTypeProperties(documentation, flowTypesPath, (propertyPath: any, typeParams: any) => {
setPropDescriptor(documentation, propertyPath, typeParams);
});
}

View File

@ -1,23 +1,23 @@
import {
propTypeHandler,
contextTypeHandler,
childContextTypeHandler,
} from './propTypeHandler';
import { propTypeHandler, contextTypeHandler, childContextTypeHandler } from './propTypeHandler';
import defaultPropsHandler from './defaultPropsHandler';
import flowTypeHandler from './flowTypeHandler';
import componentMethodsHandler from './componentMethodsHandler';
import preProcessHandler from './preProcessHandler';
const { handlers } = require('react-docgen');
const defaultHandlers = [
preProcessHandler,
propTypeHandler,
contextTypeHandler,
childContextTypeHandler,
handlers.propTypeCompositionHandler,
handlers.propDocBlockHandler,
handlers.flowTypeHandler,
flowTypeHandler,
defaultPropsHandler,
handlers.componentDocblockHandler,
handlers.displayNameHandler,
handlers.componentMethodsHandler,
componentMethodsHandler,
handlers.componentMethodsJsDocHandler,
];

View File

@ -0,0 +1,3 @@
export default function preProcessHandler(documentation: any, path: any) {
documentation.set('meta', path.__meta);
}

View File

@ -14,6 +14,9 @@ export default function parse(params: { fileContent: string; filePath: string })
return resolver(ast);
},
handlers,
{
filename: filePath,
},
);
const coms = result.reduce((res: any[], info: any) => {
if (!info || !info.props) return res;
@ -29,6 +32,7 @@ export default function parse(params: { fileContent: string; filePath: string })
res.push({
componentName: info.displayName,
props,
meta: info.meta || {},
});
return res;
}, []);

View File

@ -1,5 +1,6 @@
export default function checkIsIIFE(path: any) {
return (
path.value &&
path.value.callee &&
path.value.callee.type === 'FunctionExpression' &&
path.node.type === 'CallExpression'

View File

@ -0,0 +1,54 @@
import { namedTypes as t } from 'ast-types';
const { match, resolveToValue } = require('react-docgen').utils;
const { traverseShallow } = require('react-docgen/dist/utils/traverse');
import isReactComponentStaticMember from './isReactComponentStaticMember';
import getRoot from '../utils/getRoot';
function findAssignedMethods(scope: any, idPath: any) {
const results: any[] = [];
if (!t.Identifier.check(idPath.node)) {
return results;
}
const name = idPath.node.name;
const idScope = idPath.scope.lookup(idPath.node.name);
traverseShallow(scope.path, {
visitAssignmentExpression: function(path: any) {
const node = path.node;
if (
match(node.left, {
type: 'MemberExpression',
object: { type: 'Identifier', name },
})
// && path.scope.lookup(name) === idScope
) {
results.push(path);
return false;
}
return this.traverse(path);
},
});
return results.filter((x) => !isReactComponentStaticMember(x.get('left')));
}
export default findAssignedMethods;
// const findAssignedMethodsFromScopes = (scope: any, idPath: any) => {
// const rootNode = getRoot(idPath);
// let { __scope: scopes = [] } = rootNode;
// if (!scopes.find((x: any) => x.scope === scope && x.idPath === idPath)) {
// scopes = [
// ...scopes,
// {
// scope,
// idPath,
// },
// ];
// }
// return scopes.map(({ scope: s, idPath: id }: any) => findAssignedMethods(s, id)).flatMap((x: any) => x);
// };
// export { findAssignedMethodsFromScopes };

View File

@ -7,11 +7,19 @@
*/
import { namedTypes as t, visit } from 'ast-types';
import { uniqBy } from 'lodash';
import checkIsIIFE from './checkIsIIFE';
import resolveHOC from './resolveHOC';
import resolveIIFE from './resolveIIFE';
import resolveImport from './resolveImport';
import resolveImport, { isImportLike } from './resolveImport';
import resolveTranspiledClass from './resolveTranspiledClass';
import isStaticMethod from './isStaticMethod';
import findAssignedMethods from './findAssignedMethods';
import resolveExportDeclaration from './resolveExportDeclaration';
import makeProxy from '../utils/makeProxy';
const expressionTo = require('react-docgen/dist/utils/expressionTo');
import { get, set, has, ICache } from '../utils/cache';
import getName from '../utils/getName';
const {
isExportsOrModuleAssignment,
@ -20,8 +28,8 @@ const {
isReactForwardRefCall,
isStatelessComponent,
normalizeClassDefinition,
resolveExportDeclaration,
resolveToValue,
getMemberValuePath,
} = require('react-docgen').utils;
function ignore() {
@ -47,16 +55,14 @@ function resolveDefinition(definition: any) {
} else if (isReactComponentClass(definition)) {
normalizeClassDefinition(definition);
return definition;
} else if (
isStatelessComponent(definition) ||
isReactForwardRefCall(definition)
) {
} else if (isStatelessComponent(definition) || isReactForwardRefCall(definition)) {
return definition;
}
return null;
}
function getDefinition(definition: any): any {
function getDefinition(definition: any, cache: ICache = {}): any {
const { __meta: exportMeta = {} } = definition;
if (checkIsIIFE(definition)) {
definition = resolveToValue(resolveIIFE(definition));
if (!isComponentDefinition(definition)) {
@ -64,24 +70,195 @@ function getDefinition(definition: any): any {
}
} else {
definition = resolveToValue(resolveHOC(definition));
if (isComponentDefinition(definition)) {
definition = makeProxy(definition, {
__meta: exportMeta,
});
return definition;
}
if (checkIsIIFE(definition)) {
definition = resolveToValue(resolveIIFE(definition));
if (!isComponentDefinition(definition)) {
definition = resolveTranspiledClass(definition);
}
} else if (t.SequenceExpression.check(definition.node)) {
return getDefinition(
resolveToValue(definition.get('expressions').get(0)),
);
return getDefinition(resolveToValue(definition.get('expressions').get(0)), cache);
} else {
definition = resolveImport(
definition,
findAllExportedComponentDefinition,
);
return resolveImport(definition, (ast: any, sourcePath: string) => {
const importMeta: any[] = [];
if (t.ImportDeclaration.check(definition.node)) {
// @ts-ignore
const specifiers = definition.get('specifiers');
specifiers.each((spec: any) => {
const { node } = spec;
importMeta.push({
localName: node.local.name,
importedName: node.imported ? node.imported.name : 'default',
});
});
}
let result;
if (has('ast-export', ast.__path)) {
result = get('ast-export', ast.__path);
} else {
result = findAllExportedComponentDefinition(ast);
set('ast-export', ast.__path, result);
}
const exportList: any[] = [];
const importList: any[] = [];
result = result.forEach((def: any) => {
let { __meta: meta = {} } = def;
let exportName = meta.exportName;
for (let item of importMeta) {
if (exportName === item.importedName) {
exportName = item.localName;
break;
}
}
if (exportName) {
importList.push(makeProxy(def, { __meta: { exportName } }));
}
const nextMeta: any = {
exportName,
};
if (exportName === exportMeta.localName) {
nextMeta.exportName = exportMeta.exportName;
} else {
return;
}
if (exportMeta.subName) {
nextMeta.subName = exportMeta.subName;
} else if (meta.subName) {
nextMeta.subName = meta.subName;
}
exportList.push(makeProxy(def, { __meta: nextMeta }));
});
cache[sourcePath] = importList;
// result = result.filter((x) => !x.__shouldDelete);
return exportList;
});
}
}
if (definition && !definition.__meta) {
definition.__meta = exportMeta;
}
return definition;
}
export interface IMethodsPath {
subName: string;
localName: string;
value: any;
}
/**
* Extract all flow types for the methods of a react component. Doesn't
* return any react specific lifecycle methods.
*/
function getSubComponents(path: any, scope: any, cache: ICache) {
// Extract all methods from the class or object.
let methodPaths = [];
if (isReactComponentClass(path)) {
methodPaths = path.get('body', 'body').filter(isStaticMethod);
methodPaths = [...methodPaths, ...findAssignedMethods(scope || path.scope, path.get('id'))];
} else if (t.ObjectExpression.check(path.node)) {
methodPaths = path.get('properties').filter(isStaticMethod);
methodPaths = [...methodPaths, ...findAssignedMethods(scope || path.scope, path.get('id'))];
// Add the statics object properties.
const statics = getMemberValuePath(path, 'statics');
if (statics) {
statics.get('properties').each((p: any) => {
if (isStaticMethod(p)) {
p.node.static = true;
methodPaths.push(p);
}
});
}
} else if (
t.VariableDeclarator.check(path.parent.node) &&
path.parent.node.init === path.node &&
t.Identifier.check(path.parent.node.id)
) {
methodPaths = findAssignedMethods(scope || path.parent.scope, path.parent.get('id'));
} else if (
t.AssignmentExpression.check(path.parent.node) &&
path.parent.node.right === path.node &&
t.Identifier.check(path.parent.node.left)
) {
methodPaths = findAssignedMethods(scope || path.parent.scope, path.parent.get('left'));
} else if (t.FunctionDeclaration.check(path.node)) {
methodPaths = findAssignedMethods(scope || path.parent.scope, path.get('id'));
} else if (t.ArrowFunctionExpression.check(path.node)) {
methodPaths = findAssignedMethods(scope || path.parent.scope, path.parent.get('id'));
}
return (
methodPaths
.map((x: any) => {
if (t.ClassProperty.check(x.node)) {
return {
value: x.get('value'),
subName: x.node.key.name,
localName: getName(x.get('value')),
};
}
return {
value: x,
subName: x.node.left.property.name,
localName: getName(x.get('right')),
};
})
.map(({ subName, localName, value }: IMethodsPath) => ({
subName,
localName,
value: resolveToValue(value),
}))
.map(({ subName, localName, value }: IMethodsPath) => {
let def = getDefinition(
makeProxy(value, {
__meta: {
localName,
subName,
exportName: path.__meta && path.__meta.exportName,
},
}),
cache,
);
if (!Array.isArray(def)) {
def = [def];
}
return {
subName,
localName,
value: def.flatMap((x: any) => x).filter((x: any) => isComponentDefinition(x)),
};
})
.map(({ subName, localName, value }: IMethodsPath) =>
value.map((x: any) => ({
subName,
localName,
value: x,
})),
)
// @ts-ignore
.flatMap((x: any) => x)
.map(({ subName, localName, value }: IMethodsPath) => {
const __meta = {
subName: subName,
exportName: path.__meta && path.__meta.exportName,
};
return makeProxy(value, { __meta });
})
);
}
/**
* Given an AST, this function tries to find the exported component definition.
*
@ -99,6 +276,8 @@ function getDefinition(definition: any): any {
*/
export default function findAllExportedComponentDefinition(ast: any) {
const components: any[] = [];
const cache: ICache = {};
let programScope: any;
function exportDeclaration(path: any) {
const definitions = resolveExportDeclaration(path)
@ -106,7 +285,7 @@ export default function findAllExportedComponentDefinition(ast: any) {
if (isComponentDefinition(definition)) {
acc.push(definition);
} else {
definition = getDefinition(definition);
definition = getDefinition(definition, cache);
if (!Array.isArray(definition)) {
definition = [definition];
}
@ -118,7 +297,11 @@ export default function findAllExportedComponentDefinition(ast: any) {
}
return acc;
}, [])
.map((definition: any) => resolveDefinition(definition));
.map((definition: any) => {
const { __meta: meta } = definition;
const def = resolveDefinition(definition);
return makeProxy(def, { __meta: meta });
});
if (definitions.length === 0) {
return false;
@ -132,6 +315,10 @@ export default function findAllExportedComponentDefinition(ast: any) {
}
visit(ast, {
visitProgram: function(path) {
programScope = path.scope;
return this.traverse(path);
},
visitFunctionDeclaration: ignore,
visitFunctionExpression: ignore,
visitClassDeclaration: ignore,
@ -149,9 +336,7 @@ export default function findAllExportedComponentDefinition(ast: any) {
visitExportNamedDeclaration: exportDeclaration,
visitExportDefaultDeclaration: exportDeclaration,
visitExportAllDeclaration: function(path) {
components.push(
...resolveImport(path, findAllExportedComponentDefinition),
);
components.push(...resolveImport(path, findAllExportedComponentDefinition));
return false;
},
@ -161,20 +346,44 @@ export default function findAllExportedComponentDefinition(ast: any) {
if (!isExportsOrModuleAssignment(path)) {
return false;
}
const arr = expressionTo.Array(path.get('left'));
const meta: any = {
exportName: arr[1] === 'exports' ? 'default' : arr[1],
};
// Resolve the value of the right hand side. It should resolve to a call
// expression, something like React.createClass
path = resolveToValue(path.get('right'));
if (!isComponentDefinition(path)) {
path = getDefinition(path);
path = getDefinition(path, cache);
}
const definition = resolveDefinition(path);
if (definition && components.indexOf(definition) === -1) {
components.push(definition);
let definitions = resolveDefinition(path);
if (!Array.isArray(definitions)) {
definitions = [definitions];
}
definitions.forEach((definition: any) => {
if (definition && components.indexOf(definition) === -1) {
// if (definition.__meta) {
definition = makeProxy(definition, {
__meta: meta,
});
// }
components.push(definition);
}
});
return false;
},
});
return components;
const result = components.reduce((acc, item) => {
let subModuleDefinitions = [];
subModuleDefinitions = getSubComponents(item, programScope, cache);
return [...acc, item, ...subModuleDefinitions];
}, []);
const res = uniqBy(result, (x: any) => {
return `${x.__meta.exportName}/${x.__meta.subName}`;
});
return res;
}

View File

@ -0,0 +1,13 @@
import { namedTypes as t } from 'ast-types';
const { getPropertyName } = require('react-docgen').utils;
const reactStaticMembers = ['propTypes', 'defaultProps', 'contextTypes'];
export default function isReactComponentStaticMember(methodPath: any) {
let name;
if (t.MemberExpression.check(methodPath.node)) {
name = methodPath.node.property.name;
} else {
name = getPropertyName(methodPath);
}
return !!name && reactStaticMembers.indexOf(name) !== -1;
}

View File

@ -0,0 +1,14 @@
import { namedTypes as t } from 'ast-types';
import isReactComponentStaticMember from './isReactComponentStaticMember';
const { isReactComponentMethod } = require('react-docgen').utils;
/**
* judge if static method
*/
function isStaticMethod(path: any) {
const isProbablyStaticMethod = t.ClassProperty.check(path.node) && path.node.static === true;
return isProbablyStaticMethod && !isReactComponentStaticMember(path) && !isReactComponentMethod(path);
}
export default isStaticMethod;

View File

@ -0,0 +1,53 @@
import { namedTypes as t } from 'ast-types';
import makeProxy from '../utils/makeProxy';
import getName from '../utils/getName';
export default function resolveExportDeclaration(path: any) {
const definitions = [];
if (path.node.default || t.ExportDefaultDeclaration.check(path.node)) {
const def = path.get('declaration');
const meta: { [name: string]: string } = {
exportName: 'default',
localName: getName(def),
};
definitions.push(makeProxy(def, { __meta: meta }));
} else if (path.node.declaration) {
if (t.VariableDeclaration.check(path.node.declaration)) {
path.get('declaration', 'declarations').each((declarator: any) => {
definitions.push(
makeProxy(declarator, {
__meta: {
exportName: declarator.get('id').node.name,
},
}),
);
});
} else {
const def = path.get('declaration');
definitions.push(
makeProxy(def, {
__meta: {
exportName: 'default',
},
}),
);
}
} else if (path.node.specifiers) {
path.get('specifiers').each((specifier: any) => {
const def = specifier.node.id ? specifier.get('id') : specifier.get('local');
const exportName = specifier.get('exported').node.name;
const localName = def.get('local').node.name;
definitions.push(
makeProxy(def, {
__meta: {
exportName: exportName,
localName: localName,
},
}),
);
});
}
return definitions;
}

View File

@ -1,21 +1,10 @@
import { namedTypes as t } from 'ast-types';
import fs from 'fs';
import p from 'path';
import getRoot from '../utils/getRoot';
function getRoot(node: any) {
let root = node.parent;
while (root.parent) {
root = root.parent;
}
return root.node;
}
function isImportLike(node: any) {
return (
t.ImportDeclaration.check(node) ||
t.ExportAllDeclaration.check(node) ||
t.ExportNamedDeclaration.check(node)
);
export function isImportLike(node: any) {
return t.ImportDeclaration.check(node) || t.ExportAllDeclaration.check(node) || t.ExportNamedDeclaration.check(node);
}
function getPath(path: any, name: any) {
@ -27,7 +16,7 @@ function getPath(path: any, name: any) {
if (fs.existsSync(p.resolve(__path, name))) {
name = name + '/index';
}
const suffix = suffixes.find(suf => {
const suffix = suffixes.find((suf) => {
return fs.existsSync(p.resolve(__path, name + suf));
});
if (!suffix) return;
@ -35,9 +24,12 @@ function getPath(path: any, name: any) {
}
const buildParser = require('react-docgen/dist/babelParser').default;
const parser = buildParser();
const suffixes = ['.js', '.jsx', '.ts', '.tsx'];
const cache: {
[name: string]: any;
} = {};
export default function resolveImport(path: any, callback: any) {
let name;
if (path.name === 'local') {
@ -50,11 +42,19 @@ export default function resolveImport(path: any, callback: any) {
if (name) {
const __path = getPath(path, name);
if (!__path) return path;
const fileContent = fs.readFileSync(__path, 'utf8');
const ast = parser.parse(fileContent);
ast.__src = fileContent;
ast.__path = __path;
return callback(ast);
let ast;
if (!cache[__path]) {
const fileContent = fs.readFileSync(__path, 'utf8');
const parser = buildParser({ filename: __path });
ast = parser.parse(fileContent);
ast.__src = fileContent;
ast.__path = __path;
cache[__path] = ast;
} else {
ast = cache[__path];
}
return callback(ast, __path);
}
return path;
}

View File

@ -1,9 +1,9 @@
export function transformType(type: any) {
if (typeof type === 'string') return type;
const { name, elements, value = elements, computed, required } = type;
if (!value && !required) {
return name;
}
export function transformType(itemType: any) {
if (typeof itemType === 'string') return itemType;
const { name, elements, value = elements, computed, required, type } = itemType;
// if (!value && !required && !type) {
// return name;
// }
if (computed !== undefined && value) {
return eval(value);
}
@ -21,6 +21,7 @@ export function transformType(type: any) {
case 'func':
case 'symbol':
case 'object':
case 'null':
break;
case 'literal':
return eval(value);
@ -36,13 +37,24 @@ export function transformType(type: any) {
case 'boolean':
result.type = 'bool';
break;
case 'Array': {
case 'Function':
result.type = 'func';
break;
case 'unknown':
result.type = 'any';
break;
case 'Array':
case 'arrayOf': {
result.type = 'arrayOf';
const v = transformType(value[0]);
if (typeof v.type === 'string') result.value = v.type;
break;
}
case 'signature': {
if (typeof type === 'string') {
result.type = type;
break;
}
result.type = 'shape';
const {
signature: { properties },
@ -103,22 +115,28 @@ export function transformType(type: any) {
result.value = name;
break;
}
if (Object.keys(result).length === 1) {
return result.type;
}
return result;
}
export function transformItem(name: string, item: any) {
const { description, flowType, type = flowType, required, defaultValue } = item;
const { description, flowType, tsType, type = tsType || flowType, required, defaultValue } = item;
const result: any = {
name,
propType: transformType({
};
if (type) {
result.propType = transformType({
...type,
required: !!required,
}),
};
});
}
if (description) {
result.description = description;
}
if (defaultValue) {
if (defaultValue !== undefined) {
try {
const value = eval(defaultValue.value);
result.defaultValue = value;
@ -127,6 +145,5 @@ export function transformItem(name: string, item: any) {
if (result.propType === undefined) {
delete result.propType;
}
return result;
}

View File

@ -0,0 +1,18 @@
export interface ICache {
[name: string]: any;
}
const cache: ICache = {};
export function set(scope: string, name: string, value: any) {
cache[scope] = cache[scope] || {};
cache[scope][name] = value;
}
export function get(scope: string, name: string) {
return (cache[scope] || {})[name];
}
export function has(scope: string, name: string) {
return cache[scope] && cache[scope].hasOwnProperty(name);
}

View File

@ -0,0 +1,49 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { namedTypes as t } from 'ast-types';
const supportedUtilityTypes = new Set(['$Exact', '$ReadOnly']);
/**
* See `supportedUtilityTypes` for which types are supported and
* https://flow.org/en/docs/types/utilities/ for which types are available.
*/
function isSupportedUtilityType(path: any) {
if (t.GenericTypeAnnotation.check(path.node)) {
const idPath = path.get('id');
return !!idPath && supportedUtilityTypes.has(idPath.node.name);
}
return false;
}
export { isSupportedUtilityType };
function isReactUtilityType(path: any) {
if (t.TSTypeReference.check(path.node)) {
const objName = path.get('typeName', 'left').node.name;
if (objName === 'React') {
return true;
}
}
return false;
}
/**
* Unwraps well known utility types. For example:
*
* $ReadOnly<T> => T
*/
function unwrapUtilityType(path: any) {
while (isSupportedUtilityType(path) || isReactUtilityType(path)) {
path = path.get('typeParameters', 'params', 0);
}
return path;
}
export { unwrapUtilityType };

View File

@ -0,0 +1,119 @@
import { namedTypes as t } from 'ast-types';
const {
isReactComponentClass,
isReactForwardRefCall,
getTypeAnnotation,
resolveToValue,
getMemberValuePath,
} = require('react-docgen').utils;
import resolveGenericTypeAnnotation from './resolveGenericTypeAnnotation';
const getTypeParameters = require('react-docgen/dist/utils/getTypeParameters').default;
function getStatelessPropsPath(componentDefinition: any) {
const value = resolveToValue(componentDefinition);
if (isReactForwardRefCall(value)) {
const inner = resolveToValue(value.get('arguments', 0));
return inner.get('params', 0);
}
if (t.VariableDeclarator.check(componentDefinition.parent.node)) {
const id = componentDefinition.parent.get('id');
if (id.node.typeAnnotation) {
return id;
}
}
return value.get('params', 0);
}
/**
* Given an React component (stateless or class) tries to find the
* flow type for the props. If not found or not one of the supported
* component types returns null.
*/
export default (path: any) => {
let typePath = null;
if (isReactComponentClass(path)) {
const superTypes = path.get('superTypeParameters');
if (superTypes.value) {
const params = superTypes.get('params');
if (params.value.length === 3) {
typePath = params.get(1);
} else {
typePath = params.get(0);
}
} else {
const propsMemberPath = getMemberValuePath(path, 'props');
if (!propsMemberPath) {
return null;
}
typePath = getTypeAnnotation(propsMemberPath.parentPath);
}
return typePath;
}
const propsParam = getStatelessPropsPath(path);
if (propsParam) {
typePath = getTypeAnnotation(propsParam);
}
return typePath;
};
function applyToFlowTypeProperties(documentation: any, path: any, callback: any, typeParams?: any) {
if (path.node.properties) {
path.get('properties').each((propertyPath: any) => callback(propertyPath, typeParams));
} else if (path.node.members) {
path.get('members').each((propertyPath: any) => callback(propertyPath, typeParams));
} else if (path.node.type === 'InterfaceDeclaration') {
if (path.node.extends) {
applyExtends(documentation, path, callback, typeParams);
}
path.get('body', 'properties').each((propertyPath: any) => callback(propertyPath, typeParams));
} else if (path.node.type === 'TSInterfaceDeclaration') {
if (path.node.extends) {
applyExtends(documentation, path, callback, typeParams);
}
path.get('body', 'body').each((propertyPath: any) => callback(propertyPath, typeParams));
} else if (path.node.type === 'IntersectionTypeAnnotation' || path.node.type === 'TSIntersectionType') {
path
.get('types')
.each((typesPath: any) => applyToFlowTypeProperties(documentation, typesPath, callback, typeParams));
} else if (path.node.type !== 'UnionTypeAnnotation') {
// The react-docgen output format does not currently allow
// for the expression of union types
const typePath = resolveGenericTypeAnnotation(path);
if (typePath) {
applyToFlowTypeProperties(documentation, typePath, callback, typeParams);
}
}
}
function applyExtends(documentation: any, path: any, callback: any, typeParams: any) {
path.get('extends').each((extendsPath: any) => {
const resolvedPath = resolveGenericTypeAnnotation(extendsPath);
if (resolvedPath) {
if (resolvedPath.node.typeParameters && extendsPath.node.typeParameters) {
typeParams = getTypeParameters(
resolvedPath.get('typeParameters'),
extendsPath.get('typeParameters'),
typeParams,
);
}
applyToFlowTypeProperties(documentation, resolvedPath, callback, typeParams);
} else {
const id = extendsPath.node.id || extendsPath.node.typeName || extendsPath.node.expression;
if (id && id.type === 'Identifier') {
documentation.addComposes(id.name);
}
}
});
}
export { applyToFlowTypeProperties };

View File

@ -0,0 +1,164 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import getTSType from './getTSType';
const { namedTypes: t } = require('ast-types');
const {
resolveToValue,
getFlowType,
getParameterName,
getPropertyName,
getTypeAnnotation,
} = require('react-docgen').utils;
const { getDocblock } = require('react-docgen/dist/utils/docblock');
function getMethodFunctionExpression(methodPath) {
if (t.AssignmentExpression.check(methodPath.node)) {
return resolveToValue(methodPath.get('right'));
}
// Otherwise this is a method/property node
return methodPath.get('value');
}
function getMethodParamsDoc(methodPath) {
const params = [];
const functionExpression = getMethodFunctionExpression(methodPath);
// Extract param flow types.
functionExpression.get('params').each((paramPath) => {
let type = null;
const typePath = getTypeAnnotation(paramPath);
if (typePath && t.Flow.check(typePath.node)) {
type = getFlowType(typePath);
if (t.GenericTypeAnnotation.check(typePath.node)) {
type.alias = typePath.node.id.name;
}
} else if (typePath) {
type = getTSType(typePath);
if (t.TSTypeReference.check(typePath.node)) {
type.alias = typePath.node.typeName.name;
}
}
const param = {
name: getParameterName(paramPath),
optional: paramPath.node.optional,
type,
};
params.push(param);
});
return params;
}
// Extract flow return type.
function getMethodReturnDoc(methodPath) {
const functionExpression = getMethodFunctionExpression(methodPath);
if (functionExpression.node.returnType) {
const returnType = getTypeAnnotation(functionExpression.get('returnType'));
if (returnType && t.Flow.check(returnType.node)) {
return { type: getFlowType(returnType) };
}
if (returnType) {
return { type: getTSType(returnType) };
}
}
return null;
}
function getMethodModifiers(methodPath) {
if (t.AssignmentExpression.check(methodPath.node)) {
return ['static'];
}
// Otherwise this is a method/property node
const modifiers = [];
if (methodPath.node.static) {
modifiers.push('static');
}
if (methodPath.node.kind === 'get' || methodPath.node.kind === 'set') {
modifiers.push(methodPath.node.kind);
}
const functionExpression = methodPath.get('value').node;
if (functionExpression.generator) {
modifiers.push('generator');
}
if (functionExpression.async) {
modifiers.push('async');
}
return modifiers;
}
function getMethodName(methodPath) {
if (t.AssignmentExpression.check(methodPath.node) && t.MemberExpression.check(methodPath.node.left)) {
const { left } = methodPath.node;
const { property } = left;
if (!left.computed) {
return property.name;
}
if (t.Literal.check(property)) {
return String(property.value);
}
return null;
}
return getPropertyName(methodPath);
}
function getMethodAccessibility(methodPath) {
if (t.AssignmentExpression.check(methodPath.node)) {
return null;
}
// Otherwise this is a method/property node
return methodPath.node.accessibility;
}
function getMethodDocblock(methodPath) {
if (t.AssignmentExpression.check(methodPath.node)) {
let path = methodPath;
do {
path = path.parent;
} while (path && !t.ExpressionStatement.check(path.node));
if (path) {
return getDocblock(path);
}
return null;
}
// Otherwise this is a method/property node
return getDocblock(methodPath);
}
// Gets the documentation object for a component method.
// Component methods may be represented as class/object method/property nodes
// or as assignment expresions of the form `Component.foo = function() {}`
export default function getMethodDocumentation(methodPath) {
if (getMethodAccessibility(methodPath) === 'private') {
return null;
}
const name = getMethodName(methodPath);
if (!name) return null;
return {
name,
docblock: getMethodDocblock(methodPath),
modifiers: getMethodModifiers(methodPath),
params: getMethodParamsDoc(methodPath),
returns: getMethodReturnDoc(methodPath),
};
}

View File

@ -0,0 +1,14 @@
import { namedTypes as t } from 'ast-types';
export default function(def: any) {
let name = '';
if (def.node.name) {
name = def.node.name;
// hoc
} else if (t.CallExpression.check(def.node)) {
if (def.node.arguments && def.node.arguments.length && t.Identifier.check(def.get('arguments', 0).node))
name = def.get('arguments', 0).node.name;
}
return name;
}

View File

@ -0,0 +1,7 @@
export default function getRoot(path: any) {
let root = path.parent;
while (root.parent) {
root = root.parent;
}
return root.node;
}

View File

@ -0,0 +1,355 @@
/* eslint-disable */
const { namedTypes: t } = require('ast-types');
const {
getPropertyName,
printValue,
resolveToValue,
getTypeAnnotation,
resolveObjectToNameArray,
getTypeParameters,
} = require('react-docgen').utils;
const tsTypes = {
TSAnyKeyword: 'any',
TSBooleanKeyword: 'boolean',
TSUnknownKeyword: 'unknown',
TSNeverKeyword: 'never',
TSNullKeyword: 'null',
TSUndefinedKeyword: 'undefined',
TSNumberKeyword: 'number',
TSStringKeyword: 'string',
TSSymbolKeyword: 'symbol',
TSThisType: 'this',
TSObjectKeyword: 'object',
TSVoidKeyword: 'void',
};
const namedTypes = {
TSArrayType: handleTSArrayType,
TSTypeReference: handleTSTypeReference,
TSTypeLiteral: handleTSTypeLiteral,
TSInterfaceDeclaration: handleTSInterfaceDeclaration,
TSUnionType: handleTSUnionType,
TSFunctionType: handleTSFunctionType,
TSIntersectionType: handleTSIntersectionType,
TSMappedType: handleTSMappedType,
TSTupleType: handleTSTupleType,
TSTypeQuery: handleTSTypeQuery,
TSTypeOperator: handleTSTypeOperator,
TSIndexedAccessType: handleTSIndexedAccessType,
};
function handleTSArrayType(path, typeParams) {
return {
name: 'Array',
elements: [getTSTypeWithResolvedTypes(path.get('elementType'), typeParams)],
raw: printValue(path),
};
}
function handleTSTypeReference(path, typeParams) {
let type;
if (t.TSQualifiedName.check(path.node.typeName)) {
const typeName = path.get('typeName');
if (typeName.node.left.name === 'React') {
type = {
name: `${typeName.node.left.name}${typeName.node.right.name}`,
raw: printValue(typeName),
};
} else {
type = { name: printValue(typeName).replace(/<.*>$/, '') };
}
} else {
type = { name: path.node.typeName.name };
}
const resolvedPath = (typeParams && typeParams[type.name]) || resolveToValue(path.get('typeName'));
if (path.node.typeParameters && resolvedPath.node.typeParameters) {
typeParams = getTypeParameters(resolvedPath.get('typeParameters'), path.get('typeParameters'), typeParams);
}
if (typeParams && typeParams[type.name]) {
type = getTSTypeWithResolvedTypes(resolvedPath);
}
if (resolvedPath && resolvedPath.node.typeAnnotation) {
type = getTSTypeWithResolvedTypes(resolvedPath.get('typeAnnotation'), typeParams);
} else if (path.node.typeParameters) {
const params = path.get('typeParameters').get('params');
type = {
...type,
elements: params.map((param) => getTSTypeWithResolvedTypes(param, typeParams)),
raw: printValue(path),
};
}
return type;
}
function getTSTypeWithRequirements(path, typeParams) {
const type = getTSTypeWithResolvedTypes(path, typeParams);
type.required = !path.parentPath.node.optional;
return type;
}
function handleTSTypeLiteral(path, typeParams) {
const type = {
name: 'signature',
type: 'object',
raw: printValue(path),
signature: { properties: [] },
};
path.get('members').each((param) => {
if (t.TSPropertySignature.check(param.node) || t.TSMethodSignature.check(param.node)) {
const propName = getPropertyName(param);
if (!propName) {
return;
}
type.signature.properties.push({
key: propName,
value: getTSTypeWithRequirements(param.get('typeAnnotation'), typeParams),
});
} else if (t.TSCallSignatureDeclaration.check(param.node)) {
type.signature.constructor = handleTSFunctionType(param, typeParams);
} else if (t.TSIndexSignature.check(param.node)) {
type.signature.properties.push({
key: getTSTypeWithResolvedTypes(
param
.get('parameters')
.get(0)
.get('typeAnnotation'),
typeParams,
),
value: getTSTypeWithRequirements(param.get('typeAnnotation'), typeParams),
});
}
});
return type;
}
function handleTSInterfaceDeclaration(path) {
// Interfaces are handled like references which would be documented separately,
// rather than inlined like type aliases.
return {
name: path.node.id.name,
};
}
function handleTSUnionType(path, typeParams) {
return {
name: 'union',
raw: printValue(path),
elements: path.get('types').map((subType) => getTSTypeWithResolvedTypes(subType, typeParams)),
};
}
function handleTSIntersectionType(path, typeParams) {
return {
name: 'intersection',
raw: printValue(path),
elements: path.get('types').map((subType) => getTSTypeWithResolvedTypes(subType, typeParams)),
};
}
function handleTSMappedType(path, typeParams) {
const key = getTSTypeWithResolvedTypes(path.get('typeParameter').get('constraint'), typeParams);
key.required = !path.node.optional;
return {
name: 'signature',
type: 'object',
raw: printValue(path),
signature: {
properties: [
{
key,
value: getTSTypeWithResolvedTypes(path.get('typeAnnotation'), typeParams),
},
],
},
};
}
function handleTSFunctionType(path, typeParams) {
const type = {
name: 'signature',
type: 'function',
raw: printValue(path),
signature: {
arguments: [],
return: getTSTypeWithResolvedTypes(path.get('typeAnnotation'), typeParams),
},
};
path.get('parameters').each((param) => {
const typeAnnotation = getTypeAnnotation(param);
const arg = {
name: param.node.name || '',
type: typeAnnotation ? getTSTypeWithResolvedTypes(typeAnnotation, typeParams) : undefined,
};
if (param.node.name === 'this') {
type.signature.this = arg.type;
return;
}
if (param.node.type === 'RestElement') {
arg.name = param.node.argument.name;
arg.rest = true;
}
type.signature.arguments.push(arg);
});
return type;
}
function handleTSTupleType(path, typeParams) {
const type = {
name: 'tuple',
raw: printValue(path),
elements: [],
};
path.get('elementTypes').each((param) => {
type.elements.push(getTSTypeWithResolvedTypes(param, typeParams));
});
return type;
}
function handleTSTypeQuery(path, typeParams) {
const resolvedPath = resolveToValue(path.get('exprName'));
if (resolvedPath && resolvedPath.node.typeAnnotation) {
return getTSTypeWithResolvedTypes(resolvedPath.get('typeAnnotation'), typeParams);
}
return { name: path.node.exprName.name };
}
function handleTSTypeOperator(path) {
if (path.node.operator !== 'keyof') {
return null;
}
let value = path.get('typeAnnotation');
if (t.TSTypeQuery.check(value.node)) {
value = value.get('exprName');
} else if (value.node.id) {
value = value.get('id');
}
const resolvedPath = resolveToValue(value);
if (resolvedPath && (t.ObjectExpression.check(resolvedPath.node) || t.TSTypeLiteral.check(resolvedPath.node))) {
const keys = resolveObjectToNameArray(resolvedPath, true);
if (keys) {
return {
name: 'union',
raw: printValue(path),
elements: keys.map((key) => ({ name: 'literal', value: key })),
};
}
}
}
function handleTSIndexedAccessType(path, typeParams) {
// eslint-disable-next-line no-undef
const objectType = getTSTypeWithResolvedTypes(path.get('objectType'), typeParams);
// eslint-disable-next-line no-undef
const indexType = getTSTypeWithResolvedTypes(path.get('indexType'), typeParams);
// We only get the signature if the objectType is a type (vs interface)
if (!objectType.signature) {
return {
name: `${objectType.name}[${(indexType.value || indexType.name).toString()}]`,
raw: printValue(path),
};
}
const resolvedType = objectType.signature.properties.find(
(p) =>
// indexType.value = "'foo'"
p.key === indexType.value.replace(/['"]+/g, ''),
);
if (!resolvedType) {
return { name: 'unknown' };
}
return {
name: resolvedType.value.name,
raw: printValue(path),
};
}
let visitedTypes = {};
function getTSTypeWithResolvedTypes(path, typeParams) {
if (t.TSTypeAnnotation.check(path.node)) {
path = path.get('typeAnnotation');
}
const { node } = path;
let type;
const isTypeAlias = t.TSTypeAliasDeclaration.check(path.parentPath.node);
// When we see a typealias mark it as visited so that the next
// call of this function does not run into an endless loop
if (isTypeAlias) {
if (visitedTypes[path.parentPath.node.id.name] === true) {
// if we are currently visiting this node then just return the name
// as we are starting to endless loop
return { name: path.parentPath.node.id.name };
}
if (typeof visitedTypes[path.parentPath.node.id.name] === 'object') {
// if we already resolved the type simple return it
return visitedTypes[path.parentPath.node.id.name];
}
// mark the type as visited
visitedTypes[path.parentPath.node.id.name] = true;
}
if (node.type in tsTypes) {
type = { name: tsTypes[node.type] };
} else if (t.TSLiteralType.check(node)) {
type = {
name: 'literal',
value: node.literal.raw || `${node.literal.value}`,
};
} else if (node.type in namedTypes) {
type = namedTypes[node.type](path, typeParams);
}
if (!type) {
type = { name: 'unknown' };
}
if (isTypeAlias) {
// mark the type as unvisited so that further calls can resolve the type again
visitedTypes[path.parentPath.node.id.name] = type;
}
return type;
}
/**
* Tries to identify the typescript type by inspecting the path for known
* typescript type names. This method doesn't check whether the found type is actually
* existing. It simply assumes that a match is always valid.
*
* If there is no match, "unknown" is returned.
*/
export default function getTSType(path, typeParamMap) {
// Empty visited types before an after run
// Before: in case the detection threw and we rerun again
// After: cleanup memory after we are done here
visitedTypes = {};
const type = getTSTypeWithResolvedTypes(path, typeParamMap);
visitedTypes = {};
return type;
}

View File

@ -0,0 +1,19 @@
function makeProxy(target: { [name: string]: any }, meta: any = {}): any {
if (target.__isProxy) {
const value = target.__getRaw();
const rawMeta = target.__getMeta();
return makeProxy(value, Object.assign({}, rawMeta, meta));
}
return new Proxy(target, {
get: (obj, prop: string | number) => {
if (prop === '__isProxy') return true;
if (prop === '__getRaw') return () => target;
if (prop === '__getMeta') return () => meta;
return meta.hasOwnProperty(prop) ? meta[prop] : obj[prop];
// return obj[prop];
},
has: (obj, prop) => obj.hasOwnProperty(prop) || meta.hasOwnProperty(prop),
});
}
export default makeProxy;

View File

@ -0,0 +1,57 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { namedTypes as t } from 'ast-types';
const { resolveToValue } = require('react-docgen').utils;
const { unwrapUtilityType } = require('./flowUtilityTypes');
const isUnreachableFlowType = require('react-docgen/dist/utils/isUnreachableFlowType').default;
function tryResolveGenericTypeAnnotation(path: any): any {
let typePath = unwrapUtilityType(path);
let idPath;
if (typePath.node.id) {
idPath = typePath.get('id');
} else if (t.TSTypeReference.check(typePath.node)) {
idPath = typePath.get('typeName');
} else if (t.TSExpressionWithTypeArguments.check(typePath.node)) {
idPath = typePath.get('expression');
}
if (idPath) {
typePath = resolveToValue(idPath);
if (isUnreachableFlowType(typePath)) {
return;
}
if (t.TypeAlias.check(typePath.node)) {
return tryResolveGenericTypeAnnotation(typePath.get('right'));
} else if (t.TSTypeAliasDeclaration.check(typePath.node)) {
return tryResolveGenericTypeAnnotation(typePath.get('typeAnnotation'));
}
return typePath;
}
return typePath;
}
/**
* Given an React component (stateless or class) tries to find the
* flow type for the props. If not found or not one of the supported
* component types returns undefined.
*/
export default function resolveGenericTypeAnnotation(path: any) {
if (!path) return;
const typePath = tryResolveGenericTypeAnnotation(path);
if (!typePath || typePath === path) return;
return typePath;
}

View File

@ -15,35 +15,8 @@ export interface IMaterialParsedModel {
// filePath: string;
componentName: string;
props?: PropsSection['props'];
// componentNames: {
// exportedName: string;
// localName: string;
// source?: string;
// }[];
// importModules: {
// importDefaultName?: string;
// importName?: string;
// localName?: string;
// source: string;
// }[];
// exportModules: {
// exportedName: string;
// localName: string;
// source?: string;
// }[];
// /**
// * 子模块形如Demo.SubModule = value; 或者 Demo.SubModule.Sub = subValue;
// */
// subModules: {
// objectName: string[];
// propertyName: string;
// value?: string;
// // value 是否对应匿名函数
// isValueAnonymousFunc: boolean;
// }[];
// propsTypes: IPropTypes;
// propsDefaults: {
// name: string;
// defaultValue: any;
// }[];
meta?: {
exportName?: string;
subName?: string;
};
}

View File

@ -0,0 +1,14 @@
import { NodePath, Path } from 'ast-types';
export interface IFileMeta {
src: string;
path: string;
exports: IDefinitionMeta[];
}
export interface IDefinitionMeta {
subDefinitions: IDefinitionMeta[];
nodePath: typeof Path;
exportName: string;
id: string;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,70 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`antd exports modules correctly 1`] = `
Array [
"Affix",
"Anchor",
"AutoComplete",
"Alert",
"Avatar",
"BackTop",
"Badge",
"Breadcrumb",
"Button",
"Calendar",
"Card",
"Collapse",
"Carousel",
"Cascader",
"Checkbox",
"Col",
"Comment",
"ConfigProvider",
"DatePicker",
"Descriptions",
"Divider",
"Dropdown",
"Drawer",
"Empty",
"Form",
"Grid",
"Input",
"InputNumber",
"Layout",
"List",
"message",
"Menu",
"Mentions",
"Modal",
"Statistic",
"notification",
"PageHeader",
"Pagination",
"Popconfirm",
"Popover",
"Progress",
"Radio",
"Rate",
"Result",
"Row",
"Select",
"Skeleton",
"Slider",
"Space",
"Spin",
"Steps",
"Switch",
"Table",
"Transfer",
"Tree",
"TreeSelect",
"Tabs",
"Tag",
"TimePicker",
"Timeline",
"Tooltip",
"Typography",
"Upload",
"version",
]
`;

View File

@ -0,0 +1,21 @@
const OLD_NODE_ENV = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const antd = require('..');
describe('antd', () => {
afterAll(() => {
process.env.NODE_ENV = OLD_NODE_ENV;
});
it('exports modules correctly', () => {
expect(Object.keys(antd)).toMatchSnapshot();
});
it('should hint when import all components in dev mode', () => {
expect(warnSpy).toHaveBeenCalledWith(
'You are using a whole package of antd, please use https://www.npmjs.com/package/babel-plugin-import to reduce app bundle size.',
);
warnSpy.mockRestore();
});
});

View File

@ -0,0 +1,71 @@
const __NULL__ = { notExist: true };
type ElementType<P> = {
prototype: P;
};
export function spyElementPrototypes<P extends {}>(Element: ElementType<P>, properties: P) {
const propNames = Object.keys(properties);
const originDescriptors = {};
propNames.forEach(propName => {
const originDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, propName);
originDescriptors[propName] = originDescriptor || __NULL__;
const spyProp = properties[propName];
if (typeof spyProp === 'function') {
// If is a function
Element.prototype[propName] = function spyFunc(...args) {
return spyProp.call(this, originDescriptor, ...args);
};
} else {
// Otherwise tread as a property
Object.defineProperty(Element.prototype, propName, {
...spyProp,
set(value) {
if (spyProp.set) {
return spyProp.set.call(this, originDescriptor, value);
}
return originDescriptor.set(value);
},
get() {
if (spyProp.get) {
return spyProp.get.call(this, originDescriptor);
}
return originDescriptor.get();
},
});
}
});
return {
mockRestore() {
propNames.forEach(propName => {
const originDescriptor = originDescriptors[propName];
if (originDescriptor === __NULL__) {
delete Element.prototype[propName];
} else if (typeof originDescriptor === 'function') {
Element.prototype[propName] = originDescriptor;
} else {
Object.defineProperty(Element.prototype, propName, originDescriptor);
}
});
},
};
}
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T] &
string;
export function spyElementPrototype<P extends {}, K extends FunctionPropertyNames<P>>(
Element: ElementType<P>,
propName: K,
property: P[K],
) {
return spyElementPrototypes(Element, {
[propName]: property,
});
}

View File

@ -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]);
});
});

View File

@ -0,0 +1,11 @@
/**
* @jest-environment node
*/
import getScroll from '../getScroll';
describe('getScroll', () => {
it('getScroll return 0 in node envioronment', async () => {
expect(getScroll(null, true)).toBe(0);
expect(getScroll(null, false)).toBe(0);
});
});

View File

@ -0,0 +1,49 @@
import scrollTo from '../scrollTo';
import { sleep } from '../../../tests/utils';
describe('Test ScrollTo function', () => {
let dateNowMock;
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);
await sleep(20);
expect(window.pageYOffset).toBe(1000);
scrollToSpy.mockRestore();
});
it('test callback - option', async () => {
const cbMock = jest.fn();
scrollTo(1000, {
callback: cbMock,
});
await sleep(20);
expect(cbMock).toHaveBeenCalledTimes(1);
});
it('test getContainer - option', async () => {
const div = document.createElement('div');
scrollTo(1000, {
getContainer: () => div,
});
await sleep(20);
expect(div.scrollTop).toBe(1000);
});
});

View File

@ -0,0 +1,8 @@
import UnreachableException from '../unreachableException';
describe('UnreachableException', () => {
it('error thrown matches snapshot', () => {
const exception = new UnreachableException('some value');
expect(exception.message).toMatchInlineSnapshot(`"unreachable case: \\"some value\\""`);
});
});

View File

@ -0,0 +1,206 @@
import raf from 'raf';
import React from 'react';
import { mount } from 'enzyme';
import KeyCode from 'rc-util/lib/KeyCode';
import delayRaf from '../raf';
import throttleByAnimationFrame from '../throttleByAnimationFrame';
import getDataOrAriaProps from '../getDataOrAriaProps';
import Wave from '../wave';
import TransButton from '../transButton';
import openAnimation from '../openAnimation';
import { sleep } from '../../../tests/utils';
import focusTest from '../../../tests/shared/focusTest';
describe('Test utils function', () => {
focusTest(TransButton);
it('throttle function should work', async () => {
const callback = jest.fn();
const throttled = throttleByAnimationFrame(callback);
expect(callback).not.toHaveBeenCalled();
throttled();
throttled();
await sleep(20);
expect(callback).toHaveBeenCalled();
expect(callback.mock.calls.length).toBe(1);
});
it('throttle function should be canceled', async () => {
const callback = jest.fn();
const throttled = throttleByAnimationFrame(callback);
throttled();
throttled.cancel();
await sleep(20);
expect(callback).not.toHaveBeenCalled();
});
describe('getDataOrAriaProps', () => {
it('returns all data-* properties from an object', () => {
const props = {
onClick: () => {},
isOpen: true,
'data-test': 'test-id',
'data-id': 1234,
};
const results = getDataOrAriaProps(props);
expect(results).toEqual({
'data-test': 'test-id',
'data-id': 1234,
});
});
it('does not return data-__ properties from an object', () => {
const props = {
onClick: () => {},
isOpen: true,
'data-__test': 'test-id',
'data-__id': 1234,
};
const results = getDataOrAriaProps(props);
expect(results).toEqual({});
});
it('returns all aria-* properties from an object', () => {
const props = {
onClick: () => {},
isOpen: true,
'aria-labelledby': 'label-id',
'aria-label': 'some-label',
};
const results = getDataOrAriaProps(props);
expect(results).toEqual({
'aria-labelledby': 'label-id',
'aria-label': 'some-label',
});
});
it('returns role property from an object', () => {
const props = {
onClick: () => {},
isOpen: true,
role: 'search',
};
const results = getDataOrAriaProps(props);
expect(results).toEqual({ role: 'search' });
});
});
it('delayRaf', done => {
jest.useRealTimers();
let bamboo = false;
delayRaf(() => {
bamboo = true;
}, 3);
// Do nothing, but insert in the frame
// https://github.com/ant-design/ant-design/issues/16290
delayRaf(() => {}, 3);
// Variable bamboo should be false in frame 2 but true in frame 4
raf(() => {
expect(bamboo).toBe(false);
// Frame 2
raf(() => {
expect(bamboo).toBe(false);
// Frame 3
raf(() => {
// Frame 4
raf(() => {
expect(bamboo).toBe(true);
done();
});
});
});
});
});
describe('wave', () => {
it('bindAnimationEvent should return when node is null', () => {
const wrapper = mount(
<Wave>
<button type="button" disabled>
button
</button>
</Wave>,
).instance();
expect(wrapper.bindAnimationEvent()).toBe(undefined);
});
it('bindAnimationEvent.onClick should return when children is hidden', () => {
const wrapper = mount(
<Wave>
<button type="button" style={{ display: 'none' }}>
button
</button>
</Wave>,
).instance();
expect(wrapper.bindAnimationEvent()).toBe(undefined);
});
it('bindAnimationEvent.onClick should return when children is input', () => {
const wrapper = mount(
<Wave>
<input />
</Wave>,
).instance();
expect(wrapper.bindAnimationEvent()).toBe(undefined);
});
it('should not throw when click it', () => {
expect(() => {
const wrapper = mount(
<Wave>
<div />
</Wave>,
);
wrapper.simulate('click');
}).not.toThrow();
});
it('should not throw when no children', () => {
expect(() => mount(<Wave />)).not.toThrow();
});
});
describe('TransButton', () => {
it('can be focus/blur', () => {
const wrapper = mount(<TransButton>TransButton</TransButton>);
expect(typeof wrapper.instance().focus).toBe('function');
expect(typeof wrapper.instance().blur).toBe('function');
});
it('should trigger onClick when press enter', () => {
const onClick = jest.fn();
const preventDefault = jest.fn();
const wrapper = mount(<TransButton onClick={onClick}>TransButton</TransButton>);
wrapper.simulate('keyUp', { keyCode: KeyCode.ENTER });
expect(onClick).toHaveBeenCalled();
wrapper.simulate('keyDown', { keyCode: KeyCode.ENTER, preventDefault });
expect(preventDefault).toHaveBeenCalled();
});
});
describe('openAnimation', () => {
it('should support openAnimation', () => {
const done = jest.fn();
const domNode = document.createElement('div');
expect(typeof openAnimation.enter).toBe('function');
expect(typeof openAnimation.leave).toBe('function');
expect(typeof openAnimation.appear).toBe('function');
const appear = openAnimation.appear(domNode, done);
const enter = openAnimation.enter(domNode, done);
const leave = openAnimation.leave(domNode, done);
expect(typeof appear.stop).toBe('function');
expect(typeof enter.stop).toBe('function');
expect(typeof leave.stop).toBe('function');
expect(done).toHaveBeenCalled();
});
});
});

View File

@ -0,0 +1,107 @@
import React from 'react';
import { mount } from 'enzyme';
import Wave from '../wave';
import ConfigProvider from '../../config-provider';
import mountTest from '../../../tests/shared/mountTest';
import { sleep } from '../../../tests/utils';
describe('Wave component', () => {
mountTest(Wave);
afterEach(() => {
const styles = document.getElementsByTagName('style');
for (let i = 0; i < styles.length; i += 1) {
styles[i].remove();
}
});
it('isHidden works', () => {
const TEST_NODE_ENV = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
const wrapper = mount(<Wave><button type="button">button</button></Wave>);
expect(wrapper.find('button').getDOMNode().className).toBe('');
wrapper.find('button').getDOMNode().click();
expect(wrapper.find('button').getDOMNode().hasAttribute('ant-click-animating-without-extra-node')).toBe(false);
wrapper.unmount();
process.env.NODE_ENV = TEST_NODE_ENV;
});
it('isHidden is mocked', () => {
const wrapper = mount(<Wave><button type="button">button</button></Wave>);
expect(wrapper.find('button').getDOMNode().className).toBe('');
wrapper.find('button').getDOMNode().click();
expect(wrapper.find('button').getDOMNode().getAttribute('ant-click-animating-without-extra-node')).toBe('false');
wrapper.unmount();
});
it('wave color is grey', async () => {
const wrapper = mount(<Wave><button type="button" style={{ borderColor: 'rgb(0, 0, 0)' }}>button</button></Wave>);
wrapper.find('button').getDOMNode().click();
await sleep(0);
const styles = document.getElementsByTagName('style');
expect(styles.length).toBe(0);
wrapper.unmount();
});
it('wave color is not grey', async () => {
const wrapper = mount(<Wave><button type="button" style={{ borderColor: 'red' }}>button</button></Wave>);
wrapper.find('button').getDOMNode().click();
await sleep(200);
const styles = document.getElementsByTagName('style');
expect(styles.length).toBe(1);
expect(styles[0].innerHTML).toContain('--antd-wave-shadow-color: red;');
wrapper.unmount();
});
it('read wave color from border-top-color', async () => {
const wrapper = mount(<Wave><div style={{ borderTopColor: 'blue' }}>button</div></Wave>);
wrapper.find('div').getDOMNode().click();
await sleep(0);
const styles = document.getElementsByTagName('style');
expect(styles.length).toBe(1);
expect(styles[0].innerHTML).toContain('--antd-wave-shadow-color: blue;');
wrapper.unmount();
});
it('read wave color from background color', async () => {
const wrapper = mount(<Wave><div style={{ backgroundColor: 'green' }}>button</div></Wave>);
wrapper.find('div').getDOMNode().click();
await sleep(0);
const styles = document.getElementsByTagName('style');
expect(styles.length).toBe(1);
expect(styles[0].innerHTML).toContain('--antd-wave-shadow-color: green;');
wrapper.unmount();
});
it('read wave color from border firstly', async () => {
const wrapper = mount(<Wave><div style={{ borderColor: 'yellow', backgroundColor: 'green' }}>button</div></Wave>);
wrapper.find('div').getDOMNode().click();
await sleep(0);
const styles = document.getElementsByTagName('style');
expect(styles.length).toBe(1);
expect(styles[0].innerHTML).toContain('--antd-wave-shadow-color: yellow;');
wrapper.unmount();
});
it('hidden element with -leave className', async () => {
const wrapper = mount(<Wave><button type="button" className="xx-leave">button</button></Wave>);
wrapper.find('button').getDOMNode().click();
await sleep(0);
const styles = document.getElementsByTagName('style');
expect(styles.length).toBe(0);
wrapper.unmount();
});
it('ConfigProvider csp', async () => {
const wrapper = mount(
<ConfigProvider csp={{ nonce: 'YourNonceCode' }}>
<Wave><button type="button">button</button></Wave>
</ConfigProvider>,
);
wrapper.find('button').getDOMNode().click();
await sleep(0);
const styles = document.getElementsByTagName('style');
expect(styles[0].getAttribute('nonce')).toBe('YourNonceCode');
wrapper.unmount();
});
});

View File

@ -0,0 +1,22 @@
import { ElementOf, tuple } from './type';
export const PresetStatusColorTypes = tuple('success', 'processing', 'error', 'default', 'warning');
// eslint-disable-next-line import/prefer-default-export
export const PresetColorTypes = tuple(
'pink',
'red',
'yellow',
'orange',
'cyan',
'green',
'blue',
'purple',
'geekblue',
'magenta',
'volcano',
'gold',
'lime',
);
export type PresetColorType = ElementOf<typeof PresetColorTypes>;
export type PresetStatusColorType = ElementOf<typeof PresetStatusColorTypes>;

View File

@ -0,0 +1,9 @@
// eslint-disable-next-line import/prefer-default-export
export function easeInOutCubic(t: number, b: number, c: number, d: number) {
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;
}

View File

@ -0,0 +1,11 @@
export default function getDataOrAriaProps(props: any) {
return Object.keys(props).reduce((prev: any, key: string) => {
if (
(key.substr(0, 5) === 'data-' || key.substr(0, 5) === 'aria-' || key === 'role') &&
key.substr(0, 7) !== 'data-__'
) {
prev[key] = props[key];
}
return prev;
}, {});
}

View File

@ -0,0 +1,18 @@
import React from 'react';
export type RenderFunction = () => React.ReactNode;
export const getRenderPropValue = (
propValue?: React.ReactNode | RenderFunction,
): React.ReactNode => {
if (!propValue) {
return null;
}
const isRenderFunction = typeof propValue === 'function';
if (isRenderFunction) {
return (propValue as RenderFunction)();
}
return propValue;
};

View File

@ -0,0 +1,27 @@
export function isWindow(obj: any) {
return obj !== null && obj !== undefined && obj === obj.window;
}
export default function getScroll(
target: HTMLElement | Window | Document | null,
top: boolean,
): number {
if (typeof window === 'undefined') {
return 0;
}
const method = top ? 'scrollTop' : 'scrollLeft';
let result = 0;
if (isWindow(target)) {
result = (target as Window)[top ? 'pageYOffset' : 'pageXOffset'];
} else if (target instanceof Document) {
result = target.documentElement[method];
} else if (target) {
result = (target as HTMLElement)[method];
}
if (target && !isWindow(target) && typeof result !== 'number') {
result = ((target as HTMLElement).ownerDocument || (target as Document)).documentElement[
method
];
}
return result;
}

View File

@ -0,0 +1,5 @@
// https://github.com/moment/moment/issues/3650
// since we are using ts 3.5.1, it should be safe to remove.
export default function interopDefault(m: any) {
return m.default || m;
}

View File

@ -0,0 +1,5 @@
const isNumeric = (value: any): boolean => {
return !isNaN(parseFloat(value)) && isFinite(value);
};
export default isNumeric;

View File

@ -0,0 +1,40 @@
import * as React from 'react';
type MotionFunc = (element: HTMLElement) => React.CSSProperties;
interface Motion {
visible?: boolean;
motionName?: string; // It also support object, but we only use string here.
motionAppear?: boolean;
motionEnter?: boolean;
motionLeave?: boolean;
motionLeaveImmediately?: boolean; // Trigger leave motion immediately
removeOnLeave?: boolean;
leavedClassName?: string;
onAppearStart?: MotionFunc;
onAppearActive?: MotionFunc;
onAppearEnd?: MotionFunc;
onEnterStart?: MotionFunc;
onEnterActive?: MotionFunc;
onEnterEnd?: MotionFunc;
onLeaveStart?: MotionFunc;
onLeaveActive?: MotionFunc;
onLeaveEnd?: MotionFunc;
}
// ================== Collapse Motion ==================
const getCollapsedHeight: MotionFunc = () => ({ height: 0, opacity: 0 });
const getRealHeight: MotionFunc = node => ({ height: node.scrollHeight, opacity: 1 });
const getCurrentHeight: MotionFunc = node => ({ height: node.offsetHeight });
const collapseMotion: Motion = {
motionName: 'ant-motion-collapse',
onAppearStart: getCollapsedHeight,
onEnterStart: getCollapsedHeight,
onAppearActive: getRealHeight,
onEnterActive: getRealHeight,
onLeaveStart: getCurrentHeight,
onLeaveActive: getCollapsedHeight,
};
export default collapseMotion;

View File

@ -0,0 +1,54 @@
/**
* Deprecated. We should replace the animation with pure react motion instead of modify style directly.
* If you are creating new component with animation, please use `./motion`.
*/
import cssAnimation from 'css-animation';
import raf from 'raf';
function animate(node: HTMLElement, show: boolean, done: () => void) {
let height: number;
let requestAnimationFrameId: number;
return cssAnimation(node, 'ant-motion-collapse-legacy', {
start() {
if (!show) {
node.style.height = `${node.offsetHeight}px`;
node.style.opacity = '1';
} else {
height = node.offsetHeight;
node.style.height = '0px';
node.style.opacity = '0';
}
},
active() {
if (requestAnimationFrameId) {
raf.cancel(requestAnimationFrameId);
}
requestAnimationFrameId = raf(() => {
node.style.height = `${show ? height : 0}px`;
node.style.opacity = show ? '1' : '0';
});
},
end() {
if (requestAnimationFrameId) {
raf.cancel(requestAnimationFrameId);
}
node.style.height = '';
node.style.opacity = '';
done();
},
});
}
const animation = {
enter(node: HTMLElement, done: () => void) {
return animate(node, true, done);
},
leave(node: HTMLElement, done: () => void) {
return animate(node, false, done);
},
appear(node: HTMLElement, done: () => void) {
return animate(node, true, done);
},
};
export default animation;

View File

@ -0,0 +1,38 @@
import raf from 'raf';
interface RafMap {
[id: number]: number;
}
let id: number = 0;
const ids: RafMap = {};
// Support call raf with delay specified frame
export default function wrapperRaf(callback: () => void, delayFrames: number = 1): number {
const myId: number = id++;
let restFrames: number = delayFrames;
function internalCallback() {
restFrames -= 1;
if (restFrames <= 0) {
callback();
delete ids[myId];
} else {
ids[myId] = raf(internalCallback);
}
}
ids[myId] = raf(internalCallback);
return myId;
}
wrapperRaf.cancel = function cancel(pid?: number) {
if (pid === undefined) return;
raf.cancel(ids[pid]);
delete ids[pid];
};
wrapperRaf.ids = ids; // export this for test usage

View File

@ -0,0 +1,8 @@
import * as React from 'react';
// eslint-disable-next-line import/prefer-default-export
export function cloneElement(element: React.ReactNode, ...restArgs: any[]) {
if (!React.isValidElement(element)) return element;
return React.cloneElement(element, ...restArgs);
}

View File

@ -0,0 +1,17 @@
import React from 'react';
export function fillRef<T>(ref: React.Ref<T>, node: T) {
if (typeof ref === 'function') {
ref(node);
} else if (typeof ref === 'object' && ref && 'current' in ref) {
(ref as any).current = node;
}
}
export function composeRef<T>(...refs: React.Ref<T>[]): React.Ref<T> {
return (node: T) => {
refs.forEach(ref => {
fillRef(ref, node);
});
};
}

View File

@ -0,0 +1,87 @@
export type Breakpoint = 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs';
export type BreakpointMap = Partial<Record<Breakpoint, string>>;
export type ScreenMap = Partial<Record<Breakpoint, boolean>>;
export const responsiveArray: Breakpoint[] = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs'];
export const responsiveMap: BreakpointMap = {
xs: '(max-width: 575px)',
sm: '(min-width: 576px)',
md: '(min-width: 768px)',
lg: '(min-width: 992px)',
xl: '(min-width: 1200px)',
xxl: '(min-width: 1600px)',
};
type SubscribeFunc = (screens: ScreenMap) => void;
let subscribers: Array<{
token: string;
func: SubscribeFunc;
}> = [];
let subUid = -1;
let screens = {};
const responsiveObserve = {
matchHandlers: {},
dispatch(pointMap: ScreenMap) {
screens = pointMap;
if (subscribers.length < 1) {
return false;
}
subscribers.forEach(item => {
item.func(screens);
});
return true;
},
subscribe(func: SubscribeFunc) {
if (subscribers.length === 0) {
this.register();
}
const token = (++subUid).toString();
subscribers.push({
token,
func,
});
func(screens);
return token;
},
unsubscribe(token: string) {
subscribers = subscribers.filter(item => item.token !== token);
if (subscribers.length === 0) {
this.unregister();
}
},
unregister() {
Object.keys(responsiveMap).forEach((screen: Breakpoint) => {
const matchMediaQuery = responsiveMap[screen]!;
const handler = this.matchHandlers[matchMediaQuery];
if (handler && handler.mql && handler.listener) {
handler.mql.removeListener(handler.listener);
}
});
},
register() {
Object.keys(responsiveMap).forEach((screen: Breakpoint) => {
const matchMediaQuery = responsiveMap[screen]!;
const listener = ({ matches }: { matches: boolean }) => {
this.dispatch({
...screens,
[screen]: matches,
});
};
const mql = window.matchMedia(matchMediaQuery);
mql.addListener(listener);
this.matchHandlers[matchMediaQuery] = {
mql,
listener,
};
listener(mql);
});
},
};
export default responsiveObserve;

View File

@ -0,0 +1,39 @@
import raf from 'raf';
import getScroll, { isWindow } from './getScroll';
import { easeInOutCubic } from './easings';
interface ScrollToOptions {
/** Scroll container, default as window */
getContainer?: () => HTMLElement | Window | Document;
/** Scroll end callback */
callback?: () => any;
/** Animation duration, default as 450 */
duration?: number;
}
export default function scrollTo(y: number, options: ScrollToOptions = {}) {
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 (isWindow(container)) {
(container as Window).scrollTo(window.pageXOffset, nextScrollTop);
} else if (container instanceof Document) {
container.documentElement.scrollTop = nextScrollTop;
} else {
(container as HTMLElement).scrollTop = nextScrollTop;
}
if (time < duration) {
raf(frameFunc);
} else if (typeof callback === 'function') {
callback();
}
};
raf(frameFunc);
}

View File

@ -0,0 +1,13 @@
const isStyleSupport = (styleName: string | Array<string>): boolean => {
if (typeof window !== 'undefined' && window.document && window.document.documentElement) {
const styleNameList = Array.isArray(styleName) ? styleName : [styleName];
const { documentElement } = window.document;
return styleNameList.some(name => name in documentElement.style);
}
return false;
};
export const isFlexSupported = isStyleSupport(['flex', 'webkitFlex', 'Flex', 'msFlex']);
export default isStyleSupport;

View File

@ -0,0 +1,47 @@
import raf from 'raf';
export default function throttleByAnimationFrame(fn: (...args: any[]) => void) {
let requestId: number | null;
const later = (args: any[]) => () => {
requestId = null;
fn(...args);
};
const throttled = (...args: any[]) => {
if (requestId == null) {
requestId = raf(later(args));
}
};
(throttled as any).cancel = () => raf.cancel(requestId!);
return throttled;
}
export function throttleByAnimationFrameDecorator() {
// eslint-disable-next-line func-names
return function(target: any, key: string, descriptor: any) {
const fn = descriptor.value;
let definingProperty = false;
return {
configurable: true,
get() {
// eslint-disable-next-line no-prototype-builtins
if (definingProperty || this === target.prototype || this.hasOwnProperty(key)) {
return fn;
}
const boundFn = throttleByAnimationFrame(fn.bind(this));
definingProperty = true;
Object.defineProperty(this, key, {
value: boundFn,
configurable: true,
writable: true,
});
definingProperty = false;
return boundFn;
},
};
};
}

View File

@ -0,0 +1,82 @@
/**
* Wrap of sub component which need use as Button capacity (like Icon component).
* This helps accessibility reader to tread as a interactive button to operation.
*/
import * as React from 'react';
import KeyCode from 'rc-util/lib/KeyCode';
interface TransButtonProps extends React.HTMLAttributes<HTMLDivElement> {
onClick?: (e?: React.MouseEvent<HTMLDivElement>) => void;
noStyle?: boolean;
autoFocus?: boolean;
}
const inlineStyle: React.CSSProperties = {
border: 0,
background: 'transparent',
padding: 0,
lineHeight: 'inherit',
display: 'inline-block',
};
class TransButton extends React.Component<TransButtonProps> {
div?: HTMLDivElement;
lastKeyCode?: number;
componentDidMount() {
const { autoFocus } = this.props;
if (autoFocus) {
this.focus();
}
}
onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = event => {
const { keyCode } = event;
if (keyCode === KeyCode.ENTER) {
event.preventDefault();
}
};
onKeyUp: React.KeyboardEventHandler<HTMLDivElement> = event => {
const { keyCode } = event;
const { onClick } = this.props;
if (keyCode === KeyCode.ENTER && onClick) {
onClick();
}
};
setRef = (btn: HTMLDivElement) => {
this.div = btn;
};
focus() {
if (this.div) {
this.div.focus();
}
}
blur() {
if (this.div) {
this.div.blur();
}
}
render() {
const { style, noStyle, ...restProps } = this.props;
return (
<div
role="button"
tabIndex={0}
ref={this.setRef}
{...restProps}
onKeyDown={this.onKeyDown}
onKeyUp={this.onKeyUp}
style={{ ...(!noStyle ? inlineStyle : null), ...style }}
/>
);
}
}
export default TransButton;

View File

@ -0,0 +1,16 @@
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// https://stackoverflow.com/questions/46176165/ways-to-get-string-literal-type-of-array-values-without-enum-overhead
export const tuple = <T extends string[]>(...args: T) => args;
export const tupleNum = <T extends number[]>(...args: T) => args;
/**
* https://stackoverflow.com/a/59187769
* Extract the type of an element of an array/tuple without performing indexing
*/
export type ElementOf<T> = T extends (infer E)[] ? E : T extends readonly (infer E)[] ? E : never;
/**
* https://github.com/Microsoft/TypeScript/issues/29729
*/
export type LiteralUnion<T extends U, U> = T | (U & {});

View File

@ -0,0 +1,5 @@
export default class UnreachableException {
constructor(value: never) {
return new Error(`unreachable case: ${JSON.stringify(value)}`);
}
}

View File

@ -0,0 +1,18 @@
import * as React from 'react';
export default function usePatchElement(): [
React.ReactElement[],
(element: React.ReactElement) => Function,
] {
const [elements, setElements] = React.useState<React.ReactElement[]>([]);
function patchElement(element: React.ReactElement) {
setElements(originElements => [...originElements, element]);
return () => {
setElements(originElements => originElements.filter(ele => ele !== element));
};
}
return [elements, patchElement];
}

View File

@ -0,0 +1,7 @@
import warning, { resetWarned } from 'rc-util/lib/warning';
export { resetWarned };
export default (valid: boolean, component: string, message: string): void => {
warning(valid, `[antd: ${component}] ${message}`);
};

View File

@ -0,0 +1,195 @@
import * as React from 'react';
import { findDOMNode } from 'react-dom';
import TransitionEvents from 'css-animation/lib/Event';
import raf from './raf';
import { ConfigConsumer, ConfigConsumerProps, CSPConfig } from '../config-provider';
let styleForPesudo: HTMLStyleElement | null;
// Where el is the DOM element you'd like to test for visibility
function isHidden(element: HTMLElement) {
if (process.env.NODE_ENV === 'test') {
return false;
}
return !element || element.offsetParent === null;
}
function isNotGrey(color: string) {
// 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 class Wave extends React.Component<{ insertExtraNode?: boolean }> {
private instance?: {
cancel: () => void;
};
private extraNode: HTMLDivElement;
private clickWaveTimeoutId: number;
private animationStartId: number;
private animationStart: boolean = false;
private destroyed: boolean = false;
private csp?: CSPConfig;
componentDidMount() {
const node = findDOMNode(this) as HTMLElement;
if (!node || node.nodeType !== 1) {
return;
}
this.instance = this.bindAnimationEvent(node);
}
componentWillUnmount() {
if (this.instance) {
this.instance.cancel();
}
if (this.clickWaveTimeoutId) {
clearTimeout(this.clickWaveTimeoutId);
}
this.destroyed = true;
}
onClick = (node: HTMLElement, waveColor: string) => {
if (!node || isHidden(node) || node.className.indexOf('-leave') >= 0) {
return;
}
const { insertExtraNode } = this.props;
this.extraNode = document.createElement('div');
const { extraNode } = this;
extraNode.className = 'ant-click-animating-node';
const attributeName = this.getAttributeName();
node.setAttribute(attributeName, 'true');
// Not white or transparnt or grey
styleForPesudo = styleForPesudo || document.createElement('style');
if (
waveColor &&
waveColor !== '#ffffff' &&
waveColor !== 'rgb(255, 255, 255)' &&
isNotGrey(waveColor) &&
!/rgba\((?:\d*, ){3}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='true']::after, .ant-click-animating-node {
--antd-wave-shadow-color: ${waveColor};
}`;
if (!document.body.contains(styleForPesudo)) {
document.body.appendChild(styleForPesudo);
}
}
if (insertExtraNode) {
node.appendChild(extraNode);
}
TransitionEvents.addStartEventListener(node, this.onTransitionStart);
TransitionEvents.addEndEventListener(node, this.onTransitionEnd);
};
onTransitionStart = (e: AnimationEvent) => {
if (this.destroyed) {
return;
}
const node = findDOMNode(this) as HTMLElement;
if (!e || e.target !== node || this.animationStart) {
return;
}
this.resetEffect(node);
};
onTransitionEnd = (e: AnimationEvent) => {
if (!e || e.animationName !== 'fadeEffect') {
return;
}
this.resetEffect(e.target as HTMLElement);
};
getAttributeName() {
const { insertExtraNode } = this.props;
return insertExtraNode ? 'ant-click-animating' : 'ant-click-animating-without-extra-node';
}
bindAnimationEvent = (node: HTMLElement) => {
if (
!node ||
!node.getAttribute ||
node.getAttribute('disabled') ||
node.className.indexOf('disabled') >= 0
) {
return;
}
const onClick = (e: MouseEvent) => {
// Fix radio button click twice
if ((e.target as HTMLElement).tagName === 'INPUT' || isHidden(e.target as HTMLElement)) {
return;
}
this.resetEffect(node);
// Get wave color from target
const waveColor =
getComputedStyle(node).getPropertyValue('border-top-color') || // Firefox Compatible
getComputedStyle(node).getPropertyValue('border-color') ||
getComputedStyle(node).getPropertyValue('background-color');
this.clickWaveTimeoutId = window.setTimeout(() => this.onClick(node, waveColor), 0);
raf.cancel(this.animationStartId);
this.animationStart = true;
// Render to trigger transition event cost 3 frames. Let's delay 10 frames to reset this.
this.animationStartId = raf(() => {
this.animationStart = false;
}, 10);
};
node.addEventListener('click', onClick, true);
return {
cancel: () => {
node.removeEventListener('click', onClick, true);
},
};
};
resetEffect(node: HTMLElement) {
if (!node || node === this.extraNode || !(node instanceof Element)) {
return;
}
const { insertExtraNode } = this.props;
const attributeName = this.getAttributeName();
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);
}
renderWave = ({ csp }: ConfigConsumerProps) => {
const { children } = this.props;
this.csp = csp;
return children;
};
render() {
return <ConfigConsumer>{this.renderWave}</ConfigConsumer>;
}
}

View File

@ -0,0 +1,209 @@
import React from 'react';
import { mount } from 'enzyme';
import Affix from '..';
import { getObserverEntities } from '../utils';
import Button from '../../button';
import { spyElementPrototype } from '../../__tests__/util/domHook';
import rtlTest from '../../../tests/shared/rtlTest';
import { sleep } from '../../../tests/utils';
const events: any = {};
class AffixMounter extends React.Component<{
offsetBottom?: number;
offsetTop?: number;
onTestUpdatePosition?(): void;
}> {
private container: HTMLDivElement;
private affix: Affix;
componentDidMount() {
this.container.addEventListener = jest.fn().mockImplementation((event, cb) => {
events[event] = cb;
});
}
getTarget = () => this.container;
render() {
return (
<div
ref={node => {
this.container = node;
}}
className="container"
>
<Affix
className="fixed"
target={this.getTarget}
ref={ele => {
this.affix = ele;
}}
{...this.props}
>
<Button type="primary">Fixed at the top of container</Button>
</Affix>
</div>
);
}
}
describe('Affix Render', () => {
rtlTest(Affix);
let wrapper;
let domMock;
const classRect: any = {
container: {
top: 0,
bottom: 100,
},
};
beforeAll(() => {
domMock = spyElementPrototype(HTMLElement, 'getBoundingClientRect', function mockBounding() {
return (
classRect[this.className] || {
top: 0,
bottom: 0,
}
);
});
});
afterAll(() => {
domMock.mockRestore();
});
const movePlaceholder = async top => {
classRect.fixed = {
top,
bottom: top,
};
events.scroll({
type: 'scroll',
});
await sleep(20);
};
it('Anchor render perfectly', async () => {
document.body.innerHTML = '<div id="mounter" />';
wrapper = mount(<AffixMounter />, { attachTo: document.getElementById('mounter') });
await sleep(20);
await movePlaceholder(0);
expect(wrapper.instance().affix.state.affixStyle).toBeFalsy();
await movePlaceholder(-100);
expect(wrapper.instance().affix.state.affixStyle).toBeTruthy();
await movePlaceholder(0);
expect(wrapper.instance().affix.state.affixStyle).toBeFalsy();
});
it('support offsetBottom', async () => {
document.body.innerHTML = '<div id="mounter" />';
wrapper = mount(<AffixMounter offsetBottom={0} />, {
attachTo: document.getElementById('mounter'),
});
await sleep(20);
await movePlaceholder(300);
expect(wrapper.instance().affix.state.affixStyle).toBeTruthy();
await movePlaceholder(0);
expect(wrapper.instance().affix.state.affixStyle).toBeFalsy();
await movePlaceholder(300);
expect(wrapper.instance().affix.state.affixStyle).toBeTruthy();
});
it('updatePosition when offsetTop changed', async () => {
document.body.innerHTML = '<div id="mounter" />';
wrapper = mount(<AffixMounter offsetTop={0} />, {
attachTo: document.getElementById('mounter'),
});
await sleep(20);
await movePlaceholder(-100);
expect(wrapper.instance().affix.state.affixStyle.top).toBe(0);
wrapper.setProps({
offsetTop: 10,
});
await sleep(20);
expect(wrapper.instance().affix.state.affixStyle.top).toBe(10);
});
describe('updatePosition when target changed', () => {
it('function change', () => {
document.body.innerHTML = '<div id="mounter" />';
const container = document.querySelector('#id') as HTMLDivElement;
const getTarget = () => container;
wrapper = mount(<Affix target={getTarget}>{null}</Affix>);
wrapper.setProps({ target: null });
expect(wrapper.instance().state.status).toBe(0);
expect(wrapper.instance().state.affixStyle).toBe(undefined);
expect(wrapper.instance().state.placeholderStyle).toBe(undefined);
});
it('instance change', async () => {
const getObserverLength = () => Object.keys(getObserverEntities()).length;
const container = document.createElement('div');
document.body.appendChild(container);
let target = container;
const originLength = getObserverLength();
const getTarget = () => target;
wrapper = mount(<Affix target={getTarget}>{null}</Affix>);
await sleep(50);
expect(getObserverLength()).toBe(originLength + 1);
target = null;
wrapper.setProps({});
wrapper.update();
await sleep(50);
expect(getObserverLength()).toBe(originLength);
});
});
describe('updatePosition when size changed', () => {
function test(name, index) {
it(name, async () => {
document.body.innerHTML = '<div id="mounter" />';
const updateCalled = jest.fn();
wrapper = mount(<AffixMounter offsetBottom={0} onTestUpdatePosition={updateCalled} />, {
attachTo: document.getElementById('mounter'),
});
await sleep(20);
await movePlaceholder(300);
expect(wrapper.instance().affix.state.affixStyle).toBeTruthy();
await sleep(20);
wrapper.update();
// Mock trigger resize
updateCalled.mockReset();
wrapper
.find('ResizeObserver')
.at(index)
.instance()
.onResize([{ target: { getBoundingClientRect: () => ({ width: 99, height: 99 }) } }]);
await sleep(20);
expect(updateCalled).toHaveBeenCalled();
});
}
test('inner', 0);
test('outer', 1);
});
});

View File

@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Affix Render rtl render component should be rendered correctly in RTL direction 1`] = `
<div>
<div
class=""
/>
</div>
`;

View File

@ -0,0 +1,108 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/affix/demo/basic.md correctly 1`] = `
<div>
<div>
<div
class=""
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Affix top
</span>
</button>
</div>
</div>
<br />
<div>
<div
class=""
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Affix bottom
</span>
</button>
</div>
</div>
</div>
`;
exports[`renders ./components/affix/demo/debug.md correctly 1`] = `
<div
style="height:10000px"
>
<div>
Top
</div>
<div>
<div
class=""
>
<div
style="background:red"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Affix top
</span>
</button>
</div>
</div>
</div>
<div>
Bottom
</div>
</div>
`;
exports[`renders ./components/affix/demo/on-change.md correctly 1`] = `
<div>
<div
class=""
>
<button
class="ant-btn"
type="button"
>
<span>
120px to affix top
</span>
</button>
</div>
</div>
`;
exports[`renders ./components/affix/demo/target.md correctly 1`] = `
<div
class="scrollable-container"
>
<div
class="background"
>
<div>
<div
class=""
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Fixed at the top of container
</span>
</button>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('affix');

View File

@ -0,0 +1,42 @@
---
order: 0
title:
zh-CN: 基本
en-US: Basic
---
## zh-CN
最简单的用法。
## en-US
The simplest usage.
```tsx
import React, { useState } from 'react';
import { Affix, Button } from 'antd';
const Demo: React.FC = () => {
const [top, setTop] = useState(10);
const [bottom, setBottom] = useState(10);
return (
<div>
<Affix offsetTop={top}>
<Button type="primary" onClick={() => setTop(top + 10)}>
Affix top
</Button>
</Affix>
<br />
<Affix offsetBottom={bottom}>
<Button type="primary" onClick={() => setBottom(bottom + 10)}>
Affix bottom
</Button>
</Affix>
</div>
);
};
ReactDOM.render(<Demo />, mountNode);
```

View File

@ -0,0 +1,39 @@
---
order: 99
title:
zh-CN: 调整浏览器大小,观察 Affix 容器是否发生变化。跟随变化为正常。#17678
en-US:
debug: true
---
## zh-CN
DEBUG
## en-US
DEBUG
```tsx
import React, { useState } from 'react';
import { Affix, Button } from 'antd';
const Demo: React.FC = () => {
const [top, setTop] = useState(10);
return (
<div style={{ height: 10000 }}>
<div>Top</div>
<Affix offsetTop={top}>
<div style={{ background: 'red' }}>
<Button type="primary" onClick={() => setTop(top + 10)}>
Affix top
</Button>
</div>
</Affix>
<div>Bottom</div>
</div>
);
};
ReactDOM.render(<Demo />, mountNode);
```

View File

@ -0,0 +1,25 @@
---
order: 1
title:
zh-CN: 固定状态改变的回调
en-US: Callback
---
## zh-CN
可以获得是否固定的状态。
## en-US
Callback with affixed state.
```tsx
import { Affix, Button } from 'antd';
ReactDOM.render(
<Affix offsetTop={120} onChange={affixed => console.log(affixed)}>
<Button>120px to affix top</Button>
</Affix>,
mountNode,
);
```

View File

@ -0,0 +1,46 @@
---
order: 2
title:
zh-CN: 滚动容器
en-US: Container to scroll.
---
## zh-CN
`target` 设置 `Affix` 需要监听其滚动事件的元素,默认为 `window`
## en-US
Set a `target` for 'Affix', which is listen to scroll event of target element (default is `window`).
```tsx
import React, { useState } from 'react';
import { Affix, Button } from 'antd';
const Demo: React.FC = () => {
const [container, setContainer] = useState(null);
return (
<div className="scrollable-container" ref={setContainer}>
<div className="background">
<Affix target={() => container}>
<Button type="primary">Fixed at the top of container</Button>
</Affix>
</div>
</div>
);
};
ReactDOM.render(<Demo />, mountNode);
```
<style>
#components-affix-demo-target .scrollable-container {
height: 100px;
overflow-y: scroll;
}
#components-affix-demo-target .background {
padding-top: 60px;
height: 300px;
background-image: url('https://zos.alipayobjects.com/rmsportal/RmjwQiJorKyobvI.jpg');
}
</style>

View File

@ -0,0 +1,36 @@
---
category: Components
type: Navigation
title: Affix
---
Wrap Affix around another component to make it stick the viewport.
## When To Use
On longer web pages, its helpful for some content to stick to the viewport. This is common for menus and actions.
Please note that Affix should not cover other content on the page, especially when the size of the viewport is small.
## API
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| offsetBottom | Offset from the bottom of the viewport (in pixels) | number | - |
| offsetTop | Offset from the top of the viewport (in pixels) | number | 0 |
| target | Specifies the scrollable area DOM node | () => HTMLElement | () => window |
| onChange | Callback for when Affix state is changed | Function(affixed) | - |
**Note:** Children of `Affix` must not have the property `position: absolute`, but you can set `position: absolute` on `Affix` itself:
```jsx
<Affix style={{ position: 'absolute', top: y, left: x }}>...</Affix>
```
## FAQ
### Affix bind container with `target`, sometime move out of container.
We don't listen window scroll for performance consideration. You can add listener if you still want: <https://codesandbox.io/s/2xyj5zr85p>
Related issues[#3938](https://github.com/ant-design/ant-design/issues/3938) [#5642](https://github.com/ant-design/ant-design/issues/5642) [#16120](https://github.com/ant-design/ant-design/issues/16120)

View File

@ -0,0 +1,292 @@
import * as React from 'react';
import classNames from 'classnames';
import omit from 'omit.js';
import ResizeObserver from 'rc-resize-observer';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { throttleByAnimationFrameDecorator } from '../_util/throttleByAnimationFrame';
import {
addObserveTarget,
removeObserveTarget,
getTargetRect,
getFixedTop,
getFixedBottom,
} from './utils';
function getDefaultTarget() {
return typeof window !== 'undefined' ? window : null;
}
// Affix
export interface AffixProps {
/**
*
*/
offsetTop?: number;
/** 距离窗口底部达到指定偏移量后触发 */
offsetBottom?: number;
style?: React.CSSProperties;
/** 固定状态改变时触发的回调函数 */
onChange?: (affixed?: boolean) => void;
/** 设置 Affix 需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数 */
target?: () => Window | HTMLElement | null;
prefixCls?: string;
className?: string;
children: React.ReactNode;
}
enum AffixStatus {
None,
Prepare,
}
export interface AffixState {
affixStyle?: React.CSSProperties;
placeholderStyle?: React.CSSProperties;
status: AffixStatus;
lastAffix: boolean;
prevTarget: Window | HTMLElement | null;
}
class Affix extends React.Component<AffixProps, AffixState> {
static defaultProps = {
target: getDefaultTarget,
};
state: AffixState = {
status: AffixStatus.None,
lastAffix: false,
prevTarget: null,
};
placeholderNode: HTMLDivElement;
fixedNode: HTMLDivElement;
private timeout: number;
// Event handler
componentDidMount() {
const { target } = this.props;
if (target) {
// [Legacy] Wait for parent component ref has its value.
// We should use target as directly element instead of function which makes element check hard.
this.timeout = setTimeout(() => {
addObserveTarget(target(), this);
// Mock Event object.
this.updatePosition();
});
}
}
componentDidUpdate(prevProps: AffixProps) {
const { prevTarget } = this.state;
const { target } = this.props;
let newTarget = null;
if (target) {
newTarget = target() || null;
}
if (prevTarget !== newTarget) {
removeObserveTarget(this);
if (newTarget) {
addObserveTarget(newTarget, this);
// Mock Event object.
this.updatePosition();
}
this.setState({ prevTarget: newTarget });
}
if (
prevProps.offsetTop !== this.props.offsetTop ||
prevProps.offsetBottom !== this.props.offsetBottom
) {
this.updatePosition();
}
this.measure();
}
componentWillUnmount() {
clearTimeout(this.timeout);
removeObserveTarget(this);
(this.updatePosition as any).cancel();
// https://github.com/ant-design/ant-design/issues/22683
(this.lazyUpdatePosition as any).cancel();
}
getOffsetTop = () => {
const { offsetBottom } = this.props;
let { offsetTop } = this.props;
if (offsetBottom === undefined && offsetTop === undefined) {
offsetTop = 0;
}
return offsetTop;
};
getOffsetBottom = () => {
return this.props.offsetBottom;
};
savePlaceholderNode = (node: HTMLDivElement) => {
this.placeholderNode = node;
};
saveFixedNode = (node: HTMLDivElement) => {
this.fixedNode = node;
};
// =================== Measure ===================
measure = () => {
const { status, lastAffix } = this.state;
const { target, onChange } = this.props;
if (status !== AffixStatus.Prepare || !this.fixedNode || !this.placeholderNode || !target) {
return;
}
const offsetTop = this.getOffsetTop();
const offsetBottom = this.getOffsetBottom();
const targetNode = target();
if (!targetNode) {
return;
}
const newState: Partial<AffixState> = {
status: AffixStatus.None,
};
const targetRect = getTargetRect(targetNode);
const placeholderReact = getTargetRect(this.placeholderNode);
const fixedTop = getFixedTop(placeholderReact, targetRect, offsetTop);
const fixedBottom = getFixedBottom(placeholderReact, targetRect, offsetBottom);
if (fixedTop !== undefined) {
newState.affixStyle = {
position: 'fixed',
top: fixedTop,
width: placeholderReact.width,
height: placeholderReact.height,
};
newState.placeholderStyle = {
width: placeholderReact.width,
height: placeholderReact.height,
};
} else if (fixedBottom !== undefined) {
newState.affixStyle = {
position: 'fixed',
bottom: fixedBottom,
width: placeholderReact.width,
height: placeholderReact.height,
};
newState.placeholderStyle = {
width: placeholderReact.width,
height: placeholderReact.height,
};
}
newState.lastAffix = !!newState.affixStyle;
if (onChange && lastAffix !== newState.lastAffix) {
onChange(newState.lastAffix);
}
this.setState(newState as AffixState);
};
// @ts-ignore TS6133
prepareMeasure = () => {
// event param is used before. Keep compatible ts define here.
this.setState({
status: AffixStatus.Prepare,
affixStyle: undefined,
placeholderStyle: undefined,
});
// Test if `updatePosition` called
if (process.env.NODE_ENV === 'test') {
const { onTestUpdatePosition } = this.props as any;
if (onTestUpdatePosition) {
onTestUpdatePosition();
}
}
};
// Handle realign logic
@throttleByAnimationFrameDecorator()
updatePosition() {
this.prepareMeasure();
}
@throttleByAnimationFrameDecorator()
lazyUpdatePosition() {
const { target } = this.props;
const { affixStyle } = this.state;
// Check position change before measure to make Safari smooth
if (target && affixStyle) {
const offsetTop = this.getOffsetTop();
const offsetBottom = this.getOffsetBottom();
const targetNode = target();
if (targetNode && this.placeholderNode) {
const targetRect = getTargetRect(targetNode);
const placeholderReact = getTargetRect(this.placeholderNode);
const fixedTop = getFixedTop(placeholderReact, targetRect, offsetTop);
const fixedBottom = getFixedBottom(placeholderReact, targetRect, offsetBottom);
if (
(fixedTop !== undefined && affixStyle.top === fixedTop) ||
(fixedBottom !== undefined && affixStyle.bottom === fixedBottom)
) {
return;
}
}
}
// Directly call prepare measure since it's already throttled.
this.prepareMeasure();
}
// =================== Render ===================
renderAffix = ({ getPrefixCls }: ConfigConsumerProps) => {
const { affixStyle, placeholderStyle } = this.state;
const { prefixCls, children } = this.props;
const className = classNames({
[getPrefixCls('affix', prefixCls)]: affixStyle,
});
let props = omit(this.props, ['prefixCls', 'offsetTop', 'offsetBottom', 'target', 'onChange']);
// Omit this since `onTestUpdatePosition` only works on test.
if (process.env.NODE_ENV === 'test') {
props = omit(props, ['onTestUpdatePosition']);
}
return (
<ResizeObserver
onResize={() => {
this.updatePosition();
}}
>
<div {...props} ref={this.savePlaceholderNode}>
{affixStyle && <div style={placeholderStyle} aria-hidden="true" />}
<div className={className} ref={this.saveFixedNode} style={affixStyle}>
<ResizeObserver
onResize={() => {
this.updatePosition();
}}
>
{children}
</ResizeObserver>
</div>
</div>
</ResizeObserver>
);
};
render() {
return <ConfigConsumer>{this.renderAffix}</ConfigConsumer>;
}
}
export default Affix;

View File

@ -0,0 +1,37 @@
---
category: Components
subtitle: 固钉
type: 导航
title: Affix
---
将页面元素钉在可视范围。
## 何时使用
当内容区域比较长,需要滚动页面时,这部分内容对应的操作或者导航需要在滚动范围内始终展现。常用于侧边菜单和按钮组合。
页面可视范围过小时,慎用此功能以免遮挡页面内容。
## API
| 成员 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| offsetBottom | 距离窗口底部达到指定偏移量后触发 | number | - |
| offsetTop | 距离窗口顶部达到指定偏移量后触发 | number | - |
| target | 设置 `Affix` 需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数 | () => HTMLElement | () => window |
| onChange | 固定状态改变时触发的回调函数 | Function(affixed) | - |
**注意:**`Affix` 内的元素不要使用绝对定位,如需要绝对定位的效果,可以直接设置 `Affix` 为绝对定位:
```jsx
<Affix style={{ position: 'absolute', top: y, left: x }}>...</Affix>
```
## FAQ
### Affix 使用 `target` 绑定容器时,元素会跑到容器外。
从性能角度考虑,我们只监听容器滚动事件。如果希望任意滚动,你可以在窗体添加滚动监听:<https://codesandbox.io/s/2xyj5zr85p>
相关 issue[#3938](https://github.com/ant-design/ant-design/issues/3938) [#5642](https://github.com/ant-design/ant-design/issues/5642) [#16120](https://github.com/ant-design/ant-design/issues/16120)

View File

@ -0,0 +1,6 @@
@import '../../style/themes/index';
.@{ant-prefix}-affix {
position: fixed;
z-index: @zindex-affix;
}

View File

@ -0,0 +1,2 @@
import '../../style/index.less';
import './index.less';

View File

@ -0,0 +1,106 @@
import addEventListener from 'rc-util/lib/Dom/addEventListener';
import Affix from '.';
export type BindElement = HTMLElement | Window | null | undefined;
export type Rect = ClientRect | DOMRect;
export function getTargetRect(target: BindElement): ClientRect {
return target !== window
? (target as HTMLElement).getBoundingClientRect()
: ({ top: 0, bottom: window.innerHeight } as ClientRect);
}
export function getFixedTop(
placeholderReact: Rect,
targetRect: Rect,
offsetTop: number | undefined,
) {
if (offsetTop !== undefined && targetRect.top > placeholderReact.top - offsetTop) {
return offsetTop + targetRect.top;
}
return undefined;
}
export function getFixedBottom(
placeholderReact: Rect,
targetRect: Rect,
offsetBottom: number | undefined,
) {
if (offsetBottom !== undefined && targetRect.bottom < placeholderReact.bottom + offsetBottom) {
const targetBottomOffset = window.innerHeight - targetRect.bottom;
return offsetBottom + targetBottomOffset;
}
return undefined;
}
// ======================== Observer ========================
const TRIGGER_EVENTS = [
'resize',
'scroll',
'touchstart',
'touchmove',
'touchend',
'pageshow',
'load',
];
interface ObserverEntity {
target: HTMLElement | Window;
affixList: Affix[];
eventHandlers: { [eventName: string]: any };
}
let observerEntities: ObserverEntity[] = [];
export function getObserverEntities() {
// Only used in test env. Can be removed if refactor.
return observerEntities;
}
export function addObserveTarget(target: HTMLElement | Window | null, affix: Affix): void {
if (!target) return;
let entity: ObserverEntity | undefined = observerEntities.find(item => item.target === target);
if (entity) {
entity.affixList.push(affix);
} else {
entity = {
target,
affixList: [affix],
eventHandlers: {},
};
observerEntities.push(entity);
// Add listener
TRIGGER_EVENTS.forEach(eventName => {
entity!.eventHandlers[eventName] = addEventListener(target, eventName, () => {
entity!.affixList.forEach(targetAffix => {
targetAffix.lazyUpdatePosition();
});
});
});
}
}
export function removeObserveTarget(affix: Affix): void {
const observerEntity = observerEntities.find(oriObserverEntity => {
const hasAffix = oriObserverEntity.affixList.some(item => item === affix);
if (hasAffix) {
oriObserverEntity.affixList = oriObserverEntity.affixList.filter(item => item !== affix);
}
return hasAffix;
});
if (observerEntity && observerEntity.affixList.length === 0) {
observerEntities = observerEntities.filter(item => item !== observerEntity);
// Remove listener
TRIGGER_EVENTS.forEach(eventName => {
const handler = observerEntity.eventHandlers[eventName];
if (handler && handler.remove) {
handler.remove();
}
});
}
}

View File

@ -0,0 +1,42 @@
import * as React from 'react';
import Alert from '.';
interface ErrorBoundaryProps {
message?: React.ReactNode;
description?: React.ReactNode;
}
export default class ErrorBoundary extends React.Component<
ErrorBoundaryProps,
{
error?: Error | null;
info: {
componentStack?: string;
};
}
> {
state = {
error: undefined,
info: {
componentStack: '',
},
};
componentDidCatch(error: Error | null, info: object) {
this.setState({ error, info });
}
render() {
const { message, description, children } = this.props;
const { error, info } = this.state;
const componentStack = info && info.componentStack ? info.componentStack : null;
const errorMessage = typeof message === 'undefined' ? (error || '').toString() : message;
const errorDescription = typeof description === 'undefined' ? componentStack : description;
if (error) {
return (
<Alert type="error" message={errorMessage} description={<pre>{errorDescription}</pre>} />
);
}
return children;
}
}

View File

@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Alert ErrorBoundary 1`] = `
<div
class="ant-alert ant-alert-error ant-alert-with-description ant-alert-no-icon"
data-show="true"
>
<span
class="ant-alert-message"
>
ReferenceError: NotExisted is not defined
</span>
<span
class="ant-alert-description"
>
<pre>
in ThrowError
in ErrorBoundary (created by WrapperComponent)
in WrapperComponent
</pre>
</span>
</div>
`;
exports[`Alert rtl render component should be rendered correctly in RTL direction 1`] = `
<div
class="ant-alert ant-alert-info ant-alert-no-icon ant-alert-rtl"
data-show="true"
>
<span
class="ant-alert-message"
/>
<span
class="ant-alert-description"
/>
</div>
`;

View File

@ -0,0 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('alert');

View File

@ -0,0 +1,115 @@
import React from 'react';
import { mount } from 'enzyme';
import Alert from '..';
import Tooltip from '../../tooltip';
import Popconfirm from '../../popconfirm';
import rtlTest from '../../../tests/shared/rtlTest';
import { sleep } from '../../../tests/utils';
const { ErrorBoundary } = Alert;
describe('Alert', () => {
rtlTest(Alert);
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
it('could be closed', () => {
const onClose = jest.fn();
const afterClose = jest.fn();
const wrapper = mount(
<Alert
message="Warning Text Warning Text Warning TextW arning Text Warning Text Warning TextWarning Text"
type="warning"
closable
onClose={onClose}
afterClose={afterClose}
/>,
);
wrapper.find('.ant-alert-close-icon').simulate('click');
expect(onClose).toHaveBeenCalled();
jest.runAllTimers();
expect(afterClose).toHaveBeenCalled();
});
describe('data and aria props', () => {
it('sets data attributes on input', () => {
const wrapper = mount(<Alert data-test="test-id" data-id="12345" />);
const input = wrapper.find('.ant-alert').getDOMNode();
expect(input.getAttribute('data-test')).toBe('test-id');
expect(input.getAttribute('data-id')).toBe('12345');
});
it('sets aria attributes on input', () => {
const wrapper = mount(<Alert aria-describedby="some-label" />);
const input = wrapper.find('.ant-alert').getDOMNode();
expect(input.getAttribute('aria-describedby')).toBe('some-label');
});
it('sets role attribute on input', () => {
const wrapper = mount(<Alert role="status" />);
const input = wrapper.find('.ant-alert').getDOMNode();
expect(input.getAttribute('role')).toBe('status');
});
});
const testIt = process.env.REACT === '15' ? it.skip : it;
testIt('ErrorBoundary', () => {
// eslint-disable-next-line react/jsx-no-undef
const ThrowError = () => <NotExisted />;
const wrapper = mount(
<ErrorBoundary>
<ThrowError />
</ErrorBoundary>,
);
// eslint-disable-next-line jest/no-standalone-expect
expect(wrapper.render()).toMatchSnapshot();
});
it('could be used with Tooltip', async () => {
jest.useRealTimers();
const wrapper = mount(
<Tooltip title="xxx" mouseEnterDelay={0}>
<Alert
message="Warning Text Warning Text Warning TextW arning Text Warning Text Warning TextWarning Text"
type="warning"
/>
</Tooltip>,
);
wrapper.find('.ant-alert').simulate('mouseenter');
await sleep(0);
expect(
wrapper
.find(Tooltip)
.instance()
.getPopupDomNode(),
).toBeTruthy();
jest.useFakeTimers();
});
it('could be used with Popconfirm', async () => {
jest.useRealTimers();
const wrapper = mount(
<Popconfirm title="xxx">
<Alert
message="Warning Text Warning Text Warning TextW arning Text Warning Text Warning TextWarning Text"
type="warning"
/>
</Popconfirm>,
);
wrapper.find('.ant-alert').simulate('click');
await sleep(0);
expect(
wrapper
.find(Popconfirm)
.instance()
.getPopupDomNode(),
).toBeTruthy();
jest.useFakeTimers();
});
});

View File

@ -0,0 +1,36 @@
---
order: 6
iframe: 250
title:
zh-CN: 顶部公告
en-US: Banner
---
## zh-CN
页面顶部通告形式,默认有图标且 `type` 为 'warning'。
## en-US
Display Alert as a banner at top of page.
```tsx
import { Alert } from 'antd';
ReactDOM.render(
<>
<Alert message="Warning text" banner />
<br />
<Alert
message="Very long warning text warning text text text text text text text"
banner
closable
/>
<br />
<Alert showIcon={false} message="Warning text without icon" banner />
<br />
<Alert type="error" message="Error text" banner />
</>,
mountNode,
);
```

View File

@ -0,0 +1,26 @@
---
order: 0
title:
zh-CN: 基本
en-US: Basic
---
## zh-CN
最简单的用法,适用于简短的警告提示。
## en-US
The simplest usage for short messages.
```tsx
import { Alert } from 'antd';
ReactDOM.render(<Alert message="Success Text" type="success" />, mountNode);
```
<style>
.code-box-demo .ant-alert {
margin-bottom: 16px;
}
</style>

View File

@ -0,0 +1,41 @@
---
order: 2
title:
zh-CN: 可关闭的警告提示
en-US: Closable
---
## zh-CN
显示关闭按钮,点击可关闭警告提示。
## en-US
To show close button.
```tsx
import { Alert } from 'antd';
const onClose = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
console.log(e, 'I was closed.');
};
ReactDOM.render(
<div>
<Alert
message="Warning Text Warning Text Warning TextW arning Text Warning Text Warning TextWarning Text"
type="warning"
closable
onClose={onClose}
/>
<Alert
message="Error Text"
description="Error Description Error Description Error Description Error Description Error Description Error Description"
type="error"
closable
onClose={onClose}
/>
</div>,
mountNode,
);
```

View File

@ -0,0 +1,20 @@
---
order: 5
title:
zh-CN: 自定义关闭
en-US: Customized Close Text
---
## zh-CN
可以自定义关闭,自定义的文字会替换原先的关闭 `Icon`
## en-US
Replace the default icon with customized text.
```tsx
import { Alert } from 'antd';
ReactDOM.render(<Alert message="Info Text" type="info" closeText="Close Now" />, mountNode);
```

View File

@ -0,0 +1,61 @@
---
order: 12
debug: true
title:
zh-CN: 自定义图标
en-US: Custom Icon
---
## zh-CN
可口的图标让信息类型更加醒目。
## en-US
A relevant icon makes information clearer and more friendly.
```tsx
import { Alert } from 'antd';
import { SmileOutlined } from '@ant-design/icons';
const icon = <SmileOutlined />;
ReactDOM.render(
<div>
<Alert icon={icon} message="showIcon = false" type="success" />
<Alert icon={icon} message="Success Tips" type="success" showIcon />
<Alert icon={icon} message="Informational Notes" type="info" showIcon />
<Alert icon={icon} message="Warning" type="warning" showIcon />
<Alert icon={icon} message="Error" type="error" showIcon />
<Alert
icon={icon}
message="Success Tips"
description="Detailed description and advices about successful copywriting."
type="success"
showIcon
/>
<Alert
icon={icon}
message="Informational Notes"
description="Additional description and informations about copywriting."
type="info"
showIcon
/>
<Alert
icon={icon}
message="Warning"
description="This is a warning notice about copywriting."
type="warning"
showIcon
/>
<Alert
icon={icon}
message="Error"
description="This is an error message about copywriting."
type="error"
showIcon
/>
</div>,
mountNode,
);
```

View File

@ -0,0 +1,44 @@
---
order: 3
title:
zh-CN: 含有辅助性文字介绍
en-US: Description
---
## zh-CN
含有辅助性文字介绍的警告提示。
## en-US
Additional description for alert message.
```tsx
import { Alert } from 'antd';
ReactDOM.render(
<div>
<Alert
message="Success Text"
description="Success Description Success Description Success Description"
type="success"
/>
<Alert
message="Info Text"
description="Info Description Info Description Info Description Info Description"
type="info"
/>
<Alert
message="Warning Text"
description="Warning Description Warning Description Warning Description Warning Description"
type="warning"
/>
<Alert
message="Error Text"
description="Error Description Error Description Error Description Error Description"
type="error"
/>
</div>,
mountNode,
);
```

View File

@ -0,0 +1,43 @@
---
order: 8
title:
zh-CN: ErrorBoundary
en-US: React 错误处理
---
## zh-CN
友好的 [React 错误处理](https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html) 包裹组件。
## en-US
ErrorBoundary Component for making error handling easier in [React](https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html).
```tsx
import React, { useState } from 'react';
import { Button, Alert } from 'antd';
const { ErrorBoundary } = Alert;
const ThrowError: React.FC = () => {
const [error, setError] = useState<Error>();
const onClick = () => {
setError(new Error('An Uncaught Error'));
};
if (error) {
throw error;
}
return (
<Button type="danger" onClick={onClick}>
Click me to throw a error
</Button>
);
};
ReactDOM.render(
<ErrorBoundary>
<ThrowError />
</ErrorBoundary>,
mountNode,
);
```

View File

@ -0,0 +1,52 @@
---
order: 4
title:
zh-CN: 图标
en-US: Icon
---
## zh-CN
可口的图标让信息类型更加醒目。
## en-US
A relevant icon will make information clearer and more friendly.
```tsx
import { Alert } from 'antd';
ReactDOM.render(
<div>
<Alert message="Success Tips" type="success" showIcon />
<Alert message="Informational Notes" type="info" showIcon />
<Alert message="Warning" type="warning" showIcon />
<Alert message="Error" type="error" showIcon />
<Alert
message="Success Tips"
description="Detailed description and advice about successful copywriting."
type="success"
showIcon
/>
<Alert
message="Informational Notes"
description="Additional description and information about copywriting."
type="info"
showIcon
/>
<Alert
message="Warning"
description="This is a warning notice about copywriting."
type="warning"
showIcon
/>
<Alert
message="Error"
description="This is an error message about copywriting."
type="error"
showIcon
/>
</div>,
mountNode,
);
```

View File

@ -0,0 +1,34 @@
---
order: 6.1
title:
zh-CN: 轮播的公告
en-US: Loop Banner
---
## zh-CN
配合 [react-text-loop](https://npmjs.com/package/react-text-loop) 实现消息轮播通知栏。
## en-US
Show a loop banner by using with [react-text-loop](https://npmjs.com/package/react-text-loop).
```tsx
import { Alert } from 'antd';
import TextLoop from 'react-text-loop';
ReactDOM.render(
<Alert
banner
message={
<TextLoop mask>
<div>Notice message one</div>
<div>Notice message two</div>
<div>Notice message three</div>
<div>Notice message four</div>
</TextLoop>
}
/>,
mountNode,
);
```

View File

@ -0,0 +1,36 @@
---
order: 7
title:
zh-CN: 平滑地卸载
en-US: Smoothly Unmount
---
## zh-CN
平滑、自然的卸载提示。
## en-US
Smoothly unmount Alert upon close.
```tsx
import React, { useState } from 'react';
import { Alert } from 'antd';
const App: React.FC = () => {
const [visible, setVisible] = useState(true);
const handleClose = () => {
setVisible(false);
};
return (
<div>
{visible ? (
<Alert message="Alert Message Text" type="success" closable afterClose={handleClose} />
) : null}
<p>placeholder text here</p>
</div>
);
};
ReactDOM.render(<App />, mountNode);
```

View File

@ -0,0 +1,28 @@
---
order: 1
title:
zh-CN: 四种样式
en-US: More types
---
## zh-CN
共有四种样式 `success``info``warning``error`
## en-US
There are 4 types of Alert: `success`, `info`, `warning`, `error`.
```tsx
import { Alert } from 'antd';
ReactDOM.render(
<div>
<Alert message="Success Text" type="success" />
<Alert message="Info Text" type="info" />
<Alert message="Warning Text" type="warning" />
<Alert message="Error Text" type="error" />
</div>,
mountNode,
);
```

View File

@ -0,0 +1,34 @@
---
category: Components
type: Feedback
title: Alert
---
Alert component for feedback.
## When To Use
- When you need to show alert messages to users.
- When you need a persistent static container which is closable by user actions.
## API
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| afterClose | Called when close animation is finished | () => void | - |
| banner | Whether to show as banner | boolean | false |
| closable | Whether Alert can be closed | boolean | - |
| closeText | Close text to show | string\|ReactNode | - |
| description | Additional content of Alert | string\|ReactNode | - |
| icon | Custom icon, effective when `showIcon` is `true` | ReactNode | - |
| message | Content of Alert | string\|ReactNode | - |
| showIcon | Whether to show icon | boolean | false, in `banner` mode default is true |
| type | Type of Alert styles, options: `success`, `info`, `warning`, `error` | string | `info`, in `banner` mode default is `warning` |
| onClose | Callback when Alert is closed | (e: MouseEvent) => void | - |
### Alert.ErrorBoundary
| Property | Description | Type | Default | Version |
| ----------- | -------------------------------- | --------- | ------------------- | ------- |
| message | custom error message to show | ReactNode | `{{ error }}` | |
| description | custom error description to show | ReactNode | `{{ error stack }}` | |

View File

@ -0,0 +1,203 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import CheckCircleOutlined from '@ant-design/icons/CheckCircleOutlined';
import ExclamationCircleOutlined from '@ant-design/icons/ExclamationCircleOutlined';
import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined';
import CloseCircleOutlined from '@ant-design/icons/CloseCircleOutlined';
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
import InfoCircleFilled from '@ant-design/icons/InfoCircleFilled';
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import Animate from 'rc-animate';
import classNames from 'classnames';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import getDataOrAriaProps from '../_util/getDataOrAriaProps';
import ErrorBoundary from './ErrorBoundary';
function noop() {}
export interface AlertProps {
/**
* Type of Alert styles, options:`success`, `info`, `warning`, `error`
*/
type?: 'success' | 'info' | 'warning' | 'error';
/** Whether Alert can be closed */
closable?: boolean;
/** Close text to show */
closeText?: React.ReactNode;
/** Content of Alert */
message: React.ReactNode;
/** Additional content of Alert */
description?: React.ReactNode;
/** Callback when close Alert */
onClose?: React.MouseEventHandler<HTMLButtonElement>;
/** Trigger when animation ending of Alert */
afterClose?: () => void;
/** Whether to show icon */
showIcon?: boolean;
style?: React.CSSProperties;
prefixCls?: string;
className?: string;
banner?: boolean;
icon?: React.ReactNode;
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
onClick?: React.MouseEventHandler<HTMLDivElement>;
}
export interface AlertState {
closing: boolean;
closed: boolean;
}
const iconMapFilled = {
success: CheckCircleFilled,
info: InfoCircleFilled,
error: CloseCircleFilled,
warning: ExclamationCircleFilled,
};
const iconMapOutlined = {
success: CheckCircleOutlined,
info: InfoCircleOutlined,
error: CloseCircleOutlined,
warning: ExclamationCircleOutlined,
};
export default class Alert extends React.Component<AlertProps, AlertState> {
static ErrorBoundary = ErrorBoundary;
state = {
closing: false,
closed: false,
};
handleClose = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
const dom = ReactDOM.findDOMNode(this) as HTMLElement;
dom.style.height = `${dom.offsetHeight}px`;
// Magic code
// 重复一次后才能正确设置 height
dom.style.height = `${dom.offsetHeight}px`;
this.setState({
closing: true,
});
(this.props.onClose || noop)(e);
};
animationEnd = () => {
this.setState({
closing: false,
closed: true,
});
(this.props.afterClose || noop)();
};
renderAlert = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
const {
description,
prefixCls: customizePrefixCls,
message,
closeText,
banner,
className = '',
style,
icon,
onMouseEnter,
onMouseLeave,
onClick,
} = this.props;
let { closable, type, showIcon } = this.props;
const { closing, closed } = this.state;
const prefixCls = getPrefixCls('alert', customizePrefixCls);
// banner模式默认有 Icon
showIcon = banner && showIcon === undefined ? true : showIcon;
// banner模式默认为警告
type = banner && type === undefined ? 'warning' : type || 'info';
// use outline icon in alert with description
const iconType = (description ? iconMapOutlined : iconMapFilled)[type] || null;
// closeable when closeText is assigned
if (closeText) {
closable = true;
}
const alertCls = classNames(
prefixCls,
`${prefixCls}-${type}`,
{
[`${prefixCls}-closing`]: closing,
[`${prefixCls}-with-description`]: !!description,
[`${prefixCls}-no-icon`]: !showIcon,
[`${prefixCls}-banner`]: !!banner,
[`${prefixCls}-closable`]: closable,
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
);
const closeIcon = closable ? (
<button
type="button"
onClick={this.handleClose}
className={`${prefixCls}-close-icon`}
tabIndex={0}
>
{closeText ? (
<span className={`${prefixCls}-close-text`}>{closeText}</span>
) : (
<CloseOutlined />
)}
</button>
) : null;
const dataOrAriaProps = getDataOrAriaProps(this.props);
const iconNode =
(icon &&
(React.isValidElement<{ className?: string }>(icon) ? (
React.cloneElement(icon, {
className: classNames(`${prefixCls}-icon`, {
[icon.props.className as string]: icon.props.className,
}),
})
) : (
<span className={`${prefixCls}-icon`}>{icon}</span>
))) ||
React.createElement(iconType, { className: `${prefixCls}-icon` });
return closed ? null : (
<Animate
component=""
showProp="data-show"
transitionName={`${prefixCls}-slide-up`}
onEnd={this.animationEnd}
>
<div
data-show={!closing}
className={alertCls}
style={style}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={onClick}
{...dataOrAriaProps}
>
{showIcon ? iconNode : null}
<span className={`${prefixCls}-message`}>{message}</span>
<span className={`${prefixCls}-description`}>{description}</span>
{closeIcon}
</div>
</Animate>
);
};
render() {
return <ConfigConsumer>{this.renderAlert}</ConfigConsumer>;
}
}

View File

@ -0,0 +1,35 @@
---
category: Components
subtitle: 警告提示
type: 反馈
title: Alert
---
警告提示,展现需要关注的信息。
## 何时使用
- 当某个页面需要向用户显示警告的信息时。
- 非浮层的静态展现形式,始终展现,不会自动消失,用户可以点击关闭。
## API
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| afterClose | 关闭动画结束后触发的回调函数 | () => void | - |
| banner | 是否用作顶部公告 | boolean | false |
| closable | 默认不显示关闭按钮 | boolean | 无 |
| closeText | 自定义关闭按钮 | string\|ReactNode | 无 |
| description | 警告提示的辅助性文字介绍 | string\|ReactNode | 无 |
| icon | 自定义图标,`showIcon``true` 时有效 | ReactNode | - |
| message | 警告提示内容 | string\|ReactNode | 无 |
| showIcon | 是否显示辅助图标 | boolean | false`banner` 模式下默认值为 true |
| type | 指定警告提示的样式,有四种选择 `success``info``warning``error` | string | `info``banner` 模式下默认值为 `warning` |
| onClose | 关闭时触发的回调函数 | (e: MouseEvent) => void | 无 |
### Alert.ErrorBoundary
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| message | 自定义错误标题,如果未指定会展示原生报错信息 | ReactNode | `{{ error }}` | |
| description | 自定义错误内容,如果未指定会展示报错堆栈 | ReactNode | `{{ error stack }}` | |

View File

@ -0,0 +1,191 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@alert-prefix-cls: ~'@{ant-prefix}-alert';
.@{alert-prefix-cls} {
.reset-component;
position: relative;
padding: 8px 15px 8px 37px;
word-wrap: break-word;
border-radius: @border-radius-base;
&&-no-icon {
padding: @alert-no-icon-padding-vertical 15px;
}
&&-closable {
padding-right: 30px;
}
&-icon {
position: absolute;
top: 8px + @font-size-base * @line-height-base / 2 - @font-size-base / 2;
left: 16px;
}
&-description {
display: none;
font-size: @font-size-base;
line-height: 22px;
}
&-success {
background-color: @alert-success-bg-color;
border: @border-width-base @border-style-base @alert-success-border-color;
.@{alert-prefix-cls}-icon {
color: @alert-success-icon-color;
}
}
&-info {
background-color: @alert-info-bg-color;
border: @border-width-base @border-style-base @alert-info-border-color;
.@{alert-prefix-cls}-icon {
color: @alert-info-icon-color;
}
}
&-warning {
background-color: @alert-warning-bg-color;
border: @border-width-base @border-style-base @alert-warning-border-color;
.@{alert-prefix-cls}-icon {
color: @alert-warning-icon-color;
}
}
&-error {
background-color: @alert-error-bg-color;
border: @border-width-base @border-style-base @alert-error-border-color;
.@{alert-prefix-cls}-icon {
color: @alert-error-icon-color;
}
.@{alert-prefix-cls}-description > pre {
margin: 0;
padding: 0;
}
}
&-close-icon {
position: absolute;
top: @padding-xs;
right: 16px;
padding: 0;
overflow: hidden;
font-size: @font-size-sm;
line-height: 22px;
background-color: transparent;
border: none;
outline: none;
cursor: pointer;
.@{iconfont-css-prefix}-close {
color: @alert-close-color;
transition: color 0.3s;
&:hover {
color: @alert-close-hover-color;
}
}
}
&-close-text {
color: @alert-close-color;
transition: color 0.3s;
&:hover {
color: @alert-close-hover-color;
}
}
&-with-description {
position: relative;
padding: 15px 15px 15px 64px;
color: @alert-text-color;
line-height: @line-height-base;
border-radius: @border-radius-base;
}
&-with-description&-no-icon {
padding: @alert-with-description-no-icon-padding-vertical 15px;
}
&-with-description &-icon {
position: absolute;
top: 16px;
left: 24px;
font-size: 24px;
}
&-with-description &-close-icon {
position: absolute;
top: 16px;
right: 16px;
font-size: @font-size-base;
cursor: pointer;
}
&-with-description &-message {
display: block;
margin-bottom: 4px;
color: @alert-message-color;
font-size: @font-size-lg;
}
&-message {
color: @alert-message-color;
}
&-with-description &-description {
display: block;
}
&&-closing {
height: 0 !important;
margin: 0;
padding-top: 0;
padding-bottom: 0;
transform-origin: 50% 0;
transition: all 0.3s @ease-in-out-circ;
}
&-slide-up-leave {
animation: antAlertSlideUpOut 0.3s @ease-in-out-circ;
animation-fill-mode: both;
}
&-banner {
margin-bottom: 0;
border: 0;
border-radius: 0;
}
}
@keyframes antAlertSlideUpIn {
0% {
transform: scaleY(0);
transform-origin: 0% 0%;
opacity: 0;
}
100% {
transform: scaleY(1);
transform-origin: 0% 0%;
opacity: 1;
}
}
@keyframes antAlertSlideUpOut {
0% {
transform: scaleY(1);
transform-origin: 0% 0%;
opacity: 1;
}
100% {
transform: scaleY(0);
transform-origin: 0% 0%;
opacity: 0;
}
}
@import './rtl.less';

View File

@ -0,0 +1,2 @@
import '../../style/index.less';
import './index.less';

View File

@ -0,0 +1,58 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@alert-prefix-cls: ~'@{ant-prefix}-alert';
.@{alert-prefix-cls} {
&-rtl {
padding: 8px 37px 8px 15px;
direction: rtl;
}
&&-closable {
.@{alert-prefix-cls}-rtl& {
padding-right: 15px;
padding-left: 30px;
}
}
&-icon {
.@{alert-prefix-cls}-rtl & {
right: 16px;
left: auto;
}
}
&-close-icon {
.@{alert-prefix-cls}-rtl & {
right: auto;
left: 16px;
}
}
&-with-description {
.@{alert-prefix-cls}-rtl& {
padding: 15px 64px 15px 15px;
}
}
&-with-description&-no-icon {
.@{alert-prefix-cls}-rtl& {
padding: 15px;
}
}
&-with-description &-icon {
.@{alert-prefix-cls}-rtl& {
right: 24px;
left: auto;
}
}
&-with-description &-close-icon {
.@{alert-prefix-cls}-rtl& {
right: auto;
left: 16px;
}
}
}

View File

@ -0,0 +1,328 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as PropTypes from 'prop-types';
import classNames from 'classnames';
import addEventListener from 'rc-util/lib/Dom/addEventListener';
import Affix from '../affix';
import AnchorLink from './AnchorLink';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import scrollTo from '../_util/scrollTo';
import getScroll from '../_util/getScroll';
function getDefaultContainer() {
return window;
}
function getOffsetTop(element: HTMLElement, container: AnchorContainer): number {
if (!element) {
return 0;
}
if (!element.getClientRects().length) {
return 0;
}
const rect = element.getBoundingClientRect();
if (rect.width || rect.height) {
if (container === window) {
container = element.ownerDocument!.documentElement!;
return rect.top - container.clientTop;
}
return rect.top - (container as HTMLElement).getBoundingClientRect().top;
}
return rect.top;
}
const sharpMatcherRegx = /#([^#]+)$/;
type Section = {
link: string;
top: number;
};
export type AnchorContainer = HTMLElement | Window;
interface AnchorProps {
prefixCls?: string;
// className?: string;
// style?: React.CSSProperties;
// children?: React.ReactNode;
// offsetTop?: number;
// bounds?: number;
// affix?: boolean;
// showInkInFixed?: boolean;
// getContainer?: () => AnchorContainer;
// /** Return customize highlight anchor */
// getCurrentAnchor?: () => string;
// onClick?: (e: React.MouseEvent<HTMLElement>, link: { title: React.ReactNode; href: string }) => void;
// /** Scroll to target offset value, if none, it's offsetTop prop value or 0. */
// targetOffset?: number;
// /** Listening event when scrolling change active link */
// onChange?: (currentActiveLink: string) => void;
}
interface AnchorState {
activeLink: null | string;
}
interface AnchorDefaultProps extends AnchorProps {
prefixCls: string;
affix: boolean;
showInkInFixed: boolean;
getContainer: () => AnchorContainer;
}
interface AntAnchor {
registerLink: (link: string) => void;
unregisterLink: (link: string) => void;
activeLink: string | null;
scrollTo: (link: string) => void;
onClick?: (e: React.MouseEvent<HTMLElement>, link: { title: React.ReactNode; href: string }) => void;
}
export default class Anchor extends React.Component<AnchorProps, AnchorState> {
static Link: typeof AnchorLink;
static defaultProps = {
affix: true,
showInkInFixed: false,
getContainer: getDefaultContainer,
};
static childContextTypes = {
antAnchor: PropTypes.object,
};
state = {
activeLink: null,
};
private inkNode: HTMLSpanElement;
// scroll scope's container
private scrollContainer: HTMLElement | Window;
private links: string[] = [];
private scrollEvent: any;
private animating: boolean;
private prefixCls?: string;
getChildContext() {
const antAnchor: AntAnchor = {
registerLink: (link: string) => {
if (!this.links.includes(link)) {
this.links.push(link);
}
},
unregisterLink: (link: string) => {
const index = this.links.indexOf(link);
if (index !== -1) {
this.links.splice(index, 1);
}
},
activeLink: this.state.activeLink,
scrollTo: this.handleScrollTo,
onClick: this.props.onClick,
};
return { antAnchor };
}
componentDidMount() {
const { getContainer } = this.props as AnchorDefaultProps;
this.scrollContainer = getContainer();
this.scrollEvent = addEventListener(this.scrollContainer, 'scroll', this.handleScroll);
this.handleScroll();
}
componentDidUpdate() {
if (this.scrollEvent) {
const { getContainer } = this.props as AnchorDefaultProps;
const currentContainer = getContainer();
if (this.scrollContainer !== currentContainer) {
this.scrollContainer = currentContainer;
this.scrollEvent.remove();
this.scrollEvent = addEventListener(this.scrollContainer, 'scroll', this.handleScroll);
this.handleScroll();
}
}
this.updateInk();
}
componentWillUnmount() {
if (this.scrollEvent) {
this.scrollEvent.remove();
}
}
getCurrentAnchor(offsetTop = 0, bounds = 5): string {
const { getCurrentAnchor } = this.props;
if (typeof getCurrentAnchor === 'function') {
return getCurrentAnchor();
}
const activeLink = '';
if (typeof document === 'undefined') {
return activeLink;
}
const linkSections: Array<Section> = [];
const { getContainer } = this.props as AnchorDefaultProps;
const container = getContainer();
this.links.forEach((link) => {
const sharpLinkMatch = sharpMatcherRegx.exec(link.toString());
if (!sharpLinkMatch) {
return;
}
const target = document.getElementById(sharpLinkMatch[1]);
if (target) {
const top = getOffsetTop(target, container);
if (top < offsetTop + bounds) {
linkSections.push({
link,
top,
});
}
}
});
if (linkSections.length) {
const maxSection = linkSections.reduce((prev, curr) => (curr.top > prev.top ? curr : prev));
return maxSection.link;
}
return '';
}
handleScrollTo = (link: string) => {
const { offsetTop, getContainer, targetOffset } = this.props as AnchorDefaultProps;
this.setCurrentActiveLink(link);
const container = getContainer();
const scrollTop = getScroll(container, true);
const sharpLinkMatch = sharpMatcherRegx.exec(link);
if (!sharpLinkMatch) {
return;
}
const targetElement = document.getElementById(sharpLinkMatch[1]);
if (!targetElement) {
return;
}
const eleOffsetTop = getOffsetTop(targetElement, container);
let y = scrollTop + eleOffsetTop;
y -= targetOffset !== undefined ? targetOffset : offsetTop || 0;
this.animating = true;
scrollTo(y, {
callback: () => {
this.animating = false;
},
getContainer,
});
};
saveInkNode = (node: HTMLSpanElement) => {
this.inkNode = node;
};
setCurrentActiveLink = (link: string) => {
const { activeLink } = this.state;
const { onChange } = this.props;
if (activeLink !== link) {
this.setState({
activeLink: link,
});
if (onChange) {
onChange(link);
}
}
};
handleScroll = () => {
if (this.animating) {
return;
}
const { offsetTop, bounds, targetOffset } = this.props;
const currentActiveLink = this.getCurrentAnchor(targetOffset !== undefined ? targetOffset : offsetTop || 0, bounds);
this.setCurrentActiveLink(currentActiveLink);
};
updateInk = () => {
if (typeof document === 'undefined') {
return;
}
const { prefixCls } = this;
const anchorNode = ReactDOM.findDOMNode(this) as Element;
const linkNode = anchorNode.getElementsByClassName(`${prefixCls}-link-title-active`)[0];
if (linkNode) {
this.inkNode.style.top = `${(linkNode as any).offsetTop + linkNode.clientHeight / 2 - 4.5}px`;
}
};
renderAnchor = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
const {
prefixCls: customizePrefixCls,
className = '',
style,
offsetTop,
affix,
showInkInFixed,
children,
getContainer,
} = this.props;
const { activeLink } = this.state;
const prefixCls = getPrefixCls('anchor', customizePrefixCls);
// To support old version react.
// Have to add prefixCls on the instance.
// https://github.com/facebook/react/issues/12397
this.prefixCls = prefixCls;
const inkClass = classNames(`${prefixCls}-ink-ball`, {
visible: activeLink,
});
const wrapperClass = classNames(className, `${prefixCls}-wrapper`, {
[`${prefixCls}-rtl`]: direction === 'rtl',
});
const anchorClass = classNames(prefixCls, {
fixed: !affix && !showInkInFixed,
});
const wrapperStyle = {
maxHeight: offsetTop ? `calc(100vh - ${offsetTop}px)` : '100vh',
...style,
};
const anchorContent = (
<div className={wrapperClass} style={wrapperStyle}>
<div className={anchorClass}>
<div className={`${prefixCls}-ink`}>
<span className={inkClass} ref={this.saveInkNode} />
</div>
{children}
</div>
</div>
);
return !affix ? (
anchorContent
) : (
<Affix offsetTop={offsetTop} target={getContainer}>
{anchorContent}
</Affix>
);
};
render() {
return <ConfigConsumer>{this.renderAnchor}</ConfigConsumer>;
}
}

View File

@ -0,0 +1,85 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import classNames from 'classnames';
import { AntAnchor } from './Anchor';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
interface AnchorLinkProps {
prefixCls?: string;
href: string;
target?: string;
title: React.ReactNode;
children?: React.ReactNode;
className?: string;
}
class AnchorLink extends React.Component<AnchorLinkProps, any> {
static defaultProps = {
href: '#',
};
static contextTypes = {
antAnchor: PropTypes.object,
};
context: {
antAnchor: AntAnchor;
};
componentDidMount() {
this.context.antAnchor.registerLink(this.props.href);
}
componentDidUpdate({ href: prevHref }: AnchorLinkProps) {
const { href } = this.props;
if (prevHref !== href) {
this.context.antAnchor.unregisterLink(prevHref);
this.context.antAnchor.registerLink(href);
}
}
componentWillUnmount() {
this.context.antAnchor.unregisterLink(this.props.href);
}
handleClick = (e: React.MouseEvent<HTMLElement>) => {
const { scrollTo, onClick } = this.context.antAnchor;
const { href, title } = this.props;
if (onClick) {
onClick(e, { title, href });
}
scrollTo(href);
};
renderAnchorLink = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, href, title, children, className, target } = this.props;
const prefixCls = getPrefixCls('anchor', customizePrefixCls);
const active = this.context.antAnchor.activeLink === href;
const wrapperClassName = classNames(className, `${prefixCls}-link`, {
[`${prefixCls}-link-active`]: active,
});
const titleClassName = classNames(`${prefixCls}-link-title`, {
[`${prefixCls}-link-title-active`]: active,
});
return (
<div className={wrapperClassName}>
<a
className={titleClassName}
href={href}
title={typeof title === 'string' ? title : ''}
target={target}
onClick={this.handleClick}
>
{title}
</a>
{children}
</div>
);
};
render() {
return <ConfigConsumer>{this.renderAnchorLink}</ConfigConsumer>;
}
}
export default AnchorLink;

View File

@ -0,0 +1,336 @@
import React from 'react';
import { mount } from 'enzyme';
import Anchor from '..';
import { spyElementPrototypes } from '../../__tests__/util/domHook';
import { sleep } from '../../../tests/utils';
const { Link } = Anchor;
describe('Anchor Render', () => {
const getBoundingClientRectMock = jest.fn(() => ({
width: 100,
height: 100,
top: 1000,
}));
const getClientRectsMock = jest.fn(() => ({
length: 1,
}));
const headingSpy = spyElementPrototypes(HTMLHeadingElement, {
getBoundingClientRect: getBoundingClientRectMock,
getClientRects: getClientRectsMock,
});
afterAll(() => {
headingSpy.mockRestore();
});
it('Anchor render perfectly', () => {
const wrapper = mount(
<Anchor>
<Link href="#API" title="API" />
</Anchor>,
);
wrapper.find('a[href="#API"]').simulate('click');
wrapper.instance().handleScroll();
expect(wrapper.instance().state).not.toBe(null);
});
it('Anchor render perfectly for complete href - click', () => {
const wrapper = mount(
<Anchor>
<Link href="http://www.example.com/#API" title="API" />
</Anchor>,
);
wrapper.find('a[href="http://www.example.com/#API"]').simulate('click');
expect(wrapper.instance().state.activeLink).toBe('http://www.example.com/#API');
});
it('Anchor render perfectly for complete href - scroll', () => {
let root = document.getElementById('root');
if (!root) {
root = document.createElement('div', { id: 'root' });
root.id = 'root';
document.body.appendChild(root);
}
mount(<div id="API">Hello</div>, { attachTo: root });
const wrapper = mount(
<Anchor>
<Link href="http://www.example.com/#API" title="API" />
</Anchor>,
);
wrapper.instance().handleScroll();
expect(wrapper.instance().state.activeLink).toBe('http://www.example.com/#API');
});
it('Anchor render perfectly for complete href - scrollTo', async () => {
const scrollToSpy = jest.spyOn(window, 'scrollTo');
let root = document.getElementById('root');
if (!root) {
root = document.createElement('div', { id: 'root' });
root.id = 'root';
document.body.appendChild(root);
}
mount(<div id="API">Hello</div>, { attachTo: root });
const wrapper = mount(
<Anchor>
<Link href="##API" title="API" />
</Anchor>,
);
wrapper.instance().handleScrollTo('##API');
expect(wrapper.instance().state.activeLink).toBe('##API');
expect(scrollToSpy).not.toHaveBeenCalled();
await sleep(1000);
expect(scrollToSpy).toHaveBeenCalled();
});
it('should remove listener when unmount', async () => {
const wrapper = mount(
<Anchor>
<Link href="#API" title="API" />
</Anchor>,
);
const removeListenerSpy = jest.spyOn(wrapper.instance().scrollEvent, 'remove');
wrapper.unmount();
expect(removeListenerSpy).toHaveBeenCalled();
});
it('should unregister link when unmount children', async () => {
const wrapper = mount(
<Anchor>
<Link href="#API" title="API" />
</Anchor>,
);
expect(wrapper.instance().links).toEqual(['#API']);
wrapper.setProps({ children: null });
expect(wrapper.instance().links).toEqual([]);
});
it('should update links when link href update', async () => {
let anchorInstance = null;
function AnchorUpdate({ href }) {
return (
<Anchor
ref={c => {
anchorInstance = c;
}}
>
<Link href={href} title="API" />
</Anchor>
);
}
const wrapper = mount(<AnchorUpdate href="#API" />);
expect(anchorInstance.links).toEqual(['#API']);
wrapper.setProps({ href: '#API_1' });
expect(anchorInstance.links).toEqual(['#API_1']);
});
it('Anchor onClick event', () => {
let event;
let link;
const handleClick = (...arg) => {
[event, link] = arg;
};
const href = '#API';
const title = 'API';
const wrapper = mount(
<Anchor onClick={handleClick}>
<Link href={href} title={title} />
</Anchor>,
);
wrapper.find(`a[href="${href}"]`).simulate('click');
wrapper.instance().handleScroll();
expect(event).not.toBe(undefined);
expect(link).toEqual({ href, title });
});
it('Different function returns the same DOM', async () => {
let root = document.getElementById('root');
if (!root) {
root = document.createElement('div', { id: 'root' });
root.id = 'root';
document.body.appendChild(root);
}
mount(<div id="API">Hello</div>, { attachTo: root });
const getContainerA = () => {
return document.getElementById('API');
};
const getContainerB = () => {
return document.getElementById('API');
};
const wrapper = mount(
<Anchor getContainer={getContainerA}>
<Link href="#API" title="API" />
</Anchor>,
);
const removeListenerSpy = jest.spyOn(wrapper.instance().scrollEvent, 'remove');
await sleep(1000);
wrapper.setProps({ getContainer: getContainerB });
expect(removeListenerSpy).not.toHaveBeenCalled();
});
it('Different function returns different DOM', async () => {
let root = document.getElementById('root');
if (!root) {
root = document.createElement('div', { id: 'root' });
root.id = 'root';
document.body.appendChild(root);
}
mount(
<div>
<div id="API1">Hello</div>
<div id="API2">World</div>
</div>,
{ attachTo: root },
);
const getContainerA = () => {
return document.getElementById('API1');
};
const getContainerB = () => {
return document.getElementById('API2');
};
const wrapper = mount(
<Anchor getContainer={getContainerA}>
<Link href="#API1" title="API1" />
<Link href="#API2" title="API2" />
</Anchor>,
);
const removeListenerSpy = jest.spyOn(wrapper.instance().scrollEvent, 'remove');
expect(removeListenerSpy).not.toHaveBeenCalled();
await sleep(1000);
wrapper.setProps({ getContainer: getContainerB });
expect(removeListenerSpy).toHaveBeenCalled();
});
it('Same function returns the same DOM', () => {
let root = document.getElementById('root');
if (!root) {
root = document.createElement('div', { id: 'root' });
root.id = 'root';
document.body.appendChild(root);
}
mount(<div id="API">Hello</div>, { attachTo: root });
const getContainer = () => document.getElementById('API');
const wrapper = mount(
<Anchor getContainer={getContainer}>
<Link href="#API" title="API" />
</Anchor>,
);
wrapper.find('a[href="#API"]').simulate('click');
wrapper.instance().handleScroll();
expect(wrapper.instance().state).not.toBe(null);
});
it('Same function returns different DOM', async () => {
let root = document.getElementById('root');
if (!root) {
root = document.createElement('div', { id: 'root' });
root.id = 'root';
document.body.appendChild(root);
}
mount(
<div>
<div id="API1">Hello</div>
<div id="API2">World</div>
</div>,
{ attachTo: root },
);
const holdContainer = {
container: document.getElementById('API1'),
};
const getContainer = () => {
return holdContainer.container;
};
const wrapper = mount(
<Anchor getContainer={getContainer}>
<Link href="#API1" title="API1" />
<Link href="#API2" title="API2" />
</Anchor>,
);
const removeListenerSpy = jest.spyOn(wrapper.instance().scrollEvent, 'remove');
expect(removeListenerSpy).not.toHaveBeenCalled();
await sleep(1000);
holdContainer.container = document.getElementById('API2');
wrapper.setProps({ 'data-only-trigger-re-render': true });
expect(removeListenerSpy).toHaveBeenCalled();
});
it('Anchor getCurrentAnchor prop', () => {
const getCurrentAnchor = () => '#API2';
const wrapper = mount(
<Anchor getCurrentAnchor={getCurrentAnchor}>
<Link href="#API1" title="API1" />
<Link href="#API2" title="API2" />
</Anchor>,
);
expect(wrapper.instance().state.activeLink).toBe('#API2');
});
it('Anchor targetOffset prop', async () => {
let dateNowMock;
function dataNowMockFn() {
let start = 0;
const handler = () => {
return (start += 1000);
};
return jest.spyOn(Date, 'now').mockImplementation(handler);
}
dateNowMock = dataNowMockFn();
const scrollToSpy = jest.spyOn(window, 'scrollTo');
let root = document.getElementById('root');
if (!root) {
root = document.createElement('div', { id: 'root' });
root.id = 'root';
document.body.appendChild(root);
}
mount(<h1 id="API">Hello</h1>, { attachTo: root });
const wrapper = mount(
<Anchor>
<Link href="#API" title="API" />
</Anchor>,
);
wrapper.instance().handleScrollTo('#API');
await sleep(20);
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000);
dateNowMock = dataNowMockFn();
wrapper.setProps({ offsetTop: 100 });
wrapper.instance().handleScrollTo('#API');
await sleep(20);
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900);
dateNowMock = dataNowMockFn();
wrapper.setProps({ targetOffset: 200 });
wrapper.instance().handleScrollTo('#API');
await sleep(20);
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800);
dateNowMock.mockRestore();
});
it('Anchor onChange prop', async () => {
const onChange = jest.fn();
const wrapper = mount(
<Anchor onChange={onChange}>
<Link href="#API1" title="API1" />
<Link href="#API2" title="API2" />
</Anchor>,
);
expect(onChange).toHaveBeenCalledTimes(1);
wrapper.instance().handleScrollTo('#API2');
expect(onChange).toHaveBeenCalledTimes(2);
expect(onChange).toHaveBeenCalledWith('#API2');
});
});

Some files were not shown because too many files have changed in this diff Show More