• Экраны
  • Доки
1Online
ПодпискаКаталог
  • Экраны
  • Доки
1Online
ПодпискаКаталог
  • Экраны
  • Доки
1Online
ПодпискаКаталог
Новый адрес — экран из раздела «Шторка»
Шторка

Новый адрес

Шторки, всплывающие над затемнением — формы, акции, оплата и вход.

Установка

src/shared/components/blocks/add-address-new
npx astra-rn add add-address-new
pnpm dlx astra-rn add add-address-new
yarn dlx astra-rn add add-address-new
bunx --bun astra-rn add add-address-new

Зависимости

5 пакетов
npm install @gorhom/bottom-sheet expo-router react-native-gesture-handler react-native-safe-area-context react-native-svg
pnpm add @gorhom/bottom-sheet expo-router react-native-gesture-handler react-native-safe-area-context react-native-svg
yarn add @gorhom/bottom-sheet expo-router react-native-gesture-handler react-native-safe-area-context react-native-svg
bun add @gorhom/bottom-sheet expo-router react-native-gesture-handler react-native-safe-area-context react-native-svg

Исходник

3 файла
import React, { memo, useCallback, useMemo, useRef, useState } from "react";
import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
import { TextInput as GestureTextInput } from "react-native-gesture-handler";
import { useFocusEffect } from "expo-router";
import {
  BottomSheetBackdrop,
  BottomSheetModal,
  BottomSheetTextInput,
  BottomSheetView,
  type BottomSheetBackdropProps,
} from "@gorhom/bottom-sheet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Svg, { Path } from "react-native-svg";
import type { IAddAddressBottomSheet, IGlyph } from "./types";
import {
  BACKDROP_OPACITY,
  CLOSE_SIZE,
  COLORS,
  CONTENT_BOTTOM_PADDING,
  CONTENT_HORIZONTAL_PADDING,
  CONTENT_TOP_PADDING,
  DEFAULT_ADD_ADDRESS_TITLE,
  DEFAULT_COUNTRY,
  DEFAULT_COUNTRY_LABEL,
  DEFAULT_LOCATION_CAPTION,
  DEFAULT_LOCATION_TITLE,
  DEFAULT_STREET_PLACEHOLDER,
  DEFAULT_TITLE,
  FIELD_GAP,
  FIELD_RADIUS,
  LOCATION_PATH,
  LOCATION_VIEW_BOX,
  ROW_GAP,
  ROW_MARK_SIZE,
  SHEET_BOTTOM_GAP,
  SHEET_CORNER_RADIUS,
  SHEET_HORIZONTAL_MARGIN,
  STREET_FIELD_HEIGHT,
} from "./const";

const SheetTextInput =
  Platform.OS === "web" ? GestureTextInput : BottomSheetTextInput;

const CloseGlyph: React.FC<IGlyph> = ({
  size = 20,
  color = COLORS.closeMark,
}: IGlyph) => (
  <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
    <Path
      d="M6 6L18 18M18 6L6 18"
      stroke={color}
      strokeWidth={2.4}
      strokeLinecap="round"
    />
  </Svg>
);

const ChevronDownGlyph: React.FC<IGlyph> = ({
  size = 20,
  color = COLORS.chevron,
}: IGlyph) => (
  <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
    <Path
      d="M6 9.5L12 15.5L18 9.5"
      stroke={color}
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
    />
  </Svg>
);

const LocationGlyph: React.FC<IGlyph> = ({
  size = 25,
  color = COLORS.ink,
}: IGlyph) => (
  <Svg width={size} height={size} viewBox={LOCATION_VIEW_BOX}>
    <Path fill={color} d={LOCATION_PATH} />
  </Svg>
);

const PlusGlyph: React.FC<IGlyph> = ({
  size = 22,
  color = COLORS.ink,
}: IGlyph) => (
  <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
    <Path
      d="M12 5V19M5 12H19"
      stroke={color}
      strokeWidth={2.2}
      strokeLinecap="round"
    />
  </Svg>
);

