Fix Fast Refresh in <Text> files (#6609)
* Separate non-components from components * Mark old Text as deprecated * Move emoji utilities to non-React file * Fix type * Fix import
This commit is contained in:
@@ -0,0 +1,135 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {TextProps as RNTextProps} from 'react-native'
|
||||||
|
import {StyleProp, TextStyle} from 'react-native'
|
||||||
|
import {UITextView} from 'react-native-uitextview'
|
||||||
|
import createEmojiRegex from 'emoji-regex'
|
||||||
|
|
||||||
|
import {isNative} from '#/platform/detection'
|
||||||
|
import {Alf, applyFonts, atoms, flatten} from '#/alf'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Util to calculate lineHeight from a text size atom and a leading atom
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* `leading(atoms.text_md, atoms.leading_normal)` // => 24
|
||||||
|
*/
|
||||||
|
export function leading<
|
||||||
|
Size extends {fontSize?: number},
|
||||||
|
Leading extends {lineHeight?: number},
|
||||||
|
>(textSize: Size, leading: Leading) {
|
||||||
|
const size = textSize?.fontSize || atoms.text_md.fontSize
|
||||||
|
const lineHeight = leading?.lineHeight || atoms.leading_normal.lineHeight
|
||||||
|
return Math.round(size * lineHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that `lineHeight` defaults to a relative value of `1`, or applies
|
||||||
|
* other relative leading atoms.
|
||||||
|
*
|
||||||
|
* If the `lineHeight` value is > 2, we assume it's an absolute value and
|
||||||
|
* returns it as-is.
|
||||||
|
*/
|
||||||
|
export function normalizeTextStyles(
|
||||||
|
styles: StyleProp<TextStyle>,
|
||||||
|
{
|
||||||
|
fontScale,
|
||||||
|
fontFamily,
|
||||||
|
}: {
|
||||||
|
fontScale: number
|
||||||
|
fontFamily: Alf['fonts']['family']
|
||||||
|
} & Pick<Alf, 'flags'>,
|
||||||
|
) {
|
||||||
|
const s = flatten(styles)
|
||||||
|
// should always be defined on these components
|
||||||
|
s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale
|
||||||
|
|
||||||
|
if (s?.lineHeight) {
|
||||||
|
if (s.lineHeight !== 0 && s.lineHeight <= 2) {
|
||||||
|
s.lineHeight = Math.round(s.fontSize * s.lineHeight)
|
||||||
|
}
|
||||||
|
} else if (!isNative) {
|
||||||
|
s.lineHeight = s.fontSize
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFonts(s, fontFamily)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StringChild = string | (string | null)[]
|
||||||
|
export type TextProps = Omit<RNTextProps, 'children'> & {
|
||||||
|
/**
|
||||||
|
* Lets the user select text, to use the native copy and paste functionality.
|
||||||
|
*/
|
||||||
|
selectable?: boolean
|
||||||
|
/**
|
||||||
|
* Provides `data-*` attributes to the underlying `UITextView` component on
|
||||||
|
* web only.
|
||||||
|
*/
|
||||||
|
dataSet?: Record<string, string | number | undefined>
|
||||||
|
/**
|
||||||
|
* Appears as a small tooltip on web hover.
|
||||||
|
*/
|
||||||
|
title?: string
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
emoji?: true
|
||||||
|
children: StringChild
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
emoji?: false
|
||||||
|
children: RNTextProps['children']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const EMOJI = createEmojiRegex()
|
||||||
|
|
||||||
|
export function childHasEmoji(children: React.ReactNode) {
|
||||||
|
return (Array.isArray(children) ? children : [children]).some(
|
||||||
|
child => typeof child === 'string' && createEmojiRegex().test(child),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function childIsString(
|
||||||
|
children: React.ReactNode,
|
||||||
|
): children is StringChild {
|
||||||
|
return (
|
||||||
|
typeof children === 'string' ||
|
||||||
|
(Array.isArray(children) &&
|
||||||
|
children.every(child => typeof child === 'string' || child === null))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderChildrenWithEmoji(
|
||||||
|
children: StringChild,
|
||||||
|
props: Omit<TextProps, 'children'> = {},
|
||||||
|
) {
|
||||||
|
const normalized = Array.isArray(children) ? children : [children]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UITextView {...props}>
|
||||||
|
{normalized.map(child => {
|
||||||
|
if (typeof child !== 'string') return child
|
||||||
|
|
||||||
|
const emojis = child.match(EMOJI)
|
||||||
|
|
||||||
|
if (emojis === null) {
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
return child.split(EMOJI).map((stringPart, index) => (
|
||||||
|
<UITextView key={index} {...props}>
|
||||||
|
{stringPart}
|
||||||
|
{emojis[index] ? (
|
||||||
|
<UITextView
|
||||||
|
{...props}
|
||||||
|
style={[props?.style, {color: 'black', fontFamily: 'System'}]}>
|
||||||
|
{emojis[index]}
|
||||||
|
</UITextView>
|
||||||
|
) : null}
|
||||||
|
</UITextView>
|
||||||
|
))
|
||||||
|
})}
|
||||||
|
</UITextView>
|
||||||
|
)
|
||||||
|
}
|
||||||
+11
-134
@@ -1,140 +1,17 @@
|
|||||||
import React from 'react'
|
|
||||||
import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
|
|
||||||
import {UITextView} from 'react-native-uitextview'
|
import {UITextView} from 'react-native-uitextview'
|
||||||
import createEmojiRegex from 'emoji-regex'
|
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isIOS, isNative} from '#/platform/detection'
|
import {isIOS} from '#/platform/detection'
|
||||||
import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf'
|
import {atoms, flatten, useAlf, useTheme, web} from '#/alf'
|
||||||
|
import {
|
||||||
|
childHasEmoji,
|
||||||
|
childIsString,
|
||||||
|
normalizeTextStyles,
|
||||||
|
renderChildrenWithEmoji,
|
||||||
|
TextProps,
|
||||||
|
} from '#/alf/typography'
|
||||||
import {IS_DEV} from '#/env'
|
import {IS_DEV} from '#/env'
|
||||||
|
export type {TextProps}
|
||||||
export type StringChild = string | (string | null)[]
|
|
||||||
|
|
||||||
export type TextProps = Omit<RNTextProps, 'children'> & {
|
|
||||||
/**
|
|
||||||
* Lets the user select text, to use the native copy and paste functionality.
|
|
||||||
*/
|
|
||||||
selectable?: boolean
|
|
||||||
/**
|
|
||||||
* Provides `data-*` attributes to the underlying `UITextView` component on
|
|
||||||
* web only.
|
|
||||||
*/
|
|
||||||
dataSet?: Record<string, string | number | undefined>
|
|
||||||
/**
|
|
||||||
* Appears as a small tooltip on web hover.
|
|
||||||
*/
|
|
||||||
title?: string
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
emoji?: true
|
|
||||||
children: StringChild
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
emoji?: false
|
|
||||||
children: RNTextProps['children']
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const EMOJI = createEmojiRegex()
|
|
||||||
|
|
||||||
export function childHasEmoji(children: React.ReactNode) {
|
|
||||||
return (Array.isArray(children) ? children : [children]).some(
|
|
||||||
child => typeof child === 'string' && createEmojiRegex().test(child),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function childIsString(
|
|
||||||
children: React.ReactNode,
|
|
||||||
): children is StringChild {
|
|
||||||
return (
|
|
||||||
typeof children === 'string' ||
|
|
||||||
(Array.isArray(children) &&
|
|
||||||
children.every(child => typeof child === 'string' || child === null))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderChildrenWithEmoji(
|
|
||||||
children: StringChild,
|
|
||||||
props: Omit<TextProps, 'children'> = {},
|
|
||||||
) {
|
|
||||||
const normalized = Array.isArray(children) ? children : [children]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UITextView {...props}>
|
|
||||||
{normalized.map(child => {
|
|
||||||
if (typeof child !== 'string') return child
|
|
||||||
|
|
||||||
const emojis = child.match(EMOJI)
|
|
||||||
|
|
||||||
if (emojis === null) {
|
|
||||||
return child
|
|
||||||
}
|
|
||||||
|
|
||||||
return child.split(EMOJI).map((stringPart, index) => (
|
|
||||||
<UITextView key={index} {...props}>
|
|
||||||
{stringPart}
|
|
||||||
{emojis[index] ? (
|
|
||||||
<UITextView
|
|
||||||
{...props}
|
|
||||||
style={[props?.style, {color: 'black', fontFamily: 'System'}]}>
|
|
||||||
{emojis[index]}
|
|
||||||
</UITextView>
|
|
||||||
) : null}
|
|
||||||
</UITextView>
|
|
||||||
))
|
|
||||||
})}
|
|
||||||
</UITextView>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Util to calculate lineHeight from a text size atom and a leading atom
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* `leading(atoms.text_md, atoms.leading_normal)` // => 24
|
|
||||||
*/
|
|
||||||
export function leading<
|
|
||||||
Size extends {fontSize?: number},
|
|
||||||
Leading extends {lineHeight?: number},
|
|
||||||
>(textSize: Size, leading: Leading) {
|
|
||||||
const size = textSize?.fontSize || atoms.text_md.fontSize
|
|
||||||
const lineHeight = leading?.lineHeight || atoms.leading_normal.lineHeight
|
|
||||||
return Math.round(size * lineHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensures that `lineHeight` defaults to a relative value of `1`, or applies
|
|
||||||
* other relative leading atoms.
|
|
||||||
*
|
|
||||||
* If the `lineHeight` value is > 2, we assume it's an absolute value and
|
|
||||||
* returns it as-is.
|
|
||||||
*/
|
|
||||||
export function normalizeTextStyles(
|
|
||||||
styles: StyleProp<TextStyle>,
|
|
||||||
{
|
|
||||||
fontScale,
|
|
||||||
fontFamily,
|
|
||||||
}: {
|
|
||||||
fontScale: number
|
|
||||||
fontFamily: Alf['fonts']['family']
|
|
||||||
} & Pick<Alf, 'flags'>,
|
|
||||||
) {
|
|
||||||
const s = flatten(styles)
|
|
||||||
// should always be defined on these components
|
|
||||||
s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale
|
|
||||||
|
|
||||||
if (s?.lineHeight) {
|
|
||||||
if (s.lineHeight !== 0 && s.lineHeight <= 2) {
|
|
||||||
s.lineHeight = Math.round(s.fontSize * s.lineHeight)
|
|
||||||
}
|
|
||||||
} else if (!isNative) {
|
|
||||||
s.lineHeight = s.fontSize
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFonts(s, fontFamily)
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Our main text component. Use this most of the time.
|
* Our main text component. Use this most of the time.
|
||||||
@@ -183,7 +60,7 @@ export function Text({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHeadingElement({level}: {level: number}) {
|
function createHeadingElement({level}: {level: number}) {
|
||||||
return function HeadingElement({style, ...rest}: TextProps) {
|
return function HeadingElement({style, ...rest}: TextProps) {
|
||||||
const attr =
|
const attr =
|
||||||
web({
|
web({
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ import {
|
|||||||
useTheme,
|
useTheme,
|
||||||
web,
|
web,
|
||||||
} from '#/alf'
|
} from '#/alf'
|
||||||
|
import {leading} from '#/alf/typography'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
||||||
import {createPortalGroup} from '#/components/Portal'
|
import {createPortalGroup} from '#/components/Portal'
|
||||||
import {leading, P, Text} from '#/components/Typography'
|
import {P, Text} from '#/components/Typography'
|
||||||
import {IS_DEV} from '#/env'
|
import {IS_DEV} from '#/env'
|
||||||
|
|
||||||
const COL_WIDTH = 420
|
const COL_WIDTH = 420
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
suggestLinkCardUri,
|
suggestLinkCardUri,
|
||||||
} from '#/view/com/composer/text-input/text-input-util'
|
} from '#/view/com/composer/text-input/text-input-util'
|
||||||
import {atoms as a, useAlf} from '#/alf'
|
import {atoms as a, useAlf} from '#/alf'
|
||||||
import {normalizeTextStyles} from '#/components/Typography'
|
import {normalizeTextStyles} from '#/alf/typography'
|
||||||
import {Autocomplete} from './mobile/Autocomplete'
|
import {Autocomplete} from './mobile/Autocomplete'
|
||||||
|
|
||||||
export interface TextInputRef {
|
export interface TextInputRef {
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ import {
|
|||||||
} from '#/view/com/composer/text-input/text-input-util'
|
} from '#/view/com/composer/text-input/text-input-util'
|
||||||
import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
|
import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
|
||||||
import {atoms as a, useAlf} from '#/alf'
|
import {atoms as a, useAlf} from '#/alf'
|
||||||
|
import {normalizeTextStyles} from '#/alf/typography'
|
||||||
import {Portal} from '#/components/Portal'
|
import {Portal} from '#/components/Portal'
|
||||||
import {normalizeTextStyles} from '#/components/Typography'
|
|
||||||
import {Text} from '../../util/text/Text'
|
import {Text} from '../../util/text/Text'
|
||||||
import {createSuggestion} from './web/Autocomplete'
|
import {createSuggestion} from './web/Autocomplete'
|
||||||
import {Emoji} from './web/EmojiPicker.web'
|
import {Emoji} from './web/EmojiPicker.web'
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
childIsString,
|
childIsString,
|
||||||
renderChildrenWithEmoji,
|
renderChildrenWithEmoji,
|
||||||
StringChild,
|
StringChild,
|
||||||
} from '#/components/Typography'
|
} from '#/alf/typography'
|
||||||
import {IS_DEV} from '#/env'
|
import {IS_DEV} from '#/env'
|
||||||
|
|
||||||
export type CustomTextProps = Omit<TextProps, 'children'> & {
|
export type CustomTextProps = Omit<TextProps, 'children'> & {
|
||||||
@@ -32,7 +32,11 @@ export type CustomTextProps = Omit<TextProps, 'children'> & {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export function Text({
|
export {Text_DEPRECATED as Text}
|
||||||
|
/**
|
||||||
|
* @deprecated use Text from Typography instead.
|
||||||
|
*/
|
||||||
|
function Text_DEPRECATED({
|
||||||
type = 'md',
|
type = 'md',
|
||||||
children,
|
children,
|
||||||
emoji,
|
emoji,
|
||||||
|
|||||||
Reference in New Issue
Block a user