@startupjs-ui/div 0.2.0 → 0.3.1
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 +22 -0
- package/README.mdx +1 -1
- package/index.d.ts +8 -6
- package/index.tsx +62 -44
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [0.3.1](https://github.com/startupjs/startupjs-ui/compare/v0.3.0...v0.3.1) (2026-06-08)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* support inherited text styles from Div, support relative lineHeight ([#32](https://github.com/startupjs/startupjs-ui/issues/32)) ([47b56ab](https://github.com/startupjs/startupjs-ui/commit/47b56abca03b1d3ef1d977309deffd95a2de709d))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# [0.3.0](https://github.com/startupjs/startupjs-ui/compare/v0.2.3...v0.3.0) (2026-05-27)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* [BREAKING] [0.3] improve accessibility props for E2E tests. Support testID everywhere ([#31](https://github.com/startupjs/startupjs-ui/issues/31)) ([882588c](https://github.com/startupjs/startupjs-ui/commit/882588ca37d5e1fd14b5717b5697cf9ed47042e4))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
# [0.2.0](https://github.com/startupjs/startupjs-ui/compare/v0.1.23...v0.2.0) (2026-05-04)
|
|
7
29
|
|
|
8
30
|
|
package/README.mdx
CHANGED
|
@@ -8,7 +8,7 @@ import Icon from '@startupjs-ui/icon'
|
|
|
8
8
|
|
|
9
9
|
# Div
|
|
10
10
|
|
|
11
|
-
Div is a basic building block for layouts. It is a wrapper around `View` with extra features like press handling (`onPress`, `onLongPress`, `onPressIn`, `onPressOut`), layout helpers (`row`, `wrap`, `reverse`, `align`, `vAlign`, `gap`), shadows (`level`), corner shapes (`shape`), spacing (`pushed`, `bleed`), tooltips (`tooltip`, `tooltipStyle`), and built-in animation support. You can pass custom `style`, provide content as `children`, and use `ref` to access the underlying native view. Accessibility
|
|
11
|
+
Div is a basic building block for layouts. It is a wrapper around `View` with extra features like press handling (`onPress`, `onLongPress`, `onPressIn`, `onPressOut`), layout helpers (`row`, `wrap`, `reverse`, `align`, `vAlign`, `gap`), shadows (`level`), corner shapes (`shape`), spacing (`pushed`, `bleed`), tooltips (`tooltip`, `tooltipStyle`), and built-in animation support. You can pass custom `style`, provide content as `children`, and use `ref` to access the underlying native view. Accessibility and E2E semantics are exposed with `role`, `aria-*`, and `testID`.
|
|
12
12
|
|
|
13
13
|
```jsx
|
|
14
14
|
import { Div } from 'startupjs-ui'
|
package/index.d.ts
CHANGED
|
@@ -2,13 +2,19 @@
|
|
|
2
2
|
// DO NOT MODIFY THIS FILE - IT IS AUTOMATICALLY GENERATED ON COMMITS.
|
|
3
3
|
|
|
4
4
|
import { type ReactNode, type RefObject } from 'react';
|
|
5
|
-
import { type StyleProp, type ViewStyle, type ViewProps
|
|
5
|
+
import { type StyleProp, type ViewStyle, type ViewProps } from 'react-native';
|
|
6
|
+
import { type UIRole } from '@startupjs-ui/core';
|
|
7
|
+
type AriaHasPopup = boolean | 'dialog' | 'grid' | 'listbox' | 'menu' | 'tree';
|
|
6
8
|
declare const _default: import("react").ComponentType<DivProps>;
|
|
7
9
|
export default _default;
|
|
8
10
|
export declare const _PropsJsonSchema: {};
|
|
9
|
-
export interface DivProps extends ViewProps {
|
|
11
|
+
export interface DivProps extends Omit<ViewProps, 'role'> {
|
|
10
12
|
/** Ref to access underlying <View> or <Pressable> */
|
|
11
13
|
ref?: RefObject<any>;
|
|
14
|
+
/** Accessibility role. Includes RN roles plus web-only ARIA roles used by RNW. */
|
|
15
|
+
role?: UIRole;
|
|
16
|
+
/** Web popup type exposed through aria-haspopup */
|
|
17
|
+
'aria-haspopup'?: AriaHasPopup;
|
|
12
18
|
/** Custom styles applied to the root view */
|
|
13
19
|
style?: StyleProp<ViewStyle>;
|
|
14
20
|
/** Content rendered inside Div */
|
|
@@ -59,12 +65,8 @@ export interface DivProps extends ViewProps {
|
|
|
59
65
|
onPressOut?: (e: any) => void;
|
|
60
66
|
/** Whether view is accessible and focusable (if you can press it it's focusable by default) */
|
|
61
67
|
accessible?: boolean;
|
|
62
|
-
/** Accessibility role passed to native view (if you can press it it's a 'button') */
|
|
63
|
-
accessibilityRole?: AccessibilityRole;
|
|
64
68
|
/** Deprecated custom tooltip renderer @deprecated */
|
|
65
69
|
renderTooltip?: any;
|
|
66
70
|
/** Internal: render a native <button> host on web when the resolved role is button */
|
|
67
71
|
_webNativeButton?: boolean;
|
|
68
|
-
/** Test ID for testing purposes */
|
|
69
|
-
'data-testid'?: string;
|
|
70
72
|
}
|
package/index.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useLayoutEffect, useState, useRef, type ReactNode, type RefObject } from 'react'
|
|
1
|
+
import { useContext, useLayoutEffect, useMemo, useState, useRef, type ReactNode, type RefObject } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Pressable,
|
|
@@ -6,17 +6,24 @@ import {
|
|
|
6
6
|
StyleSheet,
|
|
7
7
|
type StyleProp,
|
|
8
8
|
type ViewStyle,
|
|
9
|
-
type ViewProps
|
|
10
|
-
type AccessibilityRole
|
|
9
|
+
type ViewProps
|
|
11
10
|
} from 'react-native'
|
|
12
11
|
import Animated from 'react-native-reanimated'
|
|
13
12
|
import { pug, observer, u, useDidUpdate } from 'startupjs'
|
|
14
|
-
import { colorToRGBA, getCssVariable, themed } from '@startupjs-ui/core'
|
|
13
|
+
import { colorToRGBA, getCssVariable, themed, type UIRole } from '@startupjs-ui/core'
|
|
14
|
+
import {
|
|
15
|
+
TextStyleContext,
|
|
16
|
+
getInheritedTextStyle,
|
|
17
|
+
mergeInheritedTextStyles,
|
|
18
|
+
omitInheritedTextStyle
|
|
19
|
+
} from '@startupjs-ui/span/textStyleContext'
|
|
15
20
|
import { useDecorateTooltipProps } from './useTooltip'
|
|
16
21
|
import STYLES from './index.cssx.styl'
|
|
17
22
|
|
|
18
23
|
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
|
|
19
24
|
|
|
25
|
+
type AriaHasPopup = boolean | 'dialog' | 'grid' | 'listbox' | 'menu' | 'tree'
|
|
26
|
+
|
|
20
27
|
const DEPRECATED_PUSHED_VALUES = ['xs', 'xl', 'xxl']
|
|
21
28
|
const PRESSABLE_PROPS = ['onPress', 'onLongPress', 'onPressIn', 'onPressOut']
|
|
22
29
|
const isWeb = Platform.OS === 'web'
|
|
@@ -33,9 +40,13 @@ export default observer(themed('Div', Div))
|
|
|
33
40
|
|
|
34
41
|
export const _PropsJsonSchema = {/* DivProps */}
|
|
35
42
|
|
|
36
|
-
export interface DivProps extends ViewProps {
|
|
43
|
+
export interface DivProps extends Omit<ViewProps, 'role'> {
|
|
37
44
|
/** Ref to access underlying <View> or <Pressable> */
|
|
38
45
|
ref?: RefObject<any>
|
|
46
|
+
/** Accessibility role. Includes RN roles plus web-only ARIA roles used by RNW. */
|
|
47
|
+
role?: UIRole
|
|
48
|
+
/** Web popup type exposed through aria-haspopup */
|
|
49
|
+
'aria-haspopup'?: AriaHasPopup
|
|
39
50
|
/** Custom styles applied to the root view */
|
|
40
51
|
style?: StyleProp<ViewStyle>
|
|
41
52
|
/** Content rendered inside Div */
|
|
@@ -86,18 +97,14 @@ export interface DivProps extends ViewProps {
|
|
|
86
97
|
onPressOut?: (e: any) => void
|
|
87
98
|
/** Whether view is accessible and focusable (if you can press it it's focusable by default) */
|
|
88
99
|
accessible?: boolean
|
|
89
|
-
/** Accessibility role passed to native view (if you can press it it's a 'button') */
|
|
90
|
-
accessibilityRole?: AccessibilityRole
|
|
91
100
|
/** Deprecated custom tooltip renderer @deprecated */
|
|
92
101
|
renderTooltip?: any // Deprecated
|
|
93
102
|
/** Internal: render a native <button> host on web when the resolved role is button */
|
|
94
103
|
_webNativeButton?: boolean
|
|
95
|
-
/** Test ID for testing purposes */
|
|
96
|
-
'data-testid'?: string
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
function Div ({
|
|
100
|
-
style = [],
|
|
107
|
+
style: rawStyle = [],
|
|
101
108
|
children,
|
|
102
109
|
variant = 'opacity',
|
|
103
110
|
row,
|
|
@@ -116,7 +123,6 @@ function Div ({
|
|
|
116
123
|
bleed,
|
|
117
124
|
full,
|
|
118
125
|
accessible,
|
|
119
|
-
accessibilityRole,
|
|
120
126
|
tooltip,
|
|
121
127
|
tooltipStyle,
|
|
122
128
|
renderTooltip,
|
|
@@ -125,20 +131,29 @@ function Div ({
|
|
|
125
131
|
...props
|
|
126
132
|
}: DivProps): ReactNode {
|
|
127
133
|
assertDeprecatedValues({ pushed, renderTooltip })
|
|
128
|
-
style = StyleSheet.flatten(
|
|
134
|
+
let style = StyleSheet.flatten(rawStyle) as ViewStyle | undefined
|
|
129
135
|
// on RN row-reverse switches margins and paddings sides, so we switch them back
|
|
130
136
|
if (isNative && reverse) style = reverseMarginPaddingSides(style)
|
|
137
|
+
|
|
138
|
+
const inheritedTextStyle = useContext(TextStyleContext)
|
|
139
|
+
const ownTextStyle = getInheritedTextStyle(style)
|
|
140
|
+
const nextInheritedTextStyleKey = simpleNumericHash(JSON.stringify([inheritedTextStyle, ownTextStyle]))
|
|
141
|
+
const nextInheritedTextStyle = useMemo(() => {
|
|
142
|
+
if (!ownTextStyle) return undefined
|
|
143
|
+
return mergeInheritedTextStyles(inheritedTextStyle, ownTextStyle)
|
|
144
|
+
}, [nextInheritedTextStyleKey]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
145
|
+
omitInheritedTextStyle(style)
|
|
146
|
+
|
|
131
147
|
if (gap === true) gap = 2
|
|
132
148
|
const isPressable = hasPressHandler(props)
|
|
133
149
|
const fallbackRef = useRef<any>(null)
|
|
134
150
|
const rootRef = ref ?? fallbackRef
|
|
135
151
|
|
|
136
152
|
let pressableStyle: StyleProp<ViewStyle> = {}
|
|
137
|
-
let deferredRole:
|
|
153
|
+
let deferredRole: string | undefined
|
|
138
154
|
;({
|
|
139
155
|
props,
|
|
140
156
|
pressableStyle,
|
|
141
|
-
accessibilityRole,
|
|
142
157
|
deferredRole
|
|
143
158
|
} = useDecoratePressableProps({
|
|
144
159
|
props,
|
|
@@ -148,22 +163,18 @@ function Div ({
|
|
|
148
163
|
variant,
|
|
149
164
|
isPressable,
|
|
150
165
|
disabled,
|
|
151
|
-
accessibilityRole,
|
|
152
166
|
feedback,
|
|
153
167
|
webNativeButton: _webNativeButton
|
|
154
168
|
}))
|
|
155
169
|
|
|
156
170
|
;({
|
|
157
171
|
props,
|
|
158
|
-
accessible
|
|
159
|
-
accessibilityRole,
|
|
160
|
-
deferredRole
|
|
172
|
+
accessible
|
|
161
173
|
} = useDecorateAccessibilityProps({
|
|
162
174
|
props,
|
|
163
175
|
rootRef,
|
|
164
176
|
disabled,
|
|
165
177
|
accessible,
|
|
166
|
-
accessibilityRole,
|
|
167
178
|
isPressable,
|
|
168
179
|
deferredRole,
|
|
169
180
|
webNativeButton: _webNativeButton
|
|
@@ -193,7 +204,7 @@ function Div ({
|
|
|
193
204
|
const Component = isPressable
|
|
194
205
|
? (isAnimated ? AnimatedPressable : Pressable)
|
|
195
206
|
: (isAnimated ? Animated.View : View)
|
|
196
|
-
const
|
|
207
|
+
const renderProps = props as Omit<typeof props, 'role'> & { role?: ViewProps['role'] }
|
|
197
208
|
const divElement = pug`
|
|
198
209
|
Component.root(
|
|
199
210
|
ref=rootRef
|
|
@@ -218,18 +229,26 @@ function Div ({
|
|
|
218
229
|
levelModifier
|
|
219
230
|
]
|
|
220
231
|
accessible=accessible
|
|
221
|
-
|
|
222
|
-
testID=testID
|
|
223
|
-
...props
|
|
232
|
+
...renderProps
|
|
224
233
|
)= children
|
|
225
234
|
`
|
|
235
|
+
const styledDivElement = nextInheritedTextStyle
|
|
236
|
+
? pug`
|
|
237
|
+
TextStyleContext.Provider(value=nextInheritedTextStyle)
|
|
238
|
+
= divElement
|
|
239
|
+
`
|
|
240
|
+
: divElement
|
|
226
241
|
|
|
227
242
|
if (tooltipElement) {
|
|
228
243
|
return pug`
|
|
229
|
-
=
|
|
244
|
+
= styledDivElement
|
|
230
245
|
= tooltipElement
|
|
231
246
|
`
|
|
232
|
-
} else return
|
|
247
|
+
} else return styledDivElement
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isWebOnlyRole (role: unknown): role is Exclude<UIRole, ViewProps['role']> {
|
|
251
|
+
return role === 'listbox' || role === 'gridcell'
|
|
233
252
|
}
|
|
234
253
|
|
|
235
254
|
function hasAnimatedProperty (style: any): boolean {
|
|
@@ -242,7 +261,6 @@ function useDecorateAccessibilityProps ({
|
|
|
242
261
|
rootRef,
|
|
243
262
|
disabled,
|
|
244
263
|
accessible,
|
|
245
|
-
accessibilityRole,
|
|
246
264
|
isPressable,
|
|
247
265
|
deferredRole,
|
|
248
266
|
webNativeButton
|
|
@@ -251,19 +269,15 @@ function useDecorateAccessibilityProps ({
|
|
|
251
269
|
rootRef: RefObject<any>
|
|
252
270
|
disabled?: boolean
|
|
253
271
|
accessible?: boolean
|
|
254
|
-
accessibilityRole?: AccessibilityRole
|
|
255
272
|
isPressable: boolean
|
|
256
|
-
deferredRole?:
|
|
273
|
+
deferredRole?: string
|
|
257
274
|
webNativeButton?: boolean
|
|
258
275
|
}): {
|
|
259
276
|
props: Record<string, any>
|
|
260
277
|
accessible?: boolean
|
|
261
|
-
accessibilityRole?: AccessibilityRole
|
|
262
|
-
deferredRole?: AccessibilityRole | string
|
|
263
278
|
} {
|
|
264
279
|
if (accessible == null && isPressable) accessible = true
|
|
265
280
|
if (accessible === false) {
|
|
266
|
-
accessibilityRole = undefined
|
|
267
281
|
deferredRole = undefined
|
|
268
282
|
props.role = undefined
|
|
269
283
|
}
|
|
@@ -272,6 +286,9 @@ function useDecorateAccessibilityProps ({
|
|
|
272
286
|
props['aria-disabled'] = disabled
|
|
273
287
|
}
|
|
274
288
|
|
|
289
|
+
if (isNative && isWebOnlyRole(props.role)) delete props.role
|
|
290
|
+
if (isNative) delete props['aria-haspopup']
|
|
291
|
+
|
|
275
292
|
const roleProp = props.role
|
|
276
293
|
const ariaDisabled = props['aria-disabled']
|
|
277
294
|
|
|
@@ -301,7 +318,7 @@ function useDecorateAccessibilityProps ({
|
|
|
301
318
|
}
|
|
302
319
|
}, [rootRef, deferredRole, roleProp, ariaDisabled, webNativeButton, disabled])
|
|
303
320
|
|
|
304
|
-
return { props, accessible
|
|
321
|
+
return { props, accessible }
|
|
305
322
|
}
|
|
306
323
|
|
|
307
324
|
function useDecoratePressableProps ({
|
|
@@ -312,7 +329,6 @@ function useDecoratePressableProps ({
|
|
|
312
329
|
variant,
|
|
313
330
|
isPressable,
|
|
314
331
|
disabled,
|
|
315
|
-
accessibilityRole,
|
|
316
332
|
feedback,
|
|
317
333
|
webNativeButton
|
|
318
334
|
}: {
|
|
@@ -323,17 +339,15 @@ function useDecoratePressableProps ({
|
|
|
323
339
|
variant: 'opacity' | 'highlight'
|
|
324
340
|
isPressable: boolean
|
|
325
341
|
disabled?: boolean
|
|
326
|
-
accessibilityRole?: AccessibilityRole
|
|
327
342
|
feedback?: boolean
|
|
328
343
|
webNativeButton?: boolean
|
|
329
344
|
}): {
|
|
330
345
|
props: Record<string, any>
|
|
331
346
|
pressableStyle?: StyleProp<ViewStyle>
|
|
332
|
-
|
|
333
|
-
deferredRole?: AccessibilityRole | string
|
|
347
|
+
deferredRole?: string
|
|
334
348
|
} {
|
|
335
349
|
let pressableStyle: StyleProp<ViewStyle> = {}
|
|
336
|
-
let deferredRole:
|
|
350
|
+
let deferredRole: string | undefined
|
|
337
351
|
const [hover, setHover] = useState(false)
|
|
338
352
|
const [active, setActive] = useState(false)
|
|
339
353
|
|
|
@@ -350,16 +364,14 @@ function useDecoratePressableProps ({
|
|
|
350
364
|
// decorate the element state (hover, active) only if it's pressable
|
|
351
365
|
if (!isPressable) return { props }
|
|
352
366
|
|
|
353
|
-
const resolvedRole = props.role ??
|
|
354
|
-
accessibilityRole ??= typeof resolvedRole === 'string' ? resolvedRole as AccessibilityRole : undefined
|
|
367
|
+
const resolvedRole = props.role ?? 'button'
|
|
355
368
|
props.focusable ??= true
|
|
356
369
|
|
|
357
370
|
if (isWeb && resolvedRole === 'button' && !webNativeButton) {
|
|
358
371
|
delete props.role
|
|
359
|
-
accessibilityRole = undefined
|
|
360
372
|
deferredRole = 'button'
|
|
361
373
|
} else {
|
|
362
|
-
props.role ??=
|
|
374
|
+
props.role ??= resolvedRole
|
|
363
375
|
}
|
|
364
376
|
|
|
365
377
|
// setup hover and active states styles and props
|
|
@@ -406,7 +418,7 @@ function useDecoratePressableProps ({
|
|
|
406
418
|
}
|
|
407
419
|
}
|
|
408
420
|
|
|
409
|
-
return { props, pressableStyle,
|
|
421
|
+
return { props, pressableStyle, deferredRole }
|
|
410
422
|
}
|
|
411
423
|
|
|
412
424
|
function getDefaultStyle (
|
|
@@ -446,8 +458,8 @@ function hasPressHandler (props: Record<string, any>): boolean {
|
|
|
446
458
|
return PRESSABLE_PROPS.some(prop => props[prop])
|
|
447
459
|
}
|
|
448
460
|
|
|
449
|
-
function reverseMarginPaddingSides (style:
|
|
450
|
-
style
|
|
461
|
+
function reverseMarginPaddingSides (style: ViewStyle | undefined): ViewStyle | undefined {
|
|
462
|
+
if (!style) return style
|
|
451
463
|
const { paddingLeft, paddingRight, marginLeft, marginRight } = style
|
|
452
464
|
style.marginLeft = marginRight
|
|
453
465
|
style.marginRight = marginLeft
|
|
@@ -456,6 +468,12 @@ function reverseMarginPaddingSides (style: StyleProp<ViewStyle>) {
|
|
|
456
468
|
return style
|
|
457
469
|
}
|
|
458
470
|
|
|
471
|
+
function simpleNumericHash (s: string): number {
|
|
472
|
+
let h = 0
|
|
473
|
+
for (let i = 0; i < s.length; i++) h = Math.imul(31, h) + s.charCodeAt(i) | 0
|
|
474
|
+
return h
|
|
475
|
+
}
|
|
476
|
+
|
|
459
477
|
function assertDeprecatedValues ({ pushed, renderTooltip }: { pushed?: any, renderTooltip?: any }) {
|
|
460
478
|
if (DEPRECATED_PUSHED_VALUES.includes(pushed)) console.warn(ERRORS.DEPRECATED_PUSHED(pushed))
|
|
461
479
|
if (renderTooltip) console.warn(ERRORS.DEPRECATED_RENDER_TOOLTIP)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startupjs-ui/div",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
"./useTooltip": "./useTooltip.tsx"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@startupjs-ui/abstract-popover": "^0.
|
|
16
|
-
"@startupjs-ui/core": "^0.
|
|
17
|
-
"@startupjs-ui/span": "^0.
|
|
15
|
+
"@startupjs-ui/abstract-popover": "^0.3.0",
|
|
16
|
+
"@startupjs-ui/core": "^0.3.0",
|
|
17
|
+
"@startupjs-ui/span": "^0.3.1"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"react": "*",
|
|
@@ -22,5 +22,5 @@
|
|
|
22
22
|
"react-native-reanimated": ">=4.0.0",
|
|
23
23
|
"startupjs": "*"
|
|
24
24
|
},
|
|
25
|
-
"gitHead": "
|
|
25
|
+
"gitHead": "60311773bdc83f354c797a272774304502d28c58"
|
|
26
26
|
}
|