const AddAddressBottomSheet: React.FC<IAddAddressBottomSheet> = ({
  title = DEFAULT_TITLE,
  countryLabel = DEFAULT_COUNTRY_LABEL,
  country = DEFAULT_COUNTRY,
  streetPlaceholder = DEFAULT_STREET_PLACEHOLDER,
  streetValue = "",
  locationTitle = DEFAULT_LOCATION_TITLE,
  locationCaption = DEFAULT_LOCATION_CAPTION,
  addAddressTitle = DEFAULT_ADD_ADDRESS_TITLE,
  hideCloseButton = false,
  openOnFocus = true,
  onCountryPress,
  onStreetChange,
  onLocationPress,
  onAddAddressPress,
  onClose,
}: IAddAddressBottomSheet) => {
  const sheetRef = useRef<BottomSheetModal>(null);
  const insets = useSafeAreaInsets();
  const [street, setStreet] = useState<string>(streetValue);
  const [pressedKey, setPressedKey] = useState<string | null>(null);

  useFocusEffect(
    useCallback(() => {
      if (openOnFocus) sheetRef.current?.present();
      return () => sheetRef.current?.dismiss();
    }, [openOnFocus]),
  );

  const handleClose = useCallback((): void => {
    sheetRef.current?.dismiss();
  }, []);

  const handleStreetChange = useCallback(
    (value: string): void => {
      setStreet(value);
      onStreetChange?.(value);
    },
    [onStreetChange],
  );

  const handlePressOut = useCallback((): void => setPressedKey(null), []);

  const renderBackdrop = useCallback(
    (props: BottomSheetBackdropProps) => (
      <BottomSheetBackdrop
        {...props}
        appearsOnIndex={0}
        disappearsOnIndex={-1}
        opacity={BACKDROP_OPACITY}
        pressBehavior="close"
      />
    ),
    [],
  );

  const bottomInset = useMemo(
    () => insets.bottom + SHEET_BOTTOM_GAP,
    [insets.bottom],
  );

  return (
    <BottomSheetModal
      ref={sheetRef}
      detached
      bottomInset={bottomInset}
      enablePanDownToClose
      enableDynamicSizing
      keyboardBehavior="interactive"
      keyboardBlurBehavior="restore"
      android_keyboardInputMode="adjustResize"
      onDismiss={onClose}
      backdropComponent={renderBackdrop}
      handleIndicatorStyle={styles.handleIndicator}
      backgroundStyle={styles.background}
      style={styles.sheet}
    >
      <BottomSheetView style={styles.content}>
        <View style={styles.form}>
          <View style={styles.header}>
            <Text style={styles.title} numberOfLines={1}>
              {title}
            </Text>

            {hideCloseButton ? null : (
              <Pressable
                accessibilityRole="button"
                accessibilityLabel="Close"
                hitSlop={12}
                onPress={handleClose}
                onPressIn={() => setPressedKey("close")}
                onPressOut={handlePressOut}
                style={[
                  styles.close,
                  pressedKey === "close" ? styles.closePressed : null,
                ]}
              >
                <CloseGlyph />
              </Pressable>
            )}
          </View>

          <Pressable
            accessibilityRole="button"
            accessibilityLabel={`${countryLabel}: ${country}`}
            onPress={onCountryPress}
            onPressIn={() => setPressedKey("country")}
            onPressOut={handlePressOut}
            style={[
              styles.countryField,
              pressedKey === "country" ? styles.fieldPressed : null,
            ]}
          >
            <View style={styles.countryText}>
              <Text style={styles.countryLabel} numberOfLines={1}>
                {countryLabel}
              </Text>
              <Text style={styles.countryValue} numberOfLines={1}>
                {country}
              </Text>
            </View>

            <ChevronDownGlyph />
          </Pressable>

          <View style={styles.streetField}>
            <SheetTextInput
              value={street}
              onChangeText={handleStreetChange}
              placeholder={streetPlaceholder}
              placeholderTextColor={COLORS.muted}
              style={styles.streetInput}
              autoCorrect={false}
              autoCapitalize="words"
              returnKeyType="done"
            />
          </View>
        </View>

        <View style={styles.divider} />

        <View style={styles.rows}>
          <Pressable
            accessibilityRole="button"
            accessibilityLabel={locationTitle}
            onPress={onLocationPress}
            onPressIn={() => setPressedKey("location")}
            onPressOut={handlePressOut}
            style={styles.row}
          >
            <View
              style={[
                styles.rowMark,
                pressedKey === "location" ? styles.rowMarkPressed : null,
              ]}
            >
              <LocationGlyph />
            </View>

            <View style={styles.rowText}>
              <Text style={styles.rowTitle} numberOfLines={1}>
                {locationTitle}
              </Text>
              {locationCaption ? (
                <Text style={styles.rowCaption} numberOfLines={1}>
                  {locationCaption}
                </Text>
              ) : null}
            </View>
          </Pressable>

          <Pressable
            accessibilityRole="button"
            accessibilityLabel={addAddressTitle}
            onPress={onAddAddressPress}
            onPressIn={() => setPressedKey("add")}
            onPressOut={handlePressOut}
            style={styles.row}
          >
            <View
              style={[
                styles.rowMark,
                pressedKey === "add" ? styles.rowMarkPressed : null,
              ]}
            >
              <PlusGlyph />
            </View>

            <View style={styles.rowText}>
              <Text style={styles.rowTitle} numberOfLines={1}>
                {addAddressTitle}
              </Text>
            </View>
          </Pressable>
        </View>
      </BottomSheetView>
    </BottomSheetModal>
  );
};

