@scripso-homepad/ui 0.3.1 → 0.3.3
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/README.md +9 -3
- package/dist/index.cjs +33 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -6
- package/dist/index.d.ts +4 -6
- package/dist/index.js +35 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Button.tsx +14 -31
- package/src/utils/useApplyWebClassName.ts +62 -0
package/README.md
CHANGED
|
@@ -115,18 +115,24 @@ export function Example() {
|
|
|
115
115
|
|
|
116
116
|
#### Tailwind classes (React web)
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
`className` is applied to the same `TouchableOpacity` / `Text` DOM nodes as the default styles — layout and padding stay consistent with native:
|
|
119
119
|
|
|
120
120
|
```tsx
|
|
121
121
|
<Button
|
|
122
122
|
title="Save"
|
|
123
123
|
onPress={handleSave}
|
|
124
|
-
className="
|
|
124
|
+
className="!bg-violet-600 shadow-lg"
|
|
125
125
|
textClassName="text-sm font-bold uppercase"
|
|
126
126
|
/>
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
Use `!` (important) on Tailwind utilities when overriding default colors, e.g. `!bg-red-500`, because react-native-web applies inline styles from `StyleSheet`.
|
|
130
|
+
|
|
131
|
+
Ensure Tailwind scans the package if needed:
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
content: ["./src/**/*.{js,ts,jsx,tsx}", "./node_modules/@scripso-homepad/ui/**/*.{js,ts,jsx,tsx}"],
|
|
135
|
+
```
|
|
130
136
|
|
|
131
137
|
## Development
|
|
132
138
|
|
package/dist/index.cjs
CHANGED
|
@@ -5,6 +5,33 @@ var reactNative = require('react-native');
|
|
|
5
5
|
var jsxRuntime = require('react/jsx-runtime');
|
|
6
6
|
|
|
7
7
|
// src/components/Button.tsx
|
|
8
|
+
function hasClassList(node) {
|
|
9
|
+
return typeof node === "object" && node !== null && "classList" in node && typeof node.classList?.add === "function";
|
|
10
|
+
}
|
|
11
|
+
function resolveWebElement(ref) {
|
|
12
|
+
const node = ref.current;
|
|
13
|
+
if (!node) return null;
|
|
14
|
+
if (hasClassList(node)) return node;
|
|
15
|
+
const host = node;
|
|
16
|
+
if (hasClassList(host._touchableNode)) return host._touchableNode;
|
|
17
|
+
if (typeof host.getScrollableNode === "function") {
|
|
18
|
+
const scrollNode = host.getScrollableNode();
|
|
19
|
+
if (hasClassList(scrollNode)) return scrollNode;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function useApplyWebClassName(ref, className) {
|
|
24
|
+
react.useLayoutEffect(() => {
|
|
25
|
+
if (reactNative.Platform.OS !== "web" || !className?.trim()) return;
|
|
26
|
+
const element = resolveWebElement(ref);
|
|
27
|
+
if (!element) return;
|
|
28
|
+
const classes = className.trim().split(/\s+/);
|
|
29
|
+
element.classList.add(...classes);
|
|
30
|
+
return () => {
|
|
31
|
+
element.classList.remove(...classes);
|
|
32
|
+
};
|
|
33
|
+
}, [ref, className]);
|
|
34
|
+
}
|
|
8
35
|
function Button({
|
|
9
36
|
title,
|
|
10
37
|
onPress,
|
|
@@ -14,41 +41,23 @@ function Button({
|
|
|
14
41
|
className,
|
|
15
42
|
textClassName
|
|
16
43
|
}) {
|
|
44
|
+
const containerRef = react.useRef(null);
|
|
45
|
+
const textRef = react.useRef(null);
|
|
46
|
+
useApplyWebClassName(containerRef, className);
|
|
47
|
+
useApplyWebClassName(textRef, textClassName);
|
|
17
48
|
const containerStyle = [styles.button, disabled && styles.buttonDisabled, style];
|
|
18
49
|
const labelStyle = [styles.text, disabled && styles.textDisabled, textStyle];
|
|
19
|
-
if (reactNative.Platform.OS === "web") {
|
|
20
|
-
return react.createElement(
|
|
21
|
-
"button",
|
|
22
|
-
{
|
|
23
|
-
type: "button",
|
|
24
|
-
className,
|
|
25
|
-
style: reactNative.StyleSheet.flatten(containerStyle),
|
|
26
|
-
disabled,
|
|
27
|
-
onClick: (event) => {
|
|
28
|
-
onPress(event);
|
|
29
|
-
},
|
|
30
|
-
"aria-disabled": disabled
|
|
31
|
-
},
|
|
32
|
-
react.createElement(
|
|
33
|
-
"span",
|
|
34
|
-
{
|
|
35
|
-
className: textClassName,
|
|
36
|
-
style: reactNative.StyleSheet.flatten(labelStyle)
|
|
37
|
-
},
|
|
38
|
-
title
|
|
39
|
-
)
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
50
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
43
51
|
reactNative.TouchableOpacity,
|
|
44
52
|
{
|
|
53
|
+
ref: containerRef,
|
|
45
54
|
style: containerStyle,
|
|
46
55
|
onPress,
|
|
47
56
|
disabled,
|
|
48
57
|
activeOpacity: 0.7,
|
|
49
58
|
accessibilityRole: "button",
|
|
50
59
|
accessibilityState: { disabled },
|
|
51
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: labelStyle, children: title })
|
|
60
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { ref: textRef, style: labelStyle, children: title })
|
|
52
61
|
}
|
|
53
62
|
);
|
|
54
63
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/Button.tsx"],"names":["
|
|
1
|
+
{"version":3,"sources":["../src/utils/useApplyWebClassName.ts","../src/components/Button.tsx"],"names":["useLayoutEffect","Platform","useRef","jsx","TouchableOpacity","Text","StyleSheet"],"mappings":"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","file":"index.cjs","sourcesContent":["import { useLayoutEffect } from \"react\";\nimport { Platform } from \"react-native\";\n\ninterface ClassListElement {\n classList: {\n add: (...classes: string[]) => void;\n remove: (...classes: string[]) => void;\n };\n}\n\nfunction hasClassList(node: unknown): node is ClassListElement {\n return (\n typeof node === \"object\" &&\n node !== null &&\n \"classList\" in node &&\n typeof (node as ClassListElement).classList?.add === \"function\"\n );\n}\n\nfunction resolveWebElement(ref: React.RefObject<unknown>): ClassListElement | null {\n const node = ref.current;\n if (!node) return null;\n\n if (hasClassList(node)) return node;\n\n const host = node as {\n _touchableNode?: unknown;\n getScrollableNode?: () => unknown;\n };\n\n if (hasClassList(host._touchableNode)) return host._touchableNode;\n\n if (typeof host.getScrollableNode === \"function\") {\n const scrollNode = host.getScrollableNode();\n if (hasClassList(scrollNode)) return scrollNode;\n }\n\n return null;\n}\n\n/**\n * Applies CSS class names to the underlying DOM node on web.\n * Keeps TouchableOpacity/Text as the render path so default RN styles stay intact.\n */\nexport function useApplyWebClassName(\n ref: React.RefObject<unknown>,\n className?: string,\n): void {\n useLayoutEffect(() => {\n if (Platform.OS !== \"web\" || !className?.trim()) return;\n\n const element = resolveWebElement(ref);\n if (!element) return;\n\n const classes = className.trim().split(/\\s+/);\n element.classList.add(...classes);\n\n return () => {\n element.classList.remove(...classes);\n };\n }, [ref, className]);\n}\n","import { useRef, type ComponentRef } from \"react\";\nimport {\n StyleSheet,\n Text,\n TouchableOpacity,\n type GestureResponderEvent,\n type StyleProp,\n type TextStyle,\n type ViewStyle,\n} from \"react-native\";\nimport { useApplyWebClassName } from \"../utils/useApplyWebClassName\";\n\nexport interface ButtonProps {\n title: string;\n onPress: (event: GestureResponderEvent) => void;\n disabled?: boolean;\n /** Additional container styles (works on web and native). */\n style?: StyleProp<ViewStyle>;\n /** Additional label styles (works on web and native). */\n textStyle?: StyleProp<TextStyle>;\n /**\n * CSS class names for the container (web: applied to the same DOM node as default styles).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n className?: string;\n /**\n * CSS class names for the label (web).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n textClassName?: string;\n}\n\nexport function Button({\n title,\n onPress,\n disabled = false,\n style,\n textStyle,\n className,\n textClassName,\n}: ButtonProps) {\n const containerRef = useRef<ComponentRef<typeof TouchableOpacity>>(null);\n const textRef = useRef<ComponentRef<typeof Text>>(null);\n\n useApplyWebClassName(containerRef, className);\n useApplyWebClassName(textRef, textClassName);\n\n const containerStyle = [styles.button, disabled && styles.buttonDisabled, style];\n const labelStyle = [styles.text, disabled && styles.textDisabled, textStyle];\n\n return (\n <TouchableOpacity\n ref={containerRef}\n style={containerStyle}\n onPress={onPress}\n disabled={disabled}\n activeOpacity={0.7}\n accessibilityRole=\"button\"\n accessibilityState={{ disabled }}\n >\n <Text ref={textRef} style={labelStyle}>\n {title}\n </Text>\n </TouchableOpacity>\n );\n}\n\nconst styles = StyleSheet.create({\n button: {\n backgroundColor: \"#2563eb\",\n paddingVertical: 12,\n paddingHorizontal: 24,\n borderRadius: 8,\n alignItems: \"center\",\n justifyContent: \"center\",\n minWidth: 120,\n borderWidth: 0,\n },\n buttonDisabled: {\n backgroundColor: \"#93c5fd\",\n opacity: 0.7,\n },\n text: {\n color: \"#ffffff\",\n fontSize: 16,\n fontWeight: \"600\",\n },\n textDisabled: {\n color: \"#e5e7eb\",\n },\n});\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as react from 'react';
|
|
2
2
|
import { GestureResponderEvent, StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
3
3
|
|
|
4
4
|
interface ButtonProps {
|
|
@@ -10,18 +10,16 @@ interface ButtonProps {
|
|
|
10
10
|
/** Additional label styles (works on web and native). */
|
|
11
11
|
textStyle?: StyleProp<TextStyle>;
|
|
12
12
|
/**
|
|
13
|
-
* CSS class names for the container.
|
|
14
|
-
* On web: applied to the underlying `<button>` element (Tailwind works).
|
|
13
|
+
* CSS class names for the container (web: applied to the same DOM node as default styles).
|
|
15
14
|
* On native: ignored unless using NativeWind with cssInterop.
|
|
16
15
|
*/
|
|
17
16
|
className?: string;
|
|
18
17
|
/**
|
|
19
|
-
* CSS class names for the label.
|
|
20
|
-
* On web: applied to the underlying `<span>` element (Tailwind works).
|
|
18
|
+
* CSS class names for the label (web).
|
|
21
19
|
* On native: ignored unless using NativeWind with cssInterop.
|
|
22
20
|
*/
|
|
23
21
|
textClassName?: string;
|
|
24
22
|
}
|
|
25
|
-
declare function Button({ title, onPress, disabled, style, textStyle, className, textClassName, }: ButtonProps):
|
|
23
|
+
declare function Button({ title, onPress, disabled, style, textStyle, className, textClassName, }: ButtonProps): react.JSX.Element;
|
|
26
24
|
|
|
27
25
|
export { Button, type ButtonProps };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as react from 'react';
|
|
2
2
|
import { GestureResponderEvent, StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
3
3
|
|
|
4
4
|
interface ButtonProps {
|
|
@@ -10,18 +10,16 @@ interface ButtonProps {
|
|
|
10
10
|
/** Additional label styles (works on web and native). */
|
|
11
11
|
textStyle?: StyleProp<TextStyle>;
|
|
12
12
|
/**
|
|
13
|
-
* CSS class names for the container.
|
|
14
|
-
* On web: applied to the underlying `<button>` element (Tailwind works).
|
|
13
|
+
* CSS class names for the container (web: applied to the same DOM node as default styles).
|
|
15
14
|
* On native: ignored unless using NativeWind with cssInterop.
|
|
16
15
|
*/
|
|
17
16
|
className?: string;
|
|
18
17
|
/**
|
|
19
|
-
* CSS class names for the label.
|
|
20
|
-
* On web: applied to the underlying `<span>` element (Tailwind works).
|
|
18
|
+
* CSS class names for the label (web).
|
|
21
19
|
* On native: ignored unless using NativeWind with cssInterop.
|
|
22
20
|
*/
|
|
23
21
|
textClassName?: string;
|
|
24
22
|
}
|
|
25
|
-
declare function Button({ title, onPress, disabled, style, textStyle, className, textClassName, }: ButtonProps):
|
|
23
|
+
declare function Button({ title, onPress, disabled, style, textStyle, className, textClassName, }: ButtonProps): react.JSX.Element;
|
|
26
24
|
|
|
27
25
|
export { Button, type ButtonProps };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { StyleSheet,
|
|
1
|
+
import { useRef, useLayoutEffect } from 'react';
|
|
2
|
+
import { StyleSheet, TouchableOpacity, Text, Platform } from 'react-native';
|
|
3
3
|
import { jsx } from 'react/jsx-runtime';
|
|
4
4
|
|
|
5
5
|
// src/components/Button.tsx
|
|
6
|
+
function hasClassList(node) {
|
|
7
|
+
return typeof node === "object" && node !== null && "classList" in node && typeof node.classList?.add === "function";
|
|
8
|
+
}
|
|
9
|
+
function resolveWebElement(ref) {
|
|
10
|
+
const node = ref.current;
|
|
11
|
+
if (!node) return null;
|
|
12
|
+
if (hasClassList(node)) return node;
|
|
13
|
+
const host = node;
|
|
14
|
+
if (hasClassList(host._touchableNode)) return host._touchableNode;
|
|
15
|
+
if (typeof host.getScrollableNode === "function") {
|
|
16
|
+
const scrollNode = host.getScrollableNode();
|
|
17
|
+
if (hasClassList(scrollNode)) return scrollNode;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
function useApplyWebClassName(ref, className) {
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
if (Platform.OS !== "web" || !className?.trim()) return;
|
|
24
|
+
const element = resolveWebElement(ref);
|
|
25
|
+
if (!element) return;
|
|
26
|
+
const classes = className.trim().split(/\s+/);
|
|
27
|
+
element.classList.add(...classes);
|
|
28
|
+
return () => {
|
|
29
|
+
element.classList.remove(...classes);
|
|
30
|
+
};
|
|
31
|
+
}, [ref, className]);
|
|
32
|
+
}
|
|
6
33
|
function Button({
|
|
7
34
|
title,
|
|
8
35
|
onPress,
|
|
@@ -12,41 +39,23 @@ function Button({
|
|
|
12
39
|
className,
|
|
13
40
|
textClassName
|
|
14
41
|
}) {
|
|
42
|
+
const containerRef = useRef(null);
|
|
43
|
+
const textRef = useRef(null);
|
|
44
|
+
useApplyWebClassName(containerRef, className);
|
|
45
|
+
useApplyWebClassName(textRef, textClassName);
|
|
15
46
|
const containerStyle = [styles.button, disabled && styles.buttonDisabled, style];
|
|
16
47
|
const labelStyle = [styles.text, disabled && styles.textDisabled, textStyle];
|
|
17
|
-
if (Platform.OS === "web") {
|
|
18
|
-
return createElement(
|
|
19
|
-
"button",
|
|
20
|
-
{
|
|
21
|
-
type: "button",
|
|
22
|
-
className,
|
|
23
|
-
style: StyleSheet.flatten(containerStyle),
|
|
24
|
-
disabled,
|
|
25
|
-
onClick: (event) => {
|
|
26
|
-
onPress(event);
|
|
27
|
-
},
|
|
28
|
-
"aria-disabled": disabled
|
|
29
|
-
},
|
|
30
|
-
createElement(
|
|
31
|
-
"span",
|
|
32
|
-
{
|
|
33
|
-
className: textClassName,
|
|
34
|
-
style: StyleSheet.flatten(labelStyle)
|
|
35
|
-
},
|
|
36
|
-
title
|
|
37
|
-
)
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
48
|
return /* @__PURE__ */ jsx(
|
|
41
49
|
TouchableOpacity,
|
|
42
50
|
{
|
|
51
|
+
ref: containerRef,
|
|
43
52
|
style: containerStyle,
|
|
44
53
|
onPress,
|
|
45
54
|
disabled,
|
|
46
55
|
activeOpacity: 0.7,
|
|
47
56
|
accessibilityRole: "button",
|
|
48
57
|
accessibilityState: { disabled },
|
|
49
|
-
children: /* @__PURE__ */ jsx(Text, { style: labelStyle, children: title })
|
|
58
|
+
children: /* @__PURE__ */ jsx(Text, { ref: textRef, style: labelStyle, children: title })
|
|
50
59
|
}
|
|
51
60
|
);
|
|
52
61
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/Button.tsx"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"sources":["../src/utils/useApplyWebClassName.ts","../src/components/Button.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { useLayoutEffect } from \"react\";\nimport { Platform } from \"react-native\";\n\ninterface ClassListElement {\n classList: {\n add: (...classes: string[]) => void;\n remove: (...classes: string[]) => void;\n };\n}\n\nfunction hasClassList(node: unknown): node is ClassListElement {\n return (\n typeof node === \"object\" &&\n node !== null &&\n \"classList\" in node &&\n typeof (node as ClassListElement).classList?.add === \"function\"\n );\n}\n\nfunction resolveWebElement(ref: React.RefObject<unknown>): ClassListElement | null {\n const node = ref.current;\n if (!node) return null;\n\n if (hasClassList(node)) return node;\n\n const host = node as {\n _touchableNode?: unknown;\n getScrollableNode?: () => unknown;\n };\n\n if (hasClassList(host._touchableNode)) return host._touchableNode;\n\n if (typeof host.getScrollableNode === \"function\") {\n const scrollNode = host.getScrollableNode();\n if (hasClassList(scrollNode)) return scrollNode;\n }\n\n return null;\n}\n\n/**\n * Applies CSS class names to the underlying DOM node on web.\n * Keeps TouchableOpacity/Text as the render path so default RN styles stay intact.\n */\nexport function useApplyWebClassName(\n ref: React.RefObject<unknown>,\n className?: string,\n): void {\n useLayoutEffect(() => {\n if (Platform.OS !== \"web\" || !className?.trim()) return;\n\n const element = resolveWebElement(ref);\n if (!element) return;\n\n const classes = className.trim().split(/\\s+/);\n element.classList.add(...classes);\n\n return () => {\n element.classList.remove(...classes);\n };\n }, [ref, className]);\n}\n","import { useRef, type ComponentRef } from \"react\";\nimport {\n StyleSheet,\n Text,\n TouchableOpacity,\n type GestureResponderEvent,\n type StyleProp,\n type TextStyle,\n type ViewStyle,\n} from \"react-native\";\nimport { useApplyWebClassName } from \"../utils/useApplyWebClassName\";\n\nexport interface ButtonProps {\n title: string;\n onPress: (event: GestureResponderEvent) => void;\n disabled?: boolean;\n /** Additional container styles (works on web and native). */\n style?: StyleProp<ViewStyle>;\n /** Additional label styles (works on web and native). */\n textStyle?: StyleProp<TextStyle>;\n /**\n * CSS class names for the container (web: applied to the same DOM node as default styles).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n className?: string;\n /**\n * CSS class names for the label (web).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n textClassName?: string;\n}\n\nexport function Button({\n title,\n onPress,\n disabled = false,\n style,\n textStyle,\n className,\n textClassName,\n}: ButtonProps) {\n const containerRef = useRef<ComponentRef<typeof TouchableOpacity>>(null);\n const textRef = useRef<ComponentRef<typeof Text>>(null);\n\n useApplyWebClassName(containerRef, className);\n useApplyWebClassName(textRef, textClassName);\n\n const containerStyle = [styles.button, disabled && styles.buttonDisabled, style];\n const labelStyle = [styles.text, disabled && styles.textDisabled, textStyle];\n\n return (\n <TouchableOpacity\n ref={containerRef}\n style={containerStyle}\n onPress={onPress}\n disabled={disabled}\n activeOpacity={0.7}\n accessibilityRole=\"button\"\n accessibilityState={{ disabled }}\n >\n <Text ref={textRef} style={labelStyle}>\n {title}\n </Text>\n </TouchableOpacity>\n );\n}\n\nconst styles = StyleSheet.create({\n button: {\n backgroundColor: \"#2563eb\",\n paddingVertical: 12,\n paddingHorizontal: 24,\n borderRadius: 8,\n alignItems: \"center\",\n justifyContent: \"center\",\n minWidth: 120,\n borderWidth: 0,\n },\n buttonDisabled: {\n backgroundColor: \"#93c5fd\",\n opacity: 0.7,\n },\n text: {\n color: \"#ffffff\",\n fontSize: 16,\n fontWeight: \"600\",\n },\n textDisabled: {\n color: \"#e5e7eb\",\n },\n});\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useRef, type ComponentRef } from "react";
|
|
2
2
|
import {
|
|
3
|
-
Platform,
|
|
4
3
|
StyleSheet,
|
|
5
4
|
Text,
|
|
6
5
|
TouchableOpacity,
|
|
@@ -9,6 +8,7 @@ import {
|
|
|
9
8
|
type TextStyle,
|
|
10
9
|
type ViewStyle,
|
|
11
10
|
} from "react-native";
|
|
11
|
+
import { useApplyWebClassName } from "../utils/useApplyWebClassName";
|
|
12
12
|
|
|
13
13
|
export interface ButtonProps {
|
|
14
14
|
title: string;
|
|
@@ -19,14 +19,12 @@ export interface ButtonProps {
|
|
|
19
19
|
/** Additional label styles (works on web and native). */
|
|
20
20
|
textStyle?: StyleProp<TextStyle>;
|
|
21
21
|
/**
|
|
22
|
-
* CSS class names for the container.
|
|
23
|
-
* On web: applied to the underlying `<button>` element (Tailwind works).
|
|
22
|
+
* CSS class names for the container (web: applied to the same DOM node as default styles).
|
|
24
23
|
* On native: ignored unless using NativeWind with cssInterop.
|
|
25
24
|
*/
|
|
26
25
|
className?: string;
|
|
27
26
|
/**
|
|
28
|
-
* CSS class names for the label.
|
|
29
|
-
* On web: applied to the underlying `<span>` element (Tailwind works).
|
|
27
|
+
* CSS class names for the label (web).
|
|
30
28
|
* On native: ignored unless using NativeWind with cssInterop.
|
|
31
29
|
*/
|
|
32
30
|
textClassName?: string;
|
|
@@ -41,35 +39,18 @@ export function Button({
|
|
|
41
39
|
className,
|
|
42
40
|
textClassName,
|
|
43
41
|
}: ButtonProps) {
|
|
42
|
+
const containerRef = useRef<ComponentRef<typeof TouchableOpacity>>(null);
|
|
43
|
+
const textRef = useRef<ComponentRef<typeof Text>>(null);
|
|
44
|
+
|
|
45
|
+
useApplyWebClassName(containerRef, className);
|
|
46
|
+
useApplyWebClassName(textRef, textClassName);
|
|
47
|
+
|
|
44
48
|
const containerStyle = [styles.button, disabled && styles.buttonDisabled, style];
|
|
45
49
|
const labelStyle = [styles.text, disabled && styles.textDisabled, textStyle];
|
|
46
50
|
|
|
47
|
-
if (Platform.OS === "web") {
|
|
48
|
-
return createElement(
|
|
49
|
-
"button",
|
|
50
|
-
{
|
|
51
|
-
type: "button",
|
|
52
|
-
className,
|
|
53
|
-
style: StyleSheet.flatten(containerStyle),
|
|
54
|
-
disabled,
|
|
55
|
-
onClick: (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
56
|
-
onPress(event as unknown as GestureResponderEvent);
|
|
57
|
-
},
|
|
58
|
-
"aria-disabled": disabled,
|
|
59
|
-
},
|
|
60
|
-
createElement(
|
|
61
|
-
"span",
|
|
62
|
-
{
|
|
63
|
-
className: textClassName,
|
|
64
|
-
style: StyleSheet.flatten(labelStyle),
|
|
65
|
-
},
|
|
66
|
-
title,
|
|
67
|
-
),
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
51
|
return (
|
|
72
52
|
<TouchableOpacity
|
|
53
|
+
ref={containerRef}
|
|
73
54
|
style={containerStyle}
|
|
74
55
|
onPress={onPress}
|
|
75
56
|
disabled={disabled}
|
|
@@ -77,7 +58,9 @@ export function Button({
|
|
|
77
58
|
accessibilityRole="button"
|
|
78
59
|
accessibilityState={{ disabled }}
|
|
79
60
|
>
|
|
80
|
-
<Text style={labelStyle}>
|
|
61
|
+
<Text ref={textRef} style={labelStyle}>
|
|
62
|
+
{title}
|
|
63
|
+
</Text>
|
|
81
64
|
</TouchableOpacity>
|
|
82
65
|
);
|
|
83
66
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useLayoutEffect } from "react";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
|
|
4
|
+
interface ClassListElement {
|
|
5
|
+
classList: {
|
|
6
|
+
add: (...classes: string[]) => void;
|
|
7
|
+
remove: (...classes: string[]) => void;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function hasClassList(node: unknown): node is ClassListElement {
|
|
12
|
+
return (
|
|
13
|
+
typeof node === "object" &&
|
|
14
|
+
node !== null &&
|
|
15
|
+
"classList" in node &&
|
|
16
|
+
typeof (node as ClassListElement).classList?.add === "function"
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveWebElement(ref: React.RefObject<unknown>): ClassListElement | null {
|
|
21
|
+
const node = ref.current;
|
|
22
|
+
if (!node) return null;
|
|
23
|
+
|
|
24
|
+
if (hasClassList(node)) return node;
|
|
25
|
+
|
|
26
|
+
const host = node as {
|
|
27
|
+
_touchableNode?: unknown;
|
|
28
|
+
getScrollableNode?: () => unknown;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (hasClassList(host._touchableNode)) return host._touchableNode;
|
|
32
|
+
|
|
33
|
+
if (typeof host.getScrollableNode === "function") {
|
|
34
|
+
const scrollNode = host.getScrollableNode();
|
|
35
|
+
if (hasClassList(scrollNode)) return scrollNode;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Applies CSS class names to the underlying DOM node on web.
|
|
43
|
+
* Keeps TouchableOpacity/Text as the render path so default RN styles stay intact.
|
|
44
|
+
*/
|
|
45
|
+
export function useApplyWebClassName(
|
|
46
|
+
ref: React.RefObject<unknown>,
|
|
47
|
+
className?: string,
|
|
48
|
+
): void {
|
|
49
|
+
useLayoutEffect(() => {
|
|
50
|
+
if (Platform.OS !== "web" || !className?.trim()) return;
|
|
51
|
+
|
|
52
|
+
const element = resolveWebElement(ref);
|
|
53
|
+
if (!element) return;
|
|
54
|
+
|
|
55
|
+
const classes = className.trim().split(/\s+/);
|
|
56
|
+
element.classList.add(...classes);
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
element.classList.remove(...classes);
|
|
60
|
+
};
|
|
61
|
+
}, [ref, className]);
|
|
62
|
+
}
|