AutosizedTextarea, minor tweaks
This commit is contained in:
+37
-1
@@ -1,3 +1,39 @@
|
||||
import {StyleSheet} from 'react-native'
|
||||
import {type DimensionValue, StyleSheet} from 'react-native'
|
||||
|
||||
export const flatten = StyleSheet.flatten
|
||||
|
||||
/**
|
||||
* Coerce a style value to a number. Padding values are typed as
|
||||
* `DimensionValue` (numbers, percentages, "auto", etc.) but our ALF atoms
|
||||
* are always plain numbers. Non-numeric values are treated as 0.
|
||||
*/
|
||||
function num(v: unknown): number {
|
||||
return typeof v === 'number' ? v : 0
|
||||
}
|
||||
|
||||
interface PaddingStyle {
|
||||
padding?: DimensionValue
|
||||
paddingHorizontal?: DimensionValue
|
||||
paddingVertical?: DimensionValue
|
||||
paddingTop?: DimensionValue
|
||||
paddingBottom?: DimensionValue
|
||||
paddingLeft?: DimensionValue
|
||||
paddingRight?: DimensionValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract resolved padding values from a style object. Returns numbers for
|
||||
* each side, resolving shorthand properties (padding → paddingVertical →
|
||||
* paddingTop/paddingBottom, etc.). Values are expected to be numbers — any
|
||||
* non-numeric `DimensionValue` (e.g. percentages) is treated as 0.
|
||||
*/
|
||||
export function extractPadding(style: PaddingStyle | PaddingStyle[]) {
|
||||
const s = flatten(style as any) ?? {}
|
||||
const base = num(s.padding)
|
||||
return {
|
||||
paddingTop: num(s.paddingTop) || num(s.paddingVertical) || base,
|
||||
paddingBottom: num(s.paddingBottom) || num(s.paddingVertical) || base,
|
||||
paddingLeft: num(s.paddingLeft) || num(s.paddingHorizontal) || base,
|
||||
paddingRight: num(s.paddingRight) || num(s.paddingHorizontal) || base,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
TextInput,
|
||||
type TextInput,
|
||||
type TextInputProps,
|
||||
type TextInputSubmitEditingEvent,
|
||||
View,
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
useTapper,
|
||||
} from '@bsky.app/tapper'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {mergeRefs} from '#/lib/merge-refs'
|
||||
import {
|
||||
atoms as a,
|
||||
@@ -46,13 +45,10 @@ import {
|
||||
parseAutocompleteItemType,
|
||||
useAutocomplete,
|
||||
} from '#/components/Autocomplete'
|
||||
import {AutosizedTextarea} from '#/components/forms/AutosizedTextarea'
|
||||
import {useOnKeyboard} from '#/components/hooks/useOnKeyboard'
|
||||
import {Span, Text} from '#/components/Typography'
|
||||
import {IS_ANDROID, IS_IOS, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
|
||||
|
||||
/*
|
||||
* ─── Types ────────────────────────────────────────────────────────────────────
|
||||
*/
|
||||
import {IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
|
||||
|
||||
export type SubmitRequest =
|
||||
| {
|
||||
@@ -67,9 +63,9 @@ export type SubmitRequest =
|
||||
}
|
||||
|
||||
/**
|
||||
* Bail-out API for special cases where a parent component needs to
|
||||
* imperatively control the Composer (e.g. clearing the input on submit).
|
||||
* Prefer props/callbacks for normal data flow.
|
||||
* Imperative API exposed via `internalApiRef` prop for parent components that
|
||||
* need to control the composer programmatically, e.g. to clear the input or
|
||||
* insert text at the current cursor position.
|
||||
*/
|
||||
export type ComposerInternalApi = {
|
||||
input?: ReturnType<typeof useTapper>['input']
|
||||
@@ -106,7 +102,6 @@ export type ComposerProps = Omit<
|
||||
paddingRight?: number
|
||||
}
|
||||
textStyle?: TextStyleProp['style']
|
||||
initialNumberOfLines?: number
|
||||
maxNumberOfLines?: number
|
||||
initialText?: string
|
||||
onChange?: (text: string) => void
|
||||
@@ -114,6 +109,10 @@ export type ComposerProps = Omit<
|
||||
onFacetCommitted?: (facet: TapperFacet) => void
|
||||
onRequestSubmit?: (request: SubmitRequest) => void
|
||||
internalApiRef?: React.Ref<ComposerInternalApi>
|
||||
autocompletePlacement?: Exclude<
|
||||
Parameters<typeof useSift>[0],
|
||||
undefined
|
||||
>['placement']
|
||||
}
|
||||
|
||||
export function Composer({
|
||||
@@ -123,7 +122,6 @@ export function Composer({
|
||||
style,
|
||||
padding,
|
||||
textStyle: rawTextStyle,
|
||||
initialNumberOfLines = 1,
|
||||
maxNumberOfLines,
|
||||
initialText,
|
||||
onChange: onChangeOuter,
|
||||
@@ -131,29 +129,34 @@ export function Composer({
|
||||
onFacetCommitted: onFacetCommittedOuter,
|
||||
onRequestSubmit,
|
||||
internalApiRef,
|
||||
autocompletePlacement,
|
||||
...rest
|
||||
}: ComposerProps) {
|
||||
const {theme: t, fonts} = useAlf()
|
||||
const textInputRef = useRef<TextInput>(null)
|
||||
|
||||
/*
|
||||
* Meat and potatoes
|
||||
*/
|
||||
const tapper = useTapper({initialText})
|
||||
const sift = useSift({
|
||||
offset: a.p_sm.padding,
|
||||
placement: 'top-start',
|
||||
placement: autocompletePlacement,
|
||||
dynamicWidth: IS_WEB,
|
||||
})
|
||||
const inputScrollSharedValue = useSharedValue(0)
|
||||
|
||||
/*
|
||||
* Active facet state for controlling the visibility of the Autocomplete.
|
||||
*/
|
||||
const [activeFacet, setActiveFacet] = useState<TapperActiveFacet | null>(null)
|
||||
|
||||
const callbackRefs = useRef({
|
||||
onActiveFacetOuter,
|
||||
onFacetCommittedOuter,
|
||||
})
|
||||
callbackRefs.current = {
|
||||
onActiveFacetOuter,
|
||||
onFacetCommittedOuter,
|
||||
}
|
||||
/*
|
||||
* Reanimated shared value for syncing scroll on all platforms.
|
||||
*/
|
||||
const inputScrollSharedValue = useSharedValue(0)
|
||||
|
||||
/*
|
||||
* Expose imperative internal API
|
||||
*/
|
||||
useImperativeHandle(
|
||||
internalApiRef,
|
||||
() => ({
|
||||
@@ -180,6 +183,17 @@ export function Composer({
|
||||
onChangeOuter?.(tapper.state.text)
|
||||
}, [tapper.state.text, onChangeOuter])
|
||||
|
||||
/*
|
||||
* Tapper callbacks
|
||||
*/
|
||||
const callbackRefs = useRef({
|
||||
onActiveFacetOuter,
|
||||
onFacetCommittedOuter,
|
||||
})
|
||||
callbackRefs.current = {
|
||||
onActiveFacetOuter,
|
||||
onFacetCommittedOuter,
|
||||
}
|
||||
useEffect(() => {
|
||||
const offActiveFacet = tapper.on('activeFacet', facet => {
|
||||
setActiveFacet(facet)
|
||||
@@ -198,105 +212,23 @@ export function Composer({
|
||||
}
|
||||
}, [tapper.on, tapper.input])
|
||||
|
||||
// ─── Text style computation ───────────────────────────────────────────
|
||||
|
||||
const {textStyle, textAreaStyle, minHeight, maxHeight} = useMemo(() => {
|
||||
const ts = normalizeTextStyles(
|
||||
[a.leading_snug, rawTextStyle, t.atoms.text],
|
||||
{
|
||||
fontScale: fonts.scaleMultiplier,
|
||||
fontFamily: fonts.family,
|
||||
flags: {},
|
||||
},
|
||||
)
|
||||
const lineHeight = ts.lineHeight || 20
|
||||
const verticalSpace =
|
||||
(padding?.paddingTop || 0) + (padding?.paddingBottom || 0)
|
||||
const mh = lineHeight * initialNumberOfLines + verticalSpace
|
||||
const xh = maxNumberOfLines
|
||||
? lineHeight * maxNumberOfLines + verticalSpace
|
||||
: 999
|
||||
/*
|
||||
* On web, we set an initial height and auto-resize via DOM measurement.
|
||||
* On Android, we drive height explicitly via onContentSizeChange to avoid
|
||||
* sub-pixel oscillation that causes layout jumpiness.
|
||||
* On iOS, minHeight/maxHeight works fine natively.
|
||||
*/
|
||||
const tas = IS_WEB
|
||||
? {height: lineHeight + verticalSpace}
|
||||
: IS_ANDROID
|
||||
? {height: mh}
|
||||
: {minHeight: mh, maxHeight: xh}
|
||||
|
||||
if (IS_IOS) {
|
||||
delete ts.lineHeight
|
||||
}
|
||||
|
||||
return {textStyle: ts, textAreaStyle: tas, minHeight: mh, maxHeight: xh}
|
||||
}, [t, fonts, padding, rawTextStyle, initialNumberOfLines, maxNumberOfLines])
|
||||
|
||||
/*
|
||||
* On Android, multiline TextInput oscillates between slightly different
|
||||
* contentSize values on consecutive layout passes (sub-pixel rounding).
|
||||
* This causes visible jumpiness when using minHeight/maxHeight. Instead,
|
||||
* we drive the height explicitly and ceil the value to stabilize it.
|
||||
* Styles
|
||||
*/
|
||||
const [androidInputHeight, setAndroidInputHeight] = useState(minHeight)
|
||||
|
||||
// ─── Height auto-resize + sift positioning ────────────────────────────
|
||||
|
||||
const updateAutocompletePosition = useCallback(() => {
|
||||
sift.updatePosition()
|
||||
}, [sift])
|
||||
|
||||
useOnKeyboard('keyboardDidShow', updateAutocompletePosition)
|
||||
useOnKeyboard('keyboardDidHide', updateAutocompletePosition)
|
||||
|
||||
const prevHeight = useRef(0)
|
||||
useEffect(() => {
|
||||
if (IS_WEB) {
|
||||
const el = textInputRef.current as unknown as HTMLTextAreaElement
|
||||
if (!el) return
|
||||
el.style.height = '0px'
|
||||
const scrollHeight = el.scrollHeight
|
||||
const nextHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight)
|
||||
el.style.height = `${nextHeight}px`
|
||||
el.style.overflowY = scrollHeight > maxHeight ? 'auto' : 'hidden'
|
||||
if (nextHeight !== prevHeight.current) {
|
||||
prevHeight.current = nextHeight
|
||||
updateAutocompletePosition()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (IS_IOS) {
|
||||
textInputRef.current?.measure((_x, _y, _w, h) => {
|
||||
if (h !== prevHeight.current) {
|
||||
prevHeight.current = h
|
||||
updateAutocompletePosition()
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [tapper.state.text, minHeight, maxHeight, updateAutocompletePosition])
|
||||
|
||||
/*
|
||||
* On Android, height is driven by onContentSizeChange (see the TextInput
|
||||
* below), so we update the autocomplete position when that height changes.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (IS_ANDROID) {
|
||||
updateAutocompletePosition()
|
||||
}
|
||||
}, [androidInputHeight, updateAutocompletePosition])
|
||||
|
||||
// ─── Scroll sync ──────────────────────────────────────────────────────
|
||||
|
||||
const previewScrollStyle = useAnimatedStyle(() => ({
|
||||
transform: [{translateY: -inputScrollSharedValue.value}],
|
||||
}))
|
||||
const textStyle = useMemo(() => {
|
||||
return normalizeTextStyles([a.leading_snug, rawTextStyle, t.atoms.text], {
|
||||
fontScale: fonts.scaleMultiplier,
|
||||
fontFamily: fonts.family,
|
||||
flags: {},
|
||||
})
|
||||
}, [rawTextStyle, fonts])
|
||||
|
||||
// ─── Web keyboard handling ────────────────────────────────────────────
|
||||
|
||||
/*
|
||||
* Web keyboard handling
|
||||
*/
|
||||
const isComposing = useRef(false)
|
||||
const onKeyPressWeb = useCallback(
|
||||
(e: React.KeyboardEvent | any) => {
|
||||
@@ -323,6 +255,15 @@ export function Composer({
|
||||
[onRequestSubmit],
|
||||
)
|
||||
|
||||
/*
|
||||
* Sift popover positioning
|
||||
*/
|
||||
const updateAutocompletePosition = useCallback(() => {
|
||||
sift.updatePosition()
|
||||
}, [sift])
|
||||
useOnKeyboard('keyboardDidShow', updateAutocompletePosition)
|
||||
useOnKeyboard('keyboardDidHide', updateAutocompletePosition)
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={[a.relative, style]}>
|
||||
@@ -359,47 +300,32 @@ export function Composer({
|
||||
</Text>
|
||||
</Animated.View>
|
||||
</View>
|
||||
<TextInput
|
||||
dirName="ltr"
|
||||
multiline={true}
|
||||
hitSlop={HITSLOP_10}
|
||||
<AutosizedTextarea
|
||||
maxRows={12}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={t.palette.contrast_500}
|
||||
accessibilityLabel={label}
|
||||
accessibilityHint={label}
|
||||
keyboardAppearance={t.scheme}
|
||||
submitBehavior="newline"
|
||||
onSubmitEditing={e => {
|
||||
onRequestSubmit?.({platform: 'native', nativeEvent: e})
|
||||
}}
|
||||
style={[
|
||||
textStyle,
|
||||
padding,
|
||||
a.relative,
|
||||
a.z_20,
|
||||
a.border_0,
|
||||
{
|
||||
color: 'transparent',
|
||||
background: 'transparent',
|
||||
textAlignVertical: 'top',
|
||||
includeFontPadding: false,
|
||||
},
|
||||
IS_ANDROID ? {height: androidInputHeight} : textAreaStyle,
|
||||
web({
|
||||
resize: 'none',
|
||||
outline: 'none',
|
||||
caretColor: textStyle.color ?? 'black',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
overscrollBehavior: 'none',
|
||||
...textAreaStyle,
|
||||
}),
|
||||
]}
|
||||
{...rest}
|
||||
{...tapper.inputProps}
|
||||
{...sift.targetProps}
|
||||
ref={mergeRefs([
|
||||
textInputRef,
|
||||
rest.ref,
|
||||
tapper.inputProps.ref,
|
||||
sift.targetProps.ref,
|
||||
@@ -416,15 +342,6 @@ export function Composer({
|
||||
inputScrollSharedValue.value = e.nativeEvent.contentOffset.y
|
||||
}
|
||||
}}
|
||||
onContentSizeChange={
|
||||
IS_ANDROID
|
||||
? e => {
|
||||
const h = Math.ceil(e.nativeEvent.contentSize.height)
|
||||
const clamped = Math.min(Math.max(h, minHeight), maxHeight)
|
||||
setAndroidInputHeight(clamped)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
// @ts-ignore web only
|
||||
onCompositionStart={() => {
|
||||
isComposing.current = true
|
||||
@@ -433,6 +350,7 @@ export function Composer({
|
||||
onCompositionEnd={() => {
|
||||
isComposing.current = false
|
||||
}}
|
||||
onUpdateHeight={updateAutocompletePosition}
|
||||
/>
|
||||
|
||||
{children}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||
import {
|
||||
TextInput,
|
||||
type TextInputContentSizeChangeEvent,
|
||||
type TextInputProps,
|
||||
} from 'react-native'
|
||||
|
||||
import {mergeRefs} from '#/lib/merge-refs'
|
||||
import {atoms as a, extractPadding, flatten, useAlf, web} from '#/alf'
|
||||
import {normalizeTextStyles} from '#/alf/typography'
|
||||
import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env'
|
||||
|
||||
export function AutosizedTextarea({
|
||||
ref,
|
||||
label,
|
||||
minRows = 1,
|
||||
maxRows,
|
||||
onUpdateHeight,
|
||||
|
||||
onChangeText: onChangeTextOuter,
|
||||
onContentSizeChange: onContentSizeChangeOuter,
|
||||
|
||||
style,
|
||||
...rest
|
||||
}: Omit<TextInputProps, 'multiline'> & {
|
||||
ref?: React.Ref<TextInput>
|
||||
label: string
|
||||
minRows?: number
|
||||
maxRows?: number
|
||||
onUpdateHeight?: (height: number) => void
|
||||
}) {
|
||||
const textInputRef = useRef<TextInput>(null)
|
||||
const {theme: t, fonts} = useAlf()
|
||||
const {processedStyle, minHeight, maxHeight} = useMemo(() => {
|
||||
const fs = flatten(style)
|
||||
const ts = normalizeTextStyles([a.leading_snug, fs, t.atoms.text], {
|
||||
fontScale: fonts.scaleMultiplier,
|
||||
fontFamily: fonts.family,
|
||||
flags: {},
|
||||
})
|
||||
const lineHeight = ts.lineHeight || 20
|
||||
const padding = extractPadding(fs ?? {})
|
||||
const verticalSpace = padding.paddingTop + padding.paddingBottom
|
||||
const mh = lineHeight * minRows + verticalSpace
|
||||
const xh = maxRows ? lineHeight * maxRows + verticalSpace : Infinity
|
||||
/*
|
||||
* On web, we set an initial height and auto-resize via DOM measurement.
|
||||
* On Android, we drive height explicitly via onContentSizeChange to avoid
|
||||
* sub-pixel oscillation that causes layout jumpiness.
|
||||
* On iOS, minHeight/maxHeight works fine natively.
|
||||
*/
|
||||
const tas = IS_WEB
|
||||
? {height: lineHeight + verticalSpace}
|
||||
: IS_ANDROID
|
||||
? {height: mh}
|
||||
: {minHeight: mh, maxHeight: xh}
|
||||
|
||||
if (IS_IOS) {
|
||||
delete ts.lineHeight
|
||||
}
|
||||
|
||||
return {
|
||||
processedStyle: {
|
||||
...ts,
|
||||
...tas,
|
||||
},
|
||||
minHeight: mh,
|
||||
maxHeight: xh,
|
||||
}
|
||||
}, [t, fonts, style, minRows, maxRows])
|
||||
|
||||
/*
|
||||
* On Android, multiline TextInput oscillates between slightly different
|
||||
* contentSize values on consecutive layout passes (sub-pixel rounding).
|
||||
* This causes visible jumpiness when using minHeight/maxHeight. Instead,
|
||||
* we drive the height explicitly and ceil the value to stabilize it.
|
||||
*/
|
||||
const [androidInputHeight, setAndroidInputHeight] = useState(minHeight)
|
||||
|
||||
const prevHeight = useRef(0)
|
||||
const onChangeText = useCallback(
|
||||
(text: string) => {
|
||||
if (IS_WEB) {
|
||||
const el = textInputRef.current as unknown as HTMLTextAreaElement
|
||||
if (el) {
|
||||
el.style.height = '0px'
|
||||
const scrollHeight = el.scrollHeight
|
||||
const nextHeight = Math.min(
|
||||
Math.max(scrollHeight, minHeight),
|
||||
maxHeight,
|
||||
)
|
||||
el.style.height = `${nextHeight}px`
|
||||
el.style.overflowY = scrollHeight > maxHeight ? 'auto' : 'hidden'
|
||||
if (nextHeight !== prevHeight.current) {
|
||||
prevHeight.current = nextHeight
|
||||
onUpdateHeight?.(nextHeight)
|
||||
}
|
||||
}
|
||||
} else if (IS_IOS) {
|
||||
textInputRef.current?.measure((_x, _y, _w, h) => {
|
||||
if (h !== prevHeight.current) {
|
||||
prevHeight.current = h
|
||||
onUpdateHeight?.(h)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onChangeTextOuter?.(text)
|
||||
},
|
||||
[onChangeTextOuter, minHeight, maxHeight, onUpdateHeight],
|
||||
)
|
||||
|
||||
const onContentSizeChange = useCallback(
|
||||
(e: TextInputContentSizeChangeEvent) => {
|
||||
if (IS_ANDROID) {
|
||||
const h = Math.ceil(e.nativeEvent.contentSize.height)
|
||||
const nextHeight = Math.min(Math.max(h, minHeight), maxHeight)
|
||||
if (nextHeight !== androidInputHeight) {
|
||||
setAndroidInputHeight(nextHeight)
|
||||
}
|
||||
}
|
||||
|
||||
onContentSizeChangeOuter?.(e)
|
||||
},
|
||||
[onContentSizeChangeOuter, minHeight, maxHeight, androidInputHeight],
|
||||
)
|
||||
|
||||
/*
|
||||
* On Android, height is driven by onContentSizeChange (see the TextInput
|
||||
* below), so we update the autocomplete position when that height changes.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (IS_ANDROID) {
|
||||
onUpdateHeight?.(androidInputHeight)
|
||||
}
|
||||
}, [androidInputHeight, onUpdateHeight])
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
multiline
|
||||
placeholderTextColor={t.palette.contrast_500}
|
||||
accessibilityLabel={label}
|
||||
accessibilityHint={label}
|
||||
placeholder={label}
|
||||
keyboardAppearance={t.scheme}
|
||||
submitBehavior="newline"
|
||||
style={[
|
||||
a.relative,
|
||||
a.border_0,
|
||||
IS_ANDROID ? {height: androidInputHeight} : {},
|
||||
{
|
||||
textAlignVertical: 'top',
|
||||
includeFontPadding: false,
|
||||
},
|
||||
web({
|
||||
resize: 'none',
|
||||
outline: 'none',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
overscrollBehavior: 'none',
|
||||
}),
|
||||
processedStyle,
|
||||
]}
|
||||
{...rest}
|
||||
ref={mergeRefs([textInputRef, ref])}
|
||||
onChangeText={onChangeText}
|
||||
onContentSizeChange={onContentSizeChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -3,8 +3,9 @@ import {type TextInput, View} from 'react-native'
|
||||
|
||||
import {APP_LANGUAGES} from '#/lib/../locale/languages'
|
||||
import {type CountryCode} from '#/lib/international-telephone-codes'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {AutosizedTextarea} from '#/components/forms/AutosizedTextarea'
|
||||
import {DateField, LabelText} from '#/components/forms/DateField'
|
||||
import * as SegmentedControl from '#/components/forms/SegmentedControl'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
@@ -16,6 +17,7 @@ import * as Select from '#/components/Select'
|
||||
import {H1, H3} from '#/components/Typography'
|
||||
|
||||
export function Forms() {
|
||||
const t = useTheme()
|
||||
const [toggleGroupAValues, setToggleGroupAValues] = useState(['a'])
|
||||
const [toggleGroupBValues, setToggleGroupBValues] = useState(['a', 'b'])
|
||||
const [toggleGroupCValues, setToggleGroupCValues] = useState(['a', 'b'])
|
||||
@@ -36,6 +38,32 @@ export function Forms() {
|
||||
<View style={[a.gap_4xl, a.align_start]}>
|
||||
<H1>Forms</H1>
|
||||
|
||||
<View style={[a.gap_md, a.align_start, a.w_full]}>
|
||||
<AutosizedTextarea
|
||||
label="AutosizedTextarea minRows=1 maxRows=5"
|
||||
style={[
|
||||
a.w_full,
|
||||
a.p_md,
|
||||
a.rounded_sm,
|
||||
a.border,
|
||||
t.atoms.border_contrast_medium,
|
||||
]}
|
||||
maxRows={5}
|
||||
/>
|
||||
<AutosizedTextarea
|
||||
label="AutosizedTextarea minRows=3 maxRows=10"
|
||||
style={[
|
||||
a.w_full,
|
||||
a.p_md,
|
||||
a.rounded_sm,
|
||||
a.border,
|
||||
t.atoms.border_contrast_medium,
|
||||
]}
|
||||
minRows={3}
|
||||
maxRows={10}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Select.Root value={lang} onValueChange={setLang}>
|
||||
<Select.Trigger label="Select app language">
|
||||
<Select.ValueText />
|
||||
|
||||
@@ -75,6 +75,8 @@ export default function Storybook() {
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<Forms />
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
size="small"
|
||||
@@ -109,7 +111,6 @@ export default function Storybook() {
|
||||
|
||||
<Toasts />
|
||||
<Buttons />
|
||||
<Forms />
|
||||
<Typography />
|
||||
<Spacing />
|
||||
<Shadows />
|
||||
|
||||
Reference in New Issue
Block a user