const styles = StyleSheet.create({
  sheet: {
    marginHorizontal: SHEET_HORIZONTAL_MARGIN,
    shadowColor: "#000000",
    shadowOpacity: 0.16,
    shadowRadius: 28,
    shadowOffset: { width: 0, height: 8 },
    elevation: 16,
  },

  background: {
    backgroundColor: COLORS.sheet,
    borderRadius: SHEET_CORNER_RADIUS,
  },
  handleIndicator: {
    backgroundColor: COLORS.handle,
    width: 34,
    height: 4,
  },
  content: {
    alignSelf: "stretch",
    paddingTop: CONTENT_TOP_PADDING,
    paddingBottom: CONTENT_BOTTOM_PADDING,
  },

  form: {
    paddingHorizontal: CONTENT_HORIZONTAL_PADDING,
    paddingBottom: 22,
  },
  header: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
    gap: 12,
  },
  title: {
    flex: 1,
    fontSize: 22,
    lineHeight: 28,
    fontWeight: "600",
    letterSpacing: -0.5,
    color: COLORS.ink,
  },
  close: {
    width: CLOSE_SIZE,
    height: CLOSE_SIZE,
    borderRadius: CLOSE_SIZE / 2,
    backgroundColor: COLORS.close,
    alignItems: "center",
    justifyContent: "center",
  },
  closePressed: {
    backgroundColor: COLORS.closePressed,
  },
  countryField: {
    flexDirection: "row",
    alignItems: "center",
    gap: 12,
    marginTop: 20,
    paddingHorizontal: 16,
    paddingVertical: 10,
    borderRadius: FIELD_RADIUS,
    borderWidth: 1,
    borderColor: COLORS.fieldBorder,
    backgroundColor: COLORS.field,
  },
  fieldPressed: {
    backgroundColor: COLORS.fieldPressed,
  },
  countryText: {
    flex: 1,
  },
  countryLabel: {
    fontSize: 13,
    lineHeight: 18,
    color: COLORS.muted,
  },
  countryValue: {
    marginTop: 1,
    fontSize: 17,
    lineHeight: 23,
    fontWeight: "600",
    letterSpacing: -0.3,
    color: COLORS.ink,
  },
  streetField: {
    justifyContent: "center",
    marginTop: FIELD_GAP,
    height: STREET_FIELD_HEIGHT,
    paddingHorizontal: 16,
    borderRadius: FIELD_RADIUS,
    borderWidth: 1,
    borderColor: COLORS.fieldBorder,
    backgroundColor: COLORS.field,
  },
  streetInput: {
    fontSize: 17,
    lineHeight: 22,
    color: COLORS.ink,
    padding: 0,
  },
  divider: {
    alignSelf: "stretch",
    height: StyleSheet.hairlineWidth,
    backgroundColor: COLORS.divider,
  },
  rows: {
    paddingTop: 18,
    paddingHorizontal: CONTENT_HORIZONTAL_PADDING,
    gap: 14,
  },
  row: {
    flexDirection: "row",
    alignItems: "center",
    gap: ROW_GAP,
    paddingVertical: 8,
  },
  rowMark: {
    width: ROW_MARK_SIZE,
    height: ROW_MARK_SIZE,
    borderRadius: ROW_MARK_SIZE / 2,
    backgroundColor: COLORS.mark,
    alignItems: "center",
    justifyContent: "center",
  },
  rowMarkPressed: {
    backgroundColor: COLORS.markPressed,
  },
  rowText: {
    flex: 1,
  },
  rowTitle: {
    fontSize: 16,
    lineHeight: 22,
    fontWeight: "700",
    letterSpacing: -0.3,
    color: COLORS.ink,
  },
  rowCaption: {
    marginTop: 1,
    fontSize: 15,
    lineHeight: 20,
    color: COLORS.caption,
  },
});

