mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-01-13 01:21:58 +00:00
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:
commit
9bf2e3b4ef
3
.gitignore
vendored
3
.gitignore
vendored
@ -100,3 +100,6 @@ typings/
|
||||
|
||||
# mac config files
|
||||
.DS_Store
|
||||
|
||||
# codealike
|
||||
codealike.json
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 || '',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -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));
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
@ -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,
|
||||
];
|
||||
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export default function preProcessHandler(documentation: any, path: any) {
|
||||
documentation.set('meta', path.__meta);
|
||||
}
|
||||
@ -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;
|
||||
}, []);
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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 };
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
18
packages/material-parser/src/parse/utils/cache.ts
Normal file
18
packages/material-parser/src/parse/utils/cache.ts
Normal 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);
|
||||
}
|
||||
49
packages/material-parser/src/parse/utils/flowUtilityTypes.ts
Normal file
49
packages/material-parser/src/parse/utils/flowUtilityTypes.ts
Normal 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 };
|
||||
@ -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 };
|
||||
@ -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),
|
||||
};
|
||||
}
|
||||
14
packages/material-parser/src/parse/utils/getName.ts
Normal file
14
packages/material-parser/src/parse/utils/getName.ts
Normal 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;
|
||||
}
|
||||
7
packages/material-parser/src/parse/utils/getRoot.ts
Normal file
7
packages/material-parser/src/parse/utils/getRoot.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default function getRoot(path: any) {
|
||||
let root = path.parent;
|
||||
while (root.parent) {
|
||||
root = root.parent;
|
||||
}
|
||||
return root.node;
|
||||
}
|
||||
355
packages/material-parser/src/parse/utils/getTSType.js
Normal file
355
packages/material-parser/src/parse/utils/getTSType.js
Normal 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;
|
||||
}
|
||||
19
packages/material-parser/src/parse/utils/makeProxy.ts
Normal file
19
packages/material-parser/src/parse/utils/makeProxy.ts
Normal 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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
14
packages/material-parser/src/types/Meta.ts
Normal file
14
packages/material-parser/src/types/Meta.ts
Normal 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
Binary file not shown.
@ -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",
|
||||
]
|
||||
`;
|
||||
21
packages/material-parser/test/fixtures/antd-component/components/__tests__/index.test.js
vendored
Normal file
21
packages/material-parser/test/fixtures/antd-component/components/__tests__/index.test.js
vendored
Normal 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();
|
||||
});
|
||||
});
|
||||
71
packages/material-parser/test/fixtures/antd-component/components/__tests__/util/domHook.ts
vendored
Normal file
71
packages/material-parser/test/fixtures/antd-component/components/__tests__/util/domHook.ts
vendored
Normal 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,
|
||||
});
|
||||
}
|
||||
13
packages/material-parser/test/fixtures/antd-component/components/_util/__tests__/easings.test.js
vendored
Normal file
13
packages/material-parser/test/fixtures/antd-component/components/_util/__tests__/easings.test.js
vendored
Normal 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]);
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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\\""`);
|
||||
});
|
||||
});
|
||||
206
packages/material-parser/test/fixtures/antd-component/components/_util/__tests__/util.test.js
vendored
Normal file
206
packages/material-parser/test/fixtures/antd-component/components/_util/__tests__/util.test.js
vendored
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
107
packages/material-parser/test/fixtures/antd-component/components/_util/__tests__/wave.test.js
vendored
Normal file
107
packages/material-parser/test/fixtures/antd-component/components/_util/__tests__/wave.test.js
vendored
Normal 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();
|
||||
});
|
||||
});
|
||||
22
packages/material-parser/test/fixtures/antd-component/components/_util/colors.ts
vendored
Normal file
22
packages/material-parser/test/fixtures/antd-component/components/_util/colors.ts
vendored
Normal 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>;
|
||||
9
packages/material-parser/test/fixtures/antd-component/components/_util/easings.ts
vendored
Normal file
9
packages/material-parser/test/fixtures/antd-component/components/_util/easings.ts
vendored
Normal 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;
|
||||
}
|
||||
11
packages/material-parser/test/fixtures/antd-component/components/_util/getDataOrAriaProps.ts
vendored
Normal file
11
packages/material-parser/test/fixtures/antd-component/components/_util/getDataOrAriaProps.ts
vendored
Normal 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;
|
||||
}, {});
|
||||
}
|
||||
18
packages/material-parser/test/fixtures/antd-component/components/_util/getRenderPropValue.ts
vendored
Normal file
18
packages/material-parser/test/fixtures/antd-component/components/_util/getRenderPropValue.ts
vendored
Normal 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;
|
||||
};
|
||||
27
packages/material-parser/test/fixtures/antd-component/components/_util/getScroll.tsx
vendored
Normal file
27
packages/material-parser/test/fixtures/antd-component/components/_util/getScroll.tsx
vendored
Normal 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;
|
||||
}
|
||||
5
packages/material-parser/test/fixtures/antd-component/components/_util/interopDefault.ts
vendored
Normal file
5
packages/material-parser/test/fixtures/antd-component/components/_util/interopDefault.ts
vendored
Normal 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;
|
||||
}
|
||||
5
packages/material-parser/test/fixtures/antd-component/components/_util/isNumeric.ts
vendored
Normal file
5
packages/material-parser/test/fixtures/antd-component/components/_util/isNumeric.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
const isNumeric = (value: any): boolean => {
|
||||
return !isNaN(parseFloat(value)) && isFinite(value);
|
||||
};
|
||||
|
||||
export default isNumeric;
|
||||
40
packages/material-parser/test/fixtures/antd-component/components/_util/motion.tsx
vendored
Normal file
40
packages/material-parser/test/fixtures/antd-component/components/_util/motion.tsx
vendored
Normal 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;
|
||||
54
packages/material-parser/test/fixtures/antd-component/components/_util/openAnimation.tsx
vendored
Normal file
54
packages/material-parser/test/fixtures/antd-component/components/_util/openAnimation.tsx
vendored
Normal 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;
|
||||
38
packages/material-parser/test/fixtures/antd-component/components/_util/raf.ts
vendored
Normal file
38
packages/material-parser/test/fixtures/antd-component/components/_util/raf.ts
vendored
Normal 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
|
||||
8
packages/material-parser/test/fixtures/antd-component/components/_util/reactNode.ts
vendored
Normal file
8
packages/material-parser/test/fixtures/antd-component/components/_util/reactNode.ts
vendored
Normal 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);
|
||||
}
|
||||
17
packages/material-parser/test/fixtures/antd-component/components/_util/ref.ts
vendored
Normal file
17
packages/material-parser/test/fixtures/antd-component/components/_util/ref.ts
vendored
Normal 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);
|
||||
});
|
||||
};
|
||||
}
|
||||
87
packages/material-parser/test/fixtures/antd-component/components/_util/responsiveObserve.ts
vendored
Normal file
87
packages/material-parser/test/fixtures/antd-component/components/_util/responsiveObserve.ts
vendored
Normal 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;
|
||||
39
packages/material-parser/test/fixtures/antd-component/components/_util/scrollTo.ts
vendored
Normal file
39
packages/material-parser/test/fixtures/antd-component/components/_util/scrollTo.ts
vendored
Normal 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);
|
||||
}
|
||||
13
packages/material-parser/test/fixtures/antd-component/components/_util/styleChecker.tsx
vendored
Normal file
13
packages/material-parser/test/fixtures/antd-component/components/_util/styleChecker.tsx
vendored
Normal 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;
|
||||
@ -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;
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
82
packages/material-parser/test/fixtures/antd-component/components/_util/transButton.tsx
vendored
Normal file
82
packages/material-parser/test/fixtures/antd-component/components/_util/transButton.tsx
vendored
Normal 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;
|
||||
16
packages/material-parser/test/fixtures/antd-component/components/_util/type.ts
vendored
Normal file
16
packages/material-parser/test/fixtures/antd-component/components/_util/type.ts
vendored
Normal 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 & {});
|
||||
@ -0,0 +1,5 @@
|
||||
export default class UnreachableException {
|
||||
constructor(value: never) {
|
||||
return new Error(`unreachable case: ${JSON.stringify(value)}`);
|
||||
}
|
||||
}
|
||||
18
packages/material-parser/test/fixtures/antd-component/components/_util/usePatchElement.tsx
vendored
Normal file
18
packages/material-parser/test/fixtures/antd-component/components/_util/usePatchElement.tsx
vendored
Normal 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];
|
||||
}
|
||||
7
packages/material-parser/test/fixtures/antd-component/components/_util/warning.ts
vendored
Normal file
7
packages/material-parser/test/fixtures/antd-component/components/_util/warning.ts
vendored
Normal 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}`);
|
||||
};
|
||||
195
packages/material-parser/test/fixtures/antd-component/components/_util/wave.tsx
vendored
Normal file
195
packages/material-parser/test/fixtures/antd-component/components/_util/wave.tsx
vendored
Normal 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>;
|
||||
}
|
||||
}
|
||||
209
packages/material-parser/test/fixtures/antd-component/components/affix/__tests__/Affix.test.tsx
vendored
Normal file
209
packages/material-parser/test/fixtures/antd-component/components/affix/__tests__/Affix.test.tsx
vendored
Normal 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);
|
||||
});
|
||||
});
|
||||
@ -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>
|
||||
`;
|
||||
@ -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>
|
||||
`;
|
||||
@ -0,0 +1,3 @@
|
||||
import demoTest from '../../../tests/shared/demoTest';
|
||||
|
||||
demoTest('affix');
|
||||
42
packages/material-parser/test/fixtures/antd-component/components/affix/demo/basic.md
vendored
Normal file
42
packages/material-parser/test/fixtures/antd-component/components/affix/demo/basic.md
vendored
Normal 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);
|
||||
```
|
||||
39
packages/material-parser/test/fixtures/antd-component/components/affix/demo/debug.md
vendored
Normal file
39
packages/material-parser/test/fixtures/antd-component/components/affix/demo/debug.md
vendored
Normal 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);
|
||||
```
|
||||
25
packages/material-parser/test/fixtures/antd-component/components/affix/demo/on-change.md
vendored
Normal file
25
packages/material-parser/test/fixtures/antd-component/components/affix/demo/on-change.md
vendored
Normal 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,
|
||||
);
|
||||
```
|
||||
46
packages/material-parser/test/fixtures/antd-component/components/affix/demo/target.md
vendored
Normal file
46
packages/material-parser/test/fixtures/antd-component/components/affix/demo/target.md
vendored
Normal 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>
|
||||
36
packages/material-parser/test/fixtures/antd-component/components/affix/index.en-US.md
vendored
Normal file
36
packages/material-parser/test/fixtures/antd-component/components/affix/index.en-US.md
vendored
Normal 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)
|
||||
292
packages/material-parser/test/fixtures/antd-component/components/affix/index.tsx
vendored
Normal file
292
packages/material-parser/test/fixtures/antd-component/components/affix/index.tsx
vendored
Normal 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;
|
||||
37
packages/material-parser/test/fixtures/antd-component/components/affix/index.zh-CN.md
vendored
Normal file
37
packages/material-parser/test/fixtures/antd-component/components/affix/index.zh-CN.md
vendored
Normal 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)
|
||||
6
packages/material-parser/test/fixtures/antd-component/components/affix/style/index.less
vendored
Normal file
6
packages/material-parser/test/fixtures/antd-component/components/affix/style/index.less
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
@import '../../style/themes/index';
|
||||
|
||||
.@{ant-prefix}-affix {
|
||||
position: fixed;
|
||||
z-index: @zindex-affix;
|
||||
}
|
||||
2
packages/material-parser/test/fixtures/antd-component/components/affix/style/index.tsx
vendored
Normal file
2
packages/material-parser/test/fixtures/antd-component/components/affix/style/index.tsx
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
import '../../style/index.less';
|
||||
import './index.less';
|
||||
106
packages/material-parser/test/fixtures/antd-component/components/affix/utils.ts
vendored
Normal file
106
packages/material-parser/test/fixtures/antd-component/components/affix/utils.ts
vendored
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
42
packages/material-parser/test/fixtures/antd-component/components/alert/ErrorBoundary.tsx
vendored
Normal file
42
packages/material-parser/test/fixtures/antd-component/components/alert/ErrorBoundary.tsx
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
`;
|
||||
@ -0,0 +1,3 @@
|
||||
import demoTest from '../../../tests/shared/demoTest';
|
||||
|
||||
demoTest('alert');
|
||||
115
packages/material-parser/test/fixtures/antd-component/components/alert/__tests__/index.test.js
vendored
Normal file
115
packages/material-parser/test/fixtures/antd-component/components/alert/__tests__/index.test.js
vendored
Normal 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();
|
||||
});
|
||||
});
|
||||
36
packages/material-parser/test/fixtures/antd-component/components/alert/demo/banner.md
vendored
Normal file
36
packages/material-parser/test/fixtures/antd-component/components/alert/demo/banner.md
vendored
Normal 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,
|
||||
);
|
||||
```
|
||||
26
packages/material-parser/test/fixtures/antd-component/components/alert/demo/basic.md
vendored
Normal file
26
packages/material-parser/test/fixtures/antd-component/components/alert/demo/basic.md
vendored
Normal 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>
|
||||
41
packages/material-parser/test/fixtures/antd-component/components/alert/demo/closable.md
vendored
Normal file
41
packages/material-parser/test/fixtures/antd-component/components/alert/demo/closable.md
vendored
Normal 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,
|
||||
);
|
||||
```
|
||||
20
packages/material-parser/test/fixtures/antd-component/components/alert/demo/close-text.md
vendored
Normal file
20
packages/material-parser/test/fixtures/antd-component/components/alert/demo/close-text.md
vendored
Normal 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);
|
||||
```
|
||||
61
packages/material-parser/test/fixtures/antd-component/components/alert/demo/custom-icon.md
vendored
Normal file
61
packages/material-parser/test/fixtures/antd-component/components/alert/demo/custom-icon.md
vendored
Normal 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,
|
||||
);
|
||||
```
|
||||
44
packages/material-parser/test/fixtures/antd-component/components/alert/demo/description.md
vendored
Normal file
44
packages/material-parser/test/fixtures/antd-component/components/alert/demo/description.md
vendored
Normal 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,
|
||||
);
|
||||
```
|
||||
43
packages/material-parser/test/fixtures/antd-component/components/alert/demo/error-boundary.md
vendored
Normal file
43
packages/material-parser/test/fixtures/antd-component/components/alert/demo/error-boundary.md
vendored
Normal 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,
|
||||
);
|
||||
```
|
||||
52
packages/material-parser/test/fixtures/antd-component/components/alert/demo/icon.md
vendored
Normal file
52
packages/material-parser/test/fixtures/antd-component/components/alert/demo/icon.md
vendored
Normal 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,
|
||||
);
|
||||
```
|
||||
34
packages/material-parser/test/fixtures/antd-component/components/alert/demo/loop-banner.md
vendored
Normal file
34
packages/material-parser/test/fixtures/antd-component/components/alert/demo/loop-banner.md
vendored
Normal 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,
|
||||
);
|
||||
```
|
||||
36
packages/material-parser/test/fixtures/antd-component/components/alert/demo/smooth-closed.md
vendored
Normal file
36
packages/material-parser/test/fixtures/antd-component/components/alert/demo/smooth-closed.md
vendored
Normal 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);
|
||||
```
|
||||
28
packages/material-parser/test/fixtures/antd-component/components/alert/demo/style.md
vendored
Normal file
28
packages/material-parser/test/fixtures/antd-component/components/alert/demo/style.md
vendored
Normal 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,
|
||||
);
|
||||
```
|
||||
34
packages/material-parser/test/fixtures/antd-component/components/alert/index.en-US.md
vendored
Normal file
34
packages/material-parser/test/fixtures/antd-component/components/alert/index.en-US.md
vendored
Normal 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 }}` | |
|
||||
203
packages/material-parser/test/fixtures/antd-component/components/alert/index.tsx
vendored
Executable file
203
packages/material-parser/test/fixtures/antd-component/components/alert/index.tsx
vendored
Executable 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>;
|
||||
}
|
||||
}
|
||||
35
packages/material-parser/test/fixtures/antd-component/components/alert/index.zh-CN.md
vendored
Normal file
35
packages/material-parser/test/fixtures/antd-component/components/alert/index.zh-CN.md
vendored
Normal 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 }}` | |
|
||||
191
packages/material-parser/test/fixtures/antd-component/components/alert/style/index.less
vendored
Normal file
191
packages/material-parser/test/fixtures/antd-component/components/alert/style/index.less
vendored
Normal 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';
|
||||
2
packages/material-parser/test/fixtures/antd-component/components/alert/style/index.tsx
vendored
Normal file
2
packages/material-parser/test/fixtures/antd-component/components/alert/style/index.tsx
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
import '../../style/index.less';
|
||||
import './index.less';
|
||||
58
packages/material-parser/test/fixtures/antd-component/components/alert/style/rtl.less
vendored
Normal file
58
packages/material-parser/test/fixtures/antd-component/components/alert/style/rtl.less
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
328
packages/material-parser/test/fixtures/antd-component/components/anchor/Anchor.tsx
vendored
Normal file
328
packages/material-parser/test/fixtures/antd-component/components/anchor/Anchor.tsx
vendored
Normal 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>;
|
||||
}
|
||||
}
|
||||
85
packages/material-parser/test/fixtures/antd-component/components/anchor/AnchorLink.tsx
vendored
Normal file
85
packages/material-parser/test/fixtures/antd-component/components/anchor/AnchorLink.tsx
vendored
Normal 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;
|
||||
336
packages/material-parser/test/fixtures/antd-component/components/anchor/__tests__/Anchor.test.js
vendored
Normal file
336
packages/material-parser/test/fixtures/antd-component/components/anchor/__tests__/Anchor.test.js
vendored
Normal 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
Loading…
x
Reference in New Issue
Block a user