@kennycorrea/ui 0.9.1 → 0.11.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/CHANGELOG.md +120 -14
- package/README.md +204 -273
- package/dist/atoms/ActionSheet/ActionSheet.d.ts +30 -0
- package/dist/atoms/ActionSheet/ActionSheet.js +42 -0
- package/dist/atoms/ActionSheet/confirm.d.ts +21 -0
- package/dist/atoms/ActionSheet/confirm.js +65 -0
- package/dist/atoms/ActionSheet/index.d.ts +2 -0
- package/dist/atoms/ActionSheet/index.js +2 -0
- package/dist/atoms/Button/Button.js +3 -3
- package/dist/atoms/Button/Button.types.d.ts +7 -1
- package/dist/atoms/Card/Card.d.ts +28 -7
- package/dist/atoms/Card/Card.js +45 -9
- package/dist/atoms/ChipGroup/ChipGroup.d.ts +31 -0
- package/dist/atoms/ChipGroup/ChipGroup.js +69 -0
- package/dist/atoms/ChipGroup/index.d.ts +1 -0
- package/dist/atoms/ChipGroup/index.js +1 -0
- package/dist/atoms/Drawer/Drawer.d.ts +1 -1
- package/dist/atoms/Drawer/Drawer.js +9 -13
- package/dist/atoms/Input/Input.js +7 -1
- package/dist/atoms/List/List.d.ts +73 -0
- package/dist/atoms/List/List.js +88 -0
- package/dist/atoms/List/index.d.ts +1 -0
- package/dist/atoms/List/index.js +1 -0
- package/dist/atoms/List/swipe.d.ts +68 -0
- package/dist/atoms/List/swipe.js +207 -0
- package/dist/atoms/NavBar/BackButton.d.ts +10 -0
- package/dist/atoms/NavBar/BackButton.js +11 -0
- package/dist/atoms/NavBar/NavBar.d.ts +22 -0
- package/dist/atoms/NavBar/NavBar.js +29 -0
- package/dist/atoms/NavBar/index.d.ts +2 -0
- package/dist/atoms/NavBar/index.js +2 -0
- package/dist/atoms/NotificationCard/NotificationCard.d.ts +52 -0
- package/dist/atoms/NotificationCard/NotificationCard.js +78 -0
- package/dist/atoms/NotificationCard/index.d.ts +1 -0
- package/dist/atoms/NotificationCard/index.js +1 -0
- package/dist/atoms/PullToRefresh/PullToRefresh.d.ts +31 -0
- package/dist/atoms/PullToRefresh/PullToRefresh.js +110 -0
- package/dist/atoms/PullToRefresh/index.d.ts +1 -0
- package/dist/atoms/PullToRefresh/index.js +1 -0
- package/dist/atoms/ResponsiveDialog/ResponsiveDialog.d.ts +13 -0
- package/dist/atoms/ResponsiveDialog/ResponsiveDialog.js +27 -0
- package/dist/atoms/ResponsiveDialog/index.d.ts +1 -0
- package/dist/atoms/ResponsiveDialog/index.js +1 -0
- package/dist/atoms/ScrollStrip/ScrollStrip.d.ts +17 -0
- package/dist/atoms/ScrollStrip/ScrollStrip.js +15 -0
- package/dist/atoms/ScrollStrip/index.d.ts +1 -0
- package/dist/atoms/ScrollStrip/index.js +1 -0
- package/dist/atoms/SegmentedControl/SegmentedControl.d.ts +23 -0
- package/dist/atoms/SegmentedControl/SegmentedControl.js +41 -0
- package/dist/atoms/SegmentedControl/index.d.ts +1 -0
- package/dist/atoms/SegmentedControl/index.js +1 -0
- package/dist/atoms/Select/Select.js +1 -1
- package/dist/atoms/Sheet/Sheet.d.ts +63 -0
- package/dist/atoms/Sheet/Sheet.js +261 -0
- package/dist/atoms/Sheet/index.d.ts +1 -0
- package/dist/atoms/Sheet/index.js +1 -0
- package/dist/atoms/StatTile/StatTile.d.ts +36 -0
- package/dist/atoms/StatTile/StatTile.js +17 -0
- package/dist/atoms/StatTile/index.d.ts +1 -0
- package/dist/atoms/StatTile/index.js +1 -0
- package/dist/atoms/TabBar/TabBar.d.ts +42 -0
- package/dist/atoms/TabBar/TabBar.js +36 -0
- package/dist/atoms/TabBar/index.d.ts +1 -0
- package/dist/atoms/TabBar/index.js +1 -0
- package/dist/atoms/Textarea/Textarea.d.ts +4 -0
- package/dist/atoms/Textarea/Textarea.js +1 -1
- package/dist/atoms/Toast/Toast.d.ts +8 -2
- package/dist/atoms/Toast/Toast.js +19 -4
- package/dist/atoms/index.d.ts +12 -0
- package/dist/atoms/index.js +12 -0
- package/dist/primitives/index.d.ts +1 -1
- package/dist/primitives/index.js +1 -1
- package/dist/strings.d.ts +13 -3
- package/dist/strings.js +5 -0
- package/dist/utils/themePortal.d.ts +19 -0
- package/dist/utils/themePortal.js +28 -0
- package/package.json +4 -2
- package/tokens.css +454 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronRightIcon } from '@radix-ui/react-icons';
|
|
3
|
+
import { Skeleton as ThemedSkeleton } from '@radix-ui/themes';
|
|
4
|
+
import { createContext, useContext, useId, useState, } from 'react';
|
|
5
|
+
import { Slot } from '../../primitives/index.js';
|
|
6
|
+
import { useUiStrings } from '../../strings.js';
|
|
7
|
+
import { cn } from '../../utils/cn.js';
|
|
8
|
+
import { ContextMenu } from '../ContextMenu/index.js';
|
|
9
|
+
import { OpenRowContext, OverflowMenu, SwipeTray, useOpenRowState, useSwipeEnabled, useSwipeRow, } from './swipe.js';
|
|
10
|
+
const VariantContext = createContext('inset');
|
|
11
|
+
/**
|
|
12
|
+
* A native-style grouped list: rows on one grouped surface with hairlines between them.
|
|
13
|
+
*
|
|
14
|
+
* The list is a real `<ul role="list">` (the explicit role keeps VoiceOver announcing it once
|
|
15
|
+
* list-style is removed) and `ListRow` supplies the `<li>`. Header and footer name and describe
|
|
16
|
+
* the list, so a screen reader hears them with it.
|
|
17
|
+
*/
|
|
18
|
+
export function ListSection({ header, footer, variant = 'inset', className, children, ...props }) {
|
|
19
|
+
const id = useId();
|
|
20
|
+
const [openRowId, setOpenRowId] = useState(null);
|
|
21
|
+
const headerId = `${id}-h`;
|
|
22
|
+
const footerId = `${id}-f`;
|
|
23
|
+
return (_jsx(VariantContext.Provider, { value: variant, children: _jsx(OpenRowContext.Provider, { value: { openId: openRowId, setOpenId: setOpenRowId }, children: _jsxs("div", { className: cn('ui-list-section', className), "data-variant": variant, ...props, children: [header != null && (_jsx("div", { id: headerId, className: "ui-list-header", children: header })), _jsx("ul", {
|
|
24
|
+
// biome-ignore lint/a11y/noRedundantRoles: Safari drops list semantics once list-style is none
|
|
25
|
+
role: "list", className: "ui-list", "aria-labelledby": header != null ? headerId : undefined, "aria-describedby": footer != null ? footerId : undefined, children: children }), footer != null && (_jsx("div", { id: footerId, className: "ui-list-footer", children: footer }))] }) }) }));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* One row. A `button` with `onClick`, an `a` with `href`, whatever you pass with `asChild`,
|
|
29
|
+
* and a plain `div` when it does nothing — so a row that is not interactive is not announced
|
|
30
|
+
* as if it were. A row holding a switch is such a row: the switch is the control.
|
|
31
|
+
*/
|
|
32
|
+
export function ListRow(props) {
|
|
33
|
+
const { leading, title, subtitle, trailing, chevron = false, disabled = false, destructive = false, asChild = false, className, swipeActions, fullSwipe = false, ...rest } = props;
|
|
34
|
+
const rowId = useId();
|
|
35
|
+
const { moreActions } = useUiStrings();
|
|
36
|
+
const swipeEnabled = useSwipeEnabled();
|
|
37
|
+
const openState = useOpenRowState();
|
|
38
|
+
const sw = useSwipeRow({
|
|
39
|
+
id: rowId,
|
|
40
|
+
enabled: swipeEnabled && !disabled,
|
|
41
|
+
actions: swipeActions ?? {},
|
|
42
|
+
fullSwipe,
|
|
43
|
+
open: openState,
|
|
44
|
+
});
|
|
45
|
+
const variant = useContext(VariantContext);
|
|
46
|
+
const interactive = asChild || props.href !== undefined || props.onClick !== undefined;
|
|
47
|
+
const body = (_jsxs(_Fragment, { children: [leading != null && _jsx("span", { className: "ui-list-lead", children: leading }), _jsxs("span", { className: "ui-list-text", children: [_jsx("span", { className: "ui-list-title", children: title }), subtitle != null && _jsx("span", { className: "ui-list-subtitle", children: subtitle })] }), trailing != null && _jsx("span", { className: "ui-list-trailing", children: trailing }), chevron && _jsx(ChevronRightIcon, { className: "ui-list-chevron", "aria-hidden": true })] }));
|
|
48
|
+
const shared = {
|
|
49
|
+
className: 'ui-list-row',
|
|
50
|
+
'data-destructive': destructive || undefined,
|
|
51
|
+
'data-disabled': disabled || undefined,
|
|
52
|
+
'data-interactive': interactive || undefined,
|
|
53
|
+
};
|
|
54
|
+
let row;
|
|
55
|
+
if (asChild) {
|
|
56
|
+
// The child's own content is kept; the slot only lends it the row's behaviour and look.
|
|
57
|
+
const { children, ...slotProps } = rest;
|
|
58
|
+
row = (_jsx(Slot, { ...shared, ...slotProps, "aria-disabled": disabled || undefined, tabIndex: disabled ? -1 : undefined, children: children }));
|
|
59
|
+
}
|
|
60
|
+
else if (props.href !== undefined) {
|
|
61
|
+
const { href, onClick, ...a } = rest;
|
|
62
|
+
row = (_jsx("a", { ...shared, ...a,
|
|
63
|
+
// A disabled link has no destination; `aria-disabled` alone would still navigate.
|
|
64
|
+
href: disabled ? undefined : href, onClick: disabled ? undefined : onClick, "aria-disabled": disabled || undefined, tabIndex: disabled ? -1 : undefined, children: body }));
|
|
65
|
+
}
|
|
66
|
+
else if (interactive) {
|
|
67
|
+
row = (_jsx("button", { type: "button", ...shared, ...rest, disabled: disabled, children: body }));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
row = (_jsx("div", { ...shared, ...rest, "aria-disabled": disabled || undefined, children: body }));
|
|
71
|
+
}
|
|
72
|
+
// Trailing first: it is the primary side, and the menus read top to bottom.
|
|
73
|
+
const all = [...(swipeActions?.trailing ?? []), ...(swipeActions?.leading ?? [])];
|
|
74
|
+
if (all.length === 0 || disabled) {
|
|
75
|
+
return (_jsx("li", { className: cn('ui-list-item', className), "data-variant": variant, "data-has-lead": leading != null || undefined, children: row }));
|
|
76
|
+
}
|
|
77
|
+
const surface = (_jsxs("div", { className: "ui-list-surface", "data-dragging": sw.dragging || undefined, "data-swipe": swipeEnabled || undefined, style: { transform: sw.offset ? `translateX(${sw.offset}px)` : undefined }, ...sw.surfaceProps, children: [row, _jsx(OverflowMenu, { label: moreActions, actions: all, hidden: swipeEnabled })] }));
|
|
78
|
+
return (_jsxs("li", { ref: sw.root, className: cn('ui-list-item', 'ui-list-item-swipe', className), "data-variant": variant, "data-has-lead": leading != null || undefined, children: [swipeEnabled && swipeActions?.leading?.length ? (_jsx(SwipeTray, { side: "leading", actions: swipeActions.leading, offset: sw.offset, active: sw.isOpen && sw.openSide === 'leading', onDone: sw.close })) : null, swipeEnabled && swipeActions?.trailing?.length ? (_jsx(SwipeTray, { side: "trailing", actions: swipeActions.trailing, offset: sw.offset, active: sw.isOpen && sw.openSide === 'trailing', onDone: sw.close })) : null, _jsx(ContextMenu, { items: all.map((a, index) => ({
|
|
79
|
+
id: String(index),
|
|
80
|
+
label: a.label,
|
|
81
|
+
destructive: a.tone === 'danger',
|
|
82
|
+
})), onSelect: (key) => all[Number(key)]?.onAction(), children: surface })] }));
|
|
83
|
+
}
|
|
84
|
+
/** A row-shaped placeholder; announces "Loading" once through the strings provider. */
|
|
85
|
+
export function ListRowSkeleton({ leading = true, subtitle = false, className, }) {
|
|
86
|
+
const { loading } = useUiStrings();
|
|
87
|
+
return (_jsx("li", { className: cn('ui-list-item', className), "data-has-lead": leading || undefined, "aria-busy": "true", children: _jsxs("div", { className: "ui-list-row", children: [_jsx("span", { className: "ui-list-sr", children: loading }), _jsxs("span", { "aria-hidden": true, className: "ui-list-skeleton", children: [leading && (_jsx("span", { className: "ui-list-lead", children: _jsx(ThemedSkeleton, { width: "100%", height: "100%", style: { borderRadius: 8 } }) })), _jsxs("span", { className: "ui-list-text", children: [_jsx(ThemedSkeleton, { height: "0.875rem", width: "55%" }), subtitle && _jsx(ThemedSkeleton, { height: "0.75rem", width: "35%" })] })] })] }) }));
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './List.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './List.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type ReactNode, type PointerEvent as ReactPointerEvent } from 'react';
|
|
2
|
+
export type RowActionTone = 'neutral' | 'brand' | 'danger';
|
|
3
|
+
export interface RowAction {
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: ReactNode;
|
|
6
|
+
/** `danger` is red in the swipe tray and destructive in the menus. Default `neutral`. */
|
|
7
|
+
tone?: RowActionTone;
|
|
8
|
+
onAction: () => void;
|
|
9
|
+
}
|
|
10
|
+
export interface SwipeActions {
|
|
11
|
+
/** Revealed by dragging the row towards the end (right in LTR). */
|
|
12
|
+
leading?: RowAction[];
|
|
13
|
+
/** Revealed by dragging the row towards the start (left in LTR). */
|
|
14
|
+
trailing?: RowAction[];
|
|
15
|
+
}
|
|
16
|
+
/** Width of one revealed action button, in px. */
|
|
17
|
+
export declare const ACTION_W = 72;
|
|
18
|
+
/**
|
|
19
|
+
* Whether the swipe gesture is offered: a touch device and no request for reduced motion.
|
|
20
|
+
* Everywhere else the visible overflow button is the way in. False on the server, so the
|
|
21
|
+
* button is what renders before hydration decides.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useSwipeEnabled(): boolean;
|
|
24
|
+
/** One row open at a time per list: the list holds which. */
|
|
25
|
+
export interface OpenRowContextValue {
|
|
26
|
+
openId: string | null;
|
|
27
|
+
setOpenId: (id: string | null) => void;
|
|
28
|
+
}
|
|
29
|
+
export declare const OpenRowContext: import("react").Context<OpenRowContextValue | null>;
|
|
30
|
+
export declare function useOpenRowState(): OpenRowContextValue;
|
|
31
|
+
interface UseSwipeArgs {
|
|
32
|
+
id: string;
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
actions: SwipeActions;
|
|
35
|
+
fullSwipe: boolean;
|
|
36
|
+
open: OpenRowContextValue;
|
|
37
|
+
}
|
|
38
|
+
export declare function useSwipeRow({ id, enabled, actions, fullSwipe, open }: UseSwipeArgs): {
|
|
39
|
+
root: import("react").RefObject<HTMLLIElement | null>;
|
|
40
|
+
offset: number;
|
|
41
|
+
dragging: boolean;
|
|
42
|
+
isOpen: boolean;
|
|
43
|
+
openSide: "leading" | "trailing" | null;
|
|
44
|
+
close: () => void;
|
|
45
|
+
surfaceProps: {
|
|
46
|
+
onPointerDown: (e: ReactPointerEvent<HTMLElement>) => void;
|
|
47
|
+
onPointerMove: (e: ReactPointerEvent<HTMLElement>) => void;
|
|
48
|
+
onPointerUp: (e: ReactPointerEvent<HTMLElement>) => void;
|
|
49
|
+
onPointerCancel: (e: ReactPointerEvent<HTMLElement>) => void;
|
|
50
|
+
onClickCapture: (e: React.MouseEvent) => void;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
/** The revealed buttons of one side. Inert unless that side is open. */
|
|
54
|
+
export declare function SwipeTray({ side, actions, offset, active, onDone, }: {
|
|
55
|
+
side: 'leading' | 'trailing';
|
|
56
|
+
actions: RowAction[];
|
|
57
|
+
offset: number;
|
|
58
|
+
active: boolean;
|
|
59
|
+
onDone: () => void;
|
|
60
|
+
}): import("react").JSX.Element;
|
|
61
|
+
/** The same actions as a visible menu button; the way in when the gesture is not offered. */
|
|
62
|
+
export declare function OverflowMenu({ label, actions, hidden, }: {
|
|
63
|
+
label: string;
|
|
64
|
+
actions: RowAction[];
|
|
65
|
+
/** Visually hidden until focused; still in the tab order for keyboards and screen readers. */
|
|
66
|
+
hidden: boolean;
|
|
67
|
+
}): import("react").JSX.Element;
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DotsHorizontalIcon } from '@radix-ui/react-icons';
|
|
3
|
+
import { DropdownMenu as ThemedDropdownMenu } from '@radix-ui/themes';
|
|
4
|
+
import { createContext, Fragment, useCallback, useContext, useEffect, useRef, useState, useSyncExternalStore, } from 'react';
|
|
5
|
+
/** Width of one revealed action button, in px. */
|
|
6
|
+
export const ACTION_W = 72;
|
|
7
|
+
const AXIS_LOCK = 8;
|
|
8
|
+
const FULL_SWIPE_RATIO = 0.6;
|
|
9
|
+
/** iOS-style resistance: the further past the limit, the less the row follows the finger. */
|
|
10
|
+
function rubber(excess, limit) {
|
|
11
|
+
const d = Math.max(limit, 1);
|
|
12
|
+
return (1 - 1 / ((excess * 0.55) / d + 1)) * d;
|
|
13
|
+
}
|
|
14
|
+
function subscribeMedia(onChange) {
|
|
15
|
+
const lists = [
|
|
16
|
+
window.matchMedia('(prefers-reduced-motion: reduce)'),
|
|
17
|
+
window.matchMedia('(pointer: coarse)'),
|
|
18
|
+
];
|
|
19
|
+
for (const l of lists)
|
|
20
|
+
l.addEventListener?.('change', onChange);
|
|
21
|
+
return () => {
|
|
22
|
+
for (const l of lists)
|
|
23
|
+
l.removeEventListener?.('change', onChange);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function swipeSnapshot() {
|
|
27
|
+
if (typeof window === 'undefined' || !window.matchMedia)
|
|
28
|
+
return false;
|
|
29
|
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches)
|
|
30
|
+
return false;
|
|
31
|
+
const touch = window.matchMedia('(pointer: coarse)').matches ||
|
|
32
|
+
(typeof navigator !== 'undefined' && navigator.maxTouchPoints > 0);
|
|
33
|
+
return touch;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether the swipe gesture is offered: a touch device and no request for reduced motion.
|
|
37
|
+
* Everywhere else the visible overflow button is the way in. False on the server, so the
|
|
38
|
+
* button is what renders before hydration decides.
|
|
39
|
+
*/
|
|
40
|
+
export function useSwipeEnabled() {
|
|
41
|
+
return useSyncExternalStore((cb) => (typeof window === 'undefined' || !window.matchMedia ? () => { } : subscribeMedia(cb)), swipeSnapshot, () => false);
|
|
42
|
+
}
|
|
43
|
+
export const OpenRowContext = createContext(null);
|
|
44
|
+
export function useOpenRowState() {
|
|
45
|
+
const ctx = useContext(OpenRowContext);
|
|
46
|
+
const [local, setLocal] = useState(null);
|
|
47
|
+
return ctx ?? { openId: local, setOpenId: setLocal };
|
|
48
|
+
}
|
|
49
|
+
export function useSwipeRow({ id, enabled, actions, fullSwipe, open }) {
|
|
50
|
+
const leadingW = (actions.leading?.length ?? 0) * ACTION_W;
|
|
51
|
+
const trailingW = (actions.trailing?.length ?? 0) * ACTION_W;
|
|
52
|
+
const [side, setSide] = useState(null);
|
|
53
|
+
const [drag, setDrag] = useState(null);
|
|
54
|
+
const isOpen = side !== null && open.openId === id;
|
|
55
|
+
const rest = !isOpen ? 0 : side === 'leading' ? leadingW : -trailingW;
|
|
56
|
+
const root = useRef(null);
|
|
57
|
+
const track = useRef(null);
|
|
58
|
+
const swallowClick = useRef(false);
|
|
59
|
+
const close = useCallback(() => {
|
|
60
|
+
setSide(null);
|
|
61
|
+
setDrag(null);
|
|
62
|
+
if (open.openId === id)
|
|
63
|
+
open.setOpenId(null);
|
|
64
|
+
}, [id, open]);
|
|
65
|
+
// Tapping elsewhere, or Escape, closes an open row.
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!isOpen)
|
|
68
|
+
return;
|
|
69
|
+
const down = (e) => {
|
|
70
|
+
if (!root.current?.contains(e.target))
|
|
71
|
+
close();
|
|
72
|
+
};
|
|
73
|
+
const key = (e) => {
|
|
74
|
+
if (e.key === 'Escape')
|
|
75
|
+
close();
|
|
76
|
+
};
|
|
77
|
+
document.addEventListener('pointerdown', down, true);
|
|
78
|
+
document.addEventListener('keydown', key);
|
|
79
|
+
return () => {
|
|
80
|
+
document.removeEventListener('pointerdown', down, true);
|
|
81
|
+
document.removeEventListener('keydown', key);
|
|
82
|
+
};
|
|
83
|
+
}, [isOpen, close]);
|
|
84
|
+
const settle = (offset, width) => {
|
|
85
|
+
setDrag(null);
|
|
86
|
+
if (fullSwipe && trailingW > 0 && -offset >= width * FULL_SWIPE_RATIO) {
|
|
87
|
+
close();
|
|
88
|
+
actions.trailing?.[0]?.onAction();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (offset < 0 && -offset > trailingW / 2 && trailingW > 0) {
|
|
92
|
+
setSide('trailing');
|
|
93
|
+
open.setOpenId(id);
|
|
94
|
+
}
|
|
95
|
+
else if (offset > 0 && offset > leadingW / 2 && leadingW > 0) {
|
|
96
|
+
setSide('leading');
|
|
97
|
+
open.setOpenId(id);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
close();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const onPointerDown = (e) => {
|
|
104
|
+
if (!enabled || (e.pointerType === 'mouse' && e.button !== 0))
|
|
105
|
+
return;
|
|
106
|
+
track.current = {
|
|
107
|
+
pointerId: e.pointerId,
|
|
108
|
+
x: e.clientX,
|
|
109
|
+
y: e.clientY,
|
|
110
|
+
from: rest,
|
|
111
|
+
active: false,
|
|
112
|
+
width: e.currentTarget.getBoundingClientRect().width,
|
|
113
|
+
offset: rest,
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
const onPointerMove = (e) => {
|
|
117
|
+
const t = track.current;
|
|
118
|
+
if (!t || t.pointerId !== e.pointerId)
|
|
119
|
+
return;
|
|
120
|
+
const dx = e.clientX - t.x;
|
|
121
|
+
const dy = e.clientY - t.y;
|
|
122
|
+
if (!t.active) {
|
|
123
|
+
if (Math.abs(dy) > AXIS_LOCK && Math.abs(dy) > Math.abs(dx)) {
|
|
124
|
+
track.current = null; // a vertical scroll, not ours
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (Math.abs(dx) <= AXIS_LOCK)
|
|
128
|
+
return;
|
|
129
|
+
t.active = true;
|
|
130
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
131
|
+
open.setOpenId(id);
|
|
132
|
+
}
|
|
133
|
+
const raw = t.from + dx;
|
|
134
|
+
let offset;
|
|
135
|
+
if (raw < 0) {
|
|
136
|
+
if (-raw <= trailingW)
|
|
137
|
+
offset = raw;
|
|
138
|
+
else if (fullSwipe && trailingW > 0)
|
|
139
|
+
offset = Math.max(raw, -t.width); // follows the finger to the edge
|
|
140
|
+
else
|
|
141
|
+
offset = -(trailingW + rubber(-raw - trailingW, 24));
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
offset = raw <= leadingW ? raw : leadingW + rubber(raw - leadingW, 24);
|
|
145
|
+
}
|
|
146
|
+
t.offset = offset;
|
|
147
|
+
setDrag(offset);
|
|
148
|
+
};
|
|
149
|
+
const finish = (e, cancelled) => {
|
|
150
|
+
const t = track.current;
|
|
151
|
+
if (!t || t.pointerId !== e.pointerId)
|
|
152
|
+
return;
|
|
153
|
+
track.current = null;
|
|
154
|
+
if (!t.active)
|
|
155
|
+
return;
|
|
156
|
+
swallowClick.current = true;
|
|
157
|
+
setTimeout(() => {
|
|
158
|
+
swallowClick.current = false;
|
|
159
|
+
}, 50);
|
|
160
|
+
if (cancelled) {
|
|
161
|
+
setDrag(null);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
settle(t.offset, t.width);
|
|
165
|
+
};
|
|
166
|
+
const onClickCapture = (e) => {
|
|
167
|
+
if (swallowClick.current) {
|
|
168
|
+
swallowClick.current = false;
|
|
169
|
+
e.preventDefault();
|
|
170
|
+
e.stopPropagation();
|
|
171
|
+
}
|
|
172
|
+
else if (isOpen) {
|
|
173
|
+
// A tap on an open row closes it instead of activating it.
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
e.stopPropagation();
|
|
176
|
+
close();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
const offset = drag ?? rest;
|
|
180
|
+
return {
|
|
181
|
+
root,
|
|
182
|
+
offset,
|
|
183
|
+
dragging: drag !== null,
|
|
184
|
+
isOpen,
|
|
185
|
+
openSide: side,
|
|
186
|
+
close,
|
|
187
|
+
surfaceProps: {
|
|
188
|
+
onPointerDown,
|
|
189
|
+
onPointerMove,
|
|
190
|
+
onPointerUp: (e) => finish(e, false),
|
|
191
|
+
onPointerCancel: (e) => finish(e, true),
|
|
192
|
+
onClickCapture,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** The revealed buttons of one side. Inert unless that side is open. */
|
|
197
|
+
export function SwipeTray({ side, actions, offset, active, onDone, }) {
|
|
198
|
+
const width = side === 'leading' ? Math.max(offset, 0) : Math.max(-offset, 0);
|
|
199
|
+
return (_jsx("div", { className: "ui-list-tray", "data-side": side, style: { width }, inert: !active || width === 0, "aria-hidden": !active || undefined, children: actions.map((a, i) => (_jsxs("button", { type: "button", className: "ui-list-action", "data-tone": a.tone ?? 'neutral', onClick: () => {
|
|
200
|
+
onDone();
|
|
201
|
+
a.onAction();
|
|
202
|
+
}, children: [a.icon != null && _jsx("span", { "aria-hidden": true, children: a.icon }), _jsx("span", { className: "ui-list-action-label", children: a.label })] }, i))) }));
|
|
203
|
+
}
|
|
204
|
+
/** The same actions as a visible menu button; the way in when the gesture is not offered. */
|
|
205
|
+
export function OverflowMenu({ label, actions, hidden, }) {
|
|
206
|
+
return (_jsxs(ThemedDropdownMenu.Root, { children: [_jsx(ThemedDropdownMenu.Trigger, { children: _jsx("button", { type: "button", className: "ui-list-more", "data-hidden": hidden || undefined, "aria-label": label, children: _jsx(DotsHorizontalIcon, { "aria-hidden": true }) }) }), _jsx(ThemedDropdownMenu.Content, { size: "1", children: actions.map((a, i) => (_jsxs(Fragment, { children: [a.tone === 'danger' && i > 0 && actions[i - 1].tone !== 'danger' ? (_jsx(ThemedDropdownMenu.Separator, {})) : null, _jsx(ThemedDropdownMenu.Item, { color: a.tone === 'danger' ? 'red' : undefined, onSelect: () => a.onAction(), children: a.label })] }, i))) })] }));
|
|
207
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes } from 'react';
|
|
2
|
+
export interface BackButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
|
3
|
+
/** Visible text beside the chevron (the previous page's title, usually). */
|
|
4
|
+
label?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A chevron with an optional label. The target is at least 44px square; with no `label` the
|
|
8
|
+
* accessible name comes from `UiStrings.back`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function BackButton({ label, className, type, ...rest }: BackButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useUiStrings } from '../../strings.js';
|
|
3
|
+
import { Icon } from '../Icon/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* A chevron with an optional label. The target is at least 44px square; with no `label` the
|
|
6
|
+
* accessible name comes from `UiStrings.back`.
|
|
7
|
+
*/
|
|
8
|
+
export function BackButton({ label, className, type = 'button', ...rest }) {
|
|
9
|
+
const strings = useUiStrings();
|
|
10
|
+
return (_jsxs("button", { type: type, "aria-label": label ? undefined : strings.back, className: ['ui-backbutton', className].filter(Boolean).join(' '), ...rest, children: [_jsx(Icon, { name: "collapse", className: "ui-backbutton__icon" }), label && _jsx("span", { className: "ui-backbutton__label", children: label })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface NavBarProps {
|
|
3
|
+
title: string;
|
|
4
|
+
/** Left slot: a `BackButton`, usually. */
|
|
5
|
+
leading?: ReactNode;
|
|
6
|
+
/** Right slot: icon buttons. */
|
|
7
|
+
trailing?: ReactNode;
|
|
8
|
+
/**
|
|
9
|
+
* A large left-aligned title in the content flow. It is the page's `h1`; once it has scrolled
|
|
10
|
+
* under the bar, a small centred copy fades in (that copy is `aria-hidden`).
|
|
11
|
+
* Without it the small title is the `h1`.
|
|
12
|
+
*/
|
|
13
|
+
largeTitle?: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A sticky top bar for small screens. Render it as the first child of the scrolling content:
|
|
18
|
+
* the large title follows it in the flow. The bar is transparent at the top of the page and
|
|
19
|
+
* gains `--material-bar` and a hairline once content has moved beneath it. Scroll state comes
|
|
20
|
+
* from one IntersectionObserver, with no scroll listeners.
|
|
21
|
+
*/
|
|
22
|
+
export declare function NavBar({ title, leading, trailing, largeTitle, className }: NavBarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
/** Height of the bar proper, `--safe-top` excluded. Mirrored in tokens.css. */
|
|
4
|
+
const BAR_HEIGHT = 44;
|
|
5
|
+
/**
|
|
6
|
+
* A sticky top bar for small screens. Render it as the first child of the scrolling content:
|
|
7
|
+
* the large title follows it in the flow. The bar is transparent at the top of the page and
|
|
8
|
+
* gains `--material-bar` and a hairline once content has moved beneath it. Scroll state comes
|
|
9
|
+
* from one IntersectionObserver, with no scroll listeners.
|
|
10
|
+
*/
|
|
11
|
+
export function NavBar({ title, leading, trailing, largeTitle = false, className }) {
|
|
12
|
+
const [scrolled, setScrolled] = useState(false);
|
|
13
|
+
const target = useRef(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const el = target.current;
|
|
16
|
+
if (!el || typeof IntersectionObserver === 'undefined')
|
|
17
|
+
return;
|
|
18
|
+
// With a large title, "scrolled" means it is gone beneath the bar; without one, that the
|
|
19
|
+
// page has moved at all (the sentinel is a zero-height element at the top of the flow).
|
|
20
|
+
const io = new IntersectionObserver(([entry]) => {
|
|
21
|
+
if (entry)
|
|
22
|
+
setScrolled(!entry.isIntersecting);
|
|
23
|
+
}, { rootMargin: largeTitle ? `-${BAR_HEIGHT}px 0px 0px 0px` : '0px' });
|
|
24
|
+
io.observe(el);
|
|
25
|
+
return () => io.disconnect();
|
|
26
|
+
}, [largeTitle]);
|
|
27
|
+
const root = ['ui-navbar', className].filter(Boolean).join(' ');
|
|
28
|
+
return (_jsxs(_Fragment, { children: [_jsxs("header", { className: root, "data-scrolled": scrolled ? 'true' : 'false', "data-large": largeTitle ? 'true' : 'false', children: [_jsx("div", { className: "ui-navbar__side", children: leading }), largeTitle ? (_jsx("div", { className: "ui-navbar__title", "aria-hidden": "true", children: title })) : (_jsx("h1", { className: "ui-navbar__title", children: title })), _jsx("div", { className: "ui-navbar__side ui-navbar__side--trailing", children: trailing })] }), largeTitle ? (_jsx("div", { className: "ui-navbar__large", children: _jsx("h1", { ref: target, className: "ui-navbar__large-title", children: title }) })) : (_jsx("div", { ref: target, className: "ui-navbar__sentinel", "aria-hidden": "true" }))] }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/** The viewport corner the card floats in. */
|
|
3
|
+
export type NotificationCardPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
4
|
+
/** Why the card closed. `programmatic` is a controlled `open` turned false by the host. */
|
|
5
|
+
export type NotificationCardDismissReason = 'close-button' | 'escape' | 'programmatic';
|
|
6
|
+
export interface NotificationCardProps {
|
|
7
|
+
/** Viewport corner. Default `bottom-right`. */
|
|
8
|
+
position?: NotificationCardPosition;
|
|
9
|
+
/** Controlled visibility. */
|
|
10
|
+
open?: boolean;
|
|
11
|
+
/** Initial visibility when uncontrolled. Default `true`. */
|
|
12
|
+
defaultOpen?: boolean;
|
|
13
|
+
/** Called whenever the card asks to open or close. */
|
|
14
|
+
onOpenChange?: (open: boolean) => void;
|
|
15
|
+
/** Called once per close, with what closed it. */
|
|
16
|
+
onDismiss?: (reason: NotificationCardDismissReason) => void;
|
|
17
|
+
/** Optional leading media: an icon, an app logo. */
|
|
18
|
+
icon?: ReactNode;
|
|
19
|
+
/** Labels the section via `aria-labelledby`. */
|
|
20
|
+
title: ReactNode;
|
|
21
|
+
/** Wired up via `aria-describedby`. */
|
|
22
|
+
description?: ReactNode;
|
|
23
|
+
/** Collapsible body. When present the card shows a toggle for it. */
|
|
24
|
+
children?: ReactNode;
|
|
25
|
+
/** Footer row: buttons, links. */
|
|
26
|
+
actions?: ReactNode;
|
|
27
|
+
/** Controlled expansion of the body. */
|
|
28
|
+
expanded?: boolean;
|
|
29
|
+
/** Initial expansion when uncontrolled. Default `false`. */
|
|
30
|
+
defaultExpanded?: boolean;
|
|
31
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
32
|
+
/** Toggle text while collapsed (otherwise the `expand` UI string). */
|
|
33
|
+
expandLabel?: string;
|
|
34
|
+
/** Toggle text while expanded (otherwise the `collapse` UI string). */
|
|
35
|
+
collapseLabel?: string;
|
|
36
|
+
/** Close button's accessible name (otherwise the `close` UI string). */
|
|
37
|
+
closeLabel?: string;
|
|
38
|
+
/** Portal target. Default: the nearest `<Theme>`, else rendered in place. */
|
|
39
|
+
container?: HTMLElement | null;
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A persistent rich toast: floats in a viewport corner and stays until somebody closes it.
|
|
44
|
+
*
|
|
45
|
+
* Deliberately a labelled `section`, not `role="alert"` or a live region — it is the kind of
|
|
46
|
+
* card that is present on page load, and announcing it on every load would be noise. It is
|
|
47
|
+
* non-modal: no focus trap, and Escape closes it only while focus is inside it.
|
|
48
|
+
*
|
|
49
|
+
* It portals into the nearest `<Theme>` (wrapped in a nested `<Theme>`, which inherits the
|
|
50
|
+
* appearance) so dark mode reaches it; with no Theme above it, it renders in place.
|
|
51
|
+
*/
|
|
52
|
+
export declare function NotificationCard({ position, open: openProp, defaultOpen, onOpenChange, onDismiss, icon, title, description, children, actions, expanded: expandedProp, defaultExpanded, onExpandedChange, expandLabel, collapseLabel, closeLabel, container, className, }: NotificationCardProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useId, useRef, useState, } from 'react';
|
|
3
|
+
import { useUiStrings } from '../../strings.js';
|
|
4
|
+
import { cn } from '../../utils/cn.js';
|
|
5
|
+
import { ThemePortal, useThemePortalTarget } from '../../utils/themePortal.js';
|
|
6
|
+
/** Longest the exit animation may take before the card unmounts anyway. */
|
|
7
|
+
const EXIT_MS = 250;
|
|
8
|
+
function useControllable(value, initial, onChange) {
|
|
9
|
+
const [inner, setInner] = useState(initial);
|
|
10
|
+
const controlled = value !== undefined;
|
|
11
|
+
const current = controlled ? value : inner;
|
|
12
|
+
const set = useCallback((next) => {
|
|
13
|
+
if (!controlled)
|
|
14
|
+
setInner(next);
|
|
15
|
+
onChange?.(next);
|
|
16
|
+
}, [controlled, onChange]);
|
|
17
|
+
return [current, set];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A persistent rich toast: floats in a viewport corner and stays until somebody closes it.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately a labelled `section`, not `role="alert"` or a live region — it is the kind of
|
|
23
|
+
* card that is present on page load, and announcing it on every load would be noise. It is
|
|
24
|
+
* non-modal: no focus trap, and Escape closes it only while focus is inside it.
|
|
25
|
+
*
|
|
26
|
+
* It portals into the nearest `<Theme>` (wrapped in a nested `<Theme>`, which inherits the
|
|
27
|
+
* appearance) so dark mode reaches it; with no Theme above it, it renders in place.
|
|
28
|
+
*/
|
|
29
|
+
export function NotificationCard({ position = 'bottom-right', open: openProp, defaultOpen = true, onOpenChange, onDismiss, icon, title, description, children, actions, expanded: expandedProp, defaultExpanded = false, onExpandedChange, expandLabel, collapseLabel, closeLabel, container, className, }) {
|
|
30
|
+
const strings = useUiStrings();
|
|
31
|
+
const [open, setOpen] = useControllable(openProp, defaultOpen, onOpenChange);
|
|
32
|
+
const [expanded, setExpanded] = useControllable(expandedProp, defaultExpanded, onExpandedChange);
|
|
33
|
+
const { anchorRef, target, resolved } = useThemePortalTarget(container);
|
|
34
|
+
const id = useId();
|
|
35
|
+
const titleId = `${id}-title`;
|
|
36
|
+
const descId = `${id}-desc`;
|
|
37
|
+
const bodyId = `${id}-body`;
|
|
38
|
+
// Mounted while open and through the exit animation.
|
|
39
|
+
const [mounted, setMounted] = useState(open);
|
|
40
|
+
// What the card itself asked to close for; a close it did not ask for is the host's.
|
|
41
|
+
const reason = useRef(null);
|
|
42
|
+
const wasOpen = useRef(open);
|
|
43
|
+
const onDismissRef = useRef(onDismiss);
|
|
44
|
+
onDismissRef.current = onDismiss;
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const was = wasOpen.current;
|
|
47
|
+
wasOpen.current = open;
|
|
48
|
+
if (open) {
|
|
49
|
+
setMounted(true);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!was)
|
|
53
|
+
return;
|
|
54
|
+
onDismissRef.current?.(reason.current ?? 'programmatic');
|
|
55
|
+
reason.current = null;
|
|
56
|
+
const t = setTimeout(() => setMounted(false), EXIT_MS);
|
|
57
|
+
return () => clearTimeout(t);
|
|
58
|
+
}, [open]);
|
|
59
|
+
const close = (why) => {
|
|
60
|
+
if (!open)
|
|
61
|
+
return;
|
|
62
|
+
reason.current = why;
|
|
63
|
+
setOpen(false);
|
|
64
|
+
};
|
|
65
|
+
const onKeyDown = (e) => {
|
|
66
|
+
if (e.key !== 'Escape')
|
|
67
|
+
return;
|
|
68
|
+
e.stopPropagation();
|
|
69
|
+
close('escape');
|
|
70
|
+
};
|
|
71
|
+
const anchor = _jsx("span", { ref: anchorRef, hidden: true, "data-notification-anchor": "" });
|
|
72
|
+
if (!mounted || !resolved)
|
|
73
|
+
return anchor;
|
|
74
|
+
return (_jsxs(_Fragment, { children: [anchor, _jsx(ThemePortal, { target: target, children: _jsxs("section", { "aria-labelledby": titleId, "aria-describedby": description ? descId : undefined, "data-state": open ? 'open' : 'closed', "data-position": position, onKeyDown: onKeyDown, onAnimationEnd: () => {
|
|
75
|
+
if (!open)
|
|
76
|
+
setMounted(false);
|
|
77
|
+
}, className: cn('ui-notification-card', className), children: [_jsxs("div", { className: "ui-notification-card__header", children: [icon ? _jsx("div", { className: "ui-notification-card__icon", children: icon }) : null, _jsxs("div", { className: "ui-notification-card__text", children: [_jsx("h2", { id: titleId, className: "ui-notification-card__title", children: title }), description ? (_jsx("p", { id: descId, className: "ui-notification-card__description", children: description })) : null] }), _jsx("button", { type: "button", "aria-label": closeLabel ?? strings.close, onClick: () => close('close-button'), className: "ui-notification-card__close", children: _jsx("svg", { "aria-hidden": "true", width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", children: _jsx("path", { d: "M3.5 3.5l8 8m0-8l-8 8", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }) }) })] }), children ? (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", "aria-expanded": expanded, "aria-controls": bodyId, onClick: () => setExpanded(!expanded), className: "ui-notification-card__toggle", children: [expanded ? (collapseLabel ?? strings.collapse) : (expandLabel ?? strings.expand), _jsx("svg", { "aria-hidden": "true", width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", children: _jsx("path", { d: expanded ? 'M4 9.5l3.5-3.5 3.5 3.5' : 'M4 6l3.5 3.5L11 6', stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) })] }), _jsx("div", { id: bodyId, hidden: !expanded, className: "ui-notification-card__body", children: children })] })) : null, actions ? _jsx("div", { className: "ui-notification-card__actions", children: actions }) : null] }) })] }));
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './NotificationCard.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './NotificationCard.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface PullToRefreshProps {
|
|
3
|
+
/** Called once the pull passes the threshold. The indicator is held until the promise settles. */
|
|
4
|
+
onRefresh: () => Promise<void>;
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
/**
|
|
7
|
+
* Which thing scrolls. `document` (default) watches the page and the wrapper is a plain block;
|
|
8
|
+
* `self` makes the wrapper the scroll container (give it a height via `className`).
|
|
9
|
+
*/
|
|
10
|
+
scroller?: 'document' | 'self';
|
|
11
|
+
/** Pixels of (damped) pull that commit a refresh. Default 64. */
|
|
12
|
+
threshold?: number;
|
|
13
|
+
/** Most the content is pushed down. Default 112. */
|
|
14
|
+
maxPull?: number;
|
|
15
|
+
/** Turns the gesture off. */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Overrides the announced label (otherwise the `refreshing` UI string). */
|
|
18
|
+
label?: string;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Pull-to-refresh for touch screens.
|
|
23
|
+
*
|
|
24
|
+
* **A shortcut, never the only way**: the parent must still offer a visible refresh action,
|
|
25
|
+
* because the gesture is invisible, touch-only and unavailable to switch and keyboard users.
|
|
26
|
+
*
|
|
27
|
+
* Active only on coarse pointers, only when the scroller is at the very top when the touch
|
|
28
|
+
* begins, and only for a downward drag; otherwise the browser's own scroll is untouched.
|
|
29
|
+
* `onRefresh` should handle its own errors — a rejection just ends the wait.
|
|
30
|
+
*/
|
|
31
|
+
export declare function PullToRefresh({ onRefresh, children, scroller, threshold, maxPull, disabled, label, className, }: PullToRefreshProps): import("react").JSX.Element;
|