@hzblj/kara-components 0.17.0 → 0.18.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 +49 -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/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/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/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
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import type {ReactNode} from 'react'
|
|
2
2
|
|
|
3
|
+
import type {TextFieldAutoCapitalize} from '../../atoms/text-field'
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* A search field drawn the same on both platforms: a rounded box with a magnifier, a `TextInput` and a clear button
|
|
5
|
-
* while there is text. The
|
|
7
|
+
* while there is text. The keyboard's return key reads Search, and a query is taken as typed: no capitals and no
|
|
8
|
+
* corrections unless asked for. The React Native tier: it needs no host.
|
|
6
9
|
*/
|
|
7
10
|
export type SearchBarProps = {
|
|
11
|
+
/** When the keyboard turns Shift on by itself. `none` by default: a query is not a sentence. */
|
|
12
|
+
autoCapitalize?: TextFieldAutoCapitalize
|
|
13
|
+
/** Lets the keyboard correct words as they are typed. Off by default, so a query is searched as typed. */
|
|
14
|
+
autoCorrect?: boolean
|
|
15
|
+
/** Takes focus, and brings up the keyboard, as soon as the field appears. Off by default. */
|
|
16
|
+
autoFocus?: boolean
|
|
8
17
|
/** Every edit, and `''` when the clear button is pressed. */
|
|
9
18
|
onChangeText?: (text: string) => void
|
|
19
|
+
/** Called with the query when the keyboard's Search key is pressed. The keyboard goes down after it. */
|
|
20
|
+
onSubmit?: (text: string) => void
|
|
10
21
|
/** Shown while the field is empty. `Search` by default. */
|
|
11
22
|
placeholder?: string
|
|
12
23
|
/** Test identifier, put on the field's container view rather than on the `TextInput`. */
|
|
@@ -7,25 +7,45 @@ import {useHostGuard} from '../../host-context'
|
|
|
7
7
|
import type {StepperComponent, StepperProps} from './stepper.types'
|
|
8
8
|
|
|
9
9
|
export const Stepper: StepperComponent = ({
|
|
10
|
+
decrementLabel = 'Decrement',
|
|
11
|
+
disabled = false,
|
|
12
|
+
incrementLabel = 'Increment',
|
|
10
13
|
label,
|
|
11
14
|
max = Number.MAX_SAFE_INTEGER,
|
|
12
15
|
min = Number.MIN_SAFE_INTEGER,
|
|
13
16
|
onValueChange,
|
|
14
17
|
step = 1,
|
|
18
|
+
testID,
|
|
15
19
|
value,
|
|
16
20
|
}: StepperProps) => {
|
|
17
21
|
useHostGuard('Stepper')
|
|
18
22
|
|
|
19
23
|
const onDecrement = useCallback(() => onValueChange(Math.max(min, value - step)), [min, onValueChange, step, value])
|
|
20
24
|
const onIncrement = useCallback(() => onValueChange(Math.min(max, value + step)), [max, onValueChange, step, value])
|
|
25
|
+
const color = disabled ? 'content.tertiary' : 'content.primary'
|
|
21
26
|
|
|
22
27
|
return (
|
|
23
28
|
<HStack align="center" spacing="s8" wide>
|
|
24
|
-
<Text>{label}</Text>
|
|
29
|
+
<Text color={color}>{label}</Text>
|
|
25
30
|
<Spacer />
|
|
26
|
-
|
|
27
|
-
<
|
|
28
|
-
|
|
31
|
+
{/* The control in a row of its own, the same gap apart, so the id lands on it and not on the label. */}
|
|
32
|
+
<HStack align="center" spacing="s8" testID={testID}>
|
|
33
|
+
<GlyphButton
|
|
34
|
+
accessibilityLabel={decrementLabel}
|
|
35
|
+
disabled={disabled || value <= min}
|
|
36
|
+
name="minus"
|
|
37
|
+
onPress={onDecrement}
|
|
38
|
+
/>
|
|
39
|
+
<Text color={color} variant="labelMedium">
|
|
40
|
+
{String(value)}
|
|
41
|
+
</Text>
|
|
42
|
+
<GlyphButton
|
|
43
|
+
accessibilityLabel={incrementLabel}
|
|
44
|
+
disabled={disabled || value >= max}
|
|
45
|
+
name="plus"
|
|
46
|
+
onPress={onIncrement}
|
|
47
|
+
/>
|
|
48
|
+
</HStack>
|
|
29
49
|
</HStack>
|
|
30
50
|
)
|
|
31
51
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {Stepper as SwiftUIStepper} from '@expo/ui/swift-ui'
|
|
2
|
-
import {labelsHidden} from '@expo/ui/swift-ui/modifiers'
|
|
2
|
+
import {disabled as disabledModifier, labelsHidden} from '@expo/ui/swift-ui/modifiers'
|
|
3
3
|
import {useReducedMotion} from '@hzblj/kara-theme'
|
|
4
4
|
import {useRef} from 'react'
|
|
5
5
|
|
|
@@ -12,12 +12,24 @@ import type {StepperComponent, StepperProps} from './stepper.types'
|
|
|
12
12
|
/** The label stays VoiceOver's, on the control; on screen it is the row's own text. */
|
|
13
13
|
const LABEL_HIDDEN = [labelsHidden()]
|
|
14
14
|
|
|
15
|
+
const LABEL_HIDDEN_DISABLED = [labelsHidden(), disabledModifier(true)]
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* SwiftUI's `Stepper` at the end of a row, the label leading and the value just before the control — the way iOS's own
|
|
17
19
|
* settings show a stepped value. The value rolls to the next one (`numericText`), up or down with the change, in
|
|
18
|
-
* monospaced digits so the row does not jump. With motion reduced it cross-fades instead.
|
|
20
|
+
* monospaced digits so the row does not jump. With motion reduced it cross-fades instead. Its buttons are labelled by
|
|
21
|
+
* the system, so `decrementLabel` and `incrementLabel` are Android's alone.
|
|
19
22
|
*/
|
|
20
|
-
export const Stepper: StepperComponent = ({
|
|
23
|
+
export const Stepper: StepperComponent = ({
|
|
24
|
+
disabled = false,
|
|
25
|
+
label,
|
|
26
|
+
max,
|
|
27
|
+
min,
|
|
28
|
+
onValueChange,
|
|
29
|
+
step = 1,
|
|
30
|
+
testID,
|
|
31
|
+
value,
|
|
32
|
+
}: StepperProps) => {
|
|
21
33
|
useHostGuard('Stepper')
|
|
22
34
|
const reduced = useReducedMotion()
|
|
23
35
|
const previous = useRef(value)
|
|
@@ -26,10 +38,10 @@ export const Stepper: StepperComponent = ({label, max, min, onValueChange, step
|
|
|
26
38
|
|
|
27
39
|
return (
|
|
28
40
|
<HStack align="center" spacing="s12" wide>
|
|
29
|
-
<Text>{label}</Text>
|
|
41
|
+
<Text color={disabled ? 'content.tertiary' : 'content.primary'}>{label}</Text>
|
|
30
42
|
<Spacer />
|
|
31
43
|
<Text
|
|
32
|
-
color=
|
|
44
|
+
color={disabled ? 'content.tertiary' : 'content.secondary'}
|
|
33
45
|
ios={{
|
|
34
46
|
animation: {value},
|
|
35
47
|
contentTransition: reduced ? {type: 'opacity'} : {countsDown, type: 'numericText'},
|
|
@@ -38,13 +50,16 @@ export const Stepper: StepperComponent = ({label, max, min, onValueChange, step
|
|
|
38
50
|
>
|
|
39
51
|
{String(value)}
|
|
40
52
|
</Text>
|
|
53
|
+
{/* iOS 27 stops a disabled stepper but draws its buttons at full strength. Pinned to its value, UIKit greys
|
|
54
|
+
both buttons, as it greys one at a limit. */}
|
|
41
55
|
<SwiftUIStepper
|
|
42
56
|
label={label}
|
|
43
|
-
max={max}
|
|
44
|
-
min={min}
|
|
45
|
-
modifiers={LABEL_HIDDEN}
|
|
57
|
+
max={disabled ? value : max}
|
|
58
|
+
min={disabled ? value : min}
|
|
59
|
+
modifiers={disabled ? LABEL_HIDDEN_DISABLED : LABEL_HIDDEN}
|
|
46
60
|
onValueChange={onValueChange}
|
|
47
61
|
step={step}
|
|
62
|
+
testID={testID}
|
|
48
63
|
value={value}
|
|
49
64
|
/>
|
|
50
65
|
</HStack>
|
|
@@ -5,6 +5,18 @@ import type {ReactNode} from 'react'
|
|
|
5
5
|
* two icon buttons in a row. A native atom: it renders inside `<Screen>` or `<Island>`.
|
|
6
6
|
*/
|
|
7
7
|
export type StepperProps = {
|
|
8
|
+
/**
|
|
9
|
+
* What TalkBack says for the minus button. `Decrement` by default: pass the app's own words, in its language.
|
|
10
|
+
* Android only. iOS ignores it: its native stepper labels its buttons itself, in the device's language.
|
|
11
|
+
*/
|
|
12
|
+
decrementLabel?: string
|
|
13
|
+
/** Stops the user changing the value, and dims the row. */
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
/**
|
|
16
|
+
* What TalkBack says for the plus button. `Increment` by default: pass the app's own words, in its language. Android
|
|
17
|
+
* only. iOS ignores it: its native stepper labels its buttons itself, in the device's language.
|
|
18
|
+
*/
|
|
19
|
+
incrementLabel?: string
|
|
8
20
|
/** The leading text. The value is shown too: before the control on iOS, between the buttons on Android. */
|
|
9
21
|
label: string
|
|
10
22
|
/** The highest value. When unset, Android has no upper limit and iOS stops at `100`. */
|
|
@@ -15,6 +27,11 @@ export type StepperProps = {
|
|
|
15
27
|
onValueChange: (value: number) => void
|
|
16
28
|
/** How much one press adds or takes away. `1` by default. */
|
|
17
29
|
step?: number
|
|
30
|
+
/**
|
|
31
|
+
* Test identifier, on the control rather than the row: SwiftUI's `Stepper` on iOS, the minus and plus buttons with
|
|
32
|
+
* the value between them on Android. Reach a button by what screen readers say for it.
|
|
33
|
+
*/
|
|
34
|
+
testID?: string
|
|
18
35
|
/** The current value. Update it from `onValueChange`. iOS's native stepper counts in whole numbers. */
|
|
19
36
|
value: number
|
|
20
37
|
}
|
|
@@ -4,36 +4,48 @@ import {Island} from '../../atoms/island'
|
|
|
4
4
|
import {Text} from '../../atoms/text'
|
|
5
5
|
import {VStack} from '../../atoms/v-stack'
|
|
6
6
|
import {useHostGuard} from '../../host-context'
|
|
7
|
+
import {menuActionColor} from '../menu/menu-parts'
|
|
7
8
|
import {Sheet} from '../sheet'
|
|
8
9
|
import type {ActionSheetComponent, ActionSheetProps} from './action-sheet.types'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
* Material's bottom-sheet menu: the title, then a full-width row per action, its icon leading. The
|
|
12
|
-
* Native tier, so the rows — Compose views — sit in an `Island` of their own; rendered straight into
|
|
13
|
-
* no host and threw.
|
|
12
|
+
* Material's bottom-sheet menu: the title and the message, then a full-width row per action, its icon leading. The
|
|
13
|
+
* sheet is the React Native tier, so the rows — Compose views — sit in an `Island` of their own; rendered straight into
|
|
14
|
+
* the sheet they had no host and threw. A disabled row is greyed and has no press at all, so it neither ripples nor
|
|
15
|
+
* reads as a button.
|
|
14
16
|
*/
|
|
15
|
-
export const ActionSheet: ActionSheetComponent = ({actions, onDismiss, title, visible}: ActionSheetProps) => {
|
|
17
|
+
export const ActionSheet: ActionSheetComponent = ({actions, message, onDismiss, title, visible}: ActionSheetProps) => {
|
|
16
18
|
useHostGuard('ActionSheet')
|
|
17
19
|
|
|
18
20
|
return (
|
|
19
21
|
<Sheet onDismiss={onDismiss} visible={visible}>
|
|
20
22
|
<Island>
|
|
21
23
|
<VStack pb="s16" wide>
|
|
22
|
-
|
|
24
|
+
{/* The message at the title's size, lighter: the iOS dialog's own pairing. */}
|
|
25
|
+
<VStack px="s24" py="s12" spacing="s4" wide>
|
|
23
26
|
<Text color="content.secondary" variant="labelMedium">
|
|
24
27
|
{title}
|
|
25
28
|
</Text>
|
|
26
|
-
|
|
29
|
+
{message ? (
|
|
30
|
+
<Text color="content.secondary" variant="label">
|
|
31
|
+
{message}
|
|
32
|
+
</Text>
|
|
33
|
+
) : null}
|
|
34
|
+
</VStack>
|
|
27
35
|
{actions.map(action => {
|
|
28
|
-
const color = action
|
|
36
|
+
const color = menuActionColor(action)
|
|
29
37
|
|
|
30
38
|
return (
|
|
31
39
|
<HStack
|
|
32
40
|
key={action.label}
|
|
33
|
-
onPress={
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
onPress={
|
|
42
|
+
action.disabled
|
|
43
|
+
? undefined
|
|
44
|
+
: () => {
|
|
45
|
+
onDismiss()
|
|
46
|
+
action.onPress()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
37
49
|
px="s24"
|
|
38
50
|
py="s16"
|
|
39
51
|
spacing="s16"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {Button, ConfirmationDialog, Spacer} from '@expo/ui/swift-ui'
|
|
2
|
-
import {frame} from '@expo/ui/swift-ui/modifiers'
|
|
1
|
+
import {Button, ConfirmationDialog, Spacer, Text as SwiftUIText} from '@expo/ui/swift-ui'
|
|
2
|
+
import {disabled, frame} from '@expo/ui/swift-ui/modifiers'
|
|
3
3
|
|
|
4
4
|
import {useHostGuard} from '../../host-context'
|
|
5
5
|
import type {ActionSheetComponent, ActionSheetProps} from './action-sheet.types'
|
|
@@ -11,7 +11,13 @@ import type {ActionSheetComponent, ActionSheetProps} from './action-sheet.types'
|
|
|
11
11
|
*/
|
|
12
12
|
const ANCHOR = [frame({height: 0, width: 0})]
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const DISABLED = [disabled(true)]
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `titleVisibility="visible"`: left to `automatic`, SwiftUI drops the title of a dialog presented as an action sheet,
|
|
18
|
+
* and the actions lose what they are about.
|
|
19
|
+
*/
|
|
20
|
+
export const ActionSheet: ActionSheetComponent = ({actions, message, onDismiss, title, visible}: ActionSheetProps) => {
|
|
15
21
|
useHostGuard('ActionSheet')
|
|
16
22
|
|
|
17
23
|
return (
|
|
@@ -23,6 +29,7 @@ export const ActionSheet: ActionSheetComponent = ({actions, onDismiss, title, vi
|
|
|
23
29
|
}
|
|
24
30
|
}}
|
|
25
31
|
title={title}
|
|
32
|
+
titleVisibility="visible"
|
|
26
33
|
>
|
|
27
34
|
<ConfirmationDialog.Trigger>
|
|
28
35
|
<Spacer modifiers={ANCHOR} />
|
|
@@ -32,11 +39,21 @@ export const ActionSheet: ActionSheetComponent = ({actions, onDismiss, title, vi
|
|
|
32
39
|
<Button
|
|
33
40
|
key={action.label}
|
|
34
41
|
label={action.label}
|
|
42
|
+
modifiers={action.disabled ? DISABLED : undefined}
|
|
35
43
|
onPress={action.onPress}
|
|
36
44
|
role={action.destructive ? 'destructive' : 'default'}
|
|
37
45
|
/>
|
|
38
46
|
))}
|
|
39
47
|
</ConfirmationDialog.Actions>
|
|
48
|
+
{/*
|
|
49
|
+
A native slot view, which cannot hold a bare string (see `Alert`): the dialog mounts while dismissed, so an
|
|
50
|
+
unwrapped string would crash on mount. Plain SwiftUI `Text`, so the message keeps the system's styling.
|
|
51
|
+
*/}
|
|
52
|
+
{message ? (
|
|
53
|
+
<ConfirmationDialog.Message>
|
|
54
|
+
<SwiftUIText>{message}</SwiftUIText>
|
|
55
|
+
</ConfirmationDialog.Message>
|
|
56
|
+
) : null}
|
|
40
57
|
</ConfirmationDialog>
|
|
41
58
|
)
|
|
42
59
|
}
|
|
@@ -4,14 +4,19 @@ import type {MenuAction} from '../menu'
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* A list of actions to pick one from: SwiftUI's confirmation dialog on iOS, a `Sheet` of Material rows (icon and label)
|
|
7
|
-
* under the title on Android. A native atom: it renders inside `<Screen>` or `<Island>`.
|
|
7
|
+
* under the title and message on Android. A native atom: it renders inside `<Screen>` or `<Island>`.
|
|
8
8
|
*/
|
|
9
9
|
export type ActionSheetProps = {
|
|
10
|
-
/** The choices, top to bottom. Picking one closes the sheet. */
|
|
10
|
+
/** The choices, top to bottom. Picking one closes the sheet. A `disabled` one stays in the list, greyed. */
|
|
11
11
|
actions: MenuAction[]
|
|
12
|
+
/**
|
|
13
|
+
* A line under the title saying what the choice is about: "The photo will be removed from all your devices." Unset,
|
|
14
|
+
* the title stands alone.
|
|
15
|
+
*/
|
|
16
|
+
message?: string
|
|
12
17
|
/** The sheet closed (after a pick too), so turn `visible` off. */
|
|
13
18
|
onDismiss: () => void
|
|
14
|
-
/** The heading above the actions
|
|
19
|
+
/** The heading above the actions, shown on both platforms. */
|
|
15
20
|
title: string
|
|
16
21
|
/** Presents the sheet while true. */
|
|
17
22
|
visible: boolean
|
|
@@ -7,9 +7,10 @@ import type {AlertComponent, AlertProps} from './alert.types'
|
|
|
7
7
|
/**
|
|
8
8
|
* Material's `AlertDialog`, built from its slots — title, text, confirm and dismiss buttons. Content passed as plain
|
|
9
9
|
* children has no slot to land in, and the dialog draws empty. The actions mirror the iOS alert: the confirm label
|
|
10
|
-
* (in the destructive colour when asked) and
|
|
10
|
+
* (in the destructive colour when asked) and the cancel label, whose slot stays empty when it is `null`.
|
|
11
11
|
*/
|
|
12
12
|
export const Alert: AlertComponent = ({
|
|
13
|
+
cancelLabel = 'Cancel',
|
|
13
14
|
confirmLabel = 'OK',
|
|
14
15
|
destructive = false,
|
|
15
16
|
message,
|
|
@@ -62,13 +63,15 @@ export const Alert: AlertComponent = ({
|
|
|
62
63
|
</ComposeText>
|
|
63
64
|
</TextButton>
|
|
64
65
|
</AlertDialog.ConfirmButton>
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
{cancelLabel === null ? null : (
|
|
67
|
+
<AlertDialog.DismissButton>
|
|
68
|
+
<TextButton onClick={onDismiss}>
|
|
69
|
+
<ComposeText color={theme.colorString('content.accent')} style={theme.typography.body}>
|
|
70
|
+
{cancelLabel}
|
|
71
|
+
</ComposeText>
|
|
72
|
+
</TextButton>
|
|
73
|
+
</AlertDialog.DismissButton>
|
|
74
|
+
)}
|
|
72
75
|
</AlertDialog>
|
|
73
76
|
)
|
|
74
77
|
}
|
|
@@ -11,6 +11,7 @@ import type {AlertComponent, AlertProps} from './alert.types'
|
|
|
11
11
|
const ANCHOR = [frame({height: 0, width: 0})]
|
|
12
12
|
|
|
13
13
|
export const Alert: AlertComponent = ({
|
|
14
|
+
cancelLabel = 'Cancel',
|
|
14
15
|
confirmLabel = 'OK',
|
|
15
16
|
destructive = false,
|
|
16
17
|
message,
|
|
@@ -36,7 +37,7 @@ export const Alert: AlertComponent = ({
|
|
|
36
37
|
</SwiftUIAlert.Trigger>
|
|
37
38
|
<SwiftUIAlert.Actions>
|
|
38
39
|
<Button label={confirmLabel} onPress={onConfirm ?? onDismiss} role={destructive ? 'destructive' : 'default'} />
|
|
39
|
-
<Button label=
|
|
40
|
+
{cancelLabel === null ? null : <Button label={cancelLabel} onPress={onDismiss} role="cancel" />}
|
|
40
41
|
</SwiftUIAlert.Actions>
|
|
41
42
|
{/*
|
|
42
43
|
`Alert.Message` is a native slot view, and a SwiftUI host cannot render a bare string — it
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type {ReactNode} from 'react'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* A modal alert with a confirm button and
|
|
5
|
-
* atom: it renders inside `<Screen>` or `<Island>`.
|
|
4
|
+
* A modal alert with a confirm button and a cancel button (or the confirm button alone): SwiftUI's alert on iOS,
|
|
5
|
+
* Material's `AlertDialog` on Android. A native atom: it renders inside `<Screen>` or `<Island>`.
|
|
6
6
|
*/
|
|
7
7
|
export type AlertProps = {
|
|
8
|
+
/**
|
|
9
|
+
* The cancel button's label, in the app's language: "Zrušit". `Cancel` by default. `null` leaves the button out, for
|
|
10
|
+
* an alert that only informs and is closed by its confirm button alone ("OK").
|
|
11
|
+
*/
|
|
12
|
+
cancelLabel?: string | null
|
|
8
13
|
/** The confirm button's label. `OK` by default. */
|
|
9
14
|
confirmLabel?: string
|
|
10
15
|
/** Marks the confirm button destructive: the system's destructive role on iOS, the destructive colour on Android. */
|
|
@@ -13,7 +18,7 @@ export type AlertProps = {
|
|
|
13
18
|
message?: string
|
|
14
19
|
/** The confirm button was pressed. `onDismiss` follows, as it does for every way the alert closes. */
|
|
15
20
|
onConfirm?: () => void
|
|
16
|
-
/** The alert closed (
|
|
21
|
+
/** The alert closed (any of its buttons, or on Android a tap outside or back), so turn `visible` off. */
|
|
17
22
|
onDismiss: () => void
|
|
18
23
|
/** The alert's heading. */
|
|
19
24
|
title: string
|
|
@@ -8,6 +8,7 @@ import {Icon} from '../../atoms/icon'
|
|
|
8
8
|
import {HostBoundary} from '../../host-context'
|
|
9
9
|
import {splitForRipple} from '../../ripple-clip'
|
|
10
10
|
import type {MenuAction} from '../menu'
|
|
11
|
+
import {menuActionColor} from '../menu/menu-parts'
|
|
11
12
|
import type {ContextMenuComponent, ContextMenuExclusionProps, ContextMenuProps} from './context-menu.types'
|
|
12
13
|
|
|
13
14
|
type Point = {x: number; y: number}
|
|
@@ -25,7 +26,7 @@ type MenuRowProps = {
|
|
|
25
26
|
/** A row's `onClick` closes over its own action, so memoising it needs a component per row. */
|
|
26
27
|
const MenuRow = ({action, onDismiss}: MenuRowProps) => {
|
|
27
28
|
const theme = useAppTheme()
|
|
28
|
-
const color = action
|
|
29
|
+
const color = menuActionColor(action)
|
|
29
30
|
|
|
30
31
|
const onClick = useCallback(() => {
|
|
31
32
|
onDismiss()
|
|
@@ -33,7 +34,7 @@ const MenuRow = ({action, onDismiss}: MenuRowProps) => {
|
|
|
33
34
|
}, [action, onDismiss])
|
|
34
35
|
|
|
35
36
|
return (
|
|
36
|
-
<DropdownMenuItem onClick={onClick}>
|
|
37
|
+
<DropdownMenuItem enabled={!action.disabled} onClick={onClick}>
|
|
37
38
|
<DropdownMenuItem.Text>
|
|
38
39
|
<ComposeText color={theme.colorString(color)} style={theme.typography.body}>
|
|
39
40
|
{action.label}
|
|
@@ -43,6 +43,9 @@ const FabMenuRow = ({action, onSelect}: FabMenuRowProps) => {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
+
* `MenuAction.destructive` and `disabled` are not drawn: Material 3's `FloatingActionButtonMenuItem` takes neither a
|
|
47
|
+
* role nor an `enabled` state, and its pills keep the button's colours.
|
|
48
|
+
*
|
|
46
49
|
* The menu fills its host and pins itself to the bottom end, so give it the corner of a screen — a `ZStack` over the
|
|
47
50
|
* content — rather than a row of its own. The pills take the button's colours, so the menu reads as the button
|
|
48
51
|
* unfolding rather than a second surface.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {Button, Image, Label, Menu} from '@expo/ui/swift-ui'
|
|
2
|
+
import {disabled} from '@expo/ui/swift-ui/modifiers'
|
|
2
3
|
import {iconResourceName} from '@hzblj/expo-native-icons'
|
|
3
4
|
|
|
4
5
|
import {Icon} from '../../atoms/icon'
|
|
@@ -10,6 +11,8 @@ import type {FabMenuComponent, FabMenuProps} from './fab-menu.types'
|
|
|
10
11
|
/** Matches `Fab`, so a screen can switch between the two without the button changing shape. */
|
|
11
12
|
const FAB_SIZE = 56
|
|
12
13
|
|
|
14
|
+
const DISABLED = [disabled(true)]
|
|
15
|
+
|
|
13
16
|
export const FabMenu: FabMenuComponent = ({accessibilityLabel, actions, icon, testID}: FabMenuProps) => {
|
|
14
17
|
useHostGuard('FabMenu')
|
|
15
18
|
|
|
@@ -35,7 +38,12 @@ export const FabMenu: FabMenuComponent = ({accessibilityLabel, actions, icon, te
|
|
|
35
38
|
testID={testID}
|
|
36
39
|
>
|
|
37
40
|
{actions.map(action => (
|
|
38
|
-
<Button
|
|
41
|
+
<Button
|
|
42
|
+
key={action.label}
|
|
43
|
+
modifiers={action.disabled ? DISABLED : undefined}
|
|
44
|
+
onPress={action.onPress}
|
|
45
|
+
role={action.destructive ? 'destructive' : 'default'}
|
|
46
|
+
>
|
|
39
47
|
{/* A menu row is the system's Label: our icon is its image, from the asset catalog, and UIKit sizes it. */}
|
|
40
48
|
<Label
|
|
41
49
|
icon={action.icon ? <Image assetName={iconResourceName(action.icon)} /> : undefined}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type {ColorToken} from '@hzblj/kara-theme'
|
|
2
|
+
|
|
3
|
+
import type {IconName} from '../../atoms/icon'
|
|
4
|
+
import type {MenuAction, MenuProps} from './menu.types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An action's colour in a row kara draws itself (the Android menus and sheets): greyed when disabled, as `IconButton`
|
|
8
|
+
* greys, red when destructive. A Compose `DropdownMenuItem` cannot grey its own text here, since the text sets its
|
|
9
|
+
* colour explicitly.
|
|
10
|
+
*/
|
|
11
|
+
export const menuActionColor = ({destructive, disabled}: MenuAction): ColorToken => {
|
|
12
|
+
if (disabled) {
|
|
13
|
+
return 'btn.content.disabled'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return destructive ? 'content.destructive' : 'content.primary'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A trigger with no text: the props' union guarantees its icon and the words a screen reader says for it. */
|
|
20
|
+
export const isIconOnly = (props: MenuProps): props is MenuProps & {accessibilityLabel: string; icon: IconName} =>
|
|
21
|
+
props.label === undefined
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import {Text as ComposeText, DropdownMenu, DropdownMenuItem} from '@expo/ui/jetpack-compose'
|
|
2
2
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
3
|
-
import {useCallback, useState} from 'react'
|
|
3
|
+
import {type ReactNode, useCallback, useState} from 'react'
|
|
4
4
|
import {HStack} from '../../atoms/h-stack'
|
|
5
5
|
import {Icon} from '../../atoms/icon'
|
|
6
|
+
import {IconButton} from '../../atoms/icon-button'
|
|
6
7
|
import {Text} from '../../atoms/text'
|
|
7
8
|
import {Glyph} from '../../glyph'
|
|
8
9
|
import {useHostGuard} from '../../host-context'
|
|
9
10
|
import type {MenuAction, MenuComponent, MenuProps} from './menu.types'
|
|
11
|
+
import {isIconOnly, menuActionColor} from './menu-parts'
|
|
10
12
|
|
|
11
13
|
type MenuItemProps = {
|
|
12
14
|
action: MenuAction
|
|
@@ -15,11 +17,12 @@ type MenuItemProps = {
|
|
|
15
17
|
|
|
16
18
|
const MenuItem = ({action, onDismiss}: MenuItemProps) => {
|
|
17
19
|
const theme = useAppTheme()
|
|
18
|
-
const color = action
|
|
20
|
+
const color = menuActionColor(action)
|
|
19
21
|
|
|
20
22
|
return (
|
|
21
23
|
<DropdownMenuItem
|
|
22
24
|
elementColors={{leadingIconColor: theme.color(color), textColor: theme.color(color)}}
|
|
25
|
+
enabled={!action.disabled}
|
|
23
26
|
onClick={() => {
|
|
24
27
|
onDismiss()
|
|
25
28
|
action.onPress()
|
|
@@ -39,27 +42,53 @@ const MenuItem = ({action, onDismiss}: MenuItemProps) => {
|
|
|
39
42
|
)
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
const triggerOf = (props: MenuProps, open: () => void): ReactNode => {
|
|
46
|
+
if (isIconOnly(props)) {
|
|
47
|
+
// Material's overflow menu: an icon button, its label carried by the icon.
|
|
48
|
+
return (
|
|
49
|
+
<IconButton
|
|
50
|
+
accessibilityLabel={props.accessibilityLabel}
|
|
51
|
+
name={props.icon}
|
|
52
|
+
onPress={open}
|
|
53
|
+
testID={props.testID}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const {accessibilityLabel, icon, label, testID} = props
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<HStack accessibilityLabel={accessibilityLabel} align="center" onPress={open} spacing="s4" testID={testID}>
|
|
62
|
+
{icon ? (
|
|
63
|
+
<HStack align="center" spacing="s8">
|
|
64
|
+
<Icon color="content.accent" name={icon} size="s20" />
|
|
65
|
+
<Text color="content.accent">{label}</Text>
|
|
66
|
+
</HStack>
|
|
67
|
+
) : (
|
|
68
|
+
<Text color="content.accent">{label}</Text>
|
|
69
|
+
)}
|
|
70
|
+
<Glyph color="content.accent" name="chevronDown" size="s16" />
|
|
71
|
+
</HStack>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
42
75
|
/**
|
|
43
76
|
* Material's dropdown menu, anchored to its trigger. The items live in the `Items` slot, which Compose presents in
|
|
44
77
|
* its own popup — anywhere else they would be laid out in place, on top of the trigger.
|
|
45
78
|
*/
|
|
46
|
-
export const Menu: MenuComponent = (
|
|
79
|
+
export const Menu: MenuComponent = (props: MenuProps) => {
|
|
47
80
|
useHostGuard('Menu')
|
|
48
81
|
|
|
49
82
|
const theme = useAppTheme()
|
|
50
83
|
const [expanded, setExpanded] = useState(false)
|
|
84
|
+
const open = useCallback(() => setExpanded(true), [])
|
|
51
85
|
const dismiss = useCallback(() => setExpanded(false), [])
|
|
52
86
|
|
|
53
87
|
return (
|
|
54
88
|
<DropdownMenu color={theme.color('surface.primary')} expanded={expanded} onDismissRequest={dismiss}>
|
|
55
|
-
<DropdownMenu.Trigger>
|
|
56
|
-
<HStack align="center" onPress={() => setExpanded(true)} spacing="s4">
|
|
57
|
-
<Text color="content.accent">{label}</Text>
|
|
58
|
-
<Glyph color="content.accent" name="chevronDown" size="s16" />
|
|
59
|
-
</HStack>
|
|
60
|
-
</DropdownMenu.Trigger>
|
|
89
|
+
<DropdownMenu.Trigger>{triggerOf(props, open)}</DropdownMenu.Trigger>
|
|
61
90
|
<DropdownMenu.Items>
|
|
62
|
-
{actions.map(action => (
|
|
91
|
+
{props.actions.map(action => (
|
|
63
92
|
<MenuItem action={action} key={action.label} onDismiss={dismiss} />
|
|
64
93
|
))}
|
|
65
94
|
</DropdownMenu.Items>
|
|
@@ -1,20 +1,70 @@
|
|
|
1
|
-
import {Button, Menu as SwiftUIMenu} from '@expo/ui/swift-ui'
|
|
1
|
+
import {Button, Image, Label, Menu as SwiftUIMenu} from '@expo/ui/swift-ui'
|
|
2
|
+
import {contentShape, disabled, frame, resizable, shapes} from '@expo/ui/swift-ui/modifiers'
|
|
3
|
+
import {iconResourceName} from '@hzblj/expo-native-icons'
|
|
4
|
+
import {useAccessibilityModifiers} from '@hzblj/kara-sx'
|
|
5
|
+
import {spacing} from '@hzblj/kara-theme'
|
|
6
|
+
import type {ReactNode} from 'react'
|
|
2
7
|
|
|
8
|
+
import {Icon} from '../../atoms/icon'
|
|
9
|
+
import {ZStack} from '../../atoms/z-stack'
|
|
3
10
|
import {useHostGuard} from '../../host-context'
|
|
4
11
|
import type {MenuComponent, MenuProps} from './menu.types'
|
|
12
|
+
import {isIconOnly} from './menu-parts'
|
|
5
13
|
|
|
6
|
-
|
|
14
|
+
const DISABLED = [disabled(true)]
|
|
15
|
+
|
|
16
|
+
/** Apple's least tap target, as `IconButton` takes it. */
|
|
17
|
+
const TARGET = 44
|
|
18
|
+
|
|
19
|
+
/** The whole square takes the tap, not only the glyph's own pixels. */
|
|
20
|
+
const WHOLE_TARGET = {modifiers: [contentShape(shapes.circle())]}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The icon beside the trigger's text, the size `Link` draws one beside body text. No colour of its own: the asset is a
|
|
24
|
+
* template image, so it takes the menu's tint with the text.
|
|
25
|
+
*/
|
|
26
|
+
const LABEL_ICON = [resizable(), frame({height: spacing.s20, width: spacing.s20})]
|
|
27
|
+
|
|
28
|
+
const triggerOf = (props: MenuProps): ReactNode => {
|
|
29
|
+
if (isIconOnly(props)) {
|
|
30
|
+
// An icon alone is drawn as an `IconButton`: `content.primary`, its label carried by the icon.
|
|
31
|
+
return (
|
|
32
|
+
<ZStack h={TARGET} ios={WHOLE_TARGET} w={TARGET}>
|
|
33
|
+
<Icon accessibilityLabel={props.accessibilityLabel} color="content.primary" name={props.icon} />
|
|
34
|
+
</ZStack>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return props.icon ? (
|
|
39
|
+
<Label icon={<Image assetName={iconResourceName(props.icon)} modifiers={LABEL_ICON} />} title={props.label} />
|
|
40
|
+
) : (
|
|
41
|
+
props.label
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const Menu: MenuComponent = (props: MenuProps) => {
|
|
7
46
|
useHostGuard('Menu')
|
|
47
|
+
const {accessibilityLabel, actions, label, testID} = props
|
|
48
|
+
// An icon alone carries its own label; with text, `accessibilityLabel` replaces it on the menu.
|
|
49
|
+
const modifiers = useAccessibilityModifiers({
|
|
50
|
+
accessibilityLabel: label === undefined ? undefined : accessibilityLabel,
|
|
51
|
+
})
|
|
8
52
|
|
|
9
53
|
return (
|
|
10
|
-
<SwiftUIMenu label={
|
|
54
|
+
<SwiftUIMenu label={triggerOf(props)} modifiers={modifiers} testID={testID}>
|
|
11
55
|
{actions.map(action => (
|
|
12
56
|
<Button
|
|
13
57
|
key={action.label}
|
|
14
|
-
label={action.label}
|
|
58
|
+
label={action.icon ? undefined : action.label}
|
|
59
|
+
modifiers={action.disabled ? DISABLED : undefined}
|
|
15
60
|
onPress={action.onPress}
|
|
16
61
|
role={action.destructive ? 'destructive' : 'default'}
|
|
17
|
-
|
|
62
|
+
>
|
|
63
|
+
{/* A menu row is the system's Label: our icon is its image, from the asset catalog, and UIKit sizes it. */}
|
|
64
|
+
{action.icon ? (
|
|
65
|
+
<Label icon={<Image assetName={iconResourceName(action.icon)} />} title={action.label} />
|
|
66
|
+
) : undefined}
|
|
67
|
+
</Button>
|
|
18
68
|
))}
|
|
19
69
|
</SwiftUIMenu>
|
|
20
70
|
)
|