panelui-native 0.83.0 → 0.84.0
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/lib/module/components/circular-text/circular-text-geometry.js +63 -0
- package/lib/module/components/circular-text/circular-text-geometry.js.map +1 -0
- package/lib/module/components/circular-text/index.js +146 -0
- package/lib/module/components/circular-text/index.js.map +1 -0
- package/lib/module/components/marquee/index.js +12 -5
- package/lib/module/components/marquee/index.js.map +1 -1
- package/lib/module/components/panelside/index.js +15 -417
- package/lib/module/components/panelside/index.js.map +1 -1
- package/lib/module/components/popover/index.js +119 -7
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/icons/index.js +24 -4
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js +28 -0
- package/lib/module/native/index.js.map +1 -1
- package/lib/typescript/src/components/circular-text/circular-text-geometry.d.ts +39 -0
- package/lib/typescript/src/components/circular-text/circular-text-geometry.d.ts.map +1 -0
- package/lib/typescript/src/components/circular-text/index.d.ts +69 -0
- package/lib/typescript/src/components/circular-text/index.d.ts.map +1 -0
- package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
- package/lib/typescript/src/components/panelside/index.d.ts +10 -202
- package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
- package/lib/typescript/src/components/popover/index.d.ts +25 -1
- package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +40 -0
- package/lib/typescript/src/native/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/circular-text/circular-text-geometry.ts +80 -0
- package/src/components/circular-text/index.tsx +217 -0
- package/src/components/marquee/index.tsx +12 -5
- package/src/components/panelside/index.tsx +16 -604
- package/src/components/popover/index.tsx +146 -4
- package/src/icons/index.tsx +23 -2
- package/src/index.ts +1 -6
- package/src/native/index.ts +55 -0
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* and jump into place.
|
|
31
31
|
*/
|
|
32
32
|
import {
|
|
33
|
+
Children,
|
|
33
34
|
cloneElement,
|
|
34
35
|
createContext,
|
|
35
36
|
isValidElement,
|
|
@@ -62,6 +63,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
|
62
63
|
import { FocusRestorePortal } from '../../primitives/portal';
|
|
63
64
|
import { Scrim } from '../../primitives/scrim';
|
|
64
65
|
import { useBackHandler } from '../../hooks/use-back-handler';
|
|
66
|
+
import { getSwiftUI } from '../../native';
|
|
65
67
|
import { BottomSheet } from '../bottom-sheet';
|
|
66
68
|
import { Text, type TextProps, textChildren } from '../../primitives/text';
|
|
67
69
|
import { cn } from '../../utils/cn';
|
|
@@ -116,6 +118,8 @@ interface PopoverContextValue {
|
|
|
116
118
|
setArrowOffset: (offset: number) => void;
|
|
117
119
|
/** Whether Content is presenting as a bottom sheet — Arrow is null then. */
|
|
118
120
|
presentation: PopoverPresentation;
|
|
121
|
+
/** Whether the platform is drawing the panel. Trigger and Content both read it. */
|
|
122
|
+
native: boolean;
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
export type PopoverPresentation = 'popover' | 'bottom-sheet';
|
|
@@ -176,6 +180,30 @@ export interface PopoverProps {
|
|
|
176
180
|
* sheet.
|
|
177
181
|
*/
|
|
178
182
|
presentation?: PopoverPresentation;
|
|
183
|
+
/**
|
|
184
|
+
* Present the platform's own popover instead of this one. Requires the
|
|
185
|
+
* optional `@expo/ui`.
|
|
186
|
+
*
|
|
187
|
+
* **iOS only.** SwiftUI has a popover that anchors to a view and keeps its
|
|
188
|
+
* anchored shape on a phone rather than becoming a sheet; Compose's nearest
|
|
189
|
+
* relative is a dropdown menu, which is a different control with different
|
|
190
|
+
* rules. Android and web keep the styled panel, as does an iOS device
|
|
191
|
+
* without `@expo/ui` installed.
|
|
192
|
+
*
|
|
193
|
+
* **The platform draws the container, so theme tokens do not reach it.** The
|
|
194
|
+
* panel's surface, its corner radius, its shadow and its arrow are the
|
|
195
|
+
* system's; `className` on `Popover.Content` styles what is *inside* it.
|
|
196
|
+
* `align`, `offset`, `alignOffset`, `scrim` and `blur` have no native
|
|
197
|
+
* equivalent and are ignored; `placement` becomes the edge the arrow is
|
|
198
|
+
* asked for.
|
|
199
|
+
*
|
|
200
|
+
* **Give the content a `width`.** The platform sizes its popover to what is
|
|
201
|
+
* hosted in it, and a React Native subtree with no width of its own has
|
|
202
|
+
* nothing to report — the same rule that governs every hosted view.
|
|
203
|
+
* `Popover.Content` defaults to a sensible one under `native`, but a panel
|
|
204
|
+
* whose rows need more room should say so.
|
|
205
|
+
*/
|
|
206
|
+
native?: boolean;
|
|
179
207
|
}
|
|
180
208
|
|
|
181
209
|
function PopoverRoot({
|
|
@@ -184,6 +212,7 @@ function PopoverRoot({
|
|
|
184
212
|
onOpenChange,
|
|
185
213
|
defaultOpen = false,
|
|
186
214
|
presentation = 'popover',
|
|
215
|
+
native = false,
|
|
187
216
|
}: PopoverProps) {
|
|
188
217
|
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
189
218
|
const [trigger, setTrigger] = useState<TriggerRect | null>(null);
|
|
@@ -201,6 +230,16 @@ function PopoverRoot({
|
|
|
201
230
|
[isControlled, onOpenChange]
|
|
202
231
|
);
|
|
203
232
|
|
|
233
|
+
/*
|
|
234
|
+
* The platform's popover, where it is reachable and the caller asked for it.
|
|
235
|
+
*
|
|
236
|
+
* A sheet presentation is left alone: `native` names which popover to draw,
|
|
237
|
+
* and asking for both a sheet and a popover is a contradiction rather than a
|
|
238
|
+
* combination.
|
|
239
|
+
*/
|
|
240
|
+
const swiftUI = native && presentation === 'popover' ? getSwiftUI() : null;
|
|
241
|
+
const nativeActive = swiftUI !== null;
|
|
242
|
+
|
|
204
243
|
const context = useMemo(
|
|
205
244
|
() => ({
|
|
206
245
|
open: resolvedOpen,
|
|
@@ -212,11 +251,95 @@ function PopoverRoot({
|
|
|
212
251
|
arrowOffset,
|
|
213
252
|
setArrowOffset,
|
|
214
253
|
presentation,
|
|
254
|
+
native: nativeActive,
|
|
215
255
|
}),
|
|
216
|
-
[resolvedOpen, setOpen, trigger, placement, arrowOffset, presentation]
|
|
256
|
+
[resolvedOpen, setOpen, trigger, placement, arrowOffset, presentation, nativeActive]
|
|
217
257
|
);
|
|
218
258
|
|
|
219
|
-
return
|
|
259
|
+
return (
|
|
260
|
+
<PopoverContext.Provider value={context}>
|
|
261
|
+
{swiftUI ? <NativePopover swiftUI={swiftUI}>{children}</NativePopover> : children}
|
|
262
|
+
</PopoverContext.Provider>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Default width for a hosted panel, in points. Room for a short menu row. */
|
|
267
|
+
const NATIVE_PANEL_WIDTH = 240;
|
|
268
|
+
|
|
269
|
+
/** `placement` in the platform's vocabulary. */
|
|
270
|
+
const ARROW_EDGE = {
|
|
271
|
+
top: 'top',
|
|
272
|
+
bottom: 'bottom',
|
|
273
|
+
left: 'leading',
|
|
274
|
+
right: 'trailing',
|
|
275
|
+
} as const;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The platform's popover, with our trigger and our content hosted inside it.
|
|
279
|
+
*
|
|
280
|
+
* SwiftUI attaches a popover to a view, so the two halves this component keeps
|
|
281
|
+
* as siblings have to become parent and child: the trigger is what the panel
|
|
282
|
+
* points at, and the platform will not anchor to something it cannot see. So
|
|
283
|
+
* the children are read here and placed into the two slots the platform
|
|
284
|
+
* expects, rather than the caller having to write a different tree under
|
|
285
|
+
* `native` than without it.
|
|
286
|
+
*
|
|
287
|
+
* Both halves are React Native, so both are wrapped in the host view that lets
|
|
288
|
+
* the platform measure them. The trigger is given no press handling of its
|
|
289
|
+
* own — `Popover.Trigger` already toggles the state this reads, and the
|
|
290
|
+
* platform presents from that.
|
|
291
|
+
*/
|
|
292
|
+
function NativePopover({
|
|
293
|
+
swiftUI,
|
|
294
|
+
children,
|
|
295
|
+
}: {
|
|
296
|
+
swiftUI: NonNullable<ReturnType<typeof getSwiftUI>>;
|
|
297
|
+
children: ReactNode;
|
|
298
|
+
}) {
|
|
299
|
+
const { open, setOpen } = usePopover('Popover');
|
|
300
|
+
const { Host, RNHostView, Popover: PlatformPopover } = swiftUI;
|
|
301
|
+
|
|
302
|
+
let trigger: ReactNode = null;
|
|
303
|
+
let content: ReactElement<PopoverContentProps> | null = null;
|
|
304
|
+
|
|
305
|
+
for (const child of Children.toArray(children)) {
|
|
306
|
+
if (!isValidElement(child)) continue;
|
|
307
|
+
if (child.type === PopoverTrigger) trigger = child;
|
|
308
|
+
else if (child.type === PopoverContent) {
|
|
309
|
+
content = child as ReactElement<PopoverContentProps>;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const contentProps = content?.props;
|
|
314
|
+
const width =
|
|
315
|
+
typeof contentProps?.width === 'number' ? contentProps.width : NATIVE_PANEL_WIDTH;
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<Host matchContents ignoreSafeArea="keyboard">
|
|
319
|
+
<PlatformPopover
|
|
320
|
+
isPresented={open}
|
|
321
|
+
onIsPresentedChange={setOpen}
|
|
322
|
+
arrowEdge={ARROW_EDGE[contentProps?.placement ?? 'bottom']}
|
|
323
|
+
>
|
|
324
|
+
<PlatformPopover.Trigger>
|
|
325
|
+
<RNHostView matchContents>{trigger}</RNHostView>
|
|
326
|
+
</PlatformPopover.Trigger>
|
|
327
|
+
<PlatformPopover.Content>
|
|
328
|
+
<RNHostView matchContents>
|
|
329
|
+
{/*
|
|
330
|
+
An explicit width, not a class. Inside the host there is no
|
|
331
|
+
parent for a percentage or a flex basis to resolve against, so a
|
|
332
|
+
panel that does not state its width reports none and the platform
|
|
333
|
+
sizes its popover to nothing.
|
|
334
|
+
*/}
|
|
335
|
+
<View style={{ width }} className={cn('gap-1 p-2', contentProps?.className)}>
|
|
336
|
+
{textChildren(contentProps?.children)}
|
|
337
|
+
</View>
|
|
338
|
+
</RNHostView>
|
|
339
|
+
</PlatformPopover.Content>
|
|
340
|
+
</PlatformPopover>
|
|
341
|
+
</Host>
|
|
342
|
+
);
|
|
220
343
|
}
|
|
221
344
|
|
|
222
345
|
export interface PopoverTriggerProps {
|
|
@@ -231,7 +354,7 @@ export interface PopoverTriggerProps {
|
|
|
231
354
|
* and only a wrapper we own is guaranteed to be measurable.
|
|
232
355
|
*/
|
|
233
356
|
function PopoverTrigger({ children }: PopoverTriggerProps) {
|
|
234
|
-
const { open, setOpen, setTrigger } = usePopover('Popover.Trigger');
|
|
357
|
+
const { open, setOpen, setTrigger, native } = usePopover('Popover.Trigger');
|
|
235
358
|
const ref = useRef<View>(null);
|
|
236
359
|
|
|
237
360
|
const measureThenToggle = (...args: unknown[]) => {
|
|
@@ -242,6 +365,17 @@ function PopoverTrigger({ children }: PopoverTriggerProps) {
|
|
|
242
365
|
return;
|
|
243
366
|
}
|
|
244
367
|
|
|
368
|
+
/*
|
|
369
|
+
* Nothing to measure when the platform is drawing the panel: it anchors to
|
|
370
|
+
* this trigger itself, and asking a view hosted inside the native tree for
|
|
371
|
+
* its window coordinates is a callback that may never come back — which
|
|
372
|
+
* would leave the press doing nothing at all.
|
|
373
|
+
*/
|
|
374
|
+
if (native) {
|
|
375
|
+
setOpen(true);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
245
379
|
// Measured on every open rather than on layout: the trigger may have
|
|
246
380
|
// scrolled since it was laid out, and a stale rect anchors the panel to
|
|
247
381
|
// where the trigger used to be.
|
|
@@ -363,7 +497,8 @@ function PopoverContent({
|
|
|
363
497
|
...props
|
|
364
498
|
}: PopoverContentProps) {
|
|
365
499
|
const context = usePopover('Popover.Content');
|
|
366
|
-
const { open, setOpen, trigger, setPlacement, setArrowOffset, presentation } =
|
|
500
|
+
const { open, setOpen, trigger, setPlacement, setArrowOffset, presentation, native } =
|
|
501
|
+
context;
|
|
367
502
|
|
|
368
503
|
// The anchored panel owns the Android back button while it is up. The sheet
|
|
369
504
|
// presentation is left alone — BottomSheet installs its own handler.
|
|
@@ -509,6 +644,13 @@ function PopoverContent({
|
|
|
509
644
|
* the surface it is merged into the sheet's own padding classes and replaces
|
|
510
645
|
* them, so a panel asking for `p-3` silently strips the sheet's `px-5 pt-2`.
|
|
511
646
|
*/
|
|
647
|
+
/*
|
|
648
|
+
* Already drawn. Under `native` the root reads this element's props and
|
|
649
|
+
* hosts its children inside the platform's own popover, so rendering the
|
|
650
|
+
* styled panel here as well would put a second one on the screen.
|
|
651
|
+
*/
|
|
652
|
+
if (native) return null;
|
|
653
|
+
|
|
512
654
|
if (presentation === 'bottom-sheet') {
|
|
513
655
|
return (
|
|
514
656
|
<BottomSheet open={open} onOpenChange={setOpen}>
|
package/src/icons/index.tsx
CHANGED
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
* outline could only ignore. All four are drawn here rather than mapped.
|
|
51
51
|
*/
|
|
52
52
|
import { createContext, useContext, type ReactNode } from 'react';
|
|
53
|
+
import { View } from 'react-native';
|
|
53
54
|
import { HugeiconsIcon, type IconSvgElement } from '@hugeicons/react-native';
|
|
54
55
|
import Svg, { G, Path, type SvgProps } from 'react-native-svg';
|
|
55
56
|
import { useDirection } from '../hooks/use-direction';
|
|
@@ -208,17 +209,37 @@ function icon(glyph: IconSvgElement, defaults: IconDefaults) {
|
|
|
208
209
|
}: ToggleIconProps) {
|
|
209
210
|
const resolved = useResolvedColor(color, defaults.color);
|
|
210
211
|
const rtl = useDirection() === 'rtl';
|
|
211
|
-
|
|
212
|
+
|
|
213
|
+
const drawing = (
|
|
212
214
|
<HugeiconsIcon
|
|
213
215
|
icon={glyph}
|
|
214
216
|
size={size}
|
|
215
217
|
color={resolved}
|
|
216
218
|
strokeWidth={strokeWidth ?? defaults.strokeWidth ?? STROKE}
|
|
217
219
|
fill={defaults.fillable && filled ? resolved : 'none'}
|
|
218
|
-
style={defaults.flip ?
|
|
220
|
+
style={defaults.flip ? undefined : style}
|
|
219
221
|
{...props}
|
|
220
222
|
/>
|
|
221
223
|
);
|
|
224
|
+
|
|
225
|
+
if (!defaults.flip) return drawing;
|
|
226
|
+
|
|
227
|
+
/*
|
|
228
|
+
* The mirror goes on a view around the glyph, not on the glyph.
|
|
229
|
+
*
|
|
230
|
+
* The drawing component takes a `style` prop and drops it: it destructures
|
|
231
|
+
* `style` out of its props and never puts it back, and the interop layer
|
|
232
|
+
* that would otherwise have carried it is a package this library does not
|
|
233
|
+
* use. A transform handed to it is silently discarded, which is a chevron
|
|
234
|
+
* that keeps pointing right in a right-to-left row — and a failure with
|
|
235
|
+
* nothing to see, since every other prop on the same element does arrive.
|
|
236
|
+
*
|
|
237
|
+
* A plain view cannot lose it. It also puts the caller's own `style` back
|
|
238
|
+
* on the outside, where it is applied rather than thrown away.
|
|
239
|
+
*/
|
|
240
|
+
return (
|
|
241
|
+
<View style={[{ transform: [{ scaleX: rtl ? -1 : 1 }] }, style]}>{drawing}</View>
|
|
242
|
+
);
|
|
222
243
|
}
|
|
223
244
|
|
|
224
245
|
return Icon;
|
package/src/index.ts
CHANGED
|
@@ -421,6 +421,7 @@ export {
|
|
|
421
421
|
type ChipVariant,
|
|
422
422
|
type ChipSize,
|
|
423
423
|
} from './components/chip';
|
|
424
|
+
export { CircularText, type CircularTextProps } from './components/circular-text';
|
|
424
425
|
export {
|
|
425
426
|
Collapsible,
|
|
426
427
|
type CollapsibleProps,
|
|
@@ -557,12 +558,6 @@ export {
|
|
|
557
558
|
type PanelsidePageProps,
|
|
558
559
|
type PanelsideTriggerProps,
|
|
559
560
|
type PanelsideSearchTriggerProps,
|
|
560
|
-
type PanelsideSearchSheetProps,
|
|
561
|
-
type PanelsideSearchTabsProps,
|
|
562
|
-
type PanelsideSearchTabProps,
|
|
563
|
-
type PanelsideSearchResultsProps,
|
|
564
|
-
type PanelsideSearchResultProps,
|
|
565
|
-
type PanelsideSearchFieldProps,
|
|
566
561
|
type PanelsideMode,
|
|
567
562
|
type PanelsideSwipeFrom,
|
|
568
563
|
type PanelsideItemSize,
|
package/src/native/index.ts
CHANGED
|
@@ -161,6 +161,61 @@ interface SwiftUIModifiers {
|
|
|
161
161
|
presentationBackground: (color: string) => unknown;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/**
|
|
165
|
+
* The SwiftUI-only components, for the ones the universal set does not carry.
|
|
166
|
+
*
|
|
167
|
+
* `@expo/ui` exports a portable component for everything both toolkits agree
|
|
168
|
+
* on, and that is what `getNativeUI` reaches. A popover is not on that list:
|
|
169
|
+
* SwiftUI has one that anchors to a view and adapts itself on a compact
|
|
170
|
+
* screen, and Compose's nearest relative is a dropdown menu, which is a
|
|
171
|
+
* different control with different rules. Rather than pretend the two are one
|
|
172
|
+
* thing, the iOS one is reached here and Android keeps the styled panel.
|
|
173
|
+
*/
|
|
174
|
+
interface SwiftUIComponents {
|
|
175
|
+
Host: ComponentType<{
|
|
176
|
+
children?: ReactNode;
|
|
177
|
+
matchContents?: boolean | { vertical?: boolean; horizontal?: boolean };
|
|
178
|
+
ignoreSafeArea?: unknown;
|
|
179
|
+
style?: unknown;
|
|
180
|
+
}>;
|
|
181
|
+
RNHostView: ComponentType<{ children?: ReactNode; matchContents?: boolean }>;
|
|
182
|
+
Popover: ComponentType<{
|
|
183
|
+
children?: ReactNode;
|
|
184
|
+
isPresented?: boolean;
|
|
185
|
+
onIsPresentedChange?: (isPresented: boolean) => void;
|
|
186
|
+
attachmentAnchor?: 'leading' | 'trailing' | 'center' | 'top' | 'bottom';
|
|
187
|
+
arrowEdge?: 'leading' | 'trailing' | 'top' | 'bottom' | 'none';
|
|
188
|
+
}> & {
|
|
189
|
+
Trigger: ComponentType<{ children?: ReactNode }>;
|
|
190
|
+
Content: ComponentType<{ children?: ReactNode }>;
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let swiftUIResolved = false;
|
|
195
|
+
let swiftUI: SwiftUIComponents | null = null;
|
|
196
|
+
|
|
197
|
+
export function getSwiftUI(): SwiftUIComponents | null {
|
|
198
|
+
if (swiftUIResolved) return swiftUI;
|
|
199
|
+
swiftUIResolved = true;
|
|
200
|
+
|
|
201
|
+
if (Platform.OS !== 'ios') return null;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
205
|
+
const module = require('@expo/ui/swift-ui') as Partial<SwiftUIComponents>;
|
|
206
|
+
// A version without the popover is not an error; it is a fallback to the
|
|
207
|
+
// styled panel, which is what every missing native path here does.
|
|
208
|
+
swiftUI =
|
|
209
|
+
module.Host && module.RNHostView && module.Popover
|
|
210
|
+
? (module as SwiftUIComponents)
|
|
211
|
+
: null;
|
|
212
|
+
} catch {
|
|
213
|
+
swiftUI = null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return swiftUI;
|
|
217
|
+
}
|
|
218
|
+
|
|
164
219
|
let modifiersResolved = false;
|
|
165
220
|
let modifiers: SwiftUIModifiers | null = null;
|
|
166
221
|
|