Enforce that text is wrapped in <Text>, remaining cases (#3421)
* Toggle.Button -> Toggle.ButtonWithText * Simplify Prompt.Cancel/Action * Move lines down for better diff * Remove ButtonWithText * Simplify types * Enforce Button/ButtonText nesting * Add suggested wrapper in linter error * Check <Trans> ancestry too * Also check literals * Rm ts-ignorezio/stable
parent
49266c355e
commit
46c112edfd
15
.eslintrc.js
15
.eslintrc.js
|
@ -23,17 +23,12 @@ module.exports = {
|
||||||
'bsky-internal/avoid-unwrapped-text': [
|
'bsky-internal/avoid-unwrapped-text': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
impliedTextComponents: [
|
impliedTextComponents: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P'],
|
||||||
'Button', // TODO: Not always safe.
|
|
||||||
'H1',
|
|
||||||
'H2',
|
|
||||||
'H3',
|
|
||||||
'H4',
|
|
||||||
'H5',
|
|
||||||
'H6',
|
|
||||||
'P',
|
|
||||||
],
|
|
||||||
impliedTextProps: [],
|
impliedTextProps: [],
|
||||||
|
suggestedTextWrappers: {
|
||||||
|
Button: 'ButtonText',
|
||||||
|
'ToggleButton.Button': 'ToggleButton.ButtonText',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'simple-import-sort/imports': [
|
'simple-import-sort/imports': [
|
||||||
|
|
|
@ -199,7 +199,7 @@ describe('avoid-unwrapped-text', () => {
|
||||||
|
|
||||||
{
|
{
|
||||||
code: `
|
code: `
|
||||||
<View prop={
|
<View propText={
|
||||||
<Trans><Text>foo</Text></Trans>
|
<Trans><Text>foo</Text></Trans>
|
||||||
}>
|
}>
|
||||||
<Bar />
|
<Bar />
|
||||||
|
@ -281,6 +281,170 @@ function MyText({ foo }) {
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Text>{'foo'}</Text>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Text>{foo + 'foo'}</Text>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Text><Trans>{'foo'}</Trans></Text>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
{foo['bar'] && <Bar />}
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
{(foo === 'bar') && <Bar />}
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
{(foo !== 'bar') && <Bar />}
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Text>{\`foo\`}</Text>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Text><Trans>{\`foo\`}</Trans></Text>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Text>{_(msg\`foo\`)}</Text>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Text><Trans>{_(msg\`foo\`)}</Trans></Text>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<Foo>
|
||||||
|
<View prop={stuff('foo')}>
|
||||||
|
<Bar />
|
||||||
|
</View>
|
||||||
|
</Foo>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<Foo>
|
||||||
|
<View onClick={() => stuff('foo')}>
|
||||||
|
<Bar />
|
||||||
|
</View>
|
||||||
|
</Foo>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
{renderItem('foo')}
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
{foo === 'foo' && <Bar />}
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
{foo['foo'] && <Bar />}
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
{check('foo') && <Bar />}
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
{foo.bar && <Bar />}
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<Text>
|
||||||
|
<Trans>{renderItem('foo')}</Trans>
|
||||||
|
</Text>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
{null}
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<Text>
|
||||||
|
<Trans>{null}</Trans>
|
||||||
|
</Text>
|
||||||
|
`,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
invalid: [
|
invalid: [
|
||||||
|
@ -455,6 +619,179 @@ function MyText({ foo }) {
|
||||||
`,
|
`,
|
||||||
errors: 1,
|
errors: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
{'foo'}
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
{foo && 'foo'}
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Trans>{'foo'}</Trans>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
{foo && <Trans>{'foo'}</Trans>}
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
{10}
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Trans>{10}</Trans>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Trans>{foo + 10}</Trans>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
{\`foo\`}
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Trans>{\`foo\`}</Trans>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Trans>{foo + \`foo\`}</Trans>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
{_(msg\`foo\`)}
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
{foo + _(msg\`foo\`)}
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Trans>{_(msg\`foo\`)}</Trans>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Trans>{foo + _(msg\`foo\`)}</Trans>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Trans>foo</Trans>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Trans><Trans>foo</Trans></Trans>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Trans>{foo}</Trans>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View>
|
||||||
|
<Trans>{'foo'}</Trans>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
<View prop={
|
||||||
|
<Trans><Text>foo</Text></Trans>
|
||||||
|
}>
|
||||||
|
<Bar />
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
|
errors: 1,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ exports.create = function create(context) {
|
||||||
const options = context.options[0] || {}
|
const options = context.options[0] || {}
|
||||||
const impliedTextProps = options.impliedTextProps ?? []
|
const impliedTextProps = options.impliedTextProps ?? []
|
||||||
const impliedTextComponents = options.impliedTextComponents ?? []
|
const impliedTextComponents = options.impliedTextComponents ?? []
|
||||||
|
const suggestedTextWrappers = options.suggestedTextWrappers ?? {}
|
||||||
const textProps = [...impliedTextProps]
|
const textProps = [...impliedTextProps]
|
||||||
const textComponents = ['Text', ...impliedTextComponents]
|
const textComponents = ['Text', ...impliedTextComponents]
|
||||||
|
|
||||||
|
@ -54,13 +55,13 @@ exports.create = function create(context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (tagName === 'Trans') {
|
if (tagName === 'Trans') {
|
||||||
// Skip over it and check above.
|
// Exit and rely on the traversal for <Trans> JSXElement (code below).
|
||||||
// TODO: Maybe validate that it's present.
|
// TODO: Maybe validate that it's present.
|
||||||
parent = parent.parent
|
return
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
let message = 'Wrap this string in <Text>.'
|
const suggestedWrapper = suggestedTextWrappers[tagName]
|
||||||
if (tagName !== 'View') {
|
let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.`
|
||||||
|
if (tagName !== 'View' && !suggestedWrapper) {
|
||||||
message +=
|
message +=
|
||||||
' If <' +
|
' If <' +
|
||||||
tagName +
|
tagName +
|
||||||
|
@ -112,6 +113,189 @@ exports.create = function create(context) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Literal(node) {
|
||||||
|
if (typeof node.value !== 'string' && typeof node.value !== 'number') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let parent = node.parent
|
||||||
|
while (parent) {
|
||||||
|
if (parent.type === 'JSXElement') {
|
||||||
|
const tagName = getTagName(parent)
|
||||||
|
if (isTextComponent(tagName)) {
|
||||||
|
// We're good.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (tagName === 'Trans') {
|
||||||
|
// Exit and rely on the traversal for <Trans> JSXElement (code below).
|
||||||
|
// TODO: Maybe validate that it's present.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const suggestedWrapper = suggestedTextWrappers[tagName]
|
||||||
|
let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.`
|
||||||
|
if (tagName !== 'View' && !suggestedWrapper) {
|
||||||
|
message +=
|
||||||
|
' If <' +
|
||||||
|
tagName +
|
||||||
|
'> is guaranteed to render <Text>, ' +
|
||||||
|
'rename it to <' +
|
||||||
|
tagName +
|
||||||
|
'Text> or add it to impliedTextComponents.'
|
||||||
|
}
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent.type === 'BinaryExpression' && parent.operator === '+') {
|
||||||
|
parent = parent.parent
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
parent.type === 'JSXExpressionContainer' ||
|
||||||
|
parent.type === 'LogicalExpression'
|
||||||
|
) {
|
||||||
|
parent = parent.parent
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Be conservative for other types.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TemplateLiteral(node) {
|
||||||
|
let parent = node.parent
|
||||||
|
while (parent) {
|
||||||
|
if (parent.type === 'JSXElement') {
|
||||||
|
const tagName = getTagName(parent)
|
||||||
|
if (isTextComponent(tagName)) {
|
||||||
|
// We're good.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (tagName === 'Trans') {
|
||||||
|
// Exit and rely on the traversal for <Trans> JSXElement (code below).
|
||||||
|
// TODO: Maybe validate that it's present.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const suggestedWrapper = suggestedTextWrappers[tagName]
|
||||||
|
let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.`
|
||||||
|
if (tagName !== 'View' && !suggestedWrapper) {
|
||||||
|
message +=
|
||||||
|
' If <' +
|
||||||
|
tagName +
|
||||||
|
'> is guaranteed to render <Text>, ' +
|
||||||
|
'rename it to <' +
|
||||||
|
tagName +
|
||||||
|
'Text> or add it to impliedTextComponents.'
|
||||||
|
}
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
parent.type === 'CallExpression' &&
|
||||||
|
parent.callee.type === 'Identifier' &&
|
||||||
|
parent.callee.name === '_'
|
||||||
|
) {
|
||||||
|
// This is a user-facing string, keep going up.
|
||||||
|
parent = parent.parent
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent.type === 'BinaryExpression' && parent.operator === '+') {
|
||||||
|
parent = parent.parent
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
parent.type === 'JSXExpressionContainer' ||
|
||||||
|
parent.type === 'LogicalExpression' ||
|
||||||
|
parent.type === 'TaggedTemplateExpression'
|
||||||
|
) {
|
||||||
|
parent = parent.parent
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Be conservative for other types.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
JSXElement(node) {
|
||||||
|
if (getTagName(node) !== 'Trans') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let parent = node.parent
|
||||||
|
while (parent) {
|
||||||
|
if (parent.type === 'JSXElement') {
|
||||||
|
const tagName = getTagName(parent)
|
||||||
|
if (isTextComponent(tagName)) {
|
||||||
|
// We're good.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (tagName === 'Trans') {
|
||||||
|
// Exit and rely on the traversal for this JSXElement.
|
||||||
|
// TODO: Should nested <Trans> even be allowed?
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const suggestedWrapper = suggestedTextWrappers[tagName]
|
||||||
|
let message = `Wrap this <Trans> in <${suggestedWrapper ?? 'Text'}>.`
|
||||||
|
if (tagName !== 'View' && !suggestedWrapper) {
|
||||||
|
message +=
|
||||||
|
' If <' +
|
||||||
|
tagName +
|
||||||
|
'> is guaranteed to render <Text>, ' +
|
||||||
|
'rename it to <' +
|
||||||
|
tagName +
|
||||||
|
'Text> or add it to impliedTextComponents.'
|
||||||
|
}
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
parent.type === 'JSXAttribute' &&
|
||||||
|
parent.name.type === 'JSXIdentifier' &&
|
||||||
|
parent.parent.type === 'JSXOpeningElement' &&
|
||||||
|
parent.parent.parent.type === 'JSXElement'
|
||||||
|
) {
|
||||||
|
const tagName = getTagName(parent.parent.parent)
|
||||||
|
const propName = parent.name.name
|
||||||
|
if (
|
||||||
|
textProps.includes(tagName + ' ' + propName) ||
|
||||||
|
propName === 'text' ||
|
||||||
|
propName.endsWith('Text')
|
||||||
|
) {
|
||||||
|
// We're good.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const message =
|
||||||
|
'Wrap this <Trans> in <Text>.' +
|
||||||
|
' If `' +
|
||||||
|
propName +
|
||||||
|
'` is guaranteed to be wrapped in <Text>, ' +
|
||||||
|
'rename it to `' +
|
||||||
|
propName +
|
||||||
|
'Text' +
|
||||||
|
'` or add it to impliedTextProps.'
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parent = parent.parent
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
},
|
||||||
ReturnStatement(node) {
|
ReturnStatement(node) {
|
||||||
let fnScope = context.getScope()
|
let fnScope = context.getScope()
|
||||||
while (fnScope && fnScope.type !== 'function') {
|
while (fnScope && fnScope.type !== 'function') {
|
||||||
|
|
|
@ -12,7 +12,6 @@ import {
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {LinearGradient} from 'expo-linear-gradient'
|
import {LinearGradient} from 'expo-linear-gradient'
|
||||||
import {Trans} from '@lingui/macro'
|
|
||||||
|
|
||||||
import {android, atoms as a, flatten, tokens, useTheme} from '#/alf'
|
import {android, atoms as a, flatten, tokens, useTheme} from '#/alf'
|
||||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||||
|
@ -59,6 +58,10 @@ export type ButtonState = {
|
||||||
|
|
||||||
export type ButtonContext = VariantProps & ButtonState
|
export type ButtonContext = VariantProps & ButtonState
|
||||||
|
|
||||||
|
type NonTextElements =
|
||||||
|
| React.ReactElement
|
||||||
|
| Iterable<React.ReactElement | null | undefined | boolean>
|
||||||
|
|
||||||
export type ButtonProps = Pick<
|
export type ButtonProps = Pick<
|
||||||
PressableProps,
|
PressableProps,
|
||||||
'disabled' | 'onPress' | 'testID'
|
'disabled' | 'onPress' | 'testID'
|
||||||
|
@ -68,11 +71,9 @@ export type ButtonProps = Pick<
|
||||||
testID?: string
|
testID?: string
|
||||||
label: string
|
label: string
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
children:
|
children: NonTextElements | ((context: ButtonContext) => NonTextElements)
|
||||||
| React.ReactNode
|
|
||||||
| string
|
|
||||||
| ((context: ButtonContext) => React.ReactNode | string)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
|
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
|
||||||
|
|
||||||
const Context = React.createContext<VariantProps & ButtonState>({
|
const Context = React.createContext<VariantProps & ButtonState>({
|
||||||
|
@ -404,15 +405,7 @@ export function Button({
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<Context.Provider value={context}>
|
<Context.Provider value={context}>
|
||||||
{/* @ts-ignore */}
|
{typeof children === 'function' ? children(context) : children}
|
||||||
{typeof children === 'string' || children?.type === Trans ? (
|
|
||||||
/* @ts-ignore */
|
|
||||||
<ButtonText>{children}</ButtonText>
|
|
||||||
) : typeof children === 'function' ? (
|
|
||||||
children(context)
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)}
|
|
||||||
</Context.Provider>
|
</Context.Provider>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {CenteredView} from 'view/com/util/Views'
|
import {CenteredView} from 'view/com/util/Views'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import {Error} from '#/components/Error'
|
import {Error} from '#/components/Error'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
@ -87,7 +87,9 @@ function ListFooterMaybeError({
|
||||||
a.py_sm,
|
a.py_sm,
|
||||||
]}
|
]}
|
||||||
onPress={onRetry}>
|
onPress={onRetry}>
|
||||||
<Trans>Retry</Trans>
|
<ButtonText>
|
||||||
|
<Trans>Retry</Trans>
|
||||||
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -244,7 +244,7 @@ function AppealForm({
|
||||||
size="medium"
|
size="medium"
|
||||||
onPress={onPressBack}
|
onPress={onPressBack}
|
||||||
label={_(msg`Back`)}>
|
label={_(msg`Back`)}>
|
||||||
{_(msg`Back`)}
|
<ButtonText>{_(msg`Back`)}</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
testID="submitBtn"
|
testID="submitBtn"
|
||||||
|
@ -253,7 +253,7 @@ function AppealForm({
|
||||||
size="medium"
|
size="medium"
|
||||||
onPress={onSubmit}
|
onPress={onSubmit}
|
||||||
label={_(msg`Submit`)}>
|
label={_(msg`Submit`)}>
|
||||||
{_(msg`Submit`)}
|
<ButtonText>{_(msg`Submit`)}</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {AccountList} from '#/components/AccountList'
|
import {AccountList} from '#/components/AccountList'
|
||||||
import {Button} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {FormContainer} from './FormContainer'
|
import {FormContainer} from './FormContainer'
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ export const ChooseAccountForm = ({
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="medium"
|
size="medium"
|
||||||
onPress={onPressBack}>
|
onPress={onPressBack}>
|
||||||
{_(msg`Back`)}
|
<ButtonText>{_(msg`Back`)}</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
<View style={[a.flex_1]} />
|
<View style={[a.flex_1]} />
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -237,7 +237,9 @@ export const LoginForm = ({
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="medium"
|
size="medium"
|
||||||
onPress={onPressRetryConnect}>
|
onPress={onPressRetryConnect}>
|
||||||
{_(msg`Retry`)}
|
<ButtonText>
|
||||||
|
<Trans>Retry</Trans>
|
||||||
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
) : !serviceDescription ? (
|
) : !serviceDescription ? (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
useTheme,
|
useTheme,
|
||||||
web,
|
web,
|
||||||
} from '#/alf'
|
} from '#/alf'
|
||||||
import {Button, ButtonIcon} 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 {leading, P, Text} from '#/components/Typography'
|
||||||
|
@ -73,7 +73,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||||
onPress={() => onboardDispatch({type: 'skip'})}
|
onPress={() => onboardDispatch({type: 'skip'})}
|
||||||
// DEV ONLY
|
// DEV ONLY
|
||||||
label="Clear onboarding state">
|
label="Clear onboarding state">
|
||||||
Clear
|
<ButtonText>Clear</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -167,7 +167,7 @@ export function ServerInputDialog({
|
||||||
size="small"
|
size="small"
|
||||||
onPress={() => control.close()}
|
onPress={() => control.close()}
|
||||||
label={_(msg`Done`)}>
|
label={_(msg`Done`)}>
|
||||||
{_(msg`Done`)}
|
<ButtonText>{_(msg`Done`)}</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -92,7 +92,9 @@ export function ExportCarDialog({
|
||||||
size={gtMobile ? 'small' : 'large'}
|
size={gtMobile ? 'small' : 'large'}
|
||||||
onPress={() => control.close()}
|
onPress={() => control.close()}
|
||||||
label={_(msg`Done`)}>
|
label={_(msg`Done`)}>
|
||||||
{_(msg`Done`)}
|
<ButtonText>
|
||||||
|
<Trans>Done</Trans>
|
||||||
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
|
@ -4,15 +4,15 @@ import {View} from 'react-native'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ButtonVariant,
|
|
||||||
ButtonColor,
|
ButtonColor,
|
||||||
ButtonIcon,
|
ButtonIcon,
|
||||||
ButtonText,
|
ButtonText,
|
||||||
|
ButtonVariant,
|
||||||
} from '#/components/Button'
|
} from '#/components/Button'
|
||||||
import {H1} from '#/components/Typography'
|
|
||||||
import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight'
|
import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight'
|
||||||
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
||||||
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||||
|
import {H1} from '#/components/Typography'
|
||||||
|
|
||||||
export function Buttons() {
|
export function Buttons() {
|
||||||
return (
|
return (
|
||||||
|
@ -29,7 +29,7 @@ export function Buttons() {
|
||||||
color={color as ButtonColor}
|
color={color as ButtonColor}
|
||||||
size="large"
|
size="large"
|
||||||
label="Click here">
|
label="Click here">
|
||||||
Button
|
<ButtonText>Button</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled
|
disabled
|
||||||
|
@ -37,7 +37,7 @@ export function Buttons() {
|
||||||
color={color as ButtonColor}
|
color={color as ButtonColor}
|
||||||
size="large"
|
size="large"
|
||||||
label="Click here">
|
label="Click here">
|
||||||
Button
|
<ButtonText>Button</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
|
@ -54,7 +54,7 @@ export function Buttons() {
|
||||||
color={name as ButtonColor}
|
color={name as ButtonColor}
|
||||||
size="large"
|
size="large"
|
||||||
label="Click here">
|
label="Click here">
|
||||||
Button
|
<ButtonText>Button</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled
|
disabled
|
||||||
|
@ -62,7 +62,7 @@ export function Buttons() {
|
||||||
color={name as ButtonColor}
|
color={name as ButtonColor}
|
||||||
size="large"
|
size="large"
|
||||||
label="Click here">
|
label="Click here">
|
||||||
Button
|
<ButtonText>Button</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
),
|
),
|
||||||
|
@ -77,7 +77,7 @@ export function Buttons() {
|
||||||
color={name as ButtonColor}
|
color={name as ButtonColor}
|
||||||
size="large"
|
size="large"
|
||||||
label="Click here">
|
label="Click here">
|
||||||
Button
|
<ButtonText>Button</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled
|
disabled
|
||||||
|
@ -85,7 +85,7 @@ export function Buttons() {
|
||||||
color={name as ButtonColor}
|
color={name as ButtonColor}
|
||||||
size="large"
|
size="large"
|
||||||
label="Click here">
|
label="Click here">
|
||||||
Button
|
<ButtonText>Button</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
),
|
),
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {View} from 'react-native'
|
||||||
|
|
||||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {H3, P} from '#/components/Typography'
|
import {H3, P} from '#/components/Typography'
|
||||||
|
@ -26,7 +26,7 @@ export function Dialogs() {
|
||||||
basic.open()
|
basic.open()
|
||||||
}}
|
}}
|
||||||
label="Open basic dialog">
|
label="Open basic dialog">
|
||||||
Open all dialogs
|
<ButtonText>Open all dialogs</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -37,7 +37,7 @@ export function Dialogs() {
|
||||||
scrollable.open()
|
scrollable.open()
|
||||||
}}
|
}}
|
||||||
label="Open basic dialog">
|
label="Open basic dialog">
|
||||||
Open scrollable dialog
|
<ButtonText>Open scrollable dialog</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -48,7 +48,7 @@ export function Dialogs() {
|
||||||
basic.open()
|
basic.open()
|
||||||
}}
|
}}
|
||||||
label="Open basic dialog">
|
label="Open basic dialog">
|
||||||
Open basic dialog
|
<ButtonText>Open basic dialog</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -57,7 +57,7 @@ export function Dialogs() {
|
||||||
size="small"
|
size="small"
|
||||||
onPress={() => prompt.open()}
|
onPress={() => prompt.open()}
|
||||||
label="Open prompt">
|
label="Open prompt">
|
||||||
Open prompt
|
<ButtonText>Open prompt</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Prompt.Outer control={prompt}>
|
<Prompt.Outer control={prompt}>
|
||||||
|
@ -102,7 +102,7 @@ export function Dialogs() {
|
||||||
size="small"
|
size="small"
|
||||||
onPress={closeAllDialogs}
|
onPress={closeAllDialogs}
|
||||||
label="Close all dialogs">
|
label="Close all dialogs">
|
||||||
Close all dialogs
|
<ButtonText>Close all dialogs</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
<View style={{height: 1000}} />
|
<View style={{height: 1000}} />
|
||||||
<View style={[a.flex_row, a.justify_end]}>
|
<View style={[a.flex_row, a.justify_end]}>
|
||||||
|
@ -116,7 +116,7 @@ export function Dialogs() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
label="Open basic dialog">
|
label="Open basic dialog">
|
||||||
Close dialog
|
<ButtonText>Close dialog</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import {DateField, LabelText} from '#/components/forms/DateField'
|
import {DateField, LabelText} from '#/components/forms/DateField'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
@ -191,7 +191,7 @@ export function Forms() {
|
||||||
setToggleGroupBValues(['a', 'b'])
|
setToggleGroupBValues(['a', 'b'])
|
||||||
setToggleGroupCValues(['a'])
|
setToggleGroupCValues(['a'])
|
||||||
}}>
|
}}>
|
||||||
Reset all toggles
|
<ButtonText>Reset all toggles</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<View style={[a.gap_md, a.align_start, a.w_full]}>
|
<View style={[a.gap_md, a.align_start, a.w_full]}>
|
||||||
|
|
|
@ -1,22 +1,21 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
|
||||||
|
|
||||||
import {atoms as a, useTheme, ThemeProvider} from '#/alf'
|
|
||||||
import {useSetThemePrefs} from '#/state/shell'
|
import {useSetThemePrefs} from '#/state/shell'
|
||||||
import {Button} from '#/components/Button'
|
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
||||||
|
import {atoms as a, ThemeProvider, useTheme} from '#/alf'
|
||||||
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
|
import {Breakpoints} from './Breakpoints'
|
||||||
|
import {Buttons} from './Buttons'
|
||||||
|
import {Dialogs} from './Dialogs'
|
||||||
|
import {Forms} from './Forms'
|
||||||
|
import {Icons} from './Icons'
|
||||||
|
import {Links} from './Links'
|
||||||
|
import {Menus} from './Menus'
|
||||||
|
import {Shadows} from './Shadows'
|
||||||
|
import {Spacing} from './Spacing'
|
||||||
import {Theming} from './Theming'
|
import {Theming} from './Theming'
|
||||||
import {Typography} from './Typography'
|
import {Typography} from './Typography'
|
||||||
import {Spacing} from './Spacing'
|
|
||||||
import {Buttons} from './Buttons'
|
|
||||||
import {Links} from './Links'
|
|
||||||
import {Forms} from './Forms'
|
|
||||||
import {Dialogs} from './Dialogs'
|
|
||||||
import {Breakpoints} from './Breakpoints'
|
|
||||||
import {Shadows} from './Shadows'
|
|
||||||
import {Icons} from './Icons'
|
|
||||||
import {Menus} from './Menus'
|
|
||||||
|
|
||||||
export function Storybook() {
|
export function Storybook() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
@ -33,7 +32,7 @@ export function Storybook() {
|
||||||
size="small"
|
size="small"
|
||||||
label='Set theme to "system"'
|
label='Set theme to "system"'
|
||||||
onPress={() => setColorMode('system')}>
|
onPress={() => setColorMode('system')}>
|
||||||
System
|
<ButtonText>System</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
|
@ -41,7 +40,7 @@ export function Storybook() {
|
||||||
size="small"
|
size="small"
|
||||||
label='Set theme to "light"'
|
label='Set theme to "light"'
|
||||||
onPress={() => setColorMode('light')}>
|
onPress={() => setColorMode('light')}>
|
||||||
Light
|
<ButtonText>Light</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
|
@ -52,7 +51,7 @@ export function Storybook() {
|
||||||
setColorMode('dark')
|
setColorMode('dark')
|
||||||
setDarkTheme('dim')
|
setDarkTheme('dim')
|
||||||
}}>
|
}}>
|
||||||
Dim
|
<ButtonText>Dim</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
|
@ -63,7 +62,7 @@ export function Storybook() {
|
||||||
setColorMode('dark')
|
setColorMode('dark')
|
||||||
setDarkTheme('dark')
|
setDarkTheme('dark')
|
||||||
}}>
|
}}>
|
||||||
Dark
|
<ButtonText>Dark</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue