12 |
13 | :warning: This project is highly experimental. Use at your own risk!
14 |
15 | ## Table of Contents
16 |
17 |
18 | - [Components](#components)
19 | * [TightenText](#tightentext)
20 | + [Props](#props)
21 | * [PreventWidows](#preventwidows)
22 | + [Props](#props-1)
23 | * [Justify](#justify)
24 | + [Props](#props-2)
25 | * [FontObserver](#fontobserver)
26 | + [Props](#props-3)
27 | * [FontObserver.Provider](#fontobserverprovider)
28 | + [Props](#props-4)
29 | * [Typesetting.Provider](#typesettingprovider)
30 | + [Props](#props-5)
31 |
32 |
33 | ## Components
34 |
35 |
36 |
37 | ### TightenText
38 |
39 | ```js
40 | import { TightenText } from 'react-typesetting';
41 | ```
42 |
43 | Tightens `word-spacing`, `letter-spacing`, and `font-size` (in that order)
44 | by the minimum amount necessary to ensure a minimal number of wrapped lines
45 | and overflow.
46 |
47 | The algorithm starts by setting the minimum of all values (defined by the
48 | `minWordSpacing`, `minLetterSpacing`, and `minFontSize` props) to determine
49 | whether adjusting these will result in fewer wrapped lines or less overflow.
50 | If so, then a binary search is performed (with at most `maxIterations`) to
51 | find the best fit.
52 |
53 | By default, element resizes that may necessitate refitting the text are
54 | automatically detected. By specifying the `reflowKey` prop, you can instead
55 | take manual control by changing the prop whenever you’d like the component to
56 | update.
57 |
58 | Note that unlike with typical justified text, the fit adjustments must apply
59 | to all lines of the text, not just the lines that need to be tightened,
60 | because there is no way to target individual wrapped lines. Thus, this
61 | component is best used sparingly for typographically important short runs
62 | of text, like titles or labels.
63 |
64 | #### Props
65 |
66 |
67 |
68 |
69 |
Name
70 |
Type
71 |
Default
72 |
Description
73 |
74 |
75 |
76 |
77 |
className
78 |
String
79 |
80 |
81 |
82 | The class to apply to the outer wrapper `span` created by this component.
83 |
84 |
85 |
86 |
87 |
style
88 |
Object
89 |
90 |
91 |
92 | Extra style properties to add to the outer wrapper `span` created by this
93 | component.
94 |
95 |
96 |
97 |
98 |
children
99 |
Node
100 |
101 |
102 |
103 | The content to render.
104 |
105 |
106 |
107 |
108 |
minWordSpacing
109 |
Number
110 |
-0.02
111 |
112 |
113 | Minimum word spacing in ems. Set this to 0 if word spacing should not be
114 | adjusted.
115 |
116 |
117 |
118 |
119 |
minLetterSpacing
120 |
Number
121 |
-0.02
122 |
123 |
124 | Minimum letter spacing in ems. Set this to 0 if word spacing should not
125 | be adjusted.
126 |
127 |
128 |
129 |
130 |
minFontSize
131 |
Number
132 |
0.97
133 |
134 |
135 | Minimum `font-size` in ems. Set this to 1 if font size should not be
136 | adjusted.
137 |
138 |
139 |
140 |
141 |
maxIterations
142 |
Number
143 |
5
144 |
145 |
146 | When performing a binary search to find the optimal value of each CSS
147 | property, this sets the maximum number of iterations to run before
148 | settling on a value.
149 |
150 |
151 |
152 |
153 |
reflowKey
154 |
155 | One of…
156 | Number
157 | String
158 |
159 |
160 |
161 |
162 | If specified, disables automatic reflow so that you can trigger it
163 | manually by changing this value. The prop itself does nothing, but
164 | changing it will cause React to update the component.
165 |
166 |
167 |
168 |
169 |
reflowTimeout
170 |
Number
171 |
172 |
173 |
174 | Debounces reflows so they happen at most this often in milliseconds (at
175 | the end of the given duration). If not specified, reflow is computed
176 | every time the component is rendered.
177 |
178 |
179 |
180 |
181 |
disabled
182 |
Boolean
183 |
184 |
185 |
186 | Whether to completely disable refitting the text. Any fit adjustments
187 | that have already been applied in a previous render will be preserved.
188 |
189 |
190 |
191 |
192 |
onReflow
193 |
Function
194 |
195 |
196 |
197 | A function to call when layout has been recomputed and the text is done
198 | refitting.
199 |
200 |
201 |
202 |
203 |
preset
204 |
String
205 |
206 |
207 |
208 | The name of a preset defined in an outer `Typesetting.Provider`
209 | component. If it exists, default values for all other props will come
210 | from the specified preset.
211 |
212 |
213 |
214 |
215 |
216 |
217 | ### PreventWidows
218 |
219 | ```js
220 | import { PreventWidows } from 'react-typesetting';
221 | ```
222 | Prevents [widows](https://www.fonts.com/content/learning/fontology/level-2/text-typography/rags-widows-orphans)
223 | by measuring the width of the last line of text rendered by the component’s
224 | children. Spaces will be converted to non-breaking spaces until the given
225 | minimum width or the maximum number of substitutions is reached.
226 |
227 | By default, element resizes that may necessitate recomputing line widths are
228 | automatically detected. By specifying the `reflowKey` prop, you can instead
229 | take manual control by changing the prop whenever you’d like the component to
230 | update.
231 |
232 | #### Props
233 |
234 |
235 |
236 |
237 |
Name
238 |
Type
239 |
Default
240 |
Description
241 |
242 |
243 |
244 |
245 |
className
246 |
String
247 |
248 |
249 |
250 | The class to apply to the outer wrapper `span` created by this component.
251 |
252 |
253 |
254 |
255 |
style
256 |
Object
257 |
258 |
259 |
260 | Extra style properties to add to the outer wrapper `span` created by this
261 | component.
262 |
263 |
264 |
265 |
266 |
children
267 |
Node
268 |
269 |
270 |
271 | The content to render.
272 |
273 |
274 |
275 |
276 |
maxSubstitutions
277 |
Number
278 |
3
279 |
280 |
281 | The maximum number of spaces to substitute.
282 |
283 |
284 |
285 |
286 |
minLineWidth
287 |
288 | One of…
289 | Number
290 | String
291 | Function
292 |
293 |
15%
294 |
295 |
296 | The minimum width of the last line, below which non-breaking spaces will
297 | be inserted until the minimum is met.
298 |
299 | * **Numbers** indicate an absolute pixel width.
300 | * **Strings** indicate a CSS `width` value that will be computed by
301 | temporarily injecting an element into the container and determining its
302 | width.
303 | * **Functions** will be called with relevant data to determine a dynamic
304 | number or string value to return. This can be used, for example, to
305 | have different rules at different breakpoints – like a media query.
306 |
307 |
308 |
309 |
310 |
nbspChar
311 |
312 | One of…
313 | String
314 | React Element
315 | Function
316 |
317 |
\u00A0
318 |
319 |
320 | A character or element to use when substituting spaces. Defaults to a
321 | standard non-breaking space character, which you should almost certainly
322 | stick with unless you want to visualize where non-breaking spaces are
323 | being inserted for debugging purposes, or adjust their width.
324 |
325 | * **String** values will be inserted directly into the existing Text node
326 | containing the space.
327 | * **React Element** values will be rendered into an in-memory “incubator”
328 | node, then transplanted into the DOM, splitting up the Text node in
329 | which the space was found.
330 | * **Function** values must produce a string, Text node, Element node, or
331 | React Element to insert.
332 |
333 |
334 |
335 |
336 |
reflowKey
337 |
338 | One of…
339 | Number
340 | String
341 |
342 |
343 |
344 |
345 | If specified, disables automatic reflow so that you can trigger it
346 | manually by changing this value. The prop itself does nothing, but
347 | changing it will cause React to update the component.
348 |
349 |
350 |
351 |
352 |
reflowTimeout
353 |
Number
354 |
355 |
356 |
357 | Debounces reflows so they happen at most this often in milliseconds (at
358 | the end of the given duration). If not specified, reflow is computed
359 | every time the component is rendered.
360 |
361 |
378 |
379 | A function to call when layout has been recomputed and space substitution
380 | is done.
381 |
382 |
383 |
384 |
385 |
preset
386 |
String
387 |
388 |
389 |
390 | The name of a preset defined in an outer `Typesetting.Provider`
391 | component. If it exists, default values for all other props will come
392 | from the specified preset.
393 |
394 |
395 |
396 |
397 |
398 |
399 | ### Justify
400 |
401 | ```js
402 | import { Justify } from 'react-typesetting';
403 | ```
404 |
405 | While this may include more advanced justification features in the future, it
406 | is currently very simple: it conditionally applies `text-align: justify` to
407 | its container element (a `
` by default) depending on whether or not there
408 | is enough room to avoid large, unseemly word gaps. The minimum width is
409 | defined by `minWidth` and defaults to 16 ems.
410 |
411 | You might also accomplish this with media queries, but this component can
412 | determine the exact width available to the container element instead of just
413 | the entire page.
414 |
415 | #### Props
416 |
417 |
418 |
419 |
420 |
Name
421 |
Type
422 |
Default
423 |
Description
424 |
425 |
426 |
427 |
428 |
className
429 |
String
430 |
431 |
432 |
433 | The class to apply to the outer wrapper element created by this
434 | component.
435 |
436 |
437 |
438 |
439 |
style
440 |
Object
441 |
442 |
443 |
444 | Extra style properties to add to the outer wrapper element created by
445 | this component.
446 |
447 |
448 |
449 |
450 |
children
451 |
Node
452 |
453 |
454 |
455 | The content to render.
456 |
457 |
458 |
459 |
460 |
as
461 |
462 | One of…
463 | String
464 | Function
465 | Object
466 |
467 |
p
468 |
469 |
470 | The element type in which to render the supplied children. It must be
471 | a block level element, like `p` or `div`, since `text-align` has no
472 | effect on inline elements. It may also be a custom React component, as
473 | long as it uses `forwardRef`.
474 |
475 |
476 |
477 |
478 |
minWidth
479 |
480 | One of…
481 | Number
482 | String
483 |
484 |
16em
485 |
486 |
487 | The minimum width at which to allow justified text. Numbers indicate an
488 | absolute pixel width. Strings will be applied to an element's CSS in
489 | order to perform the width calculation.
490 |
491 |
492 |
493 |
494 |
initialJustify
495 |
Boolean
496 |
true
497 |
498 |
499 | Whether or not to initially set `text-align: justify` before the
500 | available width has been determined.
501 |
502 |
503 |
504 |
505 |
reflowKey
506 |
507 | One of…
508 | Number
509 | String
510 |
511 |
512 |
513 |
514 | If specified, disables automatic reflow so that you can trigger it
515 | manually by changing this value. The prop itself does nothing, but
516 | changing it will cause React to update the component.
517 |
518 |
519 |
520 |
521 |
reflowTimeout
522 |
Number
523 |
524 |
525 |
526 | Debounces reflows so they happen at most this often in milliseconds (at
527 | the end of the given duration). If not specified, reflow is computed
528 | every time the component is rendered.
529 |
530 |
531 |
532 |
533 |
disabled
534 |
Boolean
535 |
536 |
537 |
538 | Whether to completely disable justification detection. The last
539 | alignment that was applied will be preserved.
540 |
541 |
542 |
543 |
544 |
onReflow
545 |
Function
546 |
547 |
548 |
549 | A function to call when layout has been recomputed and justification
550 | has been applied or unapplied.
551 |
552 |
553 |
554 |
555 |
preset
556 |
String
557 |
558 |
559 |
560 | The name of a preset defined in an outer `Typesetting.Provider`
561 | component. If it exists, default values for all other props will come
562 | from the specified preset.
563 |
564 |
565 |
566 |
567 |
568 |
569 | ### FontObserver
570 |
571 | ```js
572 | import { FontObserver } from 'react-typesetting';
573 | ```
574 |
575 | A component for observing the fonts specified in the `FontObserver.Provider`
576 | component.
577 |
578 | #### Props
579 |
580 |
581 |
582 |
583 |
Name
584 |
Type
585 |
Default
586 |
Description
587 |
588 |
589 |
590 |
591 |
children
592 |
Function
593 |
594 |
595 |
596 | A function that will receive the current status of the observed fonts.
597 | The argument will be an object with these properties:
598 |
599 | - `fonts`: An array of the fonts passed to `FontObserver.Provider`, each
600 | with a `loaded` and `error` property.
601 | - `loaded`: Whether all observed fonts are done loading.
602 | - `error`: If any fonts failed to load, this will be populated with the
603 | first error.
604 |
605 |
606 |
607 |
608 |
609 |
610 | ### FontObserver.Provider
611 |
612 | ```js
613 | import { FontObserver } from 'react-typesetting';
614 | ```
615 |
616 | A context provider for specifying which fonts to observe.
617 |
618 | #### Props
619 |
620 |
640 |
641 | The fonts to load and observe. The font files themselves should already
642 | be specified somewhere (in CSS), this component simply uses `FontFaceObserver`
643 | to force them to load (if necessary) and observe when they are ready.
644 |
645 | Each item in the array specifies the font `family`, `weight`, `style`,
646 | and `stretch`, with only `family` being required. Additionally, each item
647 | can contain a custom `testString` and `timeout` for that font, if they
648 | should differ from the defaults. If only the family name is needed, the
649 | array item can just be a string.
650 |
651 |
667 |
668 | A custom test string to pass to the `load` method of `FontFaceObserver`,
669 | to be used for all fonts that do not specify their own `testString`.
670 |
671 |
672 |
673 |
674 |
timeout
675 |
Number
676 |
677 |
678 |
679 | A custom timeout in milliseconds to pass to the `load` method of
680 | `FontFaceObserver`, to be used for all fonts that do not specify their
681 | own `timeout`.
682 |
683 |
684 |
685 |
686 |
children
687 |
Node
688 |
689 |
690 |
691 | The content that will have access to font loading status via context.
692 |
693 |
694 |
695 |
696 |
697 |
698 | ### Typesetting.Provider
699 |
700 | ```js
701 | import { Typesetting } from 'react-typesetting';
702 | ```
703 |
704 | A context provider for defining presets for all other `react-typesetting`
705 | components to use.
706 |
707 | #### Props
708 |
709 |
710 |
711 |
712 |
Name
713 |
Type
714 |
Default
715 |
Description
716 |
717 |
718 |
719 |
720 |
presets
721 |
Object
722 |
{}
723 |
724 |
725 | An object mapping preset names to default props. For example, given the
726 | value:
727 |
728 | ```js
729 | { myPreset: { minFontSize: 1, maxIterations: 3 } }
730 | ```
731 |
732 | …the `TightenText` component could use this preset by specifying the
733 | `preset` prop:
734 |
735 | ```jsx
736 |
737 | ```
738 |
739 |
740 |
741 |
742 |
children
743 |
Node
744 |
745 |
746 |
747 | The content that will have access to the defined presets via context.
748 |
749 |
This page is a demonstration of react-typesetting, a collection of React components for projects that emphasize text-heavy design.
<TightenText>
The TightenText component is intended to give short runs of text (like titles, labels, etc.) some “give” before wrapping. This is useful when you want to prioritize having fewer lines of text over having completely rigid visual tightness.
When a line is just slightly too long for the available space, the text will be set tighter by a barely perceptible amount to avoid wrapping. By default, adjustments are made to word spacing, letter spacing, and font size (preferentially in that order).
Try dragging to adjust the available space for this line from a cocktail recipe:
Drag to resize!
Islay single malt Scotch whisky
Notice that the text resists both wrapping (when a new line would be formed) and overflowing (when the words can’t be broken any more) – up to a certain point.
<PreventWidows>
Although the terminology varies, “widows” often refer to very short lines of text at the end of paragraphs. This tends to be undesirable during typesetting, as it gives the appearance of too much whitespace between the paragraph and any elements that follow, and can be distracting. It is generally preferable to find a way to either remove the extra line (a laTightenText) or make it longer. If possible, you can even just opt to reword your writing.
Many HTML typesetting helpers implement this in a naïve way – for example, by always joining the last word with a non-breaking space. This gives poor results, since it does not account for how long the last line actually is. Try the naïve approach to see how it fails to achieve the desired wrapping:
Drag to resize!
The Long Goodbye
The PreventWidows component instead works by actually measuring the widths of lines, and can thus support many different ways to specify the desired minimum width – including percentages, pixels, and ems. The default minimum is 15% of the available width.
In this demo, a custom nbspChar element is supplied that highlights the inserted spaces for demonstration purposes:
Drag to resize!
Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I…
The current strategy works especially well with justified text, since there is no rag on the preceding line to worry about:
Drag to resize!
One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a horrible vermin. He lay on his armour-like back, and if he lifted his head a little he could see his brown belly, slightly domed and divided by arches into stiff sections. The bedding was hardly able to cover it and seemed ready to slide off any moment. His many legs, pitifully thin compared with the size of the rest of him, waved about helplessly as he looked.
<Justify>
Sometimes you want to render justified text, but due to changing element sizes in a responsive design, it could make things worse. Justified text tends to look bad in narrow columns of text, because it forces very large spaces between the words. For example:
Drag to resize!
There was no possibility of taking a walk that day. We had been wandering, indeed, in the leafless shrubbery an hour in the morning; but since dinner (Mrs. Reed, when there was no company, dined early) the cold winter wind had brought with it clouds so sombre, and a rain so penetrating, that further out-door exercise was now out of the question.
The Justify component solves this by conditionally setting text as justified only when there is enough room, and otherwise the text will inherit its alignment as normal. Here is the same paragraph as above, but using conditional justification. Try making it wider:
Drag to resize!
There was no possibility of taking a walk that day. We had been wandering, indeed, in the leafless shrubbery an hour in the morning; but since dinner (Mrs. Reed, when there was no company, dined early) the cold winter wind had brought with it clouds so sombre, and a rain so penetrating, that further out-door exercise was now out of the question.
<FontObserver>
When creating pages with typographically important features, sometimes you’ll want to know when your custom fonts are done loading. Perhaps you’ve done some rendering calculations that are influenced by font metrics (like how wide a line of text is) and thus need to recompute them when your font is shown? The components above are great examples of this.
The FontObserver component offers an interface to this information. By supplying FontObserver.Provider a list of fonts to observe, it will use Font Face Observer to populate a React context provider with status information for each font. You can then use FontObserver anywhere in the subtree to get updates.
Below is a list of the fonts used on this page, rendered using data from FontObserver. The symbol rendered next to each font is based on the loaded and error properties that are populated for each font.
--------------------------------------------------------------------------------
/markdown.config.js:
--------------------------------------------------------------------------------
1 | const execa = require("execa");
2 |
3 | module.exports = {
4 | transforms: {
5 | COMPONENTS(content, options) {
6 | const json = execa.sync("scripts/extract-docs.js").stdout;
7 | const markdown = execa.sync("scripts/markdown-docs.js", {
8 | input: json
9 | }).stdout;
10 | return markdown;
11 | }
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | assetPrefix: ".",
3 | exportPathMap: () => ({
4 | "/": { page: "/" }
5 | })
6 | };
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-typesetting",
3 | "version": "0.3.1",
4 | "main": "dist-cjs/index.js",
5 | "author": "Brian Beck ",
6 | "license": "MIT",
7 | "files": [
8 | "dist-cjs",
9 | "dist-esm"
10 | ],
11 | "module": "dist-esm/index.js",
12 | "scripts": {
13 | "build": "npm run build:demo && npm run build:dist && npm run build:docs",
14 | "build:demo": "rimraf .next docs && next build && next export -o docs && touch docs/.nojekyll",
15 | "build:dist": "npm run build:dist-esm && npm run build:dist-cjs",
16 | "build:dist-cjs": "rimraf dist-cjs && BABEL_ENV=cjs babel src -d dist-cjs",
17 | "build:dist-esm": "rimraf dist-esm && BABEL_ENV=esm babel src -d dist-esm",
18 | "build:docs": "md-magic README.md",
19 | "format": "npm run lint -- --fix",
20 | "lint": "eslint demo pages scripts src *.js",
21 | "prepare": "npm run build:dist",
22 | "start": "next start",
23 | "start:dev": "next dev",
24 | "test": "npm run lint"
25 | },
26 | "husky": {
27 | "hooks": {
28 | "pre-commit": "npm run lint"
29 | }
30 | },
31 | "peerDependencies": {
32 | "react": "^16.3.0",
33 | "react-dom": "^16.3.0"
34 | },
35 | "dependencies": {
36 | "debug": "^4.0.1",
37 | "fontfaceobserver": "^2.0.13",
38 | "prop-types": "^15.6.2",
39 | "resize-observer-polyfill": "^1.5.0"
40 | },
41 | "devDependencies": {
42 | "@babel/cli": "^7.1.2",
43 | "@babel/core": "^7.1.2",
44 | "babel-eslint": "^10.0.1",
45 | "babel-plugin-styled-components": "^1.8.0",
46 | "eslint": "^5.6.1",
47 | "eslint-config-prettier": "^3.1.0",
48 | "eslint-config-standard": "^12.0.0",
49 | "eslint-config-standard-react": "^7.0.2",
50 | "eslint-plugin-import": ">=2.13.0",
51 | "eslint-plugin-node": ">=7.0.0",
52 | "eslint-plugin-prettier": "^3.0.0",
53 | "eslint-plugin-promise": ">=4.0.0",
54 | "eslint-plugin-react": "^7.11.1",
55 | "eslint-plugin-standard": ">=4.0.0",
56 | "execa": "^1.0.0",
57 | "husky": "^1.1.1",
58 | "jest": "^23.6.0",
59 | "markdown-magic": "^0.1.25",
60 | "next": "^7.0.1",
61 | "prettier": "^1.14.3",
62 | "react": "^16.5.2",
63 | "react-docgen": "^2.21.0",
64 | "react-docgen-displayname-handler": "^2.1.1",
65 | "react-dom": "^16.5.2",
66 | "react-draggable": "^3.0.5",
67 | "react-responsive": "^5.0.0",
68 | "react-testing-library": "^5.2.0",
69 | "rimraf": "^2.6.2",
70 | "styled-components": "^4.0.0-beta.10",
71 | "webpack": "^4.20.2"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import NextDocument, { Head, Main, NextScript } from "next/document";
3 | import { ServerStyleSheet } from "styled-components";
4 |
5 | export default class Document extends NextDocument {
6 | static getInitialProps({ renderPage }) {
7 | const sheet = new ServerStyleSheet();
8 | const page = renderPage(App => props =>
9 | sheet.collectStyles()
10 | );
11 | const styleTags = sheet.getStyleElement();
12 | return { ...page, styleTags };
13 | }
14 |
15 | render() {
16 | return (
17 |
18 |
19 |
20 |
24 | {this.props.styleTags}
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Head from "next/head";
3 | import styled, { createGlobalStyle } from "styled-components";
4 | import MediaQuery from "react-responsive";
5 | import Resizable from "../demo/Resizable";
6 | import { TightenText, PreventWidows, Justify, FontObserver } from "../src";
7 |
8 | const VisibleSpace = styled.span.attrs({
9 | children: props => "\u00a0",
10 | title: "non-breaking space inserted by "
11 | })`
12 | background: rgb(255, 230, 3);
13 | `;
14 |
15 | const GlobalStyle = createGlobalStyle`
16 | html {
17 | font-size: 14px;
18 | overflow-x: hidden;
19 |
20 | @media (min-width: 768px) {
21 | font-size: 16px;
22 | }
23 | }
24 |
25 | body {
26 | margin: 0;
27 | padding: 20px;
28 | font-family: 'Libre Baskerville', Georgia, serif;
29 | font-size: 1rem;
30 | line-height: 1.7;
31 | text-rendering: optimizeLegibility;
32 | background: rgb(232, 230, 224);
33 | color: rgb(52, 50, 47);
34 | overflow-x: hidden;
35 |
36 | @media (min-width: 768px) {
37 | padding: 50px 100px;
38 | }
39 | }
40 |
41 | code {
42 | font-family: Menlo, Monaco, Consolas, 'Source Sans Pro', monospace;
43 | padding: 2px 4px;
44 | border-radius: 2px;
45 | background: rgba(74, 65, 59, 0.1);
46 | color: rgb(66, 64, 60);
47 | }
48 |
49 | abbr {
50 | font-size: 0.9em;
51 | letter-spacing: -0.01em;
52 | }
53 |
54 | a:link {
55 | color: rgb(0, 70, 162);
56 | }
57 |
58 | a:visited {
59 | color: rgb(54, 36, 140);
60 | }
61 | `;
62 |
63 | const MainContent = styled.main`
64 | max-width: 60ch;
65 | `;
66 |
67 | const PageTitle = styled.h1`
68 | font-size: 1.8rem;
69 | font-weight: 700;
70 | letter-spacing: -0.02em;
71 | `;
72 |
73 | const SectionTitle = styled.h2`
74 | font-size: 1.5rem;
75 | font-weight: normal;
76 | `;
77 |
78 | const DemoResizable = styled(Resizable)`
79 | font-size: ${18 / 16}rem;
80 | line-height: 1.4;
81 | margin-bottom: 40px;
82 | padding-top: 0.25em;
83 | padding-bottom: 0.25em;
84 | background: #fff;
85 |
86 | @media (max-width: 767px) {
87 | max-width: 100%;
88 | }
89 | `;
90 |
91 | function ReflowResizable({ children, ...props }) {
92 | return (
93 |
94 | {width => (
95 |
96 | {isDesktop => (
97 |
98 | {status =>
99 | typeof children === "function"
100 | ? children(
101 | `${width}-${isDesktop}-${status.loaded}-${status.error}`
102 | )
103 | : children
104 | }
105 |
106 | )}
107 |
108 | )}
109 |
110 | );
111 | }
112 |
113 | export default class App extends React.Component {
114 | render() {
115 | return (
116 | <>
117 |
118 |
119 |
120 | react-typesetting ❧ React components for creating beautifully
121 | typeset designs
122 |
123 |
124 |
132 |
133 | react-typesetting
134 |
135 |
136 |
137 | This page is a demonstration of{" "}
138 |
143 | react-typesetting
144 |
145 | , a collection of React components for projects that emphasize
146 | text-heavy design.
147 |
148 |
149 |
150 |
151 | <TightenText>
152 |
153 |
154 |
155 | The TightenText component is intended to give
156 | short runs of text (like titles, labels, etc.) some “give”
157 | before wrapping. This is useful when you want to prioritize
158 | having fewer lines of text over having completely rigid visual
159 | tightness.
160 |
161 |
162 |
163 |
164 | When a line is just slightly too long for the available space,
165 | the text will be set tighter by a barely perceptible amount to
166 | avoid wrapping. By default, adjustments are made to word
167 | spacing, letter spacing, and font size (preferentially in that
168 | order).
169 |
170 |
171 |
172 |
173 | Try dragging to adjust the available space for this line from
174 | a cocktail recipe:
175 |
176 |
177 |
178 |
179 | {reflowKey => (
180 |
181 | Islay single malt Scotch whisky
182 |
183 | )}
184 |
185 |
186 |
187 |
188 | Notice that the text resists both wrapping (when a new line
189 | would be formed) and overflowing (when the words can’t be
190 | broken any more) – up to a certain point.
191 |
192 |
193 |
194 |
195 |
196 | <PreventWidows>
197 |
198 |
199 |
200 | Although the terminology varies, “widows” often refer to very
201 | short lines of text at the end of paragraphs. This tends to be
202 | undesirable during typesetting, as it gives the appearance of
203 | too much whitespace between the paragraph and any elements
204 | that follow, and can be distracting. It is generally
205 | preferable to find a way to either remove the extra line (
206 | a laTightenText) or make it longer. If
207 | possible, you can even just opt to reword your writing.
208 |
209 |
210 |
211 |
212 |
213 | Many HTML typesetting helpers implement this in a
214 | naïve way – for example, by always joining the last word with
215 | a{" "}
216 |
221 | non-breaking space
222 |
223 | . This gives poor results, since it does not account for how
224 | long the last line actually is. Try the naïve approach to see
225 | how it fails to achieve the desired wrapping:
226 |
227 |
228 |
229 |
230 | The Long Goodbye
231 |
232 |
233 |
234 |
235 | The PreventWidows component instead works by
236 | actually measuring the widths of lines, and can thus support
237 | many different ways to specify the desired minimum width –
238 | including percentages, pixels, and ems. The default minimum is
239 | 15% of the available width.
240 |
241 |
242 |
243 |
244 |
245 | In this demo, a custom nbspChar element is
246 | supplied that highlights the inserted spaces for demonstration
247 | purposes:
248 |
249 |
250 |
251 |
252 | {reflowKey => (
253 | }
255 | reflowKey={reflowKey}
256 | >
257 | Call me Ishmael. Some years ago—never mind how long
258 | precisely—having little or no money in my purse, and nothing
259 | particular to interest me on shore, I thought I would sail
260 | about a little and see the watery part of the world. It is a
261 | way I…
262 |
263 | )}
264 |
265 |
266 |
267 |
268 |
269 | The current strategy works especially well with justified text,
270 | since there is no rag on the preceding line to worry about:
271 |
272 |
273 |
274 |
275 | {reflowKey => (
276 |
277 | }
279 | reflowKey={reflowKey}
280 | >
281 | One morning, when Gregor Samsa woke from troubled dreams, he
282 | found himself transformed in his bed into a horrible vermin.
283 | He lay on his armour-like back, and if he lifted his head a
284 | little he could see his brown belly, slightly domed and
285 | divided by arches into stiff sections. The bedding was
286 | hardly able to cover it and seemed ready to slide off any
287 | moment. His many legs, pitifully thin compared with the size
288 | of the rest of him, waved about helplessly as he looked.
289 |
290 |
291 | )}
292 |
293 |
294 |
295 | <Justify>
296 |
297 |
298 |
299 | Sometimes you want to render justified text, but due to
300 | changing element sizes in a responsive design, it could make
301 | things worse. Justified text tends to look bad in narrow
302 | columns of text, because it forces very large spaces between
303 | the words. For example:
304 |
305 |
306 |
307 |
308 |
309 | There was no possibility of taking a walk that day. We had
310 | been wandering, indeed, in the leafless shrubbery an hour in
311 | the morning; but since dinner (Mrs. Reed, when there was no
312 | company, dined early) the cold winter wind had brought with it
313 | clouds so sombre, and a rain so penetrating, that further
314 | out-door exercise was now out of the question.
315 |
316 |
317 |
318 |
319 |
320 | The Justify component solves this by
321 | conditionally setting text as justified only when there is
322 | enough room, and otherwise the text will inherit its alignment
323 | as normal. Here is the same paragraph as above, but using
324 | conditional justification. Try making it wider:
325 |
326 |
327 |
328 |
329 | {reflowKey => (
330 |
331 | There was no possibility of taking a walk that day. We had
332 | been wandering, indeed, in the leafless shrubbery an hour in
333 | the morning; but since dinner (Mrs. Reed, when there was no
334 | company, dined early) the cold winter wind had brought with
335 | it clouds so sombre, and a rain so penetrating, that further
336 | out-door exercise was now out of the question.
337 |
338 | )}
339 |
340 |
341 |
342 |
343 | <FontObserver>
344 |
345 |
346 |
347 | When creating pages with typographically important features,
348 | sometimes you’ll want to know when your custom fonts are done
349 | loading. Perhaps you’ve done some rendering calculations that
350 | are influenced by font metrics (like how wide a line of text
351 | is) and thus need to recompute them when your font is shown?
352 | The components above are great examples of this.
353 |
354 |
355 |
356 |
357 | The FontObserver component offers an interface to
358 | this information. By supplying{" "}
359 | FontObserver.Provider a list of fonts to observe,
360 | it will use{" "}
361 |
366 | Font Face Observer
367 | {" "}
368 | to populate a React context provider with status information
369 | for each font. You can then use FontObserver{" "}
370 | anywhere in the subtree to get updates.
371 |
372 |
373 |
374 |
375 |
376 | Below is a list of the fonts used on this page, rendered using
377 | data from FontObserver. The symbol rendered next
378 | to each font is based on the loaded and{" "}
379 | error properties that are populated for each
380 | font.
381 |
382 |
383 |
384 |