├── .npmignore ├── tslint.json ├── server.yml ├── .dawn ├── rc.yml └── pipe.yml ├── .gitignore ├── src ├── IShadowRootOptions.ts ├── index.ts ├── example │ ├── Dialog.tsx │ ├── index.html │ └── index.tsx ├── IShadowViewProps.ts ├── EventNames.ts ├── EventBridge.ts ├── ShadowRoot.ts ├── Hooks.ts └── ShadowView.ts ├── test └── unit │ └── demo.js ├── tsconfig.json ├── package.json ├── README.md ├── .eslintrc.yml └── .eslintrc.json /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules/ 4 | .nyc_output/ 5 | coverage/ -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-dawn" 4 | ] 5 | } -------------------------------------------------------------------------------- /server.yml: -------------------------------------------------------------------------------- 1 | proxy: 2 | rules: 3 | ^/api(.*): 'https://www.aliyun.com/' -------------------------------------------------------------------------------- /.dawn/rc.yml: -------------------------------------------------------------------------------- 1 | server: https://alibaba.github.io/dawn 2 | registry: https://registry.npmjs.com/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules/ 4 | .nyc_output/ 5 | coverage/ 6 | build/ -------------------------------------------------------------------------------- /src/IShadowRootOptions.ts: -------------------------------------------------------------------------------- 1 | export interface IShadowRootOptions { 2 | delegatesFocus?: boolean; 3 | mode?: ShadowRootMode; 4 | } 5 | -------------------------------------------------------------------------------- /test/unit/demo.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | describe('Array', function () { 4 | describe('#indexOf()', function () { 5 | it('should return -1 when the value is not present', function () { 6 | assert.equal(-1, [1, 2, 3].indexOf(4)); 7 | }); 8 | }); 9 | }); -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ShadowView } from "./ShadowView"; 2 | 3 | export * from "./ShadowView"; 4 | export * from "./EventBridge"; 5 | export * from "./EventNames"; 6 | export * from "./ShadowRoot"; 7 | export * from "./IShadowRootOptions"; 8 | export * from "./IShadowViewProps"; 9 | 10 | export default ShadowView; 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./build/", 4 | "sourceMap": true, 5 | "declaration": true, 6 | "noImplicitAny": true, 7 | "module": "commonjs", 8 | "target": "es5", 9 | "jsx": "react", 10 | "noUnusedLocals": true, 11 | "esModuleInterop": true, 12 | "emitDecoratorMetadata": false, 13 | "experimentalDecorators": true, 14 | "lib": [ 15 | "dom", 16 | "es2017" 17 | ], 18 | }, 19 | "include": [ 20 | "./src/**/*.ts", 21 | "./src/**/*.tsx" 22 | ] 23 | } -------------------------------------------------------------------------------- /.dawn/pipe.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - name: pkginfo 3 | 4 | dev: 5 | - name: clean 6 | - name: typescript 7 | - name: webpack 8 | common: 9 | disabled: true 10 | watch: true 11 | entry: ./src/example/*.tsx 12 | template: ./src/example/*.html 13 | - name: server 14 | port: 5007 15 | - name: browser-sync 16 | 17 | build: 18 | - name: clean 19 | - name: shell 20 | script: 21 | - tsc 22 | 23 | test: 24 | - name: tslint 25 | 26 | publish: 27 | - name: shell 28 | script: 29 | - dn test 30 | - dn build 31 | - npm pu --registry=http://registry.npmjs.org -------------------------------------------------------------------------------- /src/example/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | export class Dialog extends React.Component { 5 | root: HTMLElement; 6 | constructor(props: any, ...args: any[]) { 7 | super(props, ...args); 8 | const doc = window.document; 9 | this.root = doc.createElement("div"); 10 | doc.body.appendChild(this.root); 11 | } 12 | 13 | render() { 14 | const { children } = this.props; 15 | return createPortal(
{children}
, this.root); 16 | } 17 | 18 | componentWillUnmount() { 19 | window.document.body.removeChild(this.root); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Shadow DOM 9 | 14 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /src/IShadowViewProps.ts: -------------------------------------------------------------------------------- 1 | import { IShadowRootOptions } from "./IShadowRootOptions"; 2 | 3 | /** 4 | * ShadowView 属性 5 | */ 6 | export interface IShadowViewProps extends IShadowRootOptions { 7 | /** 8 | * 根元素的 tagName 9 | * 默认为 `shadow-view` 10 | */ 11 | tagName?: string; 12 | 13 | /** 14 | * Shadow 容器中的子元素 15 | */ 16 | children?: React.ReactNode; 17 | 18 | /** 19 | * React Ref function 20 | */ 21 | ref?: Function; 22 | 23 | /** 24 | * 顶层元素的 className 25 | */ 26 | className?: string; 27 | 28 | /** 29 | * 顶层元素 style 30 | */ 31 | style?: any; 32 | 33 | /** 34 | * 引入的样式表单 35 | */ 36 | styleSheets?: string[]; 37 | 38 | /** 39 | * 引入的样式文本 40 | */ 41 | styleContent?: string; 42 | 43 | /** 44 | * 显示延时 45 | */ 46 | showDelay?: number; 47 | 48 | /** 49 | * 是否启用隔离,默认为 true 50 | */ 51 | scoped?: boolean; 52 | 53 | /** 54 | * 显示动画持续时间 55 | */ 56 | transitionDuration?: string; 57 | } 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shadow-view", 3 | "version": "4.0.1", 4 | "description": "", 5 | "main": "./build/index.js", 6 | "types": "./build/index.d.ts", 7 | "scripts": { 8 | "test": "dn test && dn build -e prod" 9 | }, 10 | "author": { 11 | "name": "Houfeng", 12 | "email": "admin@xhou.net", 13 | "url": "http://houfeng.net" 14 | }, 15 | "license": "MIT", 16 | "dependencies": { 17 | "@types/node": "^12.6.3", 18 | "@types/react": "^16.8.23", 19 | "@types/react-dom": "^16.8.4", 20 | "react": "^16.8.6", 21 | "react-dom": "^16.8.6" 22 | }, 23 | "devDependencies": { 24 | "dn-middleware-browser-sync": "^0.1.0", 25 | "dn-middleware-clean": "^1.0.2", 26 | "dn-middleware-copy": "^0.2.7", 27 | "dn-middleware-server": "^1.1.3", 28 | "dn-middleware-shell": "^1.1.0", 29 | "dn-middleware-tslint": "^1.1.3", 30 | "dn-middleware-typescript": "^2.0.8", 31 | "dn-middleware-webpack": "^1.11.5", 32 | "typescript": "^3.5.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/example/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | export class ShadowView extends React.Component { 5 | attachShadow = (host: Element) => { 6 | const shadowRoot = host.attachShadow({ mode: "open" }); 7 | [].slice.call(host.children).forEach((child: any) => { 8 | shadowRoot.appendChild(child); 9 | }); 10 | } 11 | render() { 12 | const { children } = this.props; 13 | return
14 | {children} 15 |
; 16 | } 17 | } 18 | 19 | export class App extends React.Component { 20 | state = { message: '...' }; 21 | onBtnClick = () => { 22 | this.setState({ message: 'haha' }); 23 | } 24 | render() { 25 | const { message } = this.state; 26 | return
27 | 28 |
{message}
29 | 30 |
31 | 32 |
33 | } 34 | } 35 | 36 | ReactDOM.render(, document.getElementById("root")); -------------------------------------------------------------------------------- /src/EventNames.ts: -------------------------------------------------------------------------------- 1 | export const BRIDGE_EVENT_NAMES = [ 2 | "abort", 3 | "animationcancel", 4 | "animationend", 5 | "animationiteration", 6 | "auxclick", 7 | "blur", 8 | "change", 9 | "click", 10 | "close", 11 | "contextmenu", 12 | "doubleclick", 13 | "error", 14 | "focus", 15 | "gotpointercapture", 16 | "input", 17 | "keydown", 18 | "keypress", 19 | "keyup", 20 | "load", 21 | "loadend", 22 | "loadstart", 23 | "lostpointercapture", 24 | "mousedown", 25 | "mousemove", 26 | "mouseout", 27 | "mouseover", 28 | "mouseup", 29 | "pointercancel", 30 | "pointerdown", 31 | "pointerenter", 32 | "pointerleave", 33 | "pointermove", 34 | "pointerout", 35 | "pointerover", 36 | "pointerup", 37 | "reset", 38 | "resize", 39 | "scroll", 40 | "select", 41 | "selectionchange", 42 | "selectstart", 43 | "submit", 44 | "touchcancel", 45 | "touchmove", 46 | "touchstart", 47 | "transitioncancel", 48 | "transitionend", 49 | "drag", 50 | "dragend", 51 | "dragenter", 52 | "dragexit", 53 | "dragleave", 54 | "dragover", 55 | "dragstart", 56 | "drop", 57 | "focusout" 58 | ]; 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ShadowView 2 | 3 | ShadowView 是一个写好的可开箱即用的面向 React 的 Shadow DOM 容器组件,利用 ShadowView 可以像普通组件一样方便的在 React 应用中创建启用 Shadow DOM 的容器元素。 4 | 5 | ShadowView 目前完整兼容支持 React 15/16,组件的「事件处理、组件渲染更新」等行为在两个版中都是一致的。 6 | 7 | 8 | ## 安装组件 9 | 10 | ```bash 11 | npm i shadow-view --save 12 | ``` 13 | 14 | 15 | ## 使用组件 16 | 17 | ```javascript 18 | import * as React from "react"; 19 | import * as ReactDOM from "react-dom"; 20 | import { ShadowView } from "shadow-view"; 21 | 22 | function App() { 23 | return ( 24 | 31 | 32 |
这是一个测试
33 |
34 | ); 35 | } 36 | 37 | ReactDOM.render(, document.getElementById('root')); 38 | ``` 39 | 40 | 41 | ## 组件属性 42 | 43 | | **属性名** | **类型** | **说明** | 44 | | :--- | :--- | :--- | 45 | | className | string | 组件自身 className | 46 | | style | any | 组件自身的内联样式 | 47 | | styleContent | string | 作用于 ShadowView 内部的样式 | 48 | | styleSheets | string[] | 作用于 ShadowView 内部的外联样式表 | 49 | | scoped | boolean | 是否开始隔离,默认为 true | 50 | | tagName | string | 外层容器 tagName,默认为 shadow-view | 51 | -------------------------------------------------------------------------------- /src/EventBridge.ts: -------------------------------------------------------------------------------- 1 | import { BRIDGE_EVENT_NAMES } from "./EventNames"; 2 | 3 | export function bridge(fromNode: Node, toNode: Node) { 4 | if (!fromNode || !toNode || fromNode === toNode) return; 5 | const define = Object.defineProperty; 6 | BRIDGE_EVENT_NAMES.forEach(eventName => { 7 | fromNode.addEventListener(eventName, (fromEvent: any) => { 8 | fromEvent.stopPropagation(); 9 | const Event = fromEvent.constructor; 10 | const toEvent = new Event(eventName, { 11 | ...fromEvent, 12 | bubbles: true, 13 | cancelable: true, 14 | composed: true 15 | }); 16 | const { 17 | path = [], 18 | target = path[0], 19 | srcElement = path[0], 20 | toElement = path[0], 21 | preventDefault 22 | } = fromEvent; 23 | define(toEvent, "path", { get: () => path }); 24 | define(toEvent, "target", { get: () => target }); 25 | define(toEvent, "srcElement", { get: () => srcElement }); 26 | define(toEvent, "toElement", { get: () => toElement }); 27 | define(toEvent, "preventDefault", { 28 | value: () => { 29 | preventDefault.call(fromEvent); 30 | return preventDefault.call(toEvent); 31 | } 32 | }); 33 | toNode.dispatchEvent(toEvent); 34 | }); 35 | }); 36 | } 37 | 38 | export function bridgeShadowRoot(shadowRoot: ShadowRoot) { 39 | bridge(shadowRoot, shadowRoot.host); 40 | } 41 | 42 | export function bridgeShadowHost(shadowHost: Element) { 43 | bridge(shadowHost.shadowRoot, shadowHost); 44 | } 45 | -------------------------------------------------------------------------------- /src/ShadowRoot.ts: -------------------------------------------------------------------------------- 1 | import { bridgeShadowRoot } from "./EventBridge"; 2 | import { IShadowRootOptions } from "./IShadowRootOptions"; 3 | import "./Hooks"; 4 | 5 | export const supportShadow = "attachShadow" in document.createElement("div"); 6 | 7 | export function attachShadow(host: HTMLElement, optinos: IShadowRootOptions) { 8 | if (!host || !supportShadow) return (host as any) as ShadowRoot; 9 | const { mode = "open", delegatesFocus } = { ...optinos }; 10 | const shadowRoot = host.attachShadow({ mode, delegatesFocus }); 11 | const define = Object.defineProperty; 12 | define(shadowRoot, "style", { get: () => host.style }); 13 | define(shadowRoot, "parentNode", { get: () => host.parentNode }); 14 | define(shadowRoot, "parentElement", { get: () => host.parentElement }); 15 | define(shadowRoot, "offsetParent", { get: () => host.offsetParent }); 16 | define(shadowRoot, "offsetTop", { get: () => host.offsetTop }); 17 | define(shadowRoot, "offsetLeft", { get: () => host.offsetLeft }); 18 | define(shadowRoot, "offsetWidth", { get: () => host.offsetWidth }); 19 | define(shadowRoot, "offsetHeight", { get: () => host.offsetHeight }); 20 | define(shadowRoot, "getAttribute", { 21 | get: () => host.getAttribute.bind(host) 22 | }); 23 | define(shadowRoot, "getBoundingClientRect", { 24 | get: () => host.getBoundingClientRect.bind(host) 25 | }); 26 | define(shadowRoot, "getClientRects", { 27 | get: () => host.getClientRects.bind(host) 28 | }); 29 | define(host, "insertBefore", { 30 | get: () => shadowRoot.insertBefore.bind(shadowRoot) 31 | }); 32 | define(host, "appendChild", { 33 | get: () => shadowRoot.appendChild.bind(shadowRoot) 34 | }); 35 | bridgeShadowRoot(shadowRoot); 36 | return shadowRoot; 37 | } 38 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parser: babel-eslint 2 | plugins: 3 | - react 4 | - html 5 | env: 6 | es6: true 7 | browser: true 8 | commonjs: true 9 | extends: 10 | - 'eslint:recommended' 11 | ecmaFeatures: 12 | jsx: true 13 | modules: true 14 | experimentalObjectRestSpread: true 15 | parserOptions: 16 | ecmaVersion: 6 17 | sourceType": module 18 | ecmaFeatures: 19 | jsx: true 20 | modules: true 21 | experimentalObjectRestSpread: true 22 | rules: 23 | strict: 24 | - 0 25 | indent: 26 | - 2 27 | - 2 28 | - SwitchCase: 1 29 | quotes: 30 | - 2 31 | - single 32 | linebreak-style: 33 | - 2 34 | - unix 35 | semi: 36 | - 2 37 | - always 38 | no-multi-spaces: 39 | - 2 40 | no-self-compare: 41 | - 2 42 | max-depth: 43 | - 2 44 | - 4 45 | max-nested-callbacks: 46 | - 2 47 | - 4 48 | max-params: 49 | - 2 50 | - 4 51 | max-statements: 52 | - 2 53 | - 25 54 | max-statements-per-line: 55 | - 2 56 | max-len: 57 | - 2 58 | - 120 59 | multiline-ternary: 60 | - 0 61 | callback-return: 62 | - 2 63 | handle-callback-err: 64 | - 2 65 | array-bracket-spacing: 66 | - 2 67 | no-const-assign: 68 | - 2 69 | no-return-assign: 70 | - 0 71 | no-inner-declarations: 72 | - 2 73 | no-var: 74 | - 2 75 | no-console: 76 | - 1 77 | no-lonely-if: 78 | - 2 79 | require-jsdoc: 80 | - 0 81 | - require: 82 | FunctionDeclaration: true 83 | MethodDefinition: true 84 | ClassDeclaration: true 85 | valid-jsdoc: 86 | - 2 87 | comma-dangle: 88 | - 2 89 | - never 90 | no-undef: 91 | - 2 92 | react/jsx-uses-react: 93 | - 2 94 | react/jsx-uses-vars: 95 | - 2 96 | react/jsx-no-undef: 97 | - 2 98 | -------------------------------------------------------------------------------- /src/Hooks.ts: -------------------------------------------------------------------------------- 1 | export function getRootNode(node: Node) { 2 | let root: any = node; 3 | while (root && !root.host && root.parentNode) root = root.parentNode; 4 | return root; 5 | } 6 | 7 | const { removeChild } = Node.prototype; 8 | Node.prototype.removeChild = function(child) { 9 | if (!removeChild) return; 10 | try { 11 | return removeChild.call(this, child); 12 | } catch (err) { 13 | return removeChild.call(child.parentNode, child); 14 | } 15 | }; 16 | 17 | const { contains } = Node.prototype; 18 | Node.prototype.contains = function(otherNode) { 19 | if (!otherNode || !contains) return false; 20 | const root = getRootNode(this); 21 | const otherRoot = getRootNode(otherNode); 22 | if (!root || !otherRoot) return false; 23 | try { 24 | return root === otherRoot 25 | ? contains.call(this, otherNode) 26 | : contains.call(this, otherRoot.host || otherNode); 27 | } catch (err) { 28 | console.error(err); 29 | return false; 30 | } 31 | }; 32 | 33 | const { compareDocumentPosition } = Node.prototype; 34 | Node.prototype.compareDocumentPosition = function(otherNode) { 35 | if (!otherNode || !compareDocumentPosition) return false; 36 | const root = getRootNode(this); 37 | const otherRoot = getRootNode(otherNode); 38 | if (!root || !otherRoot) return false; 39 | try { 40 | return root === otherRoot 41 | ? compareDocumentPosition.call(this, otherNode) 42 | : compareDocumentPosition.call(this, otherRoot.host || otherNode); 43 | } catch (err) { 44 | console.error(err); 45 | return false; 46 | } 47 | }; 48 | 49 | const parentElementDescriptor = Object.getOwnPropertyDescriptor( 50 | Node.prototype, 51 | "parentElement" 52 | ); 53 | Object.defineProperty(Node.prototype, "parentElement", { 54 | configurable: true, 55 | enumerable: true, 56 | get() { 57 | const parentElement = parentElementDescriptor.get.call(this); 58 | if (parentElement) return parentElement; 59 | const parentNode = this.parentNode; 60 | if (!parentNode) return null; 61 | if (parentNode.constructor !== (window as any).ShadowRoot) return null; 62 | return parentNode.parentElement || parentNode; 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react", 5 | "html" 6 | ], 7 | "env": { 8 | "es6": true, 9 | "browser": true, 10 | "commonjs": true 11 | }, 12 | "extends": [ 13 | "eslint:recommended" 14 | ], 15 | "ecmaFeatures": { 16 | "jsx": true, 17 | "modules": true, 18 | "experimentalObjectRestSpread": true 19 | }, 20 | "parserOptions": { 21 | "ecmaVersion": 6, 22 | "sourceType\"": "module", 23 | "ecmaFeatures": { 24 | "jsx": true, 25 | "modules": true, 26 | "experimentalObjectRestSpread": true 27 | } 28 | }, 29 | "rules": { 30 | "strict": [ 31 | 0 32 | ], 33 | "indent": [ 34 | 2, 35 | 2, 36 | { 37 | "SwitchCase": 1 38 | } 39 | ], 40 | "quotes": [ 41 | 2, 42 | "single" 43 | ], 44 | "linebreak-style": [ 45 | 2, 46 | "unix" 47 | ], 48 | "semi": [ 49 | 2, 50 | "always" 51 | ], 52 | "no-multi-spaces": [ 53 | 2 54 | ], 55 | "no-self-compare": [ 56 | 2 57 | ], 58 | "max-depth": [ 59 | 2, 60 | 4 61 | ], 62 | "max-nested-callbacks": [ 63 | 2, 64 | 4 65 | ], 66 | "max-params": [ 67 | 2, 68 | 4 69 | ], 70 | "max-statements": [ 71 | 2, 72 | 25 73 | ], 74 | "max-statements-per-line": [ 75 | 2 76 | ], 77 | "max-len": [ 78 | 2, 79 | 120 80 | ], 81 | "multiline-ternary": [ 82 | 0 83 | ], 84 | "callback-return": [ 85 | 2 86 | ], 87 | "handle-callback-err": [ 88 | 2 89 | ], 90 | "array-bracket-spacing": [ 91 | 2 92 | ], 93 | "no-const-assign": [ 94 | 2 95 | ], 96 | "no-return-assign": [ 97 | 0 98 | ], 99 | "no-inner-declarations": [ 100 | 2 101 | ], 102 | "no-var": [ 103 | 2 104 | ], 105 | "no-console": [ 106 | 1 107 | ], 108 | "no-lonely-if": [ 109 | 2 110 | ], 111 | "require-jsdoc": [ 112 | 0, 113 | { 114 | "require": { 115 | "FunctionDeclaration": true, 116 | "MethodDefinition": true, 117 | "ClassDeclaration": true 118 | } 119 | } 120 | ], 121 | "valid-jsdoc": [ 122 | 2 123 | ], 124 | "comma-dangle": [ 125 | 2, 126 | "never" 127 | ], 128 | "no-undef": [ 129 | 2 130 | ], 131 | "react/jsx-uses-react": [ 132 | 2 133 | ], 134 | "react/jsx-uses-vars": [ 135 | 2 136 | ], 137 | "react/jsx-no-undef": [ 138 | 2 139 | ] 140 | } 141 | } -------------------------------------------------------------------------------- /src/ShadowView.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { attachShadow, supportShadow } from "./ShadowRoot"; 3 | import { IShadowViewProps } from "./IShadowViewProps"; 4 | 5 | /** 6 | * ShadowView 创建一个启用了 Shadow DOM 的容器 7 | */ 8 | export class ShadowView extends React.Component { 9 | /** 10 | * ShadowRoot 11 | */ 12 | public shadowRoot: ShadowRoot; 13 | 14 | /** 15 | * DOM Root 16 | */ 17 | public root: HTMLElement; 18 | 19 | /** 20 | * 原始的 visibility 属性值 21 | */ 22 | private originVisibility: string; 23 | 24 | /** 25 | * 渲染组件内容 26 | */ 27 | public render() { 28 | const { tagName = "shadow-view", children, className, style } = this.props; 29 | const props = { className, style, ref: this.onRef }; 30 | const styleElement = this.renderStyle(); 31 | return React.createElement(tagName, props, children, styleElement); 32 | } 33 | 34 | /** 35 | * 在执行 ref 函数时 36 | */ 37 | private onRef = (root: HTMLElement) => { 38 | const { ref } = this.props; 39 | this.root = root; 40 | this.hideRoot(); 41 | this.attachShadow(); 42 | if (typeof ref === "function") ref(root); 43 | else if (typeof ref === "string") (this)[ref] = root; 44 | }; 45 | 46 | /** 47 | * 渲染局部作用域的样式 48 | */ 49 | private renderStyle() { 50 | const { styleContent, styleSheets = [] } = this.props; 51 | const styleBuffer = [ 52 | ...styleSheets.map(url => `@import url("${url}")`), 53 | ...(styleContent ? [styleContent] : []) 54 | ]; 55 | return React.createElement( 56 | "style", 57 | { key: "style" }, 58 | styleBuffer.join(";") 59 | ); 60 | } 61 | 62 | /** 63 | * 在组件挂载时 64 | */ 65 | componentDidMount() { 66 | const { showDelay = 16 } = this.props; 67 | setTimeout(this.checkRootVisibility, showDelay); 68 | } 69 | 70 | /** 71 | * 启用 Shadow DOM 72 | */ 73 | private attachShadow = () => { 74 | if (this.props.scoped === false) return; 75 | if (!supportShadow || !this.root || !this.root.children) return; 76 | const children = [].slice.call(this.root.children); 77 | const { mode = "open", delegatesFocus } = this.props; 78 | this.shadowRoot = attachShadow(this.root, { mode, delegatesFocus }); 79 | children.forEach((child: HTMLElement) => { 80 | this.shadowRoot.appendChild(child); 81 | }); 82 | }; 83 | 84 | /** 85 | * 隐藏根元素 86 | */ 87 | private hideRoot = () => { 88 | if (!this.root || !this.root.style) return; 89 | this.originVisibility = this.root.style.opacity; 90 | this.root.style.opacity = "0"; 91 | }; 92 | 93 | /** 94 | * 显示根元素 95 | */ 96 | private showRoot = () => { 97 | if (!this.root || !this.root.style) return; 98 | const { transitionDuration } = this.props; 99 | this.root.style.transitionDuration = transitionDuration || ".3s"; 100 | this.root.style.opacity = this.originVisibility; 101 | }; 102 | 103 | /** 104 | * 检查样式加载状态 105 | */ 106 | private checkRootVisibility = () => { 107 | if (!this.shadowRoot || !this.shadowRoot.styleSheets) { 108 | return this.showRoot(); 109 | } 110 | const style = this.shadowRoot.styleSheets[0] as any; 111 | if (!style) return this.showRoot(); 112 | const rules = [].slice.call(style.rules || style.cssRules || []); 113 | if (rules.length < 1) return this.showRoot(); 114 | const pending = rules.some((rule: any) => { 115 | return !(rule.styleSheet || rule.href === "") && !rule.style; 116 | }); 117 | return pending ? setTimeout(this.checkRootVisibility, 16) : this.showRoot(); 118 | }; 119 | } 120 | --------------------------------------------------------------------------------