export { AddAddressBottomSheet };
export type { IAddAddressBottomSheet, IGlyph } from "./types";
export default memo(AddAddressBottomSheet);
export const DEFAULT_TITLE = "Add new address";

export const DEFAULT_COUNTRY_LABEL = "Country";

export const DEFAULT_COUNTRY = "Iceland";

export const DEFAULT_STREET_PLACEHOLDER = "Street name and number";

export const DEFAULT_LOCATION_TITLE = "Allow access to location";

export const DEFAULT_LOCATION_CAPTION = "Used to match fits with the weather.";

export const DEFAULT_ADD_ADDRESS_TITLE = "Add new address";

export const SHEET_CORNER_RADIUS = 38;

export const BACKDROP_OPACITY = 0.4;

export const SHEET_HORIZONTAL_MARGIN = 14;

export const SHEET_BOTTOM_GAP = 1;

export const CONTENT_HORIZONTAL_PADDING = 22;

export const CONTENT_TOP_PADDING = 5;

export const CONTENT_BOTTOM_PADDING = 24;

export const FIELD_RADIUS = 14;

export const FIELD_GAP = 11;

export const STREET_FIELD_HEIGHT = 52;

export const CLOSE_SIZE = 34;

export const ROW_MARK_SIZE = 40;

export const ROW_GAP = 20;

export const LOCATION_VIEW_BOX = "0 0 32 32";

export const LOCATION_PATH =
  "M25.497 6.503l.001-.003-.004.005L3.5 15.901l11.112 1.489 1.487 11.11 9.396-21.992.005-.006z";

export const COLORS = {
  sheet: "#ffffff",
  ink: "#111111",
  muted: "#9a9a9e",
  caption: "#8e8e93",
  field: "#ffffff",
  fieldBorder: "#ededed",
  fieldPressed: "#fafafa",
  divider: "#ececec",
  mark: "#f4f4f5",
  markPressed: "#e9e9ea",
  close: "#f0f0f1",
  closePressed: "#e4e4e5",
  closeMark: "#a0a0a0",
  handle: "#d4d4d8",
  chevron: "#b0b0b5",
} as const;
export interface IGlyph {
  size?: number;
  color?: string;
}

export interface IAddAddressBottomSheet {
  title?: string;
  countryLabel?: string;
  country?: string;
  streetPlaceholder?: string;
  streetValue?: string;
  locationTitle?: string;
  locationCaption?: string;
  addAddressTitle?: string;
  hideCloseButton?: boolean;
  openOnFocus?: boolean;
  onCountryPress?: () => void;
  onStreetChange?: (value: string) => void;
  onLocationPress?: () => void;
  onAddAddressPress?: () => void;
  onClose?: () => void;
}

ПРИСОЕДИНЯЙТЕСЬ

ПолучитезначокAstra Proужесегодня

ASTRAPRO
Дизайн и разработка — ©Astra