├── .npm-version ├── .node-version ├── .gitignore ├── jest.config.js ├── .babelrc ├── .prettierrc ├── .npmignore ├── .eslintrc ├── src ├── withProps.js ├── index.js ├── index.d.ts └── isValidAttr.js ├── package.json ├── __tests__ ├── withProps.js └── Box.js └── README.md /.npm-version: -------------------------------------------------------------------------------- 1 | 8.1.2 2 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v16.13.1 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "jsdom", 3 | }; 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | __tests__ 3 | .babelrc 4 | .eslintrc 5 | .prettierrc 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["unobtrusive", "unobtrusive/import", "unobtrusive/react"], 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "parser": "@babel/eslint-parser", 10 | "parserOptions": { 11 | "requireConfigFile": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/withProps.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function withProps(TargetComponent, addedProps) { 4 | const mapProps = 5 | typeof addedProps === "function" 6 | ? addedProps 7 | : (receivedProps) => Object.assign({}, addedProps, receivedProps); 8 | 9 | const NewComponent = React.forwardRef((receivedProps, ref) => { 10 | const props = mapProps(receivedProps); 11 | return ; 12 | }); 13 | 14 | NewComponent.withProps = (addedProps) => { 15 | return withProps(NewComponent, addedProps); 16 | }; 17 | 18 | return NewComponent; 19 | } 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import isValidAttr from "./isValidAttr"; 3 | import withProps from "./withProps"; 4 | 5 | const Box = React.forwardRef(({ tagName = "div", ...props }, ref) => { 6 | const attrs = {}; 7 | const style = {}; 8 | 9 | Object.keys(props).forEach((propName) => { 10 | const propValue = props[propName]; 11 | const validity = isValidAttr(tagName, propName); 12 | if (validity === "valid") { 13 | attrs[propName] = propValue; 14 | } else if (validity === "invalid") { 15 | style[propName] = propValue; 16 | } else if (validity === "unknown") { 17 | attrs[propName] = propValue; 18 | style[propName] = propValue; 19 | } 20 | }); 21 | 22 | const TagName = tagName; 23 | return ( 24 | 25 | ); 26 | }); 27 | 28 | Box.displayName = "Box"; 29 | 30 | Box.withProps = (addedProps) => { 31 | return withProps(Box, addedProps); 32 | }; 33 | 34 | Box.default = Box; 35 | module.exports = Box; 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-boxxy", 3 | "version": "1.1.1", 4 | "description": "Base component for react-dom", 5 | "author": "Suchipi ", 6 | "repository": "suchipi/react-boxxy", 7 | "main": "dist/index.js", 8 | "license": "MIT", 9 | "keywords": [ 10 | "box", 11 | "div", 12 | "base", 13 | "component", 14 | "react", 15 | "react-dom" 16 | ], 17 | "peerDependencies": { 18 | "react": ">=16.3", 19 | "react-dom": ">=16.3" 20 | }, 21 | "dependencies": { 22 | "@types/react": "^17.0.40", 23 | "@types/react-dom": "^17.0.13" 24 | }, 25 | "devDependencies": { 26 | "@babel/cli": "^7.17.6", 27 | "@babel/core": "^7.17.7", 28 | "@babel/eslint-parser": "^7.17.0", 29 | "@babel/preset-env": "^7.16.11", 30 | "@babel/preset-react": "^7.16.7", 31 | "@testing-library/react": "^8.0.1", 32 | "eslint": "^8.11.0", 33 | "eslint-config-unobtrusive": "^1.2.5", 34 | "eslint-plugin-import": "^2.25.4", 35 | "eslint-plugin-react": "^7.29.4", 36 | "jest": "^27.5.1", 37 | "prettier": "^2.5.1", 38 | "react": "^17.0.2", 39 | "react-dom": "^17.0.2" 40 | }, 41 | "scripts": { 42 | "test": "jest", 43 | "build": "babel src --out-dir dist && cp src/index.d.ts dist", 44 | "build:watch": "babel --watch src --out-dir dist" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /__tests__/withProps.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, cleanup } from "@testing-library/react"; 3 | import Box from ".."; 4 | 5 | describe("Box.withProps", () => { 6 | afterEach(cleanup); 7 | 8 | describe("object form", () => { 9 | it("adds those props to the box as defaults", () => { 10 | const Article = Box.withProps({ 11 | tagName: "article", 12 | display: "flex", 13 | flexDirection: "column", 14 | padding: "4px", 15 | }); 16 | const { container } = render(
); 17 | expect(container.innerHTML).toMatchInlineSnapshot( 18 | `"
"` 19 | ); 20 | }); 21 | 22 | it("user-specified props override defaults", () => { 23 | const Article = Box.withProps({ 24 | tagName: "article", 25 | display: "flex", 26 | flexDirection: "column", 27 | padding: "4px", 28 | }); 29 | const { container } = render(
); 30 | expect(container.innerHTML).toMatchInlineSnapshot( 31 | `"
"` 32 | ); 33 | }); 34 | 35 | it("has a withProps that allows further specification", () => { 36 | const One = Box.withProps({ backgroundColor: "red" }); 37 | const Two = One.withProps({ color: "green" }); 38 | const { container } = render(); 39 | expect(container.innerHTML).toMatchInlineSnapshot( 40 | `"
"` 41 | ); 42 | }); 43 | }); 44 | 45 | describe("function form", () => { 46 | it("calls the function on render with the received props and spread the return value onto a Box", () => { 47 | const Toggler = Box.withProps((props) => ({ 48 | ...props, 49 | backgroundColor: props.on ? "green" : "red", 50 | })); 51 | const { container } = render(); 52 | expect(container.innerHTML).toMatchInlineSnapshot( 53 | `"
"` 54 | ); 55 | const { container: container2 } = render(); 56 | expect(container2.innerHTML).toMatchInlineSnapshot( 57 | `"
"` 58 | ); 59 | }); 60 | 61 | it("does not spread anything on its own; it leaves that up to the user", () => { 62 | const Useless = Box.withProps((props) => ({})); 63 | const { container } = render(); 64 | expect(container.innerHTML).toMatchInlineSnapshot(`"
"`); 65 | }); 66 | 67 | it("has a withProps that allows further specification", () => { 68 | const One = Box.withProps((props) => ({ 69 | ...props, 70 | backgroundColor: "red", 71 | })); 72 | const Two = One.withProps((props) => ({ ...props, color: "green" })); 73 | const { container } = render(); 74 | expect(container.innerHTML).toMatchInlineSnapshot( 75 | `"
"` 76 | ); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /__tests__/Box.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, cleanup, fireEvent } from "@testing-library/react"; 3 | import Box from ".."; 4 | 5 | describe("Box", () => { 6 | afterEach(cleanup); 7 | 8 | describe("tagName", () => { 9 | it("renders a div by default", () => { 10 | const { container } = render(); 11 | expect(container.innerHTML).toMatchInlineSnapshot(`"
"`); 12 | }); 13 | 14 | it("can also render whatever tagName you specify", () => { 15 | const { container } = render(); 16 | expect(container.innerHTML).toMatchInlineSnapshot(`""`); 17 | }); 18 | }); 19 | 20 | describe("children", () => { 21 | it("renders children", () => { 22 | const { container } = render( 23 | 24 | 25 | 26 | ); 27 | expect(container.innerHTML).toMatchInlineSnapshot( 28 | `"
"` 29 | ); 30 | }); 31 | }); 32 | 33 | describe("received props", () => { 34 | it("puts styles into styles and html attrs into html attrs", () => { 35 | const { container } = render( 36 | 37 | ); 38 | expect(container.innerHTML).toMatchInlineSnapshot( 39 | `"
"` 40 | ); 41 | }); 42 | 43 | it("handles aria attrs correctly", () => { 44 | const { container } = render(); 45 | expect(container.innerHTML).toMatchInlineSnapshot( 46 | `"
"` 47 | ); 48 | }); 49 | 50 | it("handles data attrs correctly", () => { 51 | const { container } = render(); 52 | expect(container.innerHTML).toMatchInlineSnapshot( 53 | `"
"` 54 | ); 55 | }); 56 | 57 | it("spreads props to both locations for custom elements", () => { 58 | const { container } = render(); 59 | expect(container.innerHTML).toMatchInlineSnapshot( 60 | `""` 61 | ); 62 | }); 63 | 64 | it("img lazy loading works", () => { 65 | const { container } = render(); 66 | expect(container.innerHTML).toMatchInlineSnapshot( 67 | `""` 68 | ); 69 | }); 70 | }); 71 | 72 | describe("event handler props", () => { 73 | it("binds them properly", () => { 74 | const clickHandler = jest.fn((event) => { 75 | expect(event).not.toBe(undefined); 76 | }); 77 | const { getByText } = render(Click me); 78 | fireEvent.click(getByText("Click me")); 79 | expect(clickHandler).toHaveBeenCalled(); 80 | }); 81 | }); 82 | 83 | describe("refs", () => { 84 | it("forwards to the dom element", () => { 85 | let mounted = false; 86 | const refHandler = jest.fn((el) => { 87 | if (mounted) { 88 | expect(el).toBe(null); 89 | } else { 90 | expect(el instanceof HTMLDivElement).toBe(true); 91 | mounted = true; 92 | } 93 | }); 94 | render(); 95 | expect(refHandler).toHaveBeenCalled(); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // This is an object type whose keys are tagName strings and whose values are element classes; 4 | // If written by hand, it'd be like: { div: HTMLDivElement, g: SVGGElement, ...and so forth } 5 | type ElementTagNameMap = HTMLElementTagNameMap & 6 | HTMLElementDeprecatedTagNameMap & 7 | Omit< 8 | // When there's name conflicts, we assume HTML instead of SVG (because that's probably what the user wants). Sorry, SVGAElement 9 | SVGElementTagNameMap, 10 | keyof (HTMLElementTagNameMap & HTMLElementDeprecatedTagNameMap) 11 | >; 12 | 13 | // withProps function that is aware of the underlying react-boxxy element type. 14 | type WithPropsForBoxxy< 15 | Tag extends keyof ElementTagNameMap, 16 | CustomProps extends {} = {} 17 | > = { 18 | & CustomProps>( 19 | addedProps: AddedProps 20 | ): AddedProps extends { tagName: keyof ElementTagNameMap } 21 | ? BoxxyComponent 22 | : BoxxyComponent; 23 | 24 | < 25 | InputProps extends {}, 26 | OutputProps extends BoxxyProps & CustomProps 27 | >( 28 | config: (receivedProps: InputProps) => OutputProps 29 | ): InputProps extends BoxxyProps 30 | ? OutputProps extends { tagName: keyof ElementTagNameMap } 31 | ? BoxxyComponent< 32 | OutputProps["tagName"], 33 | Omit> & 34 | CustomProps 35 | > 36 | : InputProps extends BoxxyProps 37 | ? BoxxyComponent< 38 | Tag, 39 | Omit> & CustomProps 40 | > 41 | : React.FunctionComponent & { 42 | withProps: WithPropsGeneric; 43 | } 44 | : React.FunctionComponent & { 45 | withProps: WithPropsGeneric; 46 | }; 47 | }; 48 | 49 | // withProps function for generic react component functions. 50 | type WithPropsGeneric = { 51 | (addedProps: Props): React.FunctionComponent & { 52 | withProps: WithPropsGeneric; 53 | }; 54 | ( 55 | addedProps: (newProps: NewProps) => Props 56 | ): React.FunctionComponent & { 57 | withProps: WithPropsGeneric; 58 | }; 59 | }; 60 | 61 | /** 62 | * The props that are allowed on a Boxxy component that will render an element with the provided Tag as its tagName. 63 | * 64 | * For example, BoxxyProps<"div"> describes the props that are allowed to appear on a Boxxy component that eventually renders a div. 65 | * The reason you have to specify a Tag is because different props are allowed on different elements; for instance, an input element 66 | * can have a "type" property (like ), but a div element cannot. 67 | */ 68 | export type BoxxyProps = { 69 | tagName?: keyof ElementTagNameMap; 70 | ref?: React.Ref; 71 | } & React.CSSProperties & 72 | (ElementTagNameMap[Tag] extends HTMLElement 73 | ? React.HTMLProps 74 | : ElementTagNameMap[Tag] extends SVGElement 75 | ? React.SVGProps 76 | : {}); 77 | 78 | /** 79 | * The component function for a component created by react-boxxy. 80 | * 81 | * See also BoxxyProps, which is the props for this function. 82 | */ 83 | export type BoxxyComponent< 84 | Tag extends keyof ElementTagNameMap, 85 | CustomProps extends {} = {} 86 | > = ((props: BoxxyProps & CustomProps) => JSX.Element) & { 87 | displayName?: string; 88 | withProps: WithPropsForBoxxy; 89 | }; 90 | 91 | declare const Box: BoxxyComponent<"div">; 92 | export default Box; 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `react-boxxy` 2 | 3 | An extendable base component for React DOM. 4 | 5 | ## Features 6 | 7 | - Mix styles and HTML attributes as top-level props 8 | - Choose a tagName other than `div` with the `tagName` prop 9 | - Forwards refs 10 | - Create specialized components with default/custom props 11 | 12 | ## Example Usage 13 | 14 | 15 | ```js 16 | import Box from "react-boxxy"; 17 | 18 | 26 | alert("clicked!")} 29 | > 30 | Click me! 31 | 32 | 33 | 34 | // You can create a Box with defaults using the `withProps` function: 35 | const Column = Box.withProps({ 36 | display: "flex", 37 | flexDirection: "column", 38 | }); 39 | 40 | // `Column` is a component... 41 | 42 | Hi 43 | 44 | // The above is the same as: 45 | 49 | Hi 50 | 51 | 52 | // If you have more specific needs or want to use custom props, you can pass a function to `withProps`: 53 | const Switch = Box.withProps((props) => { 54 | ...props, 55 | tagName: "input", 56 | type: "checkbox", 57 | checked: props.on, 58 | }); 59 | 60 | // You can specialize a component returned from `withProps` further: 61 | const FancySwitch = Switch.withProps({ 62 | backgroundColor: "red", 63 | color: "green" 64 | }); 65 | ``` 66 | 67 | ## API Documentation 68 | 69 | ### `Box` 70 | 71 | A component that renders an HTML (or SVG) element, and spreads the props it receives into the HTML element's attributes or styles, based on the key name. 72 | 73 | ### Accepted Props: 74 | 75 | - `tagName` - Change which DOM element is rendered. Defaults to `"div"`. 76 | - Any valid React HTML/SVG attribute name (eg `id`, `className`, etc). 77 | - Any valid `camelCase` CSS style name (eg `display`, `marginTop`, etc). 78 | 79 | `Box` forwards its ref to the DOM element it renders. 80 | 81 | ### `Box.withProps` 82 | 83 | `Box.withProps` returns a component that renders a Box with the specified props. You can call it with either an `Object` or a `Function`. 84 | 85 | If you call `Box.withProps` with an `Object`, the entries in that object will be used as the default props for the `Box`. Users can still override these defaults by specifying the props manually. 86 | 87 | ```js 88 | const Column = Box.withProps({ 89 | display: "flex", 90 | flexDirection: "column", 91 | }); 92 | ``` 93 | 94 | If you call `Box.withProps` with a `Function`, that function will be called whenever the component renders. It will receive the props that component is being rendered with, and its return value will be spread onto the `Box` rendered by the component. 95 | 96 | ```js 97 | const Column = Box.withProps((props) => { 98 | return { 99 | display: "flex", 100 | flexDirection: "column", 101 | }; 102 | }); 103 | ``` 104 | 105 | Unlike the `Object` form, the `Function` form does not allow users to override the props you provide unless you handle this manually. With Object spread, that looks like this: 106 | 107 | ```js 108 | const Column = Box.withProps((props) => { 109 | return { 110 | display: "flex", 111 | flexDirection: "column", 112 | ...props, 113 | }; 114 | }); 115 | ``` 116 | 117 | Or, if you want to make your entries always take precedence over the user's (but still allow arbitrary value to be provided for other entries), spread before your entries: 118 | 119 | ```js 120 | const Column = Box.withProps((props) => { 121 | return { 122 | ...props, 123 | display: "flex", 124 | flexDirection: "column", 125 | }; 126 | }); 127 | ``` 128 | 129 | The function form is also useful for translating custom props to DOM attributes and styles. For example, here is a `Toggler` component designed to receive a boolean `on` prop that dictates its background color: 130 | 131 | ```js 132 | const Toggler = Box.withProps(({ on, ...otherProps }) => { 133 | return { 134 | ...otherProps, 135 | backgroundColor: on ? "green" : "red", 136 | }; 137 | }); 138 | ``` 139 | 140 | The above example avoids passing the `on` down to the underlying `Box`, but since `Box` will filter out non-DOM attributes internally, this isn't necessary: 141 | 142 | ```js 143 | const Toggler = Box.withProps((props) => { 144 | return { 145 | ...props, 146 | backgroundColor: props.on ? "green" : "red", 147 | }; 148 | }); 149 | ``` 150 | 151 | The returned component from `withProps` also has a `withProps` function on it that behaves the same as `Box.withProps`, which you can use if you want to specialize a component further: 152 | 153 | ```js 154 | const Flex = Box.withProps({ 155 | display: "flex", 156 | }); 157 | 158 | const Column = Flex.withProps({ 159 | flexDirection: "column", 160 | }); 161 | 162 | const Row = Flex.withProps({ 163 | flexDirection: "row", 164 | }); 165 | ``` 166 | 167 | > **Side Note**: Internally, the `Object` form of `withProps` uses `Object.assign`, so the following two code blocks are equivalent: 168 | > 169 | > ```js 170 | > const Column = Box.withProps({ 171 | > display: "flex", 172 | > flexDirection: "column", 173 | > }); 174 | > ``` 175 | > 176 | > ```js 177 | > const Column = Box.withProps((props) => { 178 | > return Object.assign( 179 | > {}, 180 | > { 181 | > display: "flex", 182 | > flexDirection: "column", 183 | > }, 184 | > props 185 | > ); 186 | > }); 187 | > ``` 188 | 189 | ## License 190 | 191 | MIT 192 | -------------------------------------------------------------------------------- /src/isValidAttr.js: -------------------------------------------------------------------------------- 1 | const htmlGlobalAttrs = new Set([ 2 | "about", 3 | "acceptCharset", 4 | "accessKey", 5 | "allowFullScreen", 6 | "allowTransparency", 7 | "autoComplete", 8 | "autoFocus", 9 | "autoPlay", 10 | "capture", 11 | "cellPadding", 12 | "cellSpacing", 13 | "charSet", 14 | "classID", 15 | "className", 16 | "colSpan", 17 | "contentEditable", 18 | "contextMenu", 19 | "crossOrigin", 20 | "dangerouslySetInnerHTML", 21 | "datatype", 22 | "dateTime", 23 | "dir", 24 | "draggable", 25 | "encType", 26 | "formAction", 27 | "formEncType", 28 | "formMethod", 29 | "formNoValidate", 30 | "formTarget", 31 | "frameBorder", 32 | "hidden", 33 | "hrefLang", 34 | "htmlFor", 35 | "httpEquiv", 36 | "icon", 37 | "id", 38 | "inlist", 39 | "inputMode", 40 | "is", 41 | "itemID", 42 | "itemProp", 43 | "itemRef", 44 | "itemScope", 45 | "itemType", 46 | "keyParams", 47 | "keyType", 48 | "lang", 49 | "marginHeight", 50 | "marginWidth", 51 | "maxLength", 52 | "mediaGroup", 53 | "minLength", 54 | "noValidate", 55 | "prefix", 56 | "property", 57 | "radioGroup", 58 | "readOnly", 59 | "resource", 60 | "role", 61 | "rowSpan", 62 | "scoped", 63 | "seamless", 64 | "security", 65 | "spellCheck", 66 | "srcDoc", 67 | "srcLang", 68 | "srcSet", 69 | "style", 70 | "suppressContentEditableWarning", 71 | "tabIndex", 72 | "title", 73 | "typeof", 74 | "unselectable", 75 | "useMap", 76 | "vocab", 77 | "wmode", 78 | ]); 79 | 80 | const htmlElementSpecificAttrs = { 81 | a: new Set([ 82 | "coords", 83 | "download", 84 | "href", 85 | "name", 86 | "rel", 87 | "shape", 88 | "target", 89 | "type", 90 | "onClick", 91 | ]), 92 | abbr: new Set(["title"]), 93 | applet: new Set(["alt", "height", "name", "width"]), 94 | area: new Set([ 95 | "alt", 96 | "coords", 97 | "download", 98 | "href", 99 | "rel", 100 | "shape", 101 | "target", 102 | "type", 103 | ]), 104 | audio: new Set(["controls", "loop", "muted", "preload", "src"]), 105 | base: new Set(["href", "target"]), 106 | basefont: new Set(["size"]), 107 | bdo: new Set(["dir"]), 108 | blockquote: new Set(["cite"]), 109 | button: new Set(["disabled", "form", "name", "type", "value"]), 110 | canvas: new Set(["height", "width"]), 111 | col: new Set(["span", "width"]), 112 | colgroup: new Set(["span", "width"]), 113 | data: new Set(["value"]), 114 | del: new Set(["cite"]), 115 | details: new Set(["open"]), 116 | dfn: new Set(["title"]), 117 | dialog: new Set(["open"]), 118 | embed: new Set(["height", "src", "type", "width"]), 119 | fieldset: new Set(["disabled", "form", "name"]), 120 | font: new Set(["size"]), 121 | form: new Set([ 122 | "accept", 123 | "action", 124 | "method", 125 | "name", 126 | "target", 127 | "onChange", 128 | "onInput", 129 | "onInvalid", 130 | "onSubmit", 131 | ]), 132 | frame: new Set(["name", "scrolling", "src"]), 133 | frameset: new Set(["cols", "rows"]), 134 | head: new Set(["profile"]), 135 | hr: new Set(["size", "width"]), 136 | html: new Set(["manifest"]), 137 | iframe: new Set([ 138 | "height", 139 | "name", 140 | "sandbox", 141 | "scrolling", 142 | "src", 143 | "width", 144 | "loading", 145 | ]), 146 | img: new Set(["alt", "height", "name", "sizes", "src", "width", "loading"]), 147 | input: new Set([ 148 | "accept", 149 | "alt", 150 | "autoCapitalize", 151 | "autoCorrect", 152 | "autoSave", 153 | "checked", 154 | "defaultChecked", 155 | "defaultValue", 156 | "disabled", 157 | "form", 158 | "height", 159 | "list", 160 | "max", 161 | "min", 162 | "multiple", 163 | "name", 164 | "onChange", 165 | "pattern", 166 | "placeholder", 167 | "required", 168 | "results", 169 | "size", 170 | "src", 171 | "step", 172 | "title", 173 | "type", 174 | "value", 175 | "width", 176 | ]), 177 | ins: new Set(["cite"]), 178 | keygen: new Set(["challenge", "disabled", "form", "name"]), 179 | label: new Set(["form"]), 180 | li: new Set(["type", "value"]), 181 | link: new Set([ 182 | "color", 183 | "href", 184 | "integrity", 185 | "media", 186 | "nonce", 187 | "rel", 188 | "scope", 189 | "sizes", 190 | "target", 191 | "title", 192 | "type", 193 | ]), 194 | map: new Set(["name"]), 195 | meta: new Set(["content", "name"]), 196 | meter: new Set(["high", "low", "max", "min", "optimum", "value"]), 197 | object: new Set(["data", "form", "height", "name", "type", "width"]), 198 | ol: new Set(["reversed", "start", "type"]), 199 | optgroup: new Set(["disabled", "label"]), 200 | option: new Set(["disabled", "label", "selected", "value"]), 201 | output: new Set(["form", "name"]), 202 | param: new Set(["name", "type", "value"]), 203 | pre: new Set(["width"]), 204 | progress: new Set(["max", "value"]), 205 | q: new Set(["cite"]), 206 | script: new Set(["async", "defer", "integrity", "nonce", "src", "type"]), 207 | select: new Set([ 208 | "defaultValue", 209 | "disabled", 210 | "form", 211 | "multiple", 212 | "name", 213 | "onChange", 214 | "required", 215 | "size", 216 | "value", 217 | ]), 218 | slot: new Set(["name"]), 219 | source: new Set(["media", "sizes", "src", "type"]), 220 | style: new Set(["media", "nonce", "title", "type"]), 221 | table: new Set(["summary", "width"]), 222 | td: new Set(["headers", "height", "scope", "width"]), 223 | textarea: new Set([ 224 | "autoCapitalize", 225 | "autoCorrect", 226 | "cols", 227 | "defaultValue", 228 | "disabled", 229 | "form", 230 | "name", 231 | "onChange", 232 | "placeholder", 233 | "required", 234 | "rows", 235 | "value", 236 | "wrap", 237 | ]), 238 | th: new Set(["headers", "height", "scope", "width"]), 239 | track: new Set(["default", "kind", "label", "src"]), 240 | ul: new Set(["type"]), 241 | video: new Set([ 242 | "controls", 243 | "height", 244 | "loop", 245 | "muted", 246 | "playsInline", 247 | "poster", 248 | "preload", 249 | "src", 250 | "width", 251 | ]), 252 | }; 253 | 254 | const svgAttrs = new Set([ 255 | "accentHeight", 256 | "accumulate", 257 | "additive", 258 | "alignmentBaseline", 259 | "allowReorder", 260 | "alphabetic", 261 | "amplitude", 262 | "arabicForm", 263 | "ascent", 264 | "attributeName", 265 | "attributeType", 266 | "autoReverse", 267 | "azimuth", 268 | "baseFrequency", 269 | "baseProfile", 270 | "baselineShift", 271 | "bbox", 272 | "begin", 273 | "bias", 274 | "by", 275 | "calcMode", 276 | "capHeight", 277 | "clip", 278 | "clipPath", 279 | "clipPathUnits", 280 | "clipRule", 281 | "color", 282 | "colorInterpolation", 283 | "colorInterpolationFilters", 284 | "colorProfile", 285 | "colorRendering", 286 | "contentScriptType", 287 | "contentStyleType", 288 | "cursor", 289 | "cx", 290 | "cy", 291 | "d", 292 | "decelerate", 293 | "descent", 294 | "diffuseConstant", 295 | "direction", 296 | "display", 297 | "divisor", 298 | "dominantBaseline", 299 | "dur", 300 | "dx", 301 | "dy", 302 | "edgeMode", 303 | "elevation", 304 | "enableBackground", 305 | "end", 306 | "exponent", 307 | "externalResourcesRequired", 308 | "fill", 309 | "fillOpacity", 310 | "fillRule", 311 | "filter", 312 | "filterRes", 313 | "filterUnits", 314 | "floodColor", 315 | "floodOpacity", 316 | "focusable", 317 | "fontFamily", 318 | "fontSize", 319 | "fontSizeAdjust", 320 | "fontStretch", 321 | "fontStyle", 322 | "fontVariant", 323 | "fontWeight", 324 | "format", 325 | "from", 326 | "fx", 327 | "fy", 328 | "g1", 329 | "g2", 330 | "glyphName", 331 | "glyphOrientationHorizontal", 332 | "glyphOrientationVertical", 333 | "glyphRef", 334 | "gradientTransform", 335 | "gradientUnits", 336 | "hanging", 337 | "height", 338 | "horizAdvX", 339 | "horizOriginX", 340 | "ideographic", 341 | "imageRendering", 342 | "in", 343 | "in2", 344 | "intercept", 345 | "k", 346 | "k1", 347 | "k2", 348 | "k3", 349 | "k4", 350 | "kernelMatrix", 351 | "kernelUnitLength", 352 | "kerning", 353 | "keyPoints", 354 | "keySplines", 355 | "keyTimes", 356 | "lengthAdjust", 357 | "letterSpacing", 358 | "lightingColor", 359 | "limitingConeAngle", 360 | "local", 361 | "markerEnd", 362 | "markerHeight", 363 | "markerMid", 364 | "markerStart", 365 | "markerUnits", 366 | "markerWidth", 367 | "mask", 368 | "maskContentUnits", 369 | "maskUnits", 370 | "mathematical", 371 | "mode", 372 | "numOctaves", 373 | "offset", 374 | "opacity", 375 | "operator", 376 | "order", 377 | "orient", 378 | "orientation", 379 | "origin", 380 | "overflow", 381 | "overlinePosition", 382 | "overlineThickness", 383 | "paintOrder", 384 | "panose1", 385 | "pathLength", 386 | "patternContentUnits", 387 | "patternTransform", 388 | "patternUnits", 389 | "pointerEvents", 390 | "points", 391 | "pointsAtX", 392 | "pointsAtY", 393 | "pointsAtZ", 394 | "preserveAlpha", 395 | "preserveAspectRatio", 396 | "primitiveUnits", 397 | "r", 398 | "radius", 399 | "refX", 400 | "refY", 401 | "renderingIntent", 402 | "repeatCount", 403 | "repeatDur", 404 | "requiredExtensions", 405 | "requiredFeatures", 406 | "restart", 407 | "result", 408 | "rotate", 409 | "rx", 410 | "ry", 411 | "scale", 412 | "seed", 413 | "shapeRendering", 414 | "slope", 415 | "spacing", 416 | "specularConstant", 417 | "specularExponent", 418 | "speed", 419 | "spreadMethod", 420 | "startOffset", 421 | "stdDeviation", 422 | "stemh", 423 | "stemv", 424 | "stitchTiles", 425 | "stopColor", 426 | "stopOpacity", 427 | "strikethroughPosition", 428 | "strikethroughThickness", 429 | "string", 430 | "stroke", 431 | "strokeDasharray", 432 | "strokeDashoffset", 433 | "strokeLinecap", 434 | "strokeLinejoin", 435 | "strokeMiterlimit", 436 | "strokeOpacity", 437 | "strokeWidth", 438 | "surfaceScale", 439 | "systemLanguage", 440 | "tableValues", 441 | "targetX", 442 | "targetY", 443 | "textAnchor", 444 | "textDecoration", 445 | "textLength", 446 | "textRendering", 447 | "to", 448 | "transform", 449 | "u1", 450 | "u2", 451 | "underlinePosition", 452 | "underlineThickness", 453 | "unicode", 454 | "unicodeBidi", 455 | "unicodeRange", 456 | "unitsPerEm", 457 | "vAlphabetic", 458 | "vHanging", 459 | "vIdeographic", 460 | "vMathematical", 461 | "values", 462 | "vectorEffect", 463 | "version", 464 | "vertAdvY", 465 | "vertOriginX", 466 | "vertOriginY", 467 | "viewBox", 468 | "viewTarget", 469 | "visibility", 470 | "width", 471 | "widths", 472 | "wordSpacing", 473 | "writingMode", 474 | "x", 475 | "x1", 476 | "x2", 477 | "xChannelSelector", 478 | "xHeight", 479 | "xlinkActuate", 480 | "xlinkArcrole", 481 | "xlinkHref", 482 | "xlinkRole", 483 | "xlinkShow", 484 | "xlinkTitle", 485 | "xlinkType", 486 | "xmlBase", 487 | "xmlLang", 488 | "xmlSpace", 489 | "xmlns", 490 | "xmlnsXlink", 491 | "y", 492 | "y1", 493 | "y2", 494 | "yChannelSelector", 495 | "z", 496 | "zoomAndPan", 497 | ]); 498 | 499 | const elements = { 500 | html: new Set([ 501 | "a", 502 | "abbr", 503 | "address", 504 | "area", 505 | "article", 506 | "aside", 507 | "audio", 508 | "b", 509 | "base", 510 | "bdi", 511 | "bdo", 512 | "blockquote", 513 | "body", 514 | "br", 515 | "button", 516 | "canvas", 517 | "caption", 518 | "cite", 519 | "code", 520 | "col", 521 | "colgroup", 522 | "data", 523 | "datalist", 524 | "dd", 525 | "del", 526 | "details", 527 | "dfn", 528 | "dialog", 529 | "div", 530 | "dl", 531 | "dt", 532 | "em", 533 | "embed", 534 | "fieldset", 535 | "figcaption", 536 | "figure", 537 | "footer", 538 | "form", 539 | "h1", 540 | "h2", 541 | "h3", 542 | "h4", 543 | "h5", 544 | "h6", 545 | "head", 546 | "header", 547 | "hgroup", 548 | "hr", 549 | "html", 550 | "i", 551 | "iframe", 552 | "img", 553 | "input", 554 | "ins", 555 | "kbd", 556 | "keygen", 557 | "label", 558 | "legend", 559 | "li", 560 | "link", 561 | "main", 562 | "map", 563 | "mark", 564 | "math", 565 | "menu", 566 | "menuitem", 567 | "meta", 568 | "meter", 569 | "nav", 570 | "noscript", 571 | "object", 572 | "ol", 573 | "optgroup", 574 | "option", 575 | "output", 576 | "p", 577 | "param", 578 | "picture", 579 | "pre", 580 | "progress", 581 | "q", 582 | "rb", 583 | "rp", 584 | "rt", 585 | "rtc", 586 | "ruby", 587 | "s", 588 | "samp", 589 | "script", 590 | "section", 591 | "select", 592 | "slot", 593 | "small", 594 | "source", 595 | "span", 596 | "strong", 597 | "style", 598 | "sub", 599 | "summary", 600 | "sup", 601 | "svg", 602 | "table", 603 | "tbody", 604 | "td", 605 | "template", 606 | "textarea", 607 | "tfoot", 608 | "th", 609 | "thead", 610 | "time", 611 | "title", 612 | "tr", 613 | "track", 614 | "u", 615 | "ul", 616 | "var", 617 | "video", 618 | "wbr", 619 | ]), 620 | svg: new Set([ 621 | "altGlyph", 622 | "altGlyphDef", 623 | "altGlyphItem", 624 | "animate", 625 | "animateColor", 626 | "animateMotion", 627 | "animateTransform", 628 | "circle", 629 | "clipPath", 630 | "color-profile", 631 | "cursor", 632 | "defs", 633 | "desc", 634 | "ellipse", 635 | "feBlend", 636 | "feColorMatrix", 637 | "feComponentTransfer", 638 | "feComposite", 639 | "feConvolveMatrix", 640 | "feDiffuseLighting", 641 | "feDisplacementMap", 642 | "feDistantLight", 643 | "feFlood", 644 | "feFuncA", 645 | "feFuncB", 646 | "feFuncG", 647 | "feFuncR", 648 | "feGaussianBlur", 649 | "feImage", 650 | "feMerge", 651 | "feMergeNode", 652 | "feMorphology", 653 | "feOffset", 654 | "fePointLight", 655 | "feSpecularLighting", 656 | "feSpotLight", 657 | "feTile", 658 | "feTurbulence", 659 | "filter", 660 | "font", 661 | "font-face", 662 | "font-face-format", 663 | "font-face-name", 664 | "font-face-src", 665 | "font-face-uri", 666 | "foreignObject", 667 | "g", 668 | "glyph", 669 | "glyphRef", 670 | "hkern", 671 | "image", 672 | "line", 673 | "linearGradient", 674 | "marker", 675 | "mask", 676 | "metadata", 677 | "missing-glyph", 678 | "mpath", 679 | "path", 680 | "pattern", 681 | "polygon", 682 | "polyline", 683 | "radialGradient", 684 | "rect", 685 | "script", 686 | "set", 687 | "stop", 688 | "style", 689 | "svg", 690 | "switch", 691 | "symbol", 692 | "text", 693 | "textPath", 694 | "title", 695 | "tref", 696 | "tspan", 697 | "use", 698 | "view", 699 | "vkern", 700 | ]), 701 | }; 702 | 703 | const eventAttrs = new Set([ 704 | "onCopy", 705 | "onCut", 706 | "onPaste", 707 | "onCompositionEnd", 708 | "onCompositionStart", 709 | "onCompositionUpdate", 710 | "onKeyDown", 711 | "onKeyPress", 712 | "onKeyUp", 713 | "onFocus", 714 | "onBlur", 715 | "onChange", 716 | "onInput", 717 | "onInvalid", 718 | "onSubmit", 719 | "onClick", 720 | "onContextMenu", 721 | "onDoubleClick", 722 | "onDrag", 723 | "onDragEnd", 724 | "onDragEnter", 725 | "onDragExit", 726 | "onDragLeave", 727 | "onDragOver", 728 | "onDragStart", 729 | "onDrop", 730 | "onMouseDown", 731 | "onMouseEnter", 732 | "onMouseLeave", 733 | "onMouseMove", 734 | "onMouseOut", 735 | "onMouseOver", 736 | "onMouseUp", 737 | "onPointerDown", 738 | "onPointerMove", 739 | "onPointerUp", 740 | "onPointerCancel", 741 | "onGotPointerCapture", 742 | "onLostPointerCapture", 743 | "onPointerEnter", 744 | "onPointerLeave", 745 | "onPointerOver", 746 | "onPointerOut", 747 | "onSelect", 748 | "onTouchCancel", 749 | "onTouchEnd", 750 | "onTouchMove", 751 | "onTouchStart", 752 | "onScroll", 753 | "onWheel", 754 | "onAbort", 755 | "onCanPlay", 756 | "onCanPlayThrough", 757 | "onDurationChange", 758 | "onEmptied", 759 | "onEncrypted", 760 | "onEnded", 761 | "onError", 762 | "onLoadedData", 763 | "onLoadedMetadata", 764 | "onLoadStart", 765 | "onPause", 766 | "onPlay", 767 | "onPlaying", 768 | "onProgress", 769 | "onRateChange", 770 | "onSeeked", 771 | "onSeeking", 772 | "onStalled", 773 | "onSuspend", 774 | "onTimeUpdate", 775 | "onVolumeChange", 776 | "onWaiting", 777 | "onLoad", 778 | "onError", 779 | "onAnimationStart", 780 | "onAnimationEnd", 781 | "onAnimationIteration", 782 | "onTransitionEnd", 783 | "onToggle", 784 | "onCopyCapture", 785 | "onCutCapture", 786 | "onPasteCapture", 787 | "onCompositionEndCapture", 788 | "onCompositionStartCapture", 789 | "onCompositionUpdateCapture", 790 | "onKeyDownCapture", 791 | "onKeyPressCapture", 792 | "onKeyUpCapture", 793 | "onFocusCapture", 794 | "onBlurCapture", 795 | "onChangeCapture", 796 | "onInputCapture", 797 | "onInvalidCapture", 798 | "onSubmitCapture", 799 | "onClickCapture", 800 | "onContextMenuCapture", 801 | "onDoubleClickCapture", 802 | "onDragCapture", 803 | "onDragEndCapture", 804 | "onDragEnterCapture", 805 | "onDragExitCapture", 806 | "onDragLeaveCapture", 807 | "onDragOverCapture", 808 | "onDragStartCapture", 809 | "onDropCapture", 810 | "onMouseDownCapture", 811 | "onMouseEnterCapture", 812 | "onMouseLeaveCapture", 813 | "onMouseMoveCapture", 814 | "onMouseOutCapture", 815 | "onMouseOverCapture", 816 | "onMouseUpCapture", 817 | "onPointerDownCapture", 818 | "onPointerMoveCapture", 819 | "onPointerUpCapture", 820 | "onPointerCancelCapture", 821 | "onGotPointerCaptureCapture", 822 | "onLostPointerCaptureCapture", 823 | "onPointerEnterCapture", 824 | "onPointerLeaveCapture", 825 | "onPointerOverCapture", 826 | "onPointerOutCapture", 827 | "onSelectCapture", 828 | "onTouchCancelCapture", 829 | "onTouchEndCapture", 830 | "onTouchMoveCapture", 831 | "onTouchStartCapture", 832 | "onScrollCapture", 833 | "onWheelCapture", 834 | "onAbortCapture", 835 | "onCanPlayCapture", 836 | "onCanPlayThroughCapture", 837 | "onDurationChangeCapture", 838 | "onEmptiedCapture", 839 | "onEncryptedCapture", 840 | "onEndedCapture", 841 | "onErrorCapture", 842 | "onLoadedDataCapture", 843 | "onLoadedMetadataCapture", 844 | "onLoadStartCapture", 845 | "onPauseCapture", 846 | "onPlayCapture", 847 | "onPlayingCapture", 848 | "onProgressCapture", 849 | "onRateChangeCapture", 850 | "onSeekedCapture", 851 | "onSeekingCapture", 852 | "onStalledCapture", 853 | "onSuspendCapture", 854 | "onTimeUpdateCapture", 855 | "onVolumeChangeCapture", 856 | "onWaitingCapture", 857 | "onLoadCapture", 858 | "onErrorCapture", 859 | "onAnimationStartCapture", 860 | "onAnimationEndCapture", 861 | "onAnimationIterationCapture", 862 | "onTransitionEndCapture", 863 | "onToggleCapture", 864 | ]); 865 | 866 | const validAttrsByElement = {}; 867 | elements.svg.forEach((tagName) => { 868 | validAttrsByElement[tagName] = (attrKey) => { 869 | return ( 870 | svgAttrs.has(attrKey) || 871 | eventAttrs.has(attrKey) || 872 | attrKey.match(/^aria-/) || 873 | attrKey.match(/^data-/) 874 | ); 875 | }; 876 | }); 877 | elements.html.forEach((tagName) => { 878 | validAttrsByElement[tagName] = (attrKey) => { 879 | return ( 880 | htmlGlobalAttrs.has(attrKey) || 881 | eventAttrs.has(attrKey) || 882 | (htmlElementSpecificAttrs[tagName] && 883 | htmlElementSpecificAttrs[tagName].has(attrKey)) || 884 | attrKey.match(/^aria-/) || 885 | attrKey.match(/^data-/) 886 | ); 887 | }; 888 | }); 889 | 890 | const isValidAttr = (tagName, attrKey) => { 891 | const getIsValid = validAttrsByElement[tagName]; 892 | if (!getIsValid) { 893 | return "unknown"; 894 | } 895 | return getIsValid(attrKey) ? "valid" : "invalid"; 896 | }; 897 | 898 | export default isValidAttr; 899 | --------------------------------------------------------------------------------