Skip to content

Commit

Permalink
Enforce Text suffix for Text-rendering components (#3407)
Browse files Browse the repository at this point in the history
* Rm unused

* Add Text suffix to Title/Description

* Add Text suffix to text components

* Add Text suffix to props

* Validate Text components returns
  • Loading branch information
gaearon authored Apr 4, 2024
1 parent c190fd5 commit 3915bb4
Show file tree
Hide file tree
Showing 43 changed files with 430 additions and 343 deletions.
15 changes: 1 addition & 14 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const bskyEslint = require('./eslint')

module.exports = {
root: true,
extends: [
Expand Down Expand Up @@ -27,29 +25,18 @@ module.exports = {
{
impliedTextComponents: [
'Button', // TODO: Not always safe.
'ButtonText',
'DateField.Label',
'Description',
'H1',
'H2',
'H3',
'H4',
'H5',
'H6',
'InlineLink',
'Label',
'P',
'Prompt.Title',
'Prompt.Description',
'Prompt.Cancel', // TODO: Not always safe.
'Prompt.Action', // TODO: Not always safe.
'TextField.Label',
'TextField.Suffix',
'Title',
'Toggle.Label',
'ToggleButton.Button', // TODO: Not always safe.
],
impliedTextProps: ['FormContainer title'],
impliedTextProps: [],
},
],
'simple-import-sort/imports': [
Expand Down
65 changes: 65 additions & 0 deletions eslint/__tests__/avoid-unwrapped-text.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,41 @@ describe('avoid-unwrapped-text', () => {
</Foo>
`,
},

{
code: `
function Stuff() {
return <Text>foo</Text>
}
`,
},

{
code: `
function Stuff({ foo }) {
return <View>{foo}</View>
}
`,
},

{
code: `
function MyText() {
return <Text>foo</Text>
}
`,
},

{
code: `
function MyText({ foo }) {
if (foo) {
return <Text>foo</Text>
}
return <Text>foo</Text>
}
`,
},
],

invalid: [
Expand Down Expand Up @@ -390,6 +425,36 @@ describe('avoid-unwrapped-text', () => {
`,
errors: 1,
},

{
code: `
function MyText() {
return <Foo />
}
`,
errors: 1,
},

{
code: `
function MyText({ foo }) {
return <Foo>{foo}</Foo>
}
`,
errors: 1,
},

{
code: `
function MyText({ foo }) {
if (foo) {
return <Foo>{foo}</Foo>
}
return <Text>foo</Text>
}
`,
errors: 1,
},
],
}

Expand Down
38 changes: 37 additions & 1 deletion eslint/avoid-unwrapped-text.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ exports.create = function create(context) {
const impliedTextComponents = options.impliedTextComponents ?? []
const textProps = [...impliedTextProps]
const textComponents = ['Text', ...impliedTextComponents]

function isTextComponent(tagName) {
return textComponents.includes(tagName) || tagName.endsWith('Text')
}

return {
JSXText(node) {
if (typeof node.value !== 'string' || hasOnlyLineBreak(node.value)) {
Expand All @@ -44,7 +49,7 @@ exports.create = function create(context) {
while (parent) {
if (parent.type === 'JSXElement') {
const tagName = getTagName(parent)
if (textComponents.includes(tagName) || tagName.endsWith('Text')) {
if (isTextComponent(tagName)) {
// We're good.
return
}
Expand Down Expand Up @@ -107,5 +112,36 @@ exports.create = function create(context) {
continue
}
},
ReturnStatement(node) {
let fnScope = context.getScope()
while (fnScope && fnScope.type !== 'function') {
fnScope = fnScope.upper
}
if (!fnScope) {
return
}
const fn = fnScope.block
if (!fn.id || fn.id.type !== 'Identifier' || !fn.id.name) {
return
}
if (!/^[A-Z]\w*Text$/.test(fn.id.name)) {
return
}
if (!node.argument || node.argument.type !== 'JSXElement') {
return
}
const openingEl = node.argument.openingElement
if (openingEl.name.type !== 'JSXIdentifier') {
return
}
const returnedComponentName = openingEl.name.name
if (!isTextComponent(returnedComponentName)) {
context.report({
node,
message:
'Components ending with *Text must return <Text> or <SomeText>.',
})
}
},
}
}
2 changes: 1 addition & 1 deletion src/components/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
>

export function InlineLink({
export function InlineLinkText({
children,
to,
action = 'push',
Expand Down
8 changes: 4 additions & 4 deletions src/components/Prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function Outer({
)
}

export function Title({children}: React.PropsWithChildren<{}>) {
export function TitleText({children}: React.PropsWithChildren<{}>) {
const {titleId} = React.useContext(Context)
return (
<Text nativeID={titleId} style={[a.text_2xl, a.font_bold, a.pb_sm]}>
Expand All @@ -60,7 +60,7 @@ export function Title({children}: React.PropsWithChildren<{}>) {
)
}

export function Description({children}: React.PropsWithChildren<{}>) {
export function DescriptionText({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
const {descriptionId} = React.useContext(Context)
return (
Expand Down Expand Up @@ -175,8 +175,8 @@ export function Basic({
}>) {
return (
<Outer control={control} testID="confirmModal">
<Title>{title}</Title>
<Description>{description}</Description>
<TitleText>{title}</TitleText>
<DescriptionText>{description}</DescriptionText>
<Actions>
<Action
cta={confirmButtonCta}
Expand Down
10 changes: 5 additions & 5 deletions src/components/RichText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {toShortUrl} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {InlineLink} from '#/components/Link'
import {InlineLinkText} from '#/components/Link'
import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
import {Text, TextProps} from '#/components/Typography'

Expand Down Expand Up @@ -84,22 +84,22 @@ export function RichText({
!disableLinks
) {
els.push(
<InlineLink
<InlineLinkText
selectable={selectable}
key={key}
to={`/profile/${mention.did}`}
style={[...styles, {pointerEvents: 'auto'}]}
// @ts-ignore TODO
dataSet={WORD_WRAP}>
{segment.text}
</InlineLink>,
</InlineLinkText>,
)
} else if (link && AppBskyRichtextFacet.validateLink(link).success) {
if (disableLinks) {
els.push(toShortUrl(segment.text))
} else {
els.push(
<InlineLink
<InlineLinkText
selectable={selectable}
key={key}
to={link.uri}
Expand All @@ -108,7 +108,7 @@ export function RichText({
dataSet={WORD_WRAP}
shareOnLongPress>
{toShortUrl(segment.text)}
</InlineLink>,
</InlineLinkText>,
)
}
} else if (
Expand Down
35 changes: 17 additions & 18 deletions src/components/dialogs/MutedWords.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
import React from 'react'
import {Keyboard, View} from 'react-native'
import {AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api'

import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {
usePreferencesQuery,
useUpsertMutedWordsMutation,
useRemoveMutedWordMutation,
useUpsertMutedWordsMutation,
} from '#/state/queries/preferences'
import {isNative} from '#/platform/detection'
import {
atoms as a,
useTheme,
native,
useBreakpoints,
useTheme,
ViewStyleProp,
web,
native,
} from '#/alf'
import {Text} from '#/components/Typography'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import * as Dialog from '#/components/Dialog'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {Divider} from '#/components/Divider'
import * as Toggle from '#/components/forms/Toggle'
import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/PageText'
import {Divider} from '#/components/Divider'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Loader} from '#/components/Loader'
import {logger} from '#/logger'
import * as Dialog from '#/components/Dialog'
import * as Toggle from '#/components/forms/Toggle'
import * as Prompt from '#/components/Prompt'

import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {Text} from '#/components/Typography'

export function MutedWordsDialog() {
const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext()
Expand Down Expand Up @@ -130,9 +129,9 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
<TargetToggle>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<Toggle.Radio />
<Toggle.Label>
<Toggle.LabelText>
<Trans>Mute in text & tags</Trans>
</Toggle.Label>
</Toggle.LabelText>
</View>
<PageText size="sm" />
</TargetToggle>
Expand All @@ -145,9 +144,9 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
<TargetToggle>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<Toggle.Radio />
<Toggle.Label>
<Toggle.LabelText>
<Trans>Mute in tags only</Trans>
</Toggle.Label>
</Toggle.LabelText>
</View>
<Hashtag size="sm" />
</TargetToggle>
Expand Down
2 changes: 1 addition & 1 deletion src/components/forms/DateField/index.android.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as TextField from '#/components/forms/TextField'
import {DateFieldButton} from './index.shared'

export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
export const LabelText = TextField.LabelText

export function DateField({
value,
Expand Down
2 changes: 1 addition & 1 deletion src/components/forms/DateField/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import * as TextField from '#/components/forms/TextField'
import {DateFieldButton} from './index.shared'

export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
export const LabelText = TextField.LabelText

/**
* Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date
Expand Down
2 changes: 1 addition & 1 deletion src/components/forms/DateField/index.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as TextField from '#/components/forms/TextField'
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'

export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
export const LabelText = TextField.LabelText

const InputBase = React.forwardRef<HTMLInputElement, TextInputProps>(
({style, ...props}, ref) => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/forms/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ export function createInput(Component: typeof TextInput) {

export const Input = createInput(TextInput)

export function Label({
export function LabelText({
nativeID,
children,
}: React.PropsWithChildren<{nativeID?: string}>) {
Expand Down Expand Up @@ -288,7 +288,7 @@ export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) {
)
}

export function Suffix({
export function SuffixText({
children,
label,
accessibilityHint,
Expand Down
Loading

0 comments on commit 3915bb4

Please sign in to comment.