├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── docs ├── bundle.js └── index.html ├── package-lock.json ├── package.json ├── src ├── docs │ ├── demos │ │ ├── Basic.js │ │ ├── BasicFunctional.js │ │ ├── Constructable.js │ │ ├── Context.js │ │ ├── DelegatesFocus.js │ │ ├── Dynamic.js │ │ ├── Select.js │ │ └── Slots.js │ ├── index.html │ ├── index.js │ ├── pages │ │ ├── API.js │ │ ├── Basic.js │ │ ├── Constructable.js │ │ ├── Declarative.js │ │ ├── DelegatesFocus.js │ │ ├── DynamicStyles.js │ │ ├── Introduction.js │ │ └── Slots.js │ ├── styles.css │ └── utils │ │ ├── A.js │ │ ├── CodeBlock.js │ │ └── NotSupported.js └── lib │ ├── ReactShadowRoot.js │ └── index.js ├── tsconfig.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env", "@babel/react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-object-rest-spread", 5 | "@babel/plugin-proposal-class-properties" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | /lib 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs 2 | src 3 | examples 4 | .babelrc 5 | webpack.config.js 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 apearce 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-shadow-root 2 | Lets you add a shadow root to React components allowing you to use the shadow DOM. This provides scoped CSS and includes support for [constructable](https://developers.google.com/web/updates/2019/02/constructable-stylesheets) [stylesheets](https://wicg.github.io/construct-stylesheets). 3 | 4 | ## Installation 5 | `npm install --save react-shadow-root` 6 | 7 | ## Examples 8 | https://apearce.github.io/react-shadow-root/ 9 | 10 | ## ReactShadowRoot 11 | ### Usage 12 | ```jsx 13 | import React from 'react'; 14 | import ReactShadowRoot from 'react-shadow-root'; 15 | 16 | class ShadowCounter extends React.Component { 17 | state = { cnt: 0 }; 18 | 19 | increment = () => { 20 | this.setState({ 21 | cnt: this.state.cnt + 1 22 | }); 23 | } 24 | 25 | render() { 26 | const style = `span { 27 | background-color: #333; 28 | border-radius: 3px; 29 | color: #fff; 30 | padding: 1px 5px; 31 | } 32 | button { 33 | background-color: #fff; 34 | border: 1px solid currentColor; 35 | border-radius: 3px; 36 | color: #333; 37 | cursor: pointer; 38 | outline: 0; 39 | } 40 | button:active { 41 | background-color: #333; 42 | color: #fff; 43 | }`; 44 | 45 | return ( 46 |
{/* The shadow root will be attached to this DIV */} 47 | 48 | 49 | {this.state.cnt} 50 | 51 |
52 | ); 53 | } 54 | } 55 | ``` 56 | When the shadow root is created on its parent element, all children are copied into the shadow DOM. Styles in the shadow DOM are automatically scoped. You can inspect the element to confirm. [Slots](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot) work as expected; just be sure to add `{this.props.children}` _after_ the closing `ReactShadowRoot` tag. 57 | 58 | ### Static Properties 59 | | Name | Description | 60 | |------|-------------| 61 | | `constructableStylesheetsSupported` | A boolean telling you if constructable stylesheets are supported by the browser. | 62 | | `constructibleStylesheetsSupported` | An alias of `constructableStylesheetsSupported` using the ['correct' spelling](https://github.com/WICG/construct-stylesheets/issues/90). | 63 | | `shadowRootSupported` | A boolean telling you if attaching a shadow root is supported by the _browser_, not the element. | 64 | 65 | ### Props 66 | | Prop | Type | Values | Default | Description | 67 | |------|------|--------|---------|-------------| 68 | | `declarative` | `Boolean` | `true` or `false` | `false` | Creates a [Declarative Shadow Root](https://apearce.github.io/react-shadow-root/#declarative) | 69 | | `delegatesFocus` | `Boolean` | `true` or `false` | `false` | Expands the focus behavior of elements within the shadow DOM. Click [here](https://apearce.github.io/react-shadow-root/#delegates-focus) for more information. | 70 | | `mode` | `String` | `open` or `closed` | `open` | Sets the [mode](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/mode) of the shadow root. | 71 | | `stylesheets` | `Array` | `arrayOf(CSSStyleSheet)` | optional | Takes an array of [CSSStyleSheet](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet) objects for constructable stylesheets. | 72 | 73 | ## Notes 74 | - A minimum of React 16 is required. 75 | - TypeScript definitions included and should not require configuration 76 | - Works in all modern browsers except non-Chromium Edge. Click [here](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#Browser_compatibility) for current browser support. 77 | - Not all HTML elements allow you to attach a shadow root. Click [here](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#Elements_you_can_attach_a_shadow_to) for more information. 78 | - It has been tested with the Context API introduced in React 16.3.0 and it worked fine. It has not been tested with the previous API. 79 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Shadow Root Examples 10 | 11 | 12 | 13 |
14 |
15 |
16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-shadow-root", 3 | "version": "6.2.0", 4 | "description": "Adds a shadow root to React components", 5 | "main": "./lib/index.js", 6 | "types": "./lib/index.d.ts", 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "concurrently \"npm run build:watch\" \"npm run docs\"", 10 | "build": "tsc && babel src/lib -d lib --copy-files", 11 | "build:watch": "babel src/lib -w -d lib --copy-files", 12 | "docs": "webpack-dev-server --mode development", 13 | "docs:prod": "webpack --mode production" 14 | }, 15 | "peerDependencies": { 16 | "prop-types": ">=15.6.0", 17 | "react": ">=16.0.0", 18 | "react-dom": ">=16.0.0" 19 | }, 20 | "devDependencies": { 21 | "@babel/cli": "^7.8.4", 22 | "@babel/core": "^7.9.0", 23 | "@babel/plugin-proposal-class-properties": "^7.8.3", 24 | "@babel/plugin-proposal-object-rest-spread": "^7.9.0", 25 | "@babel/preset-env": "^7.9.0", 26 | "@babel/preset-react": "^7.9.4", 27 | "@types/prop-types": "^15.7.4", 28 | "@types/react": "^17.0.19", 29 | "@types/react-dom": "^17.0.9", 30 | "babel-loader": "^8.1.0", 31 | "concurrently": "^3.5.1", 32 | "css-loader": "^3.4.2", 33 | "html-webpack-plugin": "^3.2.0", 34 | "path": "^0.12.7", 35 | "prop-types": "^15.6.0", 36 | "react": "^16.13.1", 37 | "react-dom": "^16.13.1", 38 | "react-syntax-highlighter": "^11.0.2", 39 | "style-loader": "^0.23.0", 40 | "typescript": "^4.4.2", 41 | "webpack": "^4.42.1", 42 | "webpack-cli": "^3.3.11", 43 | "webpack-dev-server": "^3.11.0" 44 | }, 45 | "repository": { 46 | "type": "git", 47 | "url": "git+https://github.com/apearce/react-shadow-root.git" 48 | }, 49 | "keywords": [ 50 | "React", 51 | "Shadow DOM", 52 | "Shadow Root" 53 | ], 54 | "author": "Alan Pearce ", 55 | "bugs": { 56 | "url": "https://github.com/apearce/react-shadow-root/issues" 57 | }, 58 | "homepage": "https://github.com/apearce/react-shadow-root#readme", 59 | "dependencies": {} 60 | } 61 | -------------------------------------------------------------------------------- /src/docs/demos/Basic.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactShadowRoot from "../../../lib"; 3 | 4 | const styles = `:host { 5 | display: inline-flex; 6 | } 7 | span { 8 | background-color: #333; 9 | border-radius: 3px; 10 | color: #fff; 11 | padding: 1px 5px; 12 | } 13 | button { 14 | background-color: #fff; 15 | border: 1px solid currentColor; 16 | border-radius: 3px; 17 | cursor: pointer; 18 | outline: 0; 19 | } 20 | button:active { 21 | background-color: #333; 22 | color: #fff; 23 | } 24 | button, 25 | span { 26 | margin: 0 2px; 27 | }`; 28 | 29 | export default class extends React.Component { 30 | state = { cnt: 0 }; 31 | 32 | increment = () => { 33 | this.setState({ 34 | cnt: this.state.cnt + 1 35 | }); 36 | } 37 | 38 | render() { 39 | return ( 40 | {/* The shadow root will be attached to this element */} 41 | 42 | 43 | {this.state.cnt} 44 | 45 | 46 | 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/docs/demos/BasicFunctional.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import ReactShadowRoot from "../../../lib"; 3 | 4 | const styles = `:host { 5 | display: inline-flex; 6 | } 7 | span { 8 | background-color: #333; 9 | border-radius: 3px; 10 | color: #fff; 11 | padding: 1px 5px; 12 | } 13 | button { 14 | background-color: #fff; 15 | border: 1px solid currentColor; 16 | border-radius: 3px; 17 | cursor: pointer; 18 | outline: 0; 19 | } 20 | button:active { 21 | background-color: #333; 22 | color: #fff; 23 | } 24 | button, 25 | span { 26 | margin: 0 2px; 27 | }`; 28 | 29 | export default function() { 30 | const [cnt, setCount] = useState(0); 31 | 32 | return ( 33 | {/* The shadow root will be attached to this element */} 34 | 35 | 36 | {cnt} 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/docs/demos/Constructable.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactShadowRoot from "../../../lib"; 3 | 4 | const { constructableStylesheetsSupported } = ReactShadowRoot; 5 | const styles = `:host { 6 | display: inline-flex; 7 | flex-wrap: wrap; 8 | } 9 | span { 10 | background-color: #333; 11 | border-radius: 3px; 12 | color: #fff; 13 | padding: 1px 5px; 14 | } 15 | button { 16 | background-color: #fff; 17 | border: 1px solid currentColor; 18 | border-radius: 3px; 19 | cursor: pointer; 20 | outline: 0; 21 | } 22 | button:active { 23 | background-color: #333; 24 | color: #fff; 25 | } 26 | button, 27 | span { 28 | margin: 0 2px; 29 | } 30 | .fallback-message { 31 | background-color: transparent; 32 | color: #c00; 33 | }`; 34 | 35 | let sheet; 36 | let styleSheets; 37 | 38 | if (constructableStylesheetsSupported) { 39 | sheet = new CSSStyleSheet(); 40 | sheet.replaceSync(styles); 41 | styleSheets = [sheet]; 42 | } 43 | 44 | export default class extends React.Component { 45 | state = { cnt: 0 }; 46 | 47 | increment = () => { 48 | this.setState({ 49 | cnt: this.state.cnt + 1 50 | }); 51 | } 52 | 53 | render() { 54 | return ( 55 | 56 | 57 | {this.state.cnt} 58 | 59 | {!constructableStylesheetsSupported && 60 | <> 61 | 62 | Your browser does not support constructable stylesheets. Using fallback. 63 | 64 | 65 | 66 | } 67 | 68 | 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/docs/demos/Context.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactShadowRoot from "../../../lib"; 3 | 4 | const MyContext = React.createContext('Alan'); 5 | 6 | class Bar extends React.Component { 7 | state = {cnt: 0}; 8 | clicker = () => { 9 | this.setState({ 10 | cnt: this.state.cnt + 1 11 | }) 12 | } 13 | render() { 14 | return ( 15 | {foo => { 16 | return {foo} {this.state.cnt}; 17 | }} 18 | ); 19 | } 20 | } 21 | 22 | class Foo extends React.Component { 23 | render() { 24 | return (
); 25 | } 26 | } 27 | 28 | class ContextTest extends React.Component { 29 | render() { 30 | return ( 31 | 32 | ); 33 | } 34 | } 35 | 36 | export default ContextTest; 37 | -------------------------------------------------------------------------------- /src/docs/demos/DelegatesFocus.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactShadowRoot from "../../../lib"; 3 | 4 | export default class extends React.Component { 5 | render() { 6 | const { delegatesFocus } = this.props; 7 | const style = `:host { 8 | background-color: #fff; 9 | border: 1px dotted black; 10 | display: flex; 11 | padding: 16px; 12 | } 13 | :focus { 14 | outline: 2px solid blue; 15 | } 16 | input { 17 | margin-left: 5px; 18 | width: 150px; 19 | }`; 20 | 21 | return ( 22 | 23 | 24 | 25 |
Clickable Shadow DOM text
26 | 27 |
28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/docs/demos/Dynamic.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactShadowRoot from "../../../lib"; 3 | 4 | const { constructableStylesheetsSupported } = ReactShadowRoot; 5 | const colors = [ 6 | 'black', 'red', 'rebeccapurple', 'blue', 'brown', 7 | 'lime', 'magenta', 'green', 'orange', 'teal' 8 | ]; 9 | const styles = `:host { 10 | display: inline-flex; 11 | } 12 | span { 13 | background-color: var(--color); 14 | border-radius: 3px; 15 | color: #fff; 16 | padding: 1px 5px; 17 | } 18 | button { 19 | background-color: #fff; 20 | border: 1px solid var(--color); 21 | border-radius: 3px; 22 | color: var(--color); 23 | cursor: pointer; 24 | outline: 0; 25 | } 26 | button:active { 27 | background-color: var(--color); 28 | color: #fff; 29 | } 30 | button, 31 | span { 32 | margin: 0 2px; 33 | }`; 34 | 35 | let sheet; 36 | let styleSheets; 37 | 38 | if (constructableStylesheetsSupported) { 39 | sheet = new CSSStyleSheet(); 40 | sheet.replaceSync(styles); 41 | styleSheets = [sheet]; 42 | } 43 | 44 | export default class extends React.Component { 45 | state = { cnt: 0 }; 46 | 47 | increment = () => { 48 | this.setState({ 49 | cnt: this.state.cnt + 1 50 | }); 51 | } 52 | 53 | render() { 54 | const { reverse } = this.props; 55 | const { cnt } = this.state; 56 | const dynamicStyles = ` 57 | :host { 58 | flex-direction: ${reverse ? 'row-reverse' : 'row'}; 59 | --color: ${colors[cnt % 10]}; 60 | } 61 | `; 62 | 63 | return ( 64 | 65 | 66 | 67 | {cnt} 68 | 69 | {!constructableStylesheetsSupported && 70 | 71 | } 72 | 73 | 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/docs/demos/Select.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactShadowRoot from "../../../lib"; 3 | 4 | const styles = ` 5 | :host { 6 | display: inline-block; 7 | height: 25px; 8 | position: relative; 9 | width: 150px; 10 | } 11 | div, ul { 12 | background-color: #fff; 13 | border: 1px solid #333; 14 | border-radius: 3px; 15 | box-sizing: border-box; 16 | } 17 | div { 18 | align-items: center; 19 | display: flex; 20 | height: 100%; 21 | } 22 | output { 23 | flex-grow: 1; 24 | overflow: hidden; 25 | padding: 0 5px; 26 | text-overflow: ellipsis; 27 | white-space: nowrap; 28 | } 29 | output.placeholder { 30 | opacity: .6; 31 | } 32 | button { 33 | background-color: #fff; 34 | border: 0; 35 | border-left: 1px solid #333; 36 | border-radius: 0 3px 3px 0; 37 | cursor: pointer; 38 | height: 100%; 39 | margin-left: auto; 40 | margin-right: 0; 41 | outline: 0; 42 | } 43 | button:hover { 44 | background-color: #333; 45 | color: #fff; 46 | } 47 | ul { 48 | list-style-type: none; 49 | margin: 0; 50 | margin-top: 1px; 51 | max-height: 135px; 52 | min-width: 100%; 53 | overflow-y: auto; 54 | padding: 0; 55 | position: absolute; 56 | } 57 | li { 58 | cursor: pointer; 59 | padding: 5px 10px; 60 | white-space: nowrap; 61 | } 62 | li:hover { 63 | background-color: #333; 64 | color: #fff; 65 | } 66 | `; 67 | 68 | function Option(props) { 69 | const { 70 | value, 71 | label, 72 | ...rest 73 | } = props; 74 | 75 | return (
  • {label}
  • ); 76 | } 77 | 78 | class Select extends React.Component { 79 | state = { 80 | selected: {}, 81 | visible: false 82 | }; 83 | 84 | itemSelected = (e) => { 85 | const selected = { 86 | label: e.target.textContent, 87 | value: e.target.dataset.value 88 | }; 89 | 90 | this.props.onClick(selected); 91 | 92 | this.setState({ 93 | selected, 94 | visible: false 95 | }); 96 | } 97 | 98 | toggle = () => { 99 | this.setState({ 100 | visible: !this.state.visible 101 | }); 102 | } 103 | 104 | render() { 105 | const { selected, visible } = this.state; 106 | const arrow = visible ? 9650 : 9660; 107 | const outputClass = !selected.label && { 108 | className: 'placeholder' 109 | }; 110 | return ( 111 | 112 | 113 |
    114 | {selected.label || 'Select Something'} 115 | 116 |
    117 | {visible &&
      118 | {this.props.children} 119 |
    } 120 | 121 |
    122 |
    123 | ); 124 | } 125 | } 126 | 127 | export default class extends React.PureComponent { 128 | optionClicked(option) { 129 | console.log('Option Clicked', option); 130 | } 131 | 132 | render () { 133 | return (); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/docs/demos/Slots.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactShadowRoot from "../../../lib"; 3 | 4 | const { constructableStylesheetsSupported } = ReactShadowRoot; 5 | const styles = ` 6 | :host { background-color: #fff; display:block; } 7 | details { font-family: "Open Sans Light",Helvetica,Arial; } 8 | .name { font-weight: bold; color: #217ac0; font-size: 120%; margin-right: 5px; } 9 | h4 { margin: 10px 0 -8px 0; } 10 | h4 span { background: #217ac0; padding: 2px 6px 2px 6px; } 11 | h4 span { border: 1px solid #cee9f9; border-radius: 4px; } 12 | h4 span { color: white; } 13 | .attributes { margin-left: 22px; font-size: 90%; } 14 | .attributes p { margin-left: 16px; font-style: italic; } 15 | `; 16 | 17 | let sheet; 18 | let styleSheets; 19 | 20 | if (constructableStylesheetsSupported) { 21 | sheet = new CSSStyleSheet(); 22 | sheet.replaceSync(styles); 23 | styleSheets = [sheet]; 24 | } 25 | 26 | class ElementDetails extends React.Component { 27 | render() { 28 | return ( 29 | 30 |
    31 | 32 | 33 | <NEED NAME> 34 | NEED DESCRIPTION 35 | 36 | 37 |
    38 |

    Attributes

    39 |

    None

    40 |
    41 |
    42 |
    43 | {!constructableStylesheetsSupported && } 44 |
    45 | {this.props.children} 46 |
    ); 47 | } 48 | } 49 | 50 | export default function SlotsDemo() { 51 | return ( 52 | <> 53 | 54 | slot 55 | A placeholder inside a web 56 | component that users can fill with their own markup, 57 | with the effect of composing different DOM trees 58 | together. 59 |
    60 |
    name
    61 |
    The name of the slot.
    62 |
    63 |
    64 | 65 | template 66 | A mechanism for holding client- 67 | side content that is not to be rendered when a page is 68 | loaded but may subsequently be instantiated during 69 | runtime using JavaScript. 70 | 71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Shadow Root Examples 10 | 11 | 12 | 13 |
    14 |
    15 |
    16 | 17 |
    18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/docs/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import APIPage from "./pages/API"; 4 | import BasicPage from "./pages/Basic"; 5 | import ConstructablePage from "./pages/Constructable"; 6 | import DeclarativePage from "./pages/Declarative"; 7 | import DelegatesFocusPage from "./pages/DelegatesFocus"; 8 | import DynamicStylesPage from "./pages/DynamicStyles"; 9 | import IntroductionPage from "./pages/Introduction"; 10 | import SlotsPage from "./pages/Slots"; 11 | import ReactShadowRoot from "../lib/ReactShadowRoot"; 12 | 13 | import "./styles.css"; 14 | 15 | const shadowRootSupported = ReactShadowRoot.shadowRootSupported; 16 | 17 | const subheaders = [ 18 | "Styles go in and they don't come out", 19 | "You got your Web components in my React components!", 20 | "Put your style where your substance is" 21 | ]; 22 | 23 | function Header() { 24 | const subheader = subheaders[Math.floor(Math.random() * subheaders.length)]; 25 | 26 | return (<> 27 |

    react-shadow-root

    28 |

    {subheader}

    29 | ); 30 | } 31 | 32 | function Main() { 33 | return (<> 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | class Nav extends React.PureComponent { 46 | componentDidMount() { 47 | const headings = document.querySelectorAll('article > h2[id]'); 48 | const menu = document.createElement('ul'); 49 | 50 | headings.forEach(h => { 51 | const menuItem = document.createElement('li'); 52 | const menuLink = document.createElement('a'); 53 | menuLink.textContent = h.textContent; 54 | menuLink.href = '#' + h.id; 55 | menuItem.appendChild(menuLink); 56 | menu.appendChild(menuItem); 57 | }); 58 | 59 | document.querySelector('nav').prepend(menu); 60 | } 61 | 62 | render() { 63 | return {String.fromCharCode(9650)}; 64 | } 65 | } 66 | 67 | ReactDOM.render(
    , document.querySelector("body > header")); 68 | ReactDOM.render(
    , document.querySelector("main > section")); 69 | ReactDOM.render(