├── .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 |
--------------------------------------------------------------------------------