├── example
├── .npmignore
├── index.html
├── tsconfig.json
├── package.json
└── index.tsx
├── .gitignore
├── assets
└── fitty.gif
├── jest.config.js
├── .gitattributes
├── babel.config.js
├── .github
└── workflows
│ ├── size-limit.yml
│ └── ci.yml
├── LICENSE
├── src
├── index.integration.test.tsx
└── index.tsx
├── tsconfig.json
├── README.md
└── package.json
/example/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 | dist
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .cache
5 | dist
6 | .idea
7 |
--------------------------------------------------------------------------------
/assets/fitty.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hexetia/react-fitty/HEAD/assets/fitty.gif
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // config file for ide runners like webstorm
2 | module.exports = {
3 | testEnvironment: 'node',
4 | };
5 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set the default behavior, in case people don't have core.autocrlf set.
2 | * text=auto eol=lf
3 | # Exclude example from github language meters
4 | example/* linguist-vendored
5 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | // config file for ide runners like webstorm
2 | module.exports = {
3 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
4 | };
5 |
--------------------------------------------------------------------------------
/.github/workflows/size-limit.yml:
--------------------------------------------------------------------------------
1 | name: "size"
2 | on:
3 | pull_request:
4 | branches:
5 | - main
6 | jobs:
7 | size:
8 | runs-on: ubuntu-latest
9 | env:
10 | CI_JOB_NUMBER: 1
11 | steps:
12 | - uses: actions/checkout@v1
13 | - uses: andresz1/size-limit-action@v1
14 | with:
15 | github_token: ${{ secrets.GITHUB_TOKEN }}
16 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Playground
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": false,
4 | "target": "es5",
5 | "module": "commonjs",
6 | "jsx": "react",
7 | "moduleResolution": "node",
8 | "noImplicitAny": false,
9 | "noUnusedLocals": false,
10 | "noUnusedParameters": false,
11 | "removeComments": true,
12 | "strictNullChecks": true,
13 | "preserveConstEnums": true,
14 | "sourceMap": true,
15 | "lib": ["es2015", "es2016", "dom"],
16 | "types": ["node"]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on: [push]
4 |
5 | jobs:
6 | tests:
7 | runs-on: ubuntu-18.04
8 | timeout-minutes: 15
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v2
12 | - name: Use Node.js v12
13 | uses: actions/setup-node@v1
14 | with:
15 | node-version: 12.x
16 | - name: Install
17 | run: yarn
18 | - name: Install Server Deps
19 | run: |
20 | cd example
21 | yarn
22 | - name: Build
23 | run: yarn build
24 | - name: Tests
25 | run: yarn ci
26 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "start": "parcel index.html",
8 | "build": "parcel build index.html"
9 | },
10 | "dependencies": {
11 | "@material-ui/core": "^4.11.2",
12 | "styled-components": "^5.2.1"
13 | },
14 | "alias": {
15 | "react": "../node_modules/react",
16 | "react-dom": "../node_modules/react-dom/profiling",
17 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling",
18 | "fitty": "../node_modules/fitty"
19 | },
20 | "devDependencies": {
21 | "@types/react": "^17.0.0",
22 | "@types/react-dom": "^17.0.0",
23 | "parcel": "^1.12.4",
24 | "typescript": "^4.1.3"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 lucasljj@gmail.com
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/index.integration.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment node
3 | */
4 | import puppeteer from 'puppeteer';
5 |
6 | let browser: puppeteer.Browser | undefined = undefined;
7 |
8 | test('fit parent size', async () => {
9 | browser = await puppeteer.launch({
10 | defaultViewport: {
11 | width: 1280,
12 | height: 720,
13 | },
14 | args: ['--no-sandbox'],
15 | });
16 | const page = await browser.newPage();
17 | // Use the example folder server
18 | await page.goto('http://localhost:1234/');
19 | await page.waitForSelector('#example', { timeout: 30_000 });
20 |
21 | await page.waitForFunction(() => {
22 | const fittyText = document.querySelector('#example') as HTMLDivElement;
23 | return fittyText.offsetWidth > 1000 && fittyText.offsetWidth <= 1280;
24 | });
25 |
26 | await page.waitForFunction(() => {
27 | const fittyText = document.querySelector('#mui') as HTMLDivElement;
28 | return fittyText.offsetWidth > 1000 && fittyText.offsetWidth <= 1280;
29 | });
30 |
31 | await page.waitForFunction(() => {
32 | const fittyText = document.querySelector('#styled') as HTMLDivElement;
33 | return fittyText.offsetWidth > 1000 && fittyText.offsetWidth <= 1280;
34 | });
35 | }, 45_000);
36 |
37 | afterAll(async () => {
38 | await browser?.close();
39 | });
40 |
--------------------------------------------------------------------------------
/example/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 | import Typography from '@material-ui/core/Typography';
4 | import styled from 'styled-components';
5 | import { ReactFitty } from '../src';
6 |
7 | const StyledText = styled(ReactFitty)`
8 | color: red;
9 | text-decoration: underline;
10 | font-size: 100%;
11 | font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif;
12 | `;
13 |
14 | const flexDiv = {
15 | height: "50%",
16 | width: "100%",
17 | display: "flex",
18 | justifyContent: "center",
19 | alignItems: "center",
20 | textAlign: "center"
21 | };
22 |
23 | const App = () => {
24 | return (
25 |
26 | {/* todo support style and className on Wrapper(root div) */}
27 |
Mussum Ipsum, cacilds
28 |
29 |
30 | TEST1
31 |
32 |
33 |
Mussum Ipsum, cacilds
34 |
35 |
36 | Mussum Ipsum, cacilds
37 |
38 |
39 |
Mussum Ipsum, cacilds
40 |
41 | );
42 | };
43 |
44 | ReactDOM.render(, document.getElementById('root'));
45 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
3 | "include": ["src", "types"],
4 | "compilerOptions": {
5 | "module": "esnext",
6 | "lib": ["dom", "esnext"],
7 | "importHelpers": true,
8 | // output .d.ts declaration files for consumers
9 | "declaration": true,
10 | // output .js.map sourcemap files for consumers
11 | "sourceMap": true,
12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index
13 | "rootDir": "./src",
14 | // stricter type-checking for stronger correctness. Recommended by TS
15 | "strict": true,
16 | // linter checks for common issues
17 | "noImplicitReturns": true,
18 | "noFallthroughCasesInSwitch": true,
19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | // use Node's module resolution algorithm, instead of the legacy TS one
23 | "moduleResolution": "node",
24 | // transpile JSX to React.createElement
25 | "jsx": "react",
26 | // interop between ESM and CJS modules. Recommended by TS
27 | "esModuleInterop": true,
28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
29 | "skipLibCheck": true,
30 | // error out if import and file system have a casing mismatch. Recommended by TS
31 | "forceConsistentCasingInFileNames": true,
32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc`
33 | "noEmit": true,
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Fitty
2 | 
3 |
4 | Scales up (or down) text so it fits perfectly to its parent container.
5 |
6 | 1.5 KB
7 |
8 | Ideal for flexible and responsive websites.
9 |
10 | # Usage
11 | ```javascript
12 | import { ReactFitty } from "react-fitty";
13 |
14 | const MyComponent = () => (
15 |
16 | Text Text Text Text
17 |
18 | )
19 | ```
20 | ### with Material-UI
21 | ```javascript
22 | import { ReactFitty } from "react-fitty";
23 | import { Typography } from "@material-ui/core"
24 |
25 | const MyComponent = () => (
26 |
27 | Text Text Text
28 |
29 | )
30 | ```
31 |
32 | ### with Styled-Components
33 | ```javascript
34 | import { ReactFitty } from "react-fitty";
35 | import styled from "styled-components";
36 |
37 | const TextStyled = styled(ReactFitty)`
38 | color: red;
39 | text-decoration: underline;
40 | `;
41 |
42 | const MyComponent = () => (
43 |
44 | Text Text Text
45 |
46 | )
47 | ```
48 |
49 | ## Props
50 | | Prop | Type | Optional | Description
51 | | :---: | :---: | :---: | :---: |
52 | | minSize | number | ✓ | Min text size in pixels, default: 16
53 | | maxSize | number | ✓ | Max text size in pixels, max: 512
54 | | wrapText | boolean | ✓ | Wrap lines when using minimum font size., default: false
55 | | observeMutations | Object | ✓ | The object be will merged with the default react-fitty MutationObserver internal config [https://javascript.info/mutation-observer](https://javascript.info/mutation-observer)
56 |
57 | ## Development Commands
58 |
59 | #### Run tests in a real browser
60 | ```bash
61 | yarn ci
62 | ```
63 |
64 | ## Acknowledgements
65 | [@rikschennink](https://github.com/rikschennink) for having created [fitty](https://github.com/rikschennink/fitty).
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-fitty",
3 | "author": "lucasljj@gmail.com",
4 | "module": "dist/react-fitty.esm.js",
5 | "keywords": ["fitty", "react", "react-fitty"],
6 | "repository": "https://github.com/morhogg/react-fitty",
7 | "version": "1.0.1",
8 | "license": "MIT",
9 | "main": "dist/index.js",
10 | "typings": "dist/index.d.ts",
11 | "exports": {
12 | ".": {
13 | "import": "./dist/react-fitty.esm.js",
14 | "require": "./dist/index.js"
15 | }
16 | },
17 | "files": [
18 | "dist"
19 | ],
20 | "engines": {
21 | "node": ">=10"
22 | },
23 | "scripts": {
24 | "start-server": "cd ./example && yarn start",
25 | "start": "tsdx watch",
26 | "build": "tsdx build",
27 | "test": "tsdx test",
28 | "lint": "tsdx lint",
29 | "prepare": "tsdx build",
30 | "size": "size-limit",
31 | "analyze": "size-limit --why",
32 | "ci": "start-server-and-test 'yarn start-server' http://localhost:1234 'yarn test'"
33 | },
34 | "peerDependencies": {
35 | "react": ">=16.8"
36 | },
37 | "husky": {
38 | "hooks": {
39 | "pre-commit": "tsdx lint"
40 | }
41 | },
42 | "prettier": {
43 | "printWidth": 140,
44 | "tabWidth": 4,
45 | "semi": true,
46 | "singleQuote": true,
47 | "trailingComma": "es5"
48 | },
49 | "size-limit": [
50 | {
51 | "path": "dist/react-fitty.cjs.production.min.js",
52 | "limit": "1.5 KB"
53 | },
54 | {
55 | "path": "dist/react-fitty.esm.js",
56 | "limit": "1.5 KB"
57 | }
58 | ],
59 | "devDependencies": {
60 | "@babel/preset-typescript": "^7.12.7",
61 | "@size-limit/preset-small-lib": "^4.9.1",
62 | "@skypack/package-check": "^0.2.2",
63 | "@types/node": "^14.14.16",
64 | "@types/puppeteer": "^5.4.2",
65 | "@types/react": "^17.0.0",
66 | "@types/react-dom": "^17.0.0",
67 | "husky": "^4.3.6",
68 | "puppeteer": "^5.5.0",
69 | "react": "^17.0.1",
70 | "react-dom": "^17.0.1",
71 | "size-limit": "^4.9.1",
72 | "start-server-and-test": "^1.11.6",
73 | "tsdx": "^0.14.1",
74 | "tslib": "^2.0.3",
75 | "typescript": "^4.1.3"
76 | },
77 | "dependencies": {
78 | "fitty": "2"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import fitty from 'fitty';
3 |
4 | const fullWidth = { width: '100%' };
5 |
6 | // todo support style and className on Wrapper(root div) and Ref div
7 | // one solution could be adding style prop for root and another styleProp to ref
8 |
9 | /**
10 | * Snugly resizes text to fit its parent container width
11 | */
12 | export const ReactFitty = React.forwardRef<
13 | HTMLElement,
14 | React.HTMLAttributes & { children?: React.ReactNode; minSize?: number; maxSize?: number; wrapText?: boolean }
15 | >(function ReactFitty(
16 | { children, minSize = 12, maxSize = 512, wrapText = false, ...rest },
17 | ref: React.MutableRefObject | ((instance: any) => void) | null
18 | ) {
19 | const internalRef = React.useRef(null);
20 |
21 | /**
22 | * Need to use the correct ref because the component ref can contain a className that dynamically
23 | * change the text size
24 | */
25 | const correctRef = (ref as React.MutableRefObject) || internalRef;
26 |
27 | React.useLayoutEffect(() => {
28 | const effectRef = (ref as React.MutableRefObject) || internalRef;
29 | const fitInstance = fitty(effectRef!.current, {
30 | minSize: minSize,
31 | maxSize: maxSize,
32 | multiLine: wrapText,
33 | observeMutations: {
34 | subtree: true,
35 | childList: true,
36 | characterData: true,
37 | attributeFilter: ['class'],
38 | },
39 | });
40 |
41 | // wait browser finish text width calc with relative properties like rem and %
42 | // then, fit text in the next animation frame
43 | // maybe that needed to be handled in fitty?
44 | setTimeout(() => {
45 | fitInstance.fit();
46 | }, 0);
47 |
48 | return () => {
49 | fitty(effectRef.current!).unsubscribe();
50 | };
51 | }, []);
52 |
53 | // fitty need an extra div to avoid parent padding issue
54 | // see https://github.com/rikschennink/fitty/issues/20
55 | return (
56 |
57 |
}>
58 | {children}
59 |
60 |
61 | );
62 | });
63 |
--------------------------------------------------------------------------------