From 36b8b120f77a932fc9602eb9475094f196bd2166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20Poduszl=C3=B3?= Date: Sun, 27 Dec 2020 18:29:39 +0100 Subject: [PATCH] chore: initial commit --- .editorconfig | 15 +++++ .gitattributes | 1 + .gitignore | 4 ++ .prettierrc.json | 13 ++++ .vscode/extensions.json | 3 + .vscode/settings.json | 4 ++ CHANGELOG.md | 18 ++++++ CODE_OF_CONDUCT.md | 132 ++++++++++++++++++++++++++++++++++++++++ LICENSE | 21 +++++++ README.md | 94 ++++++++++++++++++++++++++++ empty.js | 0 index.d.ts | 56 +++++++++++++++++ package-lock.json | 42 +++++++++++++ package.json | 41 +++++++++++++ tsconfig.json | 12 ++++ 15 files changed, 456 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .prettierrc.json create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 empty.js create mode 100644 index.d.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..43323f7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +indent_style = tab +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yml] +indent_style = space + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28fd333 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.log* +.DS_Store +.idea/ +node_modules/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..e7b075a --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,13 @@ +{ + "useTabs": true, + "trailingComma": "all", + "overrides": [ + { + "files": "*.md", + "options": { + "useTabs": false, + "trailingComma": "none" + } + } + ] +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c30c76b --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["EditorConfig.EditorConfig", "esbenp.prettier-vscode"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a4570ff --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a04e1d8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.0.0] - 2020-12-27 + +### Added + +- `PolymorphicPropsWithoutRef` and `PolymorphicPropsWithRef` types for appending `as` to component props +- `PolymorphicForwardRefExoticComponent`, `PolymorphicMemoExoticComponent` and `PolymorphicLazyExoticComponent` types to support exotic components + +[unreleased]: https://github.com/kripod/react-polymorphic-types/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/kripod/react-polymorphic-types/releases/tag/v1.0.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f5770aa --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +kripod@protonmail.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][mozilla coc]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][faq]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[mozilla coc]: https://github.com/mozilla/diversity +[faq]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..abe1ad7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Kristóf Poduszló + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..09d200c --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# react-polymorphic-types + +Zero-runtime polymorphic component definitions for React + +[![npm](https://img.shields.io/npm/v/react-polymorphic-types)](https://www.npmjs.com/package/react-polymorphic-types) +[![npm bundle size](https://img.shields.io/bundlephobia/min/react-polymorphic-types)](https://bundlephobia.com/result?p=react-polymorphic-types) + +## Motivation + +Being a successor to [react-polymorphic-box](https://github.com/kripod/react-polymorphic-box), this project offers more accurate typings with less overhead. + +## Features + +- Automatic code completion, based on the value of the `as` prop +- Static type checking against the associated component’s inferred props +- HTML element name validation + +## Usage + +A `Heading` component can demonstrate the effectiveness of polymorphism: + +```tsx +Heading +Subheading +``` + +Custom components like the previous one may utilize the package as shown below. + +```tsx +import type { PolymorphicPropsWithoutRef } from "react-polymorphic-types"; + +// An HTML tag or a different React component can be rendered by default +export const HeadingDefaultElement = "h2"; + +// Component-specific props should be specified separately +export type HeadingOwnProps = { + color?: string; +}; + +// Extend own props with others inherited from the underlying element type +// Own props take precedence over the inherited ones +export type HeadingProps< + T extends React.ElementType = typeof HeadingDefaultElement +> = PolymorphicPropsWithoutRef; + +export function Heading< + T extends React.ElementType = typeof HeadingDefaultElement +>({ as, color, style, ...restProps }: HeadingProps) { + const Element: React.ElementType = as || ButtonDefaultElement; + return ; +} +``` + +### With [`React.forwardRef`](https://reactjs.org/docs/react-api.html#reactforwardref) + +```tsx +import * as React from "react"; +import type { PolymorphicForwardRefExoticComponent } from "react-polymorphic-types"; +import { Heading, HeadingDefaultElement, HeadingOwnProps } from "./Heading"; + +export const RefForwardingHeading: PolymorphicForwardRefExoticComponent< + HeadingOwnProps, + HeadingDefaultElement +> = React.forwardRef(Heading); +``` + +### With [`React.memo`](https://reactjs.org/docs/react-api.html#reactmemo) + +```tsx +import * as React from "react"; +import type { PolymorphicMemoExoticComponent } from "react-polymorphic-types"; +import { Heading, HeadingDefaultElement, HeadingOwnProps } from "./Heading"; + +export const MemoizedHeading: PolymorphicMemoExoticComponent< + HeadingOwnProps, + HeadingDefaultElement +> = React.memo(Heading); +``` + +### With [`React.lazy`](https://reactjs.org/docs/react-api.html#reactlazy) + +```tsx +import * as React from "react"; +import type { PolymorphicLazyExoticComponent } from "react-polymorphic-types"; +import type { HeadingDefaultElement, HeadingOwnProps } from "./Heading"; + +export const LazyHeading: PolymorphicLazyExoticComponent< + HeadingOwnProps, + HeadingDefaultElement +> = React.lazy(async () => { + const { Heading } = await import("./Heading"); + return { default: Heading }; +}); +``` diff --git a/empty.js b/empty.js new file mode 100644 index 0000000..e69de29 diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..1fb16a5 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,56 @@ +type Merge = Omit & U; + +type PropsWithAs = P & { as?: T }; + +export type PolymorphicPropsWithoutRef = Merge< + React.ComponentPropsWithoutRef, + PropsWithAs +>; + +export type PolymorphicPropsWithRef = Merge< + React.ComponentPropsWithRef, + PropsWithAs +>; + +// TODO: +// - PolymorphicFunctionComponent +// - PolymorphicVoidFunctionComponent (requires @types/react >=16.9.48) + +type PolymorphicExoticComponent< + P = {}, + T extends React.ElementType = React.ElementType +> = Merge< + React.ExoticComponent

, + { + /** + * **NOTE**: Exotic components are not callable. + */ + ( + props: PolymorphicPropsWithRef, + ): React.ReactElement | null; + } +>; + +export type PolymorphicForwardRefExoticComponent< + P, + T extends React.ElementType +> = Merge< + React.ForwardRefExoticComponent

, + PolymorphicExoticComponent +>; + +export type PolymorphicMemoExoticComponent< + P, + T extends React.ElementType +> = Merge< + React.MemoExoticComponent>, + PolymorphicExoticComponent +>; + +export type PolymorphicLazyExoticComponent< + P, + T extends React.ElementType +> = Merge< + React.LazyExoticComponent>, + PolymorphicExoticComponent +>; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9190df4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,42 @@ +{ + "name": "react-polymorphic-types", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", + "dev": true + }, + "@types/react": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.0.tgz", + "integrity": "sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "csstype": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.5.tgz", + "integrity": "sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ==", + "dev": true + }, + "prettier": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", + "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", + "dev": true + }, + "typescript": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", + "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c455ceb --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "react-polymorphic-types", + "version": "1.0.0", + "description": "Zero-runtime polymorphic component definitions for React", + "keywords": [ + "react", + "polymorphism", + "as-prop", + "typescript" + ], + "homepage": "https://github.com/kripod/react-polymorphic-types#readme", + "bugs": { + "url": "https://github.com/kripod/react-polymorphic-types/issues" + }, + "repository": "github:kripod/react-polymorphic-types", + "license": "MIT", + "author": "Kristóf Poduszló ", + "sideEffects": false, + "exports": { + "default": "./empty.js", + "import": "./empty.js" + }, + "main": "./empty.js", + "types": "./index.d.ts", + "files": [ + "index.d.ts", + "empty.js" + ], + "scripts": { + "format": "prettier --ignore-path ./.gitignore --write .", + "type-check": "tsc" + }, + "devDependencies": { + "@types/react": "^17.0.0", + "prettier": "^2.2.1", + "typescript": "^4.1.3" + }, + "peerDependencies": { + "@types/react": ">=16.8.6" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..fae45d4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "noEmit": true, + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Node", + "isolatedModules": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "importsNotUsedAsValues": "error" + } +}