import React, { createContext, PropsWithChildren, useContext, useMemo } from 'react' import flatMap from 'lodash/flatMap' import cloneDeep from 'lodash/cloneDeep' import merge from 'lodash/merge' import { colors as colorGroups } from 'icecream-base' import { UnionToIntersection, RecursiveRequired } from '../utils/type' import { LangData, cnData } from './langData' export * from './langData' /** 字符串字面量转成 `string`, 传入对象类型时,则会递归处理每个字段 */ type WidenString = T extends object ? { [P in keyof T]: T[P] extends string ? string : WidenString } : T extends string ? string : T type Groups = typeof colorGroups type GroupKeys = keyof (Groups) type Colors = WidenString> type PartialColors = Partial interface Time { /** * UTC 偏移量。 * * 若不设置使用客户端所在时区,设置后使用 utcOffset 对应的时区。 * * utcOffset 可设置范围: (-16, 16) ,单位为小时 */ utcOffset?: number } interface Mask { modal?: { /** 默认为 false 表示点击遮罩不关闭 */ clickable?: boolean } drawer?: { /** 默认为 true 表示点击遮罩关闭 */ clickable?: boolean } } type RequiredMask = RecursiveRequired const defaultMask: RequiredMask = { modal: { clickable: false }, drawer: { clickable: true } } export type ConfigContextValue = { /** class 前缀。所有的 icecream 组件 class 都会在此基础上拼接 */ prefix: string /** 语言数据 */ langData: LangData /** 颜色变量 */ colors: Colors /** 时间相关 */ time: Time /** 遮罩相关 */ mask: Mask } const defaultColors = flatMap(colorGroups, item => item) .reduce((acc, item) => ({ ...acc, ...item }), {} as Colors) const defaultContextValue: ConfigContextValue = { prefix: 'icecream', langData: cnData, colors: defaultColors, time: { /** 建议组件库开发者通过 `useUtcOffset` 访问这个值(而不是直接通过 context 访问) */ utcOffset: undefined }, mask: defaultMask } // TODO ConfigContextValue 提供这个 // 所有需要实现 size 的都应该用这个类型,即使不支持部分 size export type ComponentSize = 'large' | 'default' | 'small' const configContext = createContext(defaultContextValue) export function useConfig() { return useContext(configContext) } export function usePrefix(name: string, customizedPrefix?: string) { const config = useConfig() if (customizedPrefix != null) { return customizedPrefix } return config.prefix + '-' + name } export function useLangData(moduleName: T): LangData[T] export function useLangData(): LangData export function useLangData(moduleName?: keyof LangData): any { const langData = useConfig().langData if (moduleName) { return langData[moduleName] } return langData } export function useColors() { return useConfig().colors } export function useUtcOffset(): number { return useConfig().time?.utcOffset ?? -(new Date().getTimezoneOffset() / 60) } export function useMask(): RequiredMask { const maskConfig = useConfig().mask return useMemo(() => merge({}, defaultMask, maskConfig), [maskConfig]) } export type ConfigProviderProps = PropsWithChildren & { colors: PartialColors }> export default function ConfigProvider(props: ConfigProviderProps) { const { prefix, langData, time, mask, children } = props const colors = useMemo(() => { const cloned = cloneDeep(defaultColors) return merge(cloned, props.colors) }, [props.colors]) const value = useMemo( () => ({ prefix, langData, colors, time, mask }), [colors, langData, prefix, time, mask] ) return ( {children} ) } // FIXME: 使用更显眼的方式来表达 `Partial` 而不是这种 deprecated 的用法,需要全面排查一下,参考: // https://stackoverflow.com/questions/47774695/react-functional-component-default-props-vs-default-parameters // https://github.com/reactjs/rfcs/pull/107 ConfigProvider.defaultProps = { ...defaultContextValue }