From fe60451b4ad24fc879c0a5a0b0a8f8bc92f459c7 Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Wed, 8 Oct 2025 11:02:07 +0200 Subject: [PATCH] feat: support active, focused, disabled --- .../src/components/native/Pressable.tsx | 19 ++++++++- .../uniwind/src/components/native/Switch.tsx | 16 +++++--- .../uniwind/src/components/native/Text.tsx | 24 ++++++++++-- .../src/components/native/TextInput.tsx | 39 +++++++++++++++---- .../components/native/TouchableHighlight.tsx | 21 ++++++++-- .../native/TouchableNativeFeedback.tsx | 36 +++++++++++------ .../components/native/TouchableOpacity.tsx | 17 +++++++- .../native/TouchableWithoutFeedback.tsx | 17 +++++++- .../uniwind/src/components/native/useStyle.ts | 8 +++- packages/uniwind/src/core/native/store.ts | 11 ++++-- packages/uniwind/src/core/types.ts | 9 +++++ .../src/hooks/useResolveClassNames.native.ts | 9 +++-- .../src/hooks/useUniwindAccent.native.ts | 8 ++++ packages/uniwind/src/metro/processor/mq.ts | 3 ++ .../uniwind/src/metro/processor/processor.ts | 29 +++++++++++++- .../stylesheet/addMetaToStylesTemplate.ts | 9 +++++ packages/uniwind/src/metro/types.ts | 3 ++ 17 files changed, 232 insertions(+), 46 deletions(-) create mode 100644 packages/uniwind/src/hooks/useUniwindAccent.native.ts diff --git a/packages/uniwind/src/components/native/Pressable.tsx b/packages/uniwind/src/components/native/Pressable.tsx index d8beaea7..3750e1b7 100644 --- a/packages/uniwind/src/components/native/Pressable.tsx +++ b/packages/uniwind/src/components/native/Pressable.tsx @@ -1,14 +1,29 @@ import { Pressable as RNPressable, PressableProps } from 'react-native' +import { UniwindStore } from '../../core/native' import { copyComponentProperties } from '../utils' import { useStyle } from './useStyle' export const Pressable = copyComponentProperties(RNPressable, (props: PressableProps) => { - const style = useStyle(props.className) + const style = useStyle(props.className, { + isDisabled: Boolean(props.disabled), + }) return ( [style, typeof props.style === 'function' ? props.style(state) : props.style]} + style={state => { + if (state.pressed) { + return [ + UniwindStore.getStyles( + props.className, + { isDisabled: Boolean(props.disabled), isPressed: true }, + ).styles, + typeof props.style === 'function' ? props.style(state) : props.style, + ] + } + + return [style, typeof props.style === 'function' ? props.style(state) : props.style] + }} /> ) }) diff --git a/packages/uniwind/src/components/native/Switch.tsx b/packages/uniwind/src/components/native/Switch.tsx index f3f79c3f..cd4c7e2c 100644 --- a/packages/uniwind/src/components/native/Switch.tsx +++ b/packages/uniwind/src/components/native/Switch.tsx @@ -1,14 +1,18 @@ import { Switch as RNSwitch, SwitchProps } from 'react-native' -import { useUniwindAccent } from '../../hooks' +import { ComponentState } from '../../core/types' +import { useUniwindAccent } from '../../hooks/useUniwindAccent.native' import { copyComponentProperties } from '../utils' import { useStyle } from './useStyle' export const Switch = copyComponentProperties(RNSwitch, (props: SwitchProps) => { - const style = useStyle(props.className) - const trackColorOn = useUniwindAccent(props.trackColorOnClassName) - const trackColorOff = useUniwindAccent(props.trackColorOffClassName) - const thumbColor = useUniwindAccent(props.thumbColorClassName) - const ios_backgroundColor = useUniwindAccent(props.ios_backgroundColorClassName) + const state = { + isDisabled: Boolean(props.disabled), + } satisfies ComponentState + const style = useStyle(props.className, state) + const trackColorOn = useUniwindAccent(props.trackColorOnClassName, state) + const trackColorOff = useUniwindAccent(props.trackColorOffClassName, state) + const thumbColor = useUniwindAccent(props.thumbColorClassName, state) + const ios_backgroundColor = useUniwindAccent(props.ios_backgroundColorClassName, state) return ( { - const style = useStyle(props.className) - const selectionColor = useUniwindAccent(props.selectionColorClassName) + const [isPressed, setIsPressed] = useState(false) + const state = { + isPressed, + isDisabled: Boolean(props.disabled), + } satisfies ComponentState + const style = useStyle(props.className, state) + const selectionColor = useUniwindAccent(props.selectionColorClassName, state) return ( { style={[style, props.style]} selectionColor={props.selectionColor ?? selectionColor} numberOfLines={(style as StyleWithWebkitLineClamp).WebkitLineClamp ?? props.numberOfLines} + // Without onPress function Text is not clickable, so onPressIn and onPressOut are not working + onPress={event => props.onPress?.(event)} + suppressHighlighting={props.onPress ? props.suppressHighlighting : true} + onPressIn={event => { + setIsPressed(true) + props.onPressIn?.(event) + }} + onPressOut={event => { + setIsPressed(false) + props.onPressOut?.(event) + }} /> ) }) diff --git a/packages/uniwind/src/components/native/TextInput.tsx b/packages/uniwind/src/components/native/TextInput.tsx index f765ccff..fb9ed7bb 100644 --- a/packages/uniwind/src/components/native/TextInput.tsx +++ b/packages/uniwind/src/components/native/TextInput.tsx @@ -1,15 +1,24 @@ +import { useState } from 'react' import { TextInput as RNTextInput, TextInputProps } from 'react-native' -import { useUniwindAccent } from '../../hooks' +import { ComponentState } from '../../core/types' +import { useUniwindAccent } from '../../hooks/useUniwindAccent.native' import { copyComponentProperties } from '../utils' import { useStyle } from './useStyle' export const TextInput = copyComponentProperties(RNTextInput, (props: TextInputProps) => { - const style = useStyle(props.className) - const cursorColor = useUniwindAccent(props.cursorColorClassName) - const selectionColor = useUniwindAccent(props.selectionColorClassName) - const placeholderTextColor = useUniwindAccent(props.placeholderTextColorClassName) - const selectionHandleColor = useUniwindAccent(props.selectionHandleColorClassName) - const underlineColorAndroid = useUniwindAccent(props.underlineColorAndroidClassName) + const [isFocused, setIsFocused] = useState(false) + const [isPressed, setIsPressed] = useState(false) + const state = { + isDisabled: props.editable === false, + isFocused, + isPressed, + } satisfies ComponentState + const style = useStyle(props.className, state) + const cursorColor = useUniwindAccent(props.cursorColorClassName, state) + const selectionColor = useUniwindAccent(props.selectionColorClassName, state) + const placeholderTextColor = useUniwindAccent(props.placeholderTextColorClassName, state) + const selectionHandleColor = useUniwindAccent(props.selectionHandleColorClassName, state) + const underlineColorAndroid = useUniwindAccent(props.underlineColorAndroidClassName, state) return ( { + setIsFocused(true) + props.onFocus?.(event) + }} + onBlur={event => { + setIsFocused(false) + props.onBlur?.(event) + }} + onPressIn={event => { + setIsPressed(true) + props.onPressIn?.(event) + }} + onPressOut={event => { + setIsPressed(false) + props.onPressOut?.(event) + }} /> ) }) diff --git a/packages/uniwind/src/components/native/TouchableHighlight.tsx b/packages/uniwind/src/components/native/TouchableHighlight.tsx index ac2abc26..504a7afc 100644 --- a/packages/uniwind/src/components/native/TouchableHighlight.tsx +++ b/packages/uniwind/src/components/native/TouchableHighlight.tsx @@ -1,17 +1,32 @@ +import { useState } from 'react' import { TouchableHighlight as RNTouchableHighlight, TouchableHighlightProps } from 'react-native' -import { useUniwindAccent } from '../../hooks' +import { ComponentState } from '../../core/types' +import { useUniwindAccent } from '../../hooks/useUniwindAccent.native' import { copyComponentProperties } from '../utils' import { useStyle } from './useStyle' export const TouchableHighlight = copyComponentProperties(RNTouchableHighlight, (props: TouchableHighlightProps) => { - const style = useStyle(props.className) - const underlayColor = useUniwindAccent(props.underlayColorClassName) + const [isPressed, setIsPressed] = useState(false) + const state = { + isDisabled: Boolean(props.disabled), + isPressed, + } satisfies ComponentState + const style = useStyle(props.className, state) + const underlayColor = useUniwindAccent(props.underlayColorClassName, state) return ( { + setIsPressed(true) + props.onPressIn?.(event) + }} + onPressOut={event => { + setIsPressed(false) + props.onPressOut?.(event) + }} /> ) }) diff --git a/packages/uniwind/src/components/native/TouchableNativeFeedback.tsx b/packages/uniwind/src/components/native/TouchableNativeFeedback.tsx index bff75fed..b556d7b2 100644 --- a/packages/uniwind/src/components/native/TouchableNativeFeedback.tsx +++ b/packages/uniwind/src/components/native/TouchableNativeFeedback.tsx @@ -1,19 +1,31 @@ +import { useState } from 'react' import { TouchableNativeFeedback as RNTouchableNativeFeedback, TouchableNativeFeedbackProps } from 'react-native' +import { ComponentState } from '../../core/types' import { copyComponentProperties } from '../utils' import { useStyle } from './useStyle' -export const TouchableNativeFeedback = copyComponentProperties( - RNTouchableNativeFeedback, - (props: TouchableNativeFeedbackProps) => { - const style = useStyle(props.className) +export const TouchableNativeFeedback = copyComponentProperties(RNTouchableNativeFeedback, (props: TouchableNativeFeedbackProps) => { + const [isPressed, setIsPressed] = useState(false) + const state = { + isDisabled: Boolean(props.disabled), + isPressed, + } satisfies ComponentState + const style = useStyle(props.className, state) - return ( - - ) - }, -) + return ( + { + setIsPressed(true) + props.onPressIn?.(event) + }} + onPressOut={event => { + setIsPressed(false) + props.onPressOut?.(event) + }} + /> + ) +}) export default TouchableNativeFeedback diff --git a/packages/uniwind/src/components/native/TouchableOpacity.tsx b/packages/uniwind/src/components/native/TouchableOpacity.tsx index 940db798..8d98f12b 100644 --- a/packages/uniwind/src/components/native/TouchableOpacity.tsx +++ b/packages/uniwind/src/components/native/TouchableOpacity.tsx @@ -1,14 +1,29 @@ +import { useState } from 'react' import { TouchableOpacity as RNTouchableOpacity, TouchableOpacityProps } from 'react-native' +import { ComponentState } from '../../core/types' import { copyComponentProperties } from '../utils' import { useStyle } from './useStyle' export const TouchableOpacity = copyComponentProperties(RNTouchableOpacity, (props: TouchableOpacityProps) => { - const style = useStyle(props.className) + const [isPressed, setIsPressed] = useState(false) + const state = { + isDisabled: Boolean(props.disabled), + isPressed, + } satisfies ComponentState + const style = useStyle(props.className, state) return ( { + setIsPressed(true) + props.onPressIn?.(event) + }} + onPressOut={event => { + setIsPressed(false) + props.onPressOut?.(event) + }} /> ) }) diff --git a/packages/uniwind/src/components/native/TouchableWithoutFeedback.tsx b/packages/uniwind/src/components/native/TouchableWithoutFeedback.tsx index 4c5dc63f..33793648 100644 --- a/packages/uniwind/src/components/native/TouchableWithoutFeedback.tsx +++ b/packages/uniwind/src/components/native/TouchableWithoutFeedback.tsx @@ -1,14 +1,29 @@ +import { useState } from 'react' import { TouchableWithoutFeedback as RNTouchableWithoutFeedback, TouchableWithoutFeedbackProps } from 'react-native' +import { ComponentState } from '../../core/types' import { copyComponentProperties } from '../utils' import { useStyle } from './useStyle' export const TouchableWithoutFeedback = copyComponentProperties(RNTouchableWithoutFeedback, (props: TouchableWithoutFeedbackProps) => { - const style = useStyle(props.className) + const [isPressed, setIsPressed] = useState(false) + const state = { + isDisabled: Boolean(props.disabled), + isPressed, + } satisfies ComponentState + const style = useStyle(props.className, state) return ( { + setIsPressed(true) + props.onPressIn?.(event) + }} + onPressOut={event => { + setIsPressed(false) + props.onPressOut?.(event) + }} /> ) }) diff --git a/packages/uniwind/src/components/native/useStyle.ts b/packages/uniwind/src/components/native/useStyle.ts index fbaf5aa1..40bad82a 100644 --- a/packages/uniwind/src/components/native/useStyle.ts +++ b/packages/uniwind/src/components/native/useStyle.ts @@ -1,9 +1,13 @@ import { useEffect, useMemo, useReducer } from 'react' import { UniwindStore } from '../../core/native' +import { ComponentState } from '../../core/types' -export const useStyle = (className?: string) => { +export const useStyle = (className?: string, state?: ComponentState) => { const [_, rerender] = useReducer(() => ({}), {}) - const styleState = useMemo(() => UniwindStore.getStyles(className), [className, _]) + const styleState = useMemo( + () => UniwindStore.getStyles(className, state), + [className, _, state?.isDisabled, state?.isFocused, state?.isPressed], + ) useEffect(() => { const dispose = UniwindStore.subscribe(() => rerender(), styleState.dependencies) diff --git a/packages/uniwind/src/core/native/store.ts b/packages/uniwind/src/core/native/store.ts index 5b08bcab..7248d46e 100644 --- a/packages/uniwind/src/core/native/store.ts +++ b/packages/uniwind/src/core/native/store.ts @@ -1,6 +1,6 @@ import { Dimensions } from 'react-native' import { Orientation, StyleDependency } from '../../types' -import { RNStyle, Style, StyleSheets } from '../types' +import { ComponentState, RNStyle, Style, StyleSheets } from '../types' import { parseBoxShadow, parseFontVariant, parseTransformsMutation, resolveGradient } from './parsers' import { UniwindRuntime } from './runtime' @@ -30,7 +30,7 @@ export class UniwindStoreBuilder { } } - getStyles(className?: string) { + getStyles(className?: string, state?: ComponentState) { if (className === undefined) { return { styles: {} as RNStyle, @@ -56,7 +56,7 @@ export class UniwindStoreBuilder { }) .filter(Boolean) - return this.resolveStyles(styles as Array<[string, Style]>) + return this.resolveStyles(styles as Array<[string, Style]>, state) } reload = () => { @@ -67,7 +67,7 @@ export class UniwindStoreBuilder { dependencies.forEach(dep => this.listeners[dep].forEach(listener => listener())) } - private resolveStyles(styles: Array<[string, Style]>) { + private resolveStyles(styles: Array<[string, Style]>, state?: ComponentState) { const dependencies = [] as Array const filteredStyles = styles.filter(([, style]) => { dependencies.push(...style.dependencies) @@ -78,6 +78,9 @@ export class UniwindStoreBuilder { || (style.theme !== null && this.runtime.currentThemeName !== style.theme) || (style.orientation !== null && this.runtime.orientation !== style.orientation) || (style.rtl !== null && this.runtime.rtl !== style.rtl) + || (style.active !== null && state?.isPressed !== style.active) + || (style.focus !== null && state?.isFocused !== style.focus) + || (style.disabled !== null && state?.isDisabled !== style.disabled) ) { return false } diff --git a/packages/uniwind/src/core/types.ts b/packages/uniwind/src/core/types.ts index fec6b44a..fa505d1b 100644 --- a/packages/uniwind/src/core/types.ts +++ b/packages/uniwind/src/core/types.ts @@ -16,6 +16,9 @@ export type Style = { className: string importantProperties: Array complexity: number + active: boolean | null + focus: boolean | null + disabled: boolean | null } export type StyleSheets = Record> @@ -77,3 +80,9 @@ declare global { var __uniwind__hot_reload: () => void var __uniwindThemes__: ReadonlyArray | undefined } + +export type ComponentState = { + isPressed?: boolean + isDisabled?: boolean + isFocused?: boolean +} diff --git a/packages/uniwind/src/hooks/useResolveClassNames.native.ts b/packages/uniwind/src/hooks/useResolveClassNames.native.ts index 25c2e2a5..97cbdbb0 100644 --- a/packages/uniwind/src/hooks/useResolveClassNames.native.ts +++ b/packages/uniwind/src/hooks/useResolveClassNames.native.ts @@ -1,15 +1,16 @@ import { useEffect, useReducer } from 'react' import { UniwindStore } from '../core/native' +import { ComponentState } from '../core/types' -export const useResolveClassNames = (className: string) => { +export const useResolveClassNames = (className: string, state?: ComponentState) => { const [uniwindState, recreate] = useReducer( - () => UniwindStore.getStyles(className), - UniwindStore.getStyles(className), + () => UniwindStore.getStyles(className, state), + UniwindStore.getStyles(className, state), ) useEffect(() => { recreate() - }, [className]) + }, [className, state?.isDisabled, state?.isPressed, state?.isFocused]) useEffect(() => { const dispose = UniwindStore.subscribe(recreate, uniwindState.dependencies) diff --git a/packages/uniwind/src/hooks/useUniwindAccent.native.ts b/packages/uniwind/src/hooks/useUniwindAccent.native.ts new file mode 100644 index 00000000..7302a57b --- /dev/null +++ b/packages/uniwind/src/hooks/useUniwindAccent.native.ts @@ -0,0 +1,8 @@ +import { ComponentState } from '../core/types' +import { useResolveClassNames } from './useResolveClassNames.native' + +export const useUniwindAccent = (className: string | undefined, state?: ComponentState) => { + const styles = useResolveClassNames(className ?? '', state) + + return styles.accentColor +} diff --git a/packages/uniwind/src/metro/processor/mq.ts b/packages/uniwind/src/metro/processor/mq.ts index 3da85c9c..5405b8f6 100644 --- a/packages/uniwind/src/metro/processor/mq.ts +++ b/packages/uniwind/src/metro/processor/mq.ts @@ -74,6 +74,9 @@ export class MQ { colorScheme: null, orientation: null, theme: null, + active: null, + focus: null, + disabled: null, } } } diff --git a/packages/uniwind/src/metro/processor/processor.ts b/packages/uniwind/src/metro/processor/processor.ts index b5eb10d4..3f297e49 100644 --- a/packages/uniwind/src/metro/processor/processor.ts +++ b/packages/uniwind/src/metro/processor/processor.ts @@ -45,6 +45,9 @@ export class ProcessorBuilder { mediaQueries: [] as Array, root: false, theme: null as string | null, + active: null as boolean | null, + focus: null as boolean | null, + disabled: null as boolean | null, }) } @@ -71,6 +74,9 @@ export class ProcessorBuilder { style.importantProperties ??= [] style.rtl = this.declarationConfig.rtl style.theme = mq.colorScheme ?? this.declarationConfig.theme + style.active = this.declarationConfig.active + style.focus = this.declarationConfig.focus + style.disabled = this.declarationConfig.disabled } if (declaration.property === 'unparsed') { @@ -128,6 +134,9 @@ export class ProcessorBuilder { let rtl = null as boolean | null let theme = null as string | null + let active = null as boolean | null + let focus = null as boolean | null + let disabled = null as boolean | null selector.forEach(selector => { if (selector.type === 'pseudo-class' && selector.kind === 'where') { @@ -143,11 +152,26 @@ export class ProcessorBuilder { }) }) } + + if (selector.type === 'pseudo-class' && selector.kind === 'active') { + active = true + } + + if (selector.type === 'pseudo-class' && selector.kind === 'focus') { + focus = true + } + + if (selector.type === 'pseudo-class' && selector.kind === 'disabled') { + disabled = true + } }) - if (rtl !== null || theme !== null) { + if ([rtl, theme, active, focus, disabled].some(Boolean)) { this.declarationConfig.rtl = rtl this.declarationConfig.theme = theme + this.declarationConfig.active = active + this.declarationConfig.focus = focus + this.declarationConfig.disabled = disabled rule.value.declarations?.declarations?.forEach(declaration => this.addDeclaration(declaration)) rule.value.declarations?.importantDeclarations?.forEach(declaration => this.addDeclaration(declaration, true)) @@ -155,6 +179,9 @@ export class ProcessorBuilder { this.declarationConfig.rtl = null this.declarationConfig.theme = null + this.declarationConfig.active = null + this.declarationConfig.focus = null + this.declarationConfig.disabled = null return } diff --git a/packages/uniwind/src/metro/stylesheet/addMetaToStylesTemplate.ts b/packages/uniwind/src/metro/stylesheet/addMetaToStylesTemplate.ts index c0efb72b..dac6f6e3 100644 --- a/packages/uniwind/src/metro/stylesheet/addMetaToStylesTemplate.ts +++ b/packages/uniwind/src/metro/stylesheet/addMetaToStylesTemplate.ts @@ -18,6 +18,9 @@ export const addMetaToStylesTemplate = (Processor: ProcessorBuilder, currentPlat // eslint-disable-next-line @typescript-eslint/no-unused-vars important, importantProperties, + active, + focus, + disabled, ...rest } = style @@ -93,6 +96,9 @@ export const addMetaToStylesTemplate = (Processor: ProcessorBuilder, currentPlat dependencies, index, className, + active, + focus, + disabled, importantProperties: importantProperties?.map(property => property.startsWith('--') ? property : toCamelCase) ?? [], complexity: [ minWidth !== 0, @@ -100,6 +106,9 @@ export const addMetaToStylesTemplate = (Processor: ProcessorBuilder, currentPlat orientation !== null, rtl !== null, platform !== null, + active !== null, + focus !== null, + disabled !== null, ].filter(Boolean).length, } }) diff --git a/packages/uniwind/src/metro/types.ts b/packages/uniwind/src/metro/types.ts index 0c8e3f7a..0719a105 100644 --- a/packages/uniwind/src/metro/types.ts +++ b/packages/uniwind/src/metro/types.ts @@ -46,6 +46,9 @@ export type MediaQueryResolver = { colorScheme: ColorScheme | null theme: string | null orientation: Orientation | null + disabled: boolean | null + active: boolean | null + focus: boolean | null } export const enum Platform {