@hzblj/kara-components 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/lib/module/atoms/badge/badge.android.js +5 -0
- package/lib/module/atoms/badge/badge.android.js.map +1 -1
- package/lib/module/atoms/badge/badge.ios.js +2 -0
- package/lib/module/atoms/badge/badge.ios.js.map +1 -1
- package/lib/module/atoms/button/button-tokens.js +8 -0
- package/lib/module/atoms/button/button-tokens.js.map +1 -1
- package/lib/module/atoms/button/button.android.js +43 -15
- package/lib/module/atoms/button/button.android.js.map +1 -1
- package/lib/module/atoms/button/button.ios.js +66 -10
- package/lib/module/atoms/button/button.ios.js.map +1 -1
- package/lib/module/atoms/checkbox/checkbox.android.js +5 -1
- package/lib/module/atoms/checkbox/checkbox.android.js.map +1 -1
- package/lib/module/atoms/checkbox/checkbox.ios.js +3 -1
- package/lib/module/atoms/checkbox/checkbox.ios.js.map +1 -1
- package/lib/module/atoms/color-picker/color-picker.android.js +4 -0
- package/lib/module/atoms/color-picker/color-picker.android.js.map +1 -1
- package/lib/module/atoms/color-picker/color-picker.ios.js +24 -2
- package/lib/module/atoms/color-picker/color-picker.ios.js.map +1 -1
- package/lib/module/atoms/date-picker/date-picker.android.js +85 -10
- package/lib/module/atoms/date-picker/date-picker.android.js.map +1 -1
- package/lib/module/atoms/date-picker/date-picker.ios.js +9 -7
- package/lib/module/atoms/date-picker/date-picker.ios.js.map +1 -1
- package/lib/module/atoms/divider/divider.android.js +17 -4
- package/lib/module/atoms/divider/divider.android.js.map +1 -1
- package/lib/module/atoms/divider/divider.ios.js +4 -0
- package/lib/module/atoms/divider/divider.ios.js.map +1 -1
- package/lib/module/atoms/fab/fab.android.js +64 -8
- package/lib/module/atoms/fab/fab.android.js.map +1 -1
- package/lib/module/atoms/fab/fab.ios.js +68 -12
- package/lib/module/atoms/fab/fab.ios.js.map +1 -1
- package/lib/module/atoms/icon-button/icon-button.android.js +48 -5
- package/lib/module/atoms/icon-button/icon-button.android.js.map +1 -1
- package/lib/module/atoms/icon-button/icon-button.ios.js +60 -10
- package/lib/module/atoms/icon-button/icon-button.ios.js.map +1 -1
- package/lib/module/atoms/list/list.android.js +32 -4
- package/lib/module/atoms/list/list.android.js.map +1 -1
- package/lib/module/atoms/list/list.ios.js +15 -5
- package/lib/module/atoms/list/list.ios.js.map +1 -1
- package/lib/module/atoms/progress/progress.android.js +21 -7
- package/lib/module/atoms/progress/progress.android.js.map +1 -1
- package/lib/module/atoms/progress/progress.ios.js +5 -1
- package/lib/module/atoms/progress/progress.ios.js.map +1 -1
- package/lib/module/atoms/segmented-control/segmented-control.android.js +66 -12
- package/lib/module/atoms/segmented-control/segmented-control.android.js.map +1 -1
- package/lib/module/atoms/segmented-control/segmented-control.ios.js +19 -8
- package/lib/module/atoms/segmented-control/segmented-control.ios.js.map +1 -1
- package/lib/module/atoms/segmented-control/segments.js +16 -0
- package/lib/module/atoms/segmented-control/segments.js.map +1 -0
- package/lib/module/atoms/slider/slider.android.js +16 -2
- package/lib/module/atoms/slider/slider.android.js.map +1 -1
- package/lib/module/atoms/slider/slider.ios.js +22 -2
- package/lib/module/atoms/slider/slider.ios.js.map +1 -1
- package/lib/module/atoms/switch/switch.android.js +5 -1
- package/lib/module/atoms/switch/switch.android.js.map +1 -1
- package/lib/module/atoms/switch/switch.ios.js +3 -1
- package/lib/module/atoms/switch/switch.ios.js.map +1 -1
- package/lib/module/atoms/text/spans.js +2 -1
- package/lib/module/atoms/text/spans.js.map +1 -1
- package/lib/module/atoms/text/text.android.js +18 -6
- package/lib/module/atoms/text/text.android.js.map +1 -1
- package/lib/module/atoms/text/text.ios.js +6 -2
- package/lib/module/atoms/text/text.ios.js.map +1 -1
- package/lib/module/atoms/text-field/text-field-input.js +14 -0
- package/lib/module/atoms/text-field/text-field-input.js.map +1 -0
- package/lib/module/atoms/text-field/text-field.android.js +113 -6
- package/lib/module/atoms/text-field/text-field.android.js.map +1 -1
- package/lib/module/atoms/text-field/text-field.ios.js +75 -3
- package/lib/module/atoms/text-field/text-field.ios.js.map +1 -1
- package/lib/module/molecules/collapsible/collapsible.android.js +4 -3
- package/lib/module/molecules/collapsible/collapsible.android.js.map +1 -1
- package/lib/module/molecules/collapsible/collapsible.ios.js +3 -1
- package/lib/module/molecules/collapsible/collapsible.ios.js.map +1 -1
- package/lib/module/molecules/radio-group/radio-group.android.js +10 -2
- package/lib/module/molecules/radio-group/radio-group.android.js.map +1 -1
- package/lib/module/molecules/radio-group/radio-group.ios.js +6 -2
- package/lib/module/molecules/radio-group/radio-group.ios.js.map +1 -1
- package/lib/module/molecules/search-bar/search-bar.js +9 -0
- package/lib/module/molecules/search-bar/search-bar.js.map +1 -1
- package/lib/module/molecules/stepper/stepper.android.js +25 -13
- package/lib/module/molecules/stepper/stepper.android.js.map +1 -1
- package/lib/module/molecules/stepper/stepper.ios.js +12 -6
- package/lib/module/molecules/stepper/stepper.ios.js.map +1 -1
- package/lib/module/organisms/action-sheet/action-sheet.android.js +16 -8
- package/lib/module/organisms/action-sheet/action-sheet.android.js.map +1 -1
- package/lib/module/organisms/action-sheet/action-sheet.ios.js +16 -3
- package/lib/module/organisms/action-sheet/action-sheet.ios.js.map +1 -1
- package/lib/module/organisms/alert/alert.android.js +4 -3
- package/lib/module/organisms/alert/alert.android.js.map +1 -1
- package/lib/module/organisms/alert/alert.ios.js +3 -2
- package/lib/module/organisms/alert/alert.ios.js.map +1 -1
- package/lib/module/organisms/context-menu/context-menu.android.js +3 -1
- package/lib/module/organisms/context-menu/context-menu.android.js.map +1 -1
- package/lib/module/organisms/context-menu/context-menu.ios.js +1 -0
- package/lib/module/organisms/context-menu/context-menu.ios.js.map +1 -1
- package/lib/module/organisms/fab-menu/fab-menu.android.js +3 -0
- package/lib/module/organisms/fab-menu/fab-menu.android.js.map +1 -1
- package/lib/module/organisms/fab-menu/fab-menu.ios.js +3 -0
- package/lib/module/organisms/fab-menu/fab-menu.ios.js.map +1 -1
- package/lib/module/organisms/menu/menu-parts.js +20 -0
- package/lib/module/organisms/menu/menu-parts.js.map +1 -0
- package/lib/module/organisms/menu/menu.android.js +51 -19
- package/lib/module/organisms/menu/menu.android.js.map +1 -1
- package/lib/module/organisms/menu/menu.ios.js +71 -8
- package/lib/module/organisms/menu/menu.ios.js.map +1 -1
- package/lib/module/organisms/sheet/sheet.js +2 -1
- package/lib/module/organisms/sheet/sheet.js.map +1 -1
- package/lib/typescript/atoms/badge/badge.types.d.ts +2 -0
- package/lib/typescript/atoms/button/button-tokens.d.ts +6 -2
- package/lib/typescript/atoms/button/button.types.d.ts +19 -5
- package/lib/typescript/atoms/checkbox/checkbox.types.d.ts +2 -0
- package/lib/typescript/atoms/color-picker/color-picker.types.d.ts +2 -0
- package/lib/typescript/atoms/date-picker/date-picker.ios.d.ts +5 -5
- package/lib/typescript/atoms/date-picker/date-picker.types.d.ts +30 -7
- package/lib/typescript/atoms/divider/divider.ios.d.ts +4 -0
- package/lib/typescript/atoms/divider/divider.types.d.ts +13 -2
- package/lib/typescript/atoms/fab/fab.types.d.ts +21 -4
- package/lib/typescript/atoms/icon-button/icon-button.types.d.ts +23 -4
- package/lib/typescript/atoms/list/list.types.d.ts +15 -1
- package/lib/typescript/atoms/progress/progress.types.d.ts +13 -2
- package/lib/typescript/atoms/segmented-control/segmented-control.ios.d.ts +3 -1
- package/lib/typescript/atoms/segmented-control/segmented-control.types.d.ts +26 -3
- package/lib/typescript/atoms/segmented-control/segments.d.ts +6 -0
- package/lib/typescript/atoms/slider/slider.ios.d.ts +4 -0
- package/lib/typescript/atoms/slider/slider.types.d.ts +7 -0
- package/lib/typescript/atoms/switch/switch.types.d.ts +2 -0
- package/lib/typescript/atoms/text/spans.d.ts +2 -0
- package/lib/typescript/atoms/text/text.types.d.ts +9 -1
- package/lib/typescript/atoms/text-field/text-field-input.d.ts +7 -0
- package/lib/typescript/atoms/text-field/text-field.types.d.ts +77 -4
- package/lib/typescript/molecules/collapsible/collapsible.types.d.ts +2 -0
- package/lib/typescript/molecules/radio-group/radio-group.types.d.ts +4 -0
- package/lib/typescript/molecules/search-bar/search-bar.types.d.ts +11 -1
- package/lib/typescript/molecules/stepper/stepper.ios.d.ts +2 -1
- package/lib/typescript/molecules/stepper/stepper.types.d.ts +17 -0
- package/lib/typescript/organisms/action-sheet/action-sheet.ios.d.ts +4 -0
- package/lib/typescript/organisms/action-sheet/action-sheet.types.d.ts +8 -3
- package/lib/typescript/organisms/alert/alert.types.d.ts +8 -3
- package/lib/typescript/organisms/menu/menu-parts.d.ts +14 -0
- package/lib/typescript/organisms/menu/menu.types.d.ts +35 -7
- package/lib/typescript/organisms/sheet/sheet.types.d.ts +7 -1
- package/package.json +4 -4
- package/src/atoms/badge/badge.android.tsx +9 -2
- package/src/atoms/badge/badge.ios.tsx +2 -2
- package/src/atoms/badge/badge.types.ts +2 -0
- package/src/atoms/button/button-tokens.ts +6 -3
- package/src/atoms/button/button.android.tsx +35 -11
- package/src/atoms/button/button.ios.tsx +72 -8
- package/src/atoms/button/button.types.ts +20 -5
- package/src/atoms/checkbox/checkbox.android.tsx +5 -2
- package/src/atoms/checkbox/checkbox.ios.tsx +2 -2
- package/src/atoms/checkbox/checkbox.types.ts +2 -0
- package/src/atoms/color-picker/color-picker.android.tsx +10 -2
- package/src/atoms/color-picker/color-picker.ios.tsx +24 -1
- package/src/atoms/color-picker/color-picker.types.ts +2 -0
- package/src/atoms/date-picker/date-picker.android.tsx +86 -16
- package/src/atoms/date-picker/date-picker.ios.tsx +20 -8
- package/src/atoms/date-picker/date-picker.types.ts +31 -7
- package/src/atoms/divider/divider.android.tsx +14 -3
- package/src/atoms/divider/divider.ios.tsx +4 -0
- package/src/atoms/divider/divider.types.ts +14 -2
- package/src/atoms/fab/fab.android.tsx +61 -11
- package/src/atoms/fab/fab.ios.tsx +48 -13
- package/src/atoms/fab/fab.types.ts +22 -4
- package/src/atoms/icon-button/icon-button.android.tsx +60 -10
- package/src/atoms/icon-button/icon-button.ios.tsx +77 -8
- package/src/atoms/icon-button/icon-button.types.ts +24 -4
- package/src/atoms/list/list.android.tsx +26 -4
- package/src/atoms/list/list.ios.tsx +20 -5
- package/src/atoms/list/list.types.ts +16 -1
- package/src/atoms/progress/progress.android.tsx +32 -6
- package/src/atoms/progress/progress.ios.tsx +11 -5
- package/src/atoms/progress/progress.types.ts +13 -2
- package/src/atoms/segmented-control/segmented-control.android.tsx +69 -10
- package/src/atoms/segmented-control/segmented-control.ios.tsx +24 -5
- package/src/atoms/segmented-control/segmented-control.types.ts +28 -3
- package/src/atoms/segmented-control/segments.ts +16 -0
- package/src/atoms/slider/slider.android.tsx +18 -2
- package/src/atoms/slider/slider.ios.tsx +38 -2
- package/src/atoms/slider/slider.types.ts +7 -0
- package/src/atoms/switch/switch.android.tsx +5 -2
- package/src/atoms/switch/switch.ios.tsx +2 -2
- package/src/atoms/switch/switch.types.ts +2 -0
- package/src/atoms/text/spans.ts +2 -1
- package/src/atoms/text/text.android.tsx +20 -7
- package/src/atoms/text/text.ios.tsx +7 -0
- package/src/atoms/text/text.types.ts +10 -1
- package/src/atoms/text-field/text-field-input.ts +17 -0
- package/src/atoms/text-field/text-field.android.tsx +132 -9
- package/src/atoms/text-field/text-field.ios.tsx +110 -6
- package/src/atoms/text-field/text-field.types.ts +98 -4
- package/src/molecules/collapsible/collapsible.android.tsx +10 -2
- package/src/molecules/collapsible/collapsible.ios.tsx +8 -2
- package/src/molecules/collapsible/collapsible.types.ts +2 -0
- package/src/molecules/radio-group/radio-group.android.tsx +18 -4
- package/src/molecules/radio-group/radio-group.ios.tsx +19 -5
- package/src/molecules/radio-group/radio-group.types.ts +4 -0
- package/src/molecules/search-bar/search-bar.tsx +9 -0
- package/src/molecules/search-bar/search-bar.types.ts +12 -1
- package/src/molecules/stepper/stepper.android.tsx +24 -4
- package/src/molecules/stepper/stepper.ios.tsx +23 -8
- package/src/molecules/stepper/stepper.types.ts +17 -0
- package/src/organisms/action-sheet/action-sheet.android.tsx +23 -11
- package/src/organisms/action-sheet/action-sheet.ios.tsx +20 -3
- package/src/organisms/action-sheet/action-sheet.types.ts +8 -3
- package/src/organisms/alert/alert.android.tsx +11 -8
- package/src/organisms/alert/alert.ios.tsx +2 -1
- package/src/organisms/alert/alert.types.ts +8 -3
- package/src/organisms/context-menu/context-menu.android.tsx +3 -2
- package/src/organisms/context-menu/context-menu.ios.tsx +1 -0
- package/src/organisms/fab-menu/fab-menu.android.tsx +3 -0
- package/src/organisms/fab-menu/fab-menu.ios.tsx +9 -1
- package/src/organisms/menu/menu-parts.ts +21 -0
- package/src/organisms/menu/menu.android.tsx +39 -10
- package/src/organisms/menu/menu.ios.tsx +55 -5
- package/src/organisms/menu/menu.types.ts +30 -7
- package/src/organisms/sheet/sheet.tsx +2 -1
- package/src/organisms/sheet/sheet.types.ts +7 -1
|
@@ -7,7 +7,13 @@ import {useMemo} from 'react'
|
|
|
7
7
|
import {useHostGuard} from '../../host-context'
|
|
8
8
|
import type {ProgressComponent, ProgressProps} from './progress.types'
|
|
9
9
|
|
|
10
|
-
export const Progress: ProgressComponent = ({
|
|
10
|
+
export const Progress: ProgressComponent = ({
|
|
11
|
+
accessibilityLabel,
|
|
12
|
+
circular = false,
|
|
13
|
+
color = 'fill.accent.primary',
|
|
14
|
+
testID,
|
|
15
|
+
value,
|
|
16
|
+
}: ProgressProps) => {
|
|
11
17
|
useHostGuard('Progress')
|
|
12
18
|
const theme = useAppTheme()
|
|
13
19
|
const accessibilityModifiers = useAccessibilityModifiers({accessibilityLabel})
|
|
@@ -16,16 +22,16 @@ export const Progress: ProgressComponent = ({accessibilityLabel, circular = fals
|
|
|
16
22
|
const modifiers = useMemo(
|
|
17
23
|
() => [
|
|
18
24
|
ring ? gaugeStyle('circularCapacity') : progressViewStyle(circular ? 'circular' : 'linear'),
|
|
19
|
-
tint(theme.color(
|
|
25
|
+
tint(theme.color(color)),
|
|
20
26
|
...accessibilityModifiers,
|
|
21
27
|
],
|
|
22
|
-
[accessibilityModifiers, circular, ring, theme]
|
|
28
|
+
[accessibilityModifiers, circular, color, ring, theme]
|
|
23
29
|
)
|
|
24
30
|
|
|
25
31
|
// SwiftUI's circular `ProgressView` ignores its value on iOS and spins; a ring that fills is a capacity `Gauge`.
|
|
26
32
|
return ring ? (
|
|
27
|
-
<Gauge modifiers={modifiers} value={value} />
|
|
33
|
+
<Gauge modifiers={modifiers} testID={testID} value={value} />
|
|
28
34
|
) : (
|
|
29
|
-
<ProgressView modifiers={modifiers} value={value ?? null} />
|
|
35
|
+
<ProgressView modifiers={modifiers} testID={testID} value={value ?? null} />
|
|
30
36
|
)
|
|
31
37
|
}
|
|
@@ -1,14 +1,25 @@
|
|
|
1
|
+
import type {ColorToken} from '@hzblj/kara-theme'
|
|
1
2
|
import type {ReactNode} from 'react'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* A progress bar or ring, tinted `fill.accent.primary
|
|
5
|
-
* value, which `ProgressView` cannot draw on iOS) / Compose's `LinearProgressIndicator` and
|
|
5
|
+
* A progress bar or ring, tinted `fill.accent.primary` unless `color` names another: SwiftUI `ProgressView` (a capacity
|
|
6
|
+
* `Gauge` for a ring with a value, which `ProgressView` cannot draw on iOS) / Compose's `LinearProgressIndicator` and
|
|
7
|
+
* `CircularProgressIndicator`.
|
|
6
8
|
*/
|
|
7
9
|
export type ProgressProps = {
|
|
8
10
|
/** What is in progress, for VoiceOver and TalkBack: "Uploading". They read how far along it is themselves. */
|
|
9
11
|
accessibilityLabel?: string
|
|
10
12
|
/** A circular indicator instead of a bar. Off by default. */
|
|
11
13
|
circular?: boolean
|
|
14
|
+
/**
|
|
15
|
+
* Its colour: `fill.destructive.primary` for storage nearly full, `fill.success.primary` for a goal reached.
|
|
16
|
+
* `fill.accent.primary` by default. iOS tints the bar or ring with it. Android draws the indicator in it and, for any
|
|
17
|
+
* colour but the accent, the track in the same colour faded (Material's own track stays under the accent, and an
|
|
18
|
+
* indeterminate ring has none).
|
|
19
|
+
*/
|
|
20
|
+
color?: ColorToken
|
|
21
|
+
/** Test identifier, forwarded to the native view. */
|
|
22
|
+
testID?: string
|
|
12
23
|
/** Between `0` and `1`. Leave it unset for an indeterminate indicator. */
|
|
13
24
|
value?: number
|
|
14
25
|
}
|
|
@@ -1,16 +1,60 @@
|
|
|
1
1
|
import {Host, SegmentedButton, SingleChoiceSegmentedButtonRow, Text} from '@expo/ui/jetpack-compose'
|
|
2
|
-
import {fillMaxWidth, testID as testIdModifier, weight} from '@expo/ui/jetpack-compose/modifiers'
|
|
2
|
+
import {fillMaxWidth, height, testID as testIdModifier, weight} from '@expo/ui/jetpack-compose/modifiers'
|
|
3
|
+
import {iconResourceName} from '@hzblj/expo-native-icons'
|
|
3
4
|
import {haptics} from '@hzblj/expo-rich-haptics'
|
|
4
|
-
import {
|
|
5
|
+
import {DrawableIcon, NativeImage} from '@hzblj/kara-native/jetpack-compose'
|
|
6
|
+
import {spacing as spacingTokens, useAppTheme} from '@hzblj/kara-theme'
|
|
5
7
|
import {useCallback, useMemo} from 'react'
|
|
6
8
|
|
|
7
9
|
import {useInsideHost} from '../../host-context'
|
|
8
|
-
import
|
|
10
|
+
import {HStack} from '../h-stack'
|
|
11
|
+
import type {SegmentedControlComponent, SegmentedControlProps, SegmentedControlSegment} from './segmented-control.types'
|
|
12
|
+
import {imageUri, segmentOf} from './segments'
|
|
9
13
|
|
|
10
14
|
const MATCH_HEIGHT_ONLY = {horizontal: false, vertical: true}
|
|
11
15
|
|
|
12
|
-
/**
|
|
13
|
-
const
|
|
16
|
+
/** Material's own segment height, which `spacing` only ever adds to. */
|
|
17
|
+
const MATERIAL_HEIGHT = 40
|
|
18
|
+
|
|
19
|
+
type SegmentLabelProps = {
|
|
20
|
+
iconSize: number
|
|
21
|
+
segment: SegmentedControlSegment
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* What a segment shows: its label, one of the app's icons before it, or an image loaded by kara-native. The icon and a
|
|
26
|
+
* label the image falls back to take the segment's content colour, so they follow the selection and `disabled`.
|
|
27
|
+
*/
|
|
28
|
+
const SegmentLabel = ({iconSize, segment}: SegmentLabelProps) => {
|
|
29
|
+
const theme = useAppTheme()
|
|
30
|
+
const face = theme.typography.subheadlineMedium
|
|
31
|
+
|
|
32
|
+
if (segment.image !== undefined) {
|
|
33
|
+
return (
|
|
34
|
+
<NativeImage
|
|
35
|
+
fontFamily={face.fontFamily}
|
|
36
|
+
fontSize={face.fontSize}
|
|
37
|
+
fontWeight={String(face.fontWeight)}
|
|
38
|
+
imageHeight={iconSize}
|
|
39
|
+
label={segment.label}
|
|
40
|
+
source={imageUri(segment.image)}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const label = <Text style={face}>{segment.label}</Text>
|
|
46
|
+
|
|
47
|
+
if (segment.icon === undefined) {
|
|
48
|
+
return label
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<HStack align="center" spacing="s6">
|
|
53
|
+
<DrawableIcon iconSize={iconSize} resourceName={iconResourceName(segment.icon)} />
|
|
54
|
+
{label}
|
|
55
|
+
</HStack>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
14
58
|
|
|
15
59
|
/**
|
|
16
60
|
* Material's single-choice segmented buttons, the selected one filled with the accent like a checked `Switch`. Its own
|
|
@@ -20,13 +64,28 @@ const SEGMENT = [weight(1)]
|
|
|
20
64
|
*/
|
|
21
65
|
export const SegmentedControl: SegmentedControlComponent = ({
|
|
22
66
|
disabled = false,
|
|
67
|
+
iconSize = 's20',
|
|
23
68
|
onChange,
|
|
24
69
|
selectedIndex,
|
|
70
|
+
spacing,
|
|
25
71
|
testID,
|
|
26
72
|
values,
|
|
27
73
|
}: SegmentedControlProps) => {
|
|
28
74
|
const theme = useAppTheme()
|
|
29
75
|
const insideHost = useInsideHost()
|
|
76
|
+
const segments = useMemo(() => values.map(segmentOf), [values])
|
|
77
|
+
const iconPoints = spacingTokens[iconSize]
|
|
78
|
+
|
|
79
|
+
// Every segment the same width, like UIKit's: Material's would size each to its label. With `spacing`, as tall as
|
|
80
|
+
// the content and that space, and never shorter than Material's own segment.
|
|
81
|
+
const segmentModifiers = useMemo(() => {
|
|
82
|
+
if (spacing === undefined) {
|
|
83
|
+
return [weight(1)]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const content = Math.max(theme.typography.subheadlineMedium.lineHeight, iconPoints)
|
|
87
|
+
return [weight(1), height(Math.max(MATERIAL_HEIGHT, content + 2 * spacingTokens[spacing]))]
|
|
88
|
+
}, [iconPoints, spacing, theme])
|
|
30
89
|
|
|
31
90
|
// Every segment keeps the same outline, the selected one too: an accent border would run its fill out over the line
|
|
32
91
|
// the others draw inside, and make it look taller than its neighbours.
|
|
@@ -56,24 +115,24 @@ export const SegmentedControl: SegmentedControlComponent = ({
|
|
|
56
115
|
}
|
|
57
116
|
|
|
58
117
|
haptics.selection()
|
|
59
|
-
onChange?.(index,
|
|
118
|
+
onChange?.(index, segments[index]?.label ?? '')
|
|
60
119
|
},
|
|
61
|
-
[onChange,
|
|
120
|
+
[onChange, segments, selectedIndex]
|
|
62
121
|
)
|
|
63
122
|
|
|
64
123
|
const row = (
|
|
65
124
|
<SingleChoiceSegmentedButtonRow modifiers={rowModifiers}>
|
|
66
|
-
{
|
|
125
|
+
{segments.map((segment, index) => (
|
|
67
126
|
<SegmentedButton
|
|
68
127
|
colors={colors}
|
|
69
128
|
enabled={!disabled}
|
|
70
129
|
key={index}
|
|
71
|
-
modifiers={
|
|
130
|
+
modifiers={segmentModifiers}
|
|
72
131
|
onClick={() => select(index)}
|
|
73
132
|
selected={index === selectedIndex}
|
|
74
133
|
>
|
|
75
134
|
<SegmentedButton.Label>
|
|
76
|
-
<
|
|
135
|
+
<SegmentLabel iconSize={iconPoints} segment={segment} />
|
|
77
136
|
</SegmentedButton.Label>
|
|
78
137
|
</SegmentedButton>
|
|
79
138
|
))}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {RNHostView} from '@expo/ui/swift-ui'
|
|
2
|
+
import {iconResourceName} from '@hzblj/expo-native-icons'
|
|
2
3
|
import {SegmentedControl as NativeSegmentedControl} from '@hzblj/kara-native/swift-ui'
|
|
3
|
-
import {useAppTheme} from '@hzblj/kara-theme'
|
|
4
|
+
import {spacing as spacingTokens, useAppTheme} from '@hzblj/kara-theme'
|
|
4
5
|
import {useCallback, useMemo} from 'react'
|
|
5
6
|
|
|
6
7
|
import {useInsideHost} from '../../host-context'
|
|
7
8
|
import type {SegmentedControlComponent, SegmentedControlProps} from './segmented-control.types'
|
|
9
|
+
import {imageUri, segmentOf} from './segments'
|
|
8
10
|
|
|
9
11
|
const MATCH_HEIGHT_ONLY = {horizontal: false, vertical: true}
|
|
10
12
|
|
|
@@ -12,12 +14,16 @@ const MATCH_HEIGHT_ONLY = {horizontal: false, vertical: true}
|
|
|
12
14
|
* kara's own `UISegmentedControl`, with the pill in the accent and white text on it — the system control tints only the
|
|
13
15
|
* pill, and SwiftUI's segmented picker not even that. A plain `UIView` in React Native layout: it needs no host, takes
|
|
14
16
|
* the width it is given and UIKit's height. Inside a `Screen` or `Island` it is carried into that host by `RNHostView`
|
|
15
|
-
* (see `useInsideHost`), the host's width and its own height. UIKit plays the control's own feedback.
|
|
17
|
+
* (see `useInsideHost`), the host's width and its own height. UIKit plays the control's own feedback. A segment shows a
|
|
18
|
+
* title or an image, never both, so an icon segment is drawn natively as one image of the icon and the label, in the
|
|
19
|
+
* label's colour; an image segment is the image alone, loaded and drawn by kara-native at `iconSize`.
|
|
16
20
|
*/
|
|
17
21
|
export const SegmentedControl: SegmentedControlComponent = ({
|
|
18
22
|
disabled = false,
|
|
23
|
+
iconSize = 's20',
|
|
19
24
|
onChange,
|
|
20
25
|
selectedIndex,
|
|
26
|
+
spacing,
|
|
21
27
|
testID,
|
|
22
28
|
values,
|
|
23
29
|
}: SegmentedControlProps) => {
|
|
@@ -25,15 +31,26 @@ export const SegmentedControl: SegmentedControlComponent = ({
|
|
|
25
31
|
const insideHost = useInsideHost()
|
|
26
32
|
const face = theme.typography.footnoteMedium
|
|
27
33
|
|
|
34
|
+
const segments = useMemo(() => values.map(segmentOf), [values])
|
|
35
|
+
|
|
28
36
|
// The native control speaks in values; a segment's is its index, so two labels may read the same.
|
|
29
|
-
const options = useMemo(
|
|
37
|
+
const options = useMemo(
|
|
38
|
+
() =>
|
|
39
|
+
segments.map((segment, index) => ({
|
|
40
|
+
icon: segment.icon ? iconResourceName(segment.icon) : undefined,
|
|
41
|
+
image: imageUri(segment.image),
|
|
42
|
+
label: segment.label,
|
|
43
|
+
value: String(index),
|
|
44
|
+
})),
|
|
45
|
+
[segments]
|
|
46
|
+
)
|
|
30
47
|
|
|
31
48
|
const onValueChange = useCallback(
|
|
32
49
|
(value: string) => {
|
|
33
50
|
const index = Number(value)
|
|
34
|
-
onChange?.(index,
|
|
51
|
+
onChange?.(index, segments[index]?.label ?? '')
|
|
35
52
|
},
|
|
36
|
-
[onChange,
|
|
53
|
+
[onChange, segments]
|
|
37
54
|
)
|
|
38
55
|
|
|
39
56
|
const control = (
|
|
@@ -43,11 +60,13 @@ export const SegmentedControl: SegmentedControlComponent = ({
|
|
|
43
60
|
disabled={disabled}
|
|
44
61
|
fontFamily={face.fontFamily}
|
|
45
62
|
fontSize={face.fontSize}
|
|
63
|
+
iconSize={spacingTokens[iconSize]}
|
|
46
64
|
inactiveTextColor={theme.color('content.primary')}
|
|
47
65
|
onValueChange={onValueChange}
|
|
48
66
|
options={options}
|
|
49
67
|
selectedValue={String(selectedIndex)}
|
|
50
68
|
testID={testID}
|
|
69
|
+
verticalPadding={spacing === undefined ? undefined : spacingTokens[spacing]}
|
|
51
70
|
/>
|
|
52
71
|
)
|
|
53
72
|
|
|
@@ -1,14 +1,34 @@
|
|
|
1
|
+
import type {SpaceSize} from '@hzblj/kara-theme'
|
|
1
2
|
import type {ReactNode} from 'react'
|
|
3
|
+
import type {ImageSourcePropType} from 'react-native'
|
|
4
|
+
|
|
5
|
+
import type {IconName} from '../icon'
|
|
6
|
+
|
|
7
|
+
/** A segment that shows more than its label. */
|
|
8
|
+
export type SegmentedControlSegment = {
|
|
9
|
+
/** One of the app's icons, before the label and in the label's colour, the selected segment's or the others'. */
|
|
10
|
+
icon?: IconName
|
|
11
|
+
/**
|
|
12
|
+
* An image in its own colours instead of the label, such as a carrier's logo: a `require()`d file, or `{uri}` for a
|
|
13
|
+
* URL (raster or SVG). The label stays what screen readers say, and is shown instead if the image cannot be loaded.
|
|
14
|
+
*/
|
|
15
|
+
image?: ImageSourcePropType
|
|
16
|
+
/** What the segment says, and what `onChange` reports. */
|
|
17
|
+
label: string
|
|
18
|
+
}
|
|
2
19
|
|
|
3
20
|
/**
|
|
4
21
|
* A row of mutually exclusive segments, the selected one in the accent: kara's own `UISegmentedControl` on iOS,
|
|
5
22
|
* Material's single-choice segmented buttons on Android. It needs no `Screen` or `Island`: iOS is a plain view, Android
|
|
6
23
|
* brings its own host. Inside one it joins that host, so it can share a stack with other native views. It takes the
|
|
7
|
-
* width it is given; the height is the platform's
|
|
24
|
+
* width it is given; the height is the platform's, or taller with `spacing`. A segment can lead with one of the app's
|
|
25
|
+
* icons or be an image, a logo, both loaded natively (SDWebImage on iOS, androidsvg on Android, as Footshop does).
|
|
8
26
|
*/
|
|
9
27
|
export type SegmentedControlProps = {
|
|
10
28
|
/** Stops the user changing the segment, and dims the control. */
|
|
11
29
|
disabled?: boolean
|
|
30
|
+
/** The height of the segments' icons and images, a spacing token. `s20` by default. */
|
|
31
|
+
iconSize?: SpaceSize
|
|
12
32
|
/**
|
|
13
33
|
* Called with the index and label of the segment the user picked, never for a tap on the one already selected.
|
|
14
34
|
* Android plays `selection`; iOS's control plays its own feedback.
|
|
@@ -16,10 +36,15 @@ export type SegmentedControlProps = {
|
|
|
16
36
|
onChange?: (index: number, value: string) => void
|
|
17
37
|
/** The selected segment. Controlled: pass back what `onChange` reports. */
|
|
18
38
|
selectedIndex: number
|
|
39
|
+
/**
|
|
40
|
+
* Space above and below the segments' content, a spacing token: a taller control, for bigger icons or an easier
|
|
41
|
+
* target. Unset keeps the platform's own height, and the control is never shorter than that.
|
|
42
|
+
*/
|
|
43
|
+
spacing?: SpaceSize
|
|
19
44
|
/** Test identifier, on the whole control. Reach a segment by its label. */
|
|
20
45
|
testID?: string
|
|
21
|
-
/** The segments
|
|
22
|
-
values: string[]
|
|
46
|
+
/** The segments, in order: a label, or a segment with an icon or an image. */
|
|
47
|
+
values: (string | SegmentedControlSegment)[]
|
|
23
48
|
}
|
|
24
49
|
|
|
25
50
|
export type SegmentedControlComponent = (props: SegmentedControlProps) => ReactNode
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {Image, type ImageSourcePropType} from 'react-native'
|
|
2
|
+
|
|
3
|
+
import type {SegmentedControlSegment} from './segmented-control.types'
|
|
4
|
+
|
|
5
|
+
/** A plain label is a segment with nothing else. */
|
|
6
|
+
export const segmentOf = (value: string | SegmentedControlSegment): SegmentedControlSegment =>
|
|
7
|
+
typeof value === 'string' ? {label: value} : value
|
|
8
|
+
|
|
9
|
+
/** What the native loaders take: a `require()`d file's packager or file URL, or the URI itself. */
|
|
10
|
+
export const imageUri = (source: ImageSourcePropType | undefined): string | undefined => {
|
|
11
|
+
if (source === undefined) {
|
|
12
|
+
return undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return Image.resolveAssetSource(Array.isArray(source) ? source[0] : source)?.uri
|
|
16
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {Slider as ComposeSlider} from '@expo/ui/jetpack-compose'
|
|
2
|
+
import {testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
|
|
2
3
|
import {haptics} from '@hzblj/expo-rich-haptics'
|
|
3
4
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
4
|
-
import {useCallback, useEffect, useRef} from 'react'
|
|
5
|
+
import {useCallback, useEffect, useMemo, useRef} from 'react'
|
|
5
6
|
|
|
6
7
|
import {useHostGuard} from '../../host-context'
|
|
7
8
|
import type {SliderComponent, SliderProps} from './slider.types'
|
|
@@ -9,14 +10,17 @@ import type {SliderComponent, SliderProps} from './slider.types'
|
|
|
9
10
|
/**
|
|
10
11
|
* Material's slider. `step` is the increment, as SwiftUI takes it; Compose counts the stops between the two ends
|
|
11
12
|
* instead, so it is converted. A stepped slider ticks at every stop the thumb reaches — Compose's Material slider
|
|
12
|
-
* plays nothing of its own, where iOS 26's slider already does.
|
|
13
|
+
* plays nothing of its own, where iOS 26's slider already does. `onValueChangeEnd` is Material's
|
|
14
|
+
* `onValueChangeFinished`, which reports no value: it gets the last one the drag reported.
|
|
13
15
|
*/
|
|
14
16
|
export const Slider: SliderComponent = ({
|
|
15
17
|
disabled = false,
|
|
16
18
|
max = 1,
|
|
17
19
|
min = 0,
|
|
18
20
|
onValueChange,
|
|
21
|
+
onValueChangeEnd,
|
|
19
22
|
step,
|
|
23
|
+
testID,
|
|
20
24
|
value,
|
|
21
25
|
}: SliderProps) => {
|
|
22
26
|
useHostGuard('Slider')
|
|
@@ -27,6 +31,7 @@ export const Slider: SliderComponent = ({
|
|
|
27
31
|
// Compose reports faster than React re-renders the controlled value.
|
|
28
32
|
const stopOf = useCallback((at: number) => (step ? Math.round((at - min) / step) : 0), [min, step])
|
|
29
33
|
const stop = useRef(stopOf(value))
|
|
34
|
+
const dragged = useRef<number | undefined>(undefined)
|
|
30
35
|
|
|
31
36
|
useEffect(() => {
|
|
32
37
|
stop.current = stopOf(value)
|
|
@@ -41,11 +46,20 @@ export const Slider: SliderComponent = ({
|
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
stop.current = reached
|
|
49
|
+
dragged.current = next
|
|
44
50
|
onValueChange?.(next)
|
|
45
51
|
},
|
|
46
52
|
[onValueChange, stopOf, steps]
|
|
47
53
|
)
|
|
48
54
|
|
|
55
|
+
const onFinished = useCallback(() => {
|
|
56
|
+
const final = dragged.current ?? value
|
|
57
|
+
dragged.current = undefined
|
|
58
|
+
onValueChangeEnd?.(final)
|
|
59
|
+
}, [onValueChangeEnd, value])
|
|
60
|
+
|
|
61
|
+
const modifiers = useMemo(() => (testID ? [testIdModifier(testID)] : undefined), [testID])
|
|
62
|
+
|
|
49
63
|
return (
|
|
50
64
|
<ComposeSlider
|
|
51
65
|
colors={{
|
|
@@ -55,7 +69,9 @@ export const Slider: SliderComponent = ({
|
|
|
55
69
|
enabled={!disabled}
|
|
56
70
|
max={max}
|
|
57
71
|
min={min}
|
|
72
|
+
modifiers={modifiers}
|
|
58
73
|
onValueChange={onChange}
|
|
74
|
+
onValueChangeFinished={onValueChangeEnd ? onFinished : undefined}
|
|
59
75
|
steps={steps}
|
|
60
76
|
value={value}
|
|
61
77
|
/>
|
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import {Slider as SwiftUISlider} from '@expo/ui/swift-ui'
|
|
2
2
|
import {disabled as disabledModifier, tint} from '@expo/ui/swift-ui/modifiers'
|
|
3
3
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
4
|
-
import {useMemo} from 'react'
|
|
4
|
+
import {useCallback, useMemo, useRef} from 'react'
|
|
5
5
|
|
|
6
6
|
import {useHostGuard} from '../../host-context'
|
|
7
7
|
import type {SliderComponent, SliderProps} from './slider.types'
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* SwiftUI's slider. `onValueChangeEnd` is its `onEditingChanged(false)`, with the last value the drag reported: the
|
|
11
|
+
* controlled `value` can still be a render behind when the finger lifts.
|
|
12
|
+
*/
|
|
9
13
|
export const Slider: SliderComponent = ({
|
|
10
14
|
disabled = false,
|
|
11
15
|
max = 1,
|
|
12
16
|
min = 0,
|
|
13
17
|
onValueChange,
|
|
18
|
+
onValueChangeEnd,
|
|
14
19
|
step,
|
|
20
|
+
testID,
|
|
15
21
|
value,
|
|
16
22
|
}: SliderProps) => {
|
|
17
23
|
useHostGuard('Slider')
|
|
18
24
|
const theme = useAppTheme()
|
|
25
|
+
const dragged = useRef<number | undefined>(undefined)
|
|
19
26
|
|
|
20
27
|
const modifiers = useMemo(() => {
|
|
21
28
|
const list = [tint(theme.color('fill.accent.primary'))]
|
|
@@ -27,7 +34,36 @@ export const Slider: SliderComponent = ({
|
|
|
27
34
|
return list
|
|
28
35
|
}, [disabled, theme])
|
|
29
36
|
|
|
37
|
+
const onChange = useCallback(
|
|
38
|
+
(next: number) => {
|
|
39
|
+
dragged.current = next
|
|
40
|
+
onValueChange?.(next)
|
|
41
|
+
},
|
|
42
|
+
[onValueChange]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const onEditingChanged = useCallback(
|
|
46
|
+
(editing: boolean) => {
|
|
47
|
+
const final = dragged.current ?? value
|
|
48
|
+
dragged.current = undefined
|
|
49
|
+
|
|
50
|
+
if (!editing) {
|
|
51
|
+
onValueChangeEnd?.(final)
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
[onValueChangeEnd, value]
|
|
55
|
+
)
|
|
56
|
+
|
|
30
57
|
return (
|
|
31
|
-
<SwiftUISlider
|
|
58
|
+
<SwiftUISlider
|
|
59
|
+
max={max}
|
|
60
|
+
min={min}
|
|
61
|
+
modifiers={modifiers}
|
|
62
|
+
onEditingChanged={onValueChangeEnd ? onEditingChanged : undefined}
|
|
63
|
+
onValueChange={onChange}
|
|
64
|
+
step={step}
|
|
65
|
+
testID={testID}
|
|
66
|
+
value={value}
|
|
67
|
+
/>
|
|
32
68
|
)
|
|
33
69
|
}
|
|
@@ -10,8 +10,15 @@ export type SliderProps = {
|
|
|
10
10
|
min?: number
|
|
11
11
|
/** Called with the new value as the thumb moves. */
|
|
12
12
|
onValueChange?: (value: number) => void
|
|
13
|
+
/**
|
|
14
|
+
* Called once with the final value when the user lets go of the slider: where a seek or a volume commits, rather
|
|
15
|
+
* than at every step of the drag.
|
|
16
|
+
*/
|
|
17
|
+
onValueChangeEnd?: (value: number) => void
|
|
13
18
|
/** The increment between stops; leave it unset for a continuous slider. On Android every stop plays a `tick`. */
|
|
14
19
|
step?: number
|
|
20
|
+
/** Test identifier, forwarded to the native view. */
|
|
21
|
+
testID?: string
|
|
15
22
|
/** Between `min` and `max`. Controlled: pass back what `onValueChange` reports. */
|
|
16
23
|
value: number
|
|
17
24
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {Switch as ComposeSwitch, Row, Text} from '@expo/ui/jetpack-compose'
|
|
2
|
-
import {clickable, fillMaxWidth} from '@expo/ui/jetpack-compose/modifiers'
|
|
2
|
+
import {clickable, fillMaxWidth, testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
|
|
3
3
|
import {haptics} from '@hzblj/expo-rich-haptics'
|
|
4
4
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
5
5
|
import {useCallback, useMemo} from 'react'
|
|
@@ -16,7 +16,7 @@ const NOOP = () => undefined
|
|
|
16
16
|
* `toggleOn`/`toggleOff` — Android 14's constants made for switches — since Compose's Material switch plays nothing
|
|
17
17
|
* of its own. iOS stays silent, as `UISwitch` does.
|
|
18
18
|
*/
|
|
19
|
-
export const Switch: SwitchComponent = ({disabled = false, label, onValueChange, value}: SwitchProps) => {
|
|
19
|
+
export const Switch: SwitchComponent = ({disabled = false, label, onValueChange, testID, value}: SwitchProps) => {
|
|
20
20
|
useHostGuard('Switch')
|
|
21
21
|
const theme = useAppTheme()
|
|
22
22
|
const toggle = useCallback(
|
|
@@ -30,6 +30,8 @@ export const Switch: SwitchComponent = ({disabled = false, label, onValueChange,
|
|
|
30
30
|
() => (disabled || !onValueChange ? [fillMaxWidth()] : [fillMaxWidth(), clickable(() => toggle(!value))]),
|
|
31
31
|
[disabled, onValueChange, toggle, value]
|
|
32
32
|
)
|
|
33
|
+
// On the switch, not the row: the switch is its own toggleable node inside the row, and the one a test reads.
|
|
34
|
+
const controlModifiers = useMemo(() => (testID ? [testIdModifier(testID)] : undefined), [testID])
|
|
33
35
|
const control = (
|
|
34
36
|
<ComposeSwitch
|
|
35
37
|
colors={{
|
|
@@ -38,6 +40,7 @@ export const Switch: SwitchComponent = ({disabled = false, label, onValueChange,
|
|
|
38
40
|
checkedTrackColor: theme.color('fill.accent.primary'),
|
|
39
41
|
}}
|
|
40
42
|
enabled={!disabled}
|
|
43
|
+
modifiers={controlModifiers}
|
|
41
44
|
onCheckedChange={onValueChange ? toggle : NOOP}
|
|
42
45
|
value={value}
|
|
43
46
|
/>
|
|
@@ -6,7 +6,7 @@ import {useMemo} from 'react'
|
|
|
6
6
|
import {useHostGuard} from '../../host-context'
|
|
7
7
|
import type {SwitchComponent, SwitchProps} from './switch.types'
|
|
8
8
|
|
|
9
|
-
export const Switch: SwitchComponent = ({disabled = false, label, onValueChange, value}: SwitchProps) => {
|
|
9
|
+
export const Switch: SwitchComponent = ({disabled = false, label, onValueChange, testID, value}: SwitchProps) => {
|
|
10
10
|
useHostGuard('Switch')
|
|
11
11
|
const theme = useAppTheme()
|
|
12
12
|
|
|
@@ -20,5 +20,5 @@ export const Switch: SwitchComponent = ({disabled = false, label, onValueChange,
|
|
|
20
20
|
return list
|
|
21
21
|
}, [disabled, theme])
|
|
22
22
|
|
|
23
|
-
return <Toggle isOn={value} label={label} modifiers={modifiers} onIsOnChange={onValueChange} />
|
|
23
|
+
return <Toggle isOn={value} label={label} modifiers={modifiers} onIsOnChange={onValueChange} testID={testID} />
|
|
24
24
|
}
|
|
@@ -11,6 +11,8 @@ export type SwitchProps = {
|
|
|
11
11
|
label?: string
|
|
12
12
|
/** Called with the new value when the user flips it. On Android a flip plays `toggleOn` / `toggleOff`. */
|
|
13
13
|
onValueChange?: (value: boolean) => void
|
|
14
|
+
/** Test identifier, forwarded to the native switch (on Android the switch itself, not the labelled row). */
|
|
15
|
+
testID?: string
|
|
14
16
|
/** Whether it is on. Controlled: pass back what `onValueChange` reports. */
|
|
15
17
|
value: boolean
|
|
16
18
|
}
|
package/src/atoms/text/spans.ts
CHANGED
|
@@ -4,7 +4,8 @@ import {Children, isValidElement, type ReactNode} from 'react'
|
|
|
4
4
|
/** A run of the text: a string child, or a nested `Text` with a colour of its own. */
|
|
5
5
|
export type TextSpan = {color?: ColorToken; text: string}
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/** The string and number children, joined: the text of a `Text` without spans, however JSX split it. */
|
|
8
|
+
export const textOf = (children: ReactNode): string =>
|
|
8
9
|
Children.toArray(children)
|
|
9
10
|
.map(child => (typeof child === 'string' || typeof child === 'number' ? String(child) : ''))
|
|
10
11
|
.join('')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {Text as ComposeText} from '@expo/ui/jetpack-compose'
|
|
2
2
|
import {testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
|
|
3
|
-
import {AnimatedText, type TextAnimation} from '@hzblj/kara-native/jetpack-compose'
|
|
3
|
+
import {AnimatedText, type AnimatedTextOverflow, type TextAnimation} from '@hzblj/kara-native/jetpack-compose'
|
|
4
4
|
import {
|
|
5
5
|
animateConfig,
|
|
6
6
|
type SxAnimateConfig,
|
|
@@ -12,8 +12,8 @@ import {duration, easing, springPhysics, useAppTheme} from '@hzblj/kara-theme'
|
|
|
12
12
|
import {useMemo} from 'react'
|
|
13
13
|
|
|
14
14
|
import {useHostGuard} from '../../host-context'
|
|
15
|
-
import {spansOf} from './spans'
|
|
16
|
-
import type {TextComponent, TextDecoration, TextProps, TextTransform} from './text.types'
|
|
15
|
+
import {spansOf, textOf} from './spans'
|
|
16
|
+
import type {TextComponent, TextDecoration, TextProps, TextTransform, TextTruncation} from './text.types'
|
|
17
17
|
|
|
18
18
|
const DECORATION = {
|
|
19
19
|
none: 'none',
|
|
@@ -21,6 +21,12 @@ const DECORATION = {
|
|
|
21
21
|
underline: 'underline',
|
|
22
22
|
} as const
|
|
23
23
|
|
|
24
|
+
const OVERFLOW = {
|
|
25
|
+
head: 'startEllipsis',
|
|
26
|
+
middle: 'middleEllipsis',
|
|
27
|
+
tail: 'ellipsis',
|
|
28
|
+
} as const satisfies Record<TextTruncation, AnimatedTextOverflow>
|
|
29
|
+
|
|
24
30
|
const applyTransform = (children: TextProps['children'], transform: TextTransform | undefined) => {
|
|
25
31
|
if (!transform || typeof children !== 'string') {
|
|
26
32
|
return children
|
|
@@ -68,6 +74,7 @@ export const Text: TextComponent = ({
|
|
|
68
74
|
numberOfLines,
|
|
69
75
|
testID,
|
|
70
76
|
transform,
|
|
77
|
+
truncation = 'tail',
|
|
71
78
|
variant = 'body',
|
|
72
79
|
...props
|
|
73
80
|
}: TextProps) => {
|
|
@@ -83,11 +90,15 @@ export const Text: TextComponent = ({
|
|
|
83
90
|
)
|
|
84
91
|
const spans = useMemo(() => spansOf(children, Text), [children])
|
|
85
92
|
const animate = sx.animate === false ? undefined : sx.animate
|
|
93
|
+
// Compose ellipsizes at the start or in the middle of a single line only, and clips a longer text: that ends at the
|
|
94
|
+
// tail instead.
|
|
95
|
+
const overflow = numberOfLines === 1 ? OVERFLOW[truncation] : 'ellipsis'
|
|
86
96
|
|
|
87
|
-
// `@expo/ui`'s text takes spans but animates none,
|
|
88
|
-
// eases each span's colour,
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
// `@expo/ui`'s text takes spans but animates none, sets no font features and ellipsizes only at the end: with
|
|
98
|
+
// `animate`, kara's own Compose text eases each span's colour, with `monospacedDigit` asks the face for its tabular
|
|
99
|
+
// figures, and puts the ellipsis at the start or in the middle of a line.
|
|
100
|
+
if (animate !== undefined || monospacedDigit || overflow !== 'ellipsis') {
|
|
101
|
+
const runs = spans ?? [{text: textOf(children)}]
|
|
91
102
|
|
|
92
103
|
return (
|
|
93
104
|
<AnimatedText
|
|
@@ -106,6 +117,7 @@ export const Text: TextComponent = ({
|
|
|
106
117
|
textAlign={align}
|
|
107
118
|
textColor={theme.colorString(color)}
|
|
108
119
|
textDecoration={decorationOf(decoration)}
|
|
120
|
+
textOverflow={overflow}
|
|
109
121
|
/>
|
|
110
122
|
)
|
|
111
123
|
}
|
|
@@ -115,6 +127,7 @@ export const Text: TextComponent = ({
|
|
|
115
127
|
color={theme.colorString(color)}
|
|
116
128
|
maxLines={numberOfLines}
|
|
117
129
|
modifiers={modifiers}
|
|
130
|
+
overflow="ellipsis"
|
|
118
131
|
style={{
|
|
119
132
|
fontFamily: style.fontFamily,
|
|
120
133
|
fontSize: style.fontSize,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
multilineTextAlignment,
|
|
9
9
|
strikethrough,
|
|
10
10
|
textCase,
|
|
11
|
+
truncationMode,
|
|
11
12
|
underline,
|
|
12
13
|
} from '@expo/ui/swift-ui/modifiers'
|
|
13
14
|
import {scaledFont} from '@hzblj/kara-native/swift-ui'
|
|
@@ -48,6 +49,7 @@ export const Text: TextComponent = ({
|
|
|
48
49
|
numberOfLines,
|
|
49
50
|
testID,
|
|
50
51
|
transform,
|
|
52
|
+
truncation,
|
|
51
53
|
variant = 'body',
|
|
52
54
|
...props
|
|
53
55
|
}: TextProps) => {
|
|
@@ -99,6 +101,10 @@ export const Text: TextComponent = ({
|
|
|
99
101
|
list.push(lineLimit(numberOfLines))
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
if (truncation) {
|
|
105
|
+
list.push(truncationMode(truncation))
|
|
106
|
+
}
|
|
107
|
+
|
|
102
108
|
return [...list, ...transition, ...sxModifiers, ...accessibilityModifiers]
|
|
103
109
|
}, [
|
|
104
110
|
accessibilityModifiers,
|
|
@@ -112,6 +118,7 @@ export const Text: TextComponent = ({
|
|
|
112
118
|
theme,
|
|
113
119
|
transform,
|
|
114
120
|
transition,
|
|
121
|
+
truncation,
|
|
115
122
|
])
|
|
116
123
|
|
|
117
124
|
return (
|