@forthtilliath/react-native-kit 0.17.1 → 0.18.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/README.md +29 -14
- package/dist/components/picker/QuantityStepper.d.ts +21 -7
- package/dist/components/picker/QuantityStepper.d.ts.map +1 -1
- package/dist/components/picker/QuantityStepper.js +82 -25
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/usePressRepeat.d.ts +31 -0
- package/dist/hooks/usePressRepeat.d.ts.map +1 -0
- package/dist/hooks/usePressRepeat.js +47 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -277,6 +277,21 @@ useDebouncedChange([settingsData, itemsData], 5 * 60 * 1000, () => {
|
|
|
277
277
|
});
|
|
278
278
|
```
|
|
279
279
|
|
|
280
|
+
### `usePressRepeat(timings?)`
|
|
281
|
+
|
|
282
|
+
Repeats an action while a button is held, faster and faster: `start(action)` runs it at once, then every 180 ms after 450 ms, every 60 ms after 1.2 s of repeating (all four timings overridable); `stop()` ends it, and so does unmounting.
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
import { usePressRepeat } from "@forthtilliath/react-native-kit/hooks/usePressRepeat";
|
|
286
|
+
|
|
287
|
+
const repeat = usePressRepeat();
|
|
288
|
+
|
|
289
|
+
<Pressable
|
|
290
|
+
onPressIn={() => repeat.start(() => setCount((n) => n + 1))}
|
|
291
|
+
onPressOut={repeat.stop}
|
|
292
|
+
/>;
|
|
293
|
+
```
|
|
294
|
+
|
|
280
295
|
### `confirmDestructive(title, onConfirm, options?)`
|
|
281
296
|
|
|
282
297
|
Generic destructive-action confirmation (title + message + Cancel/Confirm), for anything irreversible (delete, reset...). Ships with French defaults, or English ones with `{ locale: "en" }` (`message`, `cancelLabel`, `confirmLabel` all overridable).
|
|
@@ -343,20 +358,20 @@ const colors = Colors[useResolvedScheme()];
|
|
|
343
358
|
|
|
344
359
|
Every component below has neutral default colors and a `styles` prop whose slots are each merged **after** their default (a partial override keeps the rest of the slot) — pass the app's palette there.
|
|
345
360
|
|
|
346
|
-
| Component | Import path | What it is
|
|
347
|
-
| ----------------------------------------------------------------------- | ----------------------------------- |
|
|
348
|
-
| `<BottomSheet visible onClose bottomInset?>` | `components/overlay/BottomSheet` | Modal sliding up from the bottom, content scrollable past 85 % of the screen. Pass `useSafeAreaInsets().bottom` as `bottomInset` with Android edge-to-edge.
|
|
349
|
-
| `<OptionsModal visible onClose options onSelect>` | `components/picker/OptionsModal` | Centered popup listing `{ value, label }` options; a tap selects and closes.
|
|
350
|
-
| `<SelectField label valueLabel options onSelect>` | `components/picker/SelectField` | Native-looking "select": captioned button + `OptionsModal` (styled through `styles.optionsModal`).
|
|
351
|
-
| `<SegmentedToggle options value onChange label?>` | `components/picker/SegmentedToggle` | Row of equal-width buttons, one active (Calendar / List…), announced as a radio group; each option may carry a smaller `hint` line under its label.
|
|
352
|
-
| `<QuantityStepper value onChange label min? max?>` | `components/picker/QuantityStepper` | Whole-number counter `min…max` (`0…999`): −/+ buttons (disabled at the bounds, `longPressStep` to jump on long press) and a numeric field; screen-reader labels in French or English (`a11yLabels` to override). |
|
|
353
|
-
| `<SwitchRow label description? value onChange>` | `components/settings/SwitchRow` | Settings line "label + description + switch", the whole row pressable and read as one switch; `colors` for the track and thumb.
|
|
354
|
-
| `<SettingsCard title icon?>` + `SettingsCardStatus`, `SettingsCardHint` | `components/settings/SettingsCard` | Settings card: icon + title header, then free content — status lines (small icon + text, `iconColor` per state) and a quieter hint.
|
|
355
|
-
| `<CheckboxGroup options values onChange label?>` | `components/picker/CheckboxGroup` | Wrapping chips that each check and uncheck independently (extensions, tags…), each announced as a checkbox.
|
|
356
|
-
| `<MonthDayGrid days selected onToggle>` | `components/picker/MonthDayGrid` | Monday-first month grid whose days toggle on and off (holidays, days off…); `YYYY-MM-DD` days.
|
|
357
|
-
| `<AddButton label onPress>` | `components/list/AddButton` | Dashed "+ Add …" button closing a list.
|
|
358
|
-
| `<DragHandle onPressIn accessibilityLabel>` | `components/list/DragHandle` | "⠿" grip for `react-native-draggable-flatlist` rows (gesture-handler's `Pressable`).
|
|
359
|
-
| `<ErrorFallback error retry>` | `components/feedback/ErrorFallback` | Readable crash screen with a retry button, for Expo Router's `ErrorBoundary`; texts in French or English (`labels` to override).
|
|
361
|
+
| Component | Import path | What it is |
|
|
362
|
+
| ----------------------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
363
|
+
| `<BottomSheet visible onClose bottomInset?>` | `components/overlay/BottomSheet` | Modal sliding up from the bottom, content scrollable past 85 % of the screen. Pass `useSafeAreaInsets().bottom` as `bottomInset` with Android edge-to-edge. |
|
|
364
|
+
| `<OptionsModal visible onClose options onSelect>` | `components/picker/OptionsModal` | Centered popup listing `{ value, label }` options; a tap selects and closes. |
|
|
365
|
+
| `<SelectField label valueLabel options onSelect>` | `components/picker/SelectField` | Native-looking "select": captioned button + `OptionsModal` (styled through `styles.optionsModal`). |
|
|
366
|
+
| `<SegmentedToggle options value onChange label?>` | `components/picker/SegmentedToggle` | Row of equal-width buttons, one active (Calendar / List…), announced as a radio group; each option may carry a smaller `hint` line under its label. |
|
|
367
|
+
| `<QuantityStepper value onChange label min? max?>` | `components/picker/QuantityStepper` | Whole-number counter `min…max` (`0…999`, negatives allowed with a negative `min`): −/+ buttons (disabled at the bounds, `longPressStep` to jump on long press, or `repeatOnHold` to repeat faster and faster while held) and a numeric field; screen-reader labels in French or English (`a11yLabels` to override). |
|
|
368
|
+
| `<SwitchRow label description? value onChange>` | `components/settings/SwitchRow` | Settings line "label + description + switch", the whole row pressable and read as one switch; `colors` for the track and thumb. |
|
|
369
|
+
| `<SettingsCard title icon?>` + `SettingsCardStatus`, `SettingsCardHint` | `components/settings/SettingsCard` | Settings card: icon + title header, then free content — status lines (small icon + text, `iconColor` per state) and a quieter hint. |
|
|
370
|
+
| `<CheckboxGroup options values onChange label?>` | `components/picker/CheckboxGroup` | Wrapping chips that each check and uncheck independently (extensions, tags…), each announced as a checkbox. |
|
|
371
|
+
| `<MonthDayGrid days selected onToggle>` | `components/picker/MonthDayGrid` | Monday-first month grid whose days toggle on and off (holidays, days off…); `YYYY-MM-DD` days. |
|
|
372
|
+
| `<AddButton label onPress>` | `components/list/AddButton` | Dashed "+ Add …" button closing a list. |
|
|
373
|
+
| `<DragHandle onPressIn accessibilityLabel>` | `components/list/DragHandle` | "⠿" grip for `react-native-draggable-flatlist` rows (gesture-handler's `Pressable`). |
|
|
374
|
+
| `<ErrorFallback error retry>` | `components/feedback/ErrorFallback` | Readable crash screen with a retry button, for Expo Router's `ErrorBoundary`; texts in French or English (`labels` to override). |
|
|
360
375
|
|
|
361
376
|
`SwipeableRow` also takes `fullSwipeToDelete`: swiping past half the row deletes right away (the row slides out over a full-width delete band) instead of revealing a button — pair it with an `UndoToast`.
|
|
362
377
|
|
|
@@ -25,15 +25,26 @@ export interface QuantityStepperProps {
|
|
|
25
25
|
onChange: (value: number) => void;
|
|
26
26
|
/** What is being counted — used in the screen-reader labels. */
|
|
27
27
|
label: string;
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Below 0, negative values are allowed: the field then accepts a leading
|
|
30
|
+
* "-" (and opens the keyboard that has one).
|
|
31
|
+
* @defaultValue 0
|
|
32
|
+
*/
|
|
29
33
|
min?: number;
|
|
30
34
|
/** @defaultValue 999 */
|
|
31
35
|
max?: number;
|
|
32
36
|
/**
|
|
33
37
|
* Step applied by a long press on −/+, to reach large values quickly.
|
|
34
|
-
* Unset, a long press does nothing more than a tap.
|
|
38
|
+
* Unset, a long press does nothing more than a tap. Ignored with
|
|
39
|
+
* `repeatOnHold`.
|
|
35
40
|
*/
|
|
36
41
|
longPressStep?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Holding −/+ repeats the step, faster and faster (see `usePressRepeat`),
|
|
44
|
+
* until released or a bound is reached.
|
|
45
|
+
* @defaultValue false
|
|
46
|
+
*/
|
|
47
|
+
repeatOnHold?: boolean;
|
|
37
48
|
/**
|
|
38
49
|
* Language of the default screen-reader labels — French by default, or the
|
|
39
50
|
* nearest `KitLocaleProvider`'s.
|
|
@@ -46,10 +57,12 @@ export interface QuantityStepperProps {
|
|
|
46
57
|
}
|
|
47
58
|
/**
|
|
48
59
|
* A whole-number counter from `min` (0) to `max`: −/+ buttons to adjust
|
|
49
|
-
* (optionally faster on long press), and a numeric
|
|
50
|
-
* type a large number in one go (
|
|
51
|
-
* clamped). A typed value
|
|
52
|
-
* `min
|
|
60
|
+
* (optionally faster on long press, or repeating while held), and a numeric
|
|
61
|
+
* field in the middle to type a large number in one go (other characters are
|
|
62
|
+
* dropped, the result is clamped). A typed value that is not a number yet or
|
|
63
|
+
* below `min` (an empty field, a lone "-", the "2" of "25" with `min={10}`)
|
|
64
|
+
* stays as typed until the field loses focus. A negative `min` allows
|
|
65
|
+
* negative values (penalties, defeat tokens…).
|
|
53
66
|
*
|
|
54
67
|
* @example
|
|
55
68
|
* ```tsx
|
|
@@ -57,7 +70,8 @@ export interface QuantityStepperProps {
|
|
|
57
70
|
*
|
|
58
71
|
* <QuantityStepper value={count} onChange={setCount} label="Croissants" />
|
|
59
72
|
* <QuantityStepper value={bans} onChange={setBans} label="Bannissements" min={1} max={40} longPressStep={5} />
|
|
73
|
+
* <QuantityStepper value={score} onChange={setScore} label="Points" min={-999} repeatOnHold />
|
|
60
74
|
* ```
|
|
61
75
|
*/
|
|
62
|
-
export declare function QuantityStepper({ value, onChange, label, min, max, longPressStep, locale, a11yLabels, styles, }: QuantityStepperProps): import("react").JSX.Element;
|
|
76
|
+
export declare function QuantityStepper({ value, onChange, label, min, max, longPressStep, repeatOnHold, locale, a11yLabels, styles, }: QuantityStepperProps): import("react").JSX.Element;
|
|
63
77
|
//# sourceMappingURL=QuantityStepper.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QuantityStepper.d.ts","sourceRoot":"","sources":["../../../src/components/picker/QuantityStepper.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"QuantityStepper.d.ts","sourceRoot":"","sources":["../../../src/components/picker/QuantityStepper.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAIpE,OAAO,EAAE,KAAK,SAAS,EAAgB,MAAM,sBAAsB,CAAC;AAGpE,2FAA2F;AAC3F,MAAM,WAAW,qBAAqB;IACpC,0CAA0C;IAC1C,GAAG,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC3B,0BAA0B;IAC1B,MAAM,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9B,kEAAkE;IAClE,cAAc,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACtC,4BAA4B;IAC5B,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,iEAAiE;IACjE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,oFAAoF;AACpF,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,yDAAyD;IACzD,UAAU,CAAC,EAAE,OAAO,CAAC,yBAAyB,CAAC,CAAC;IAChD,oEAAoE;IACpE,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AA0DD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,KAAK,EACL,GAAO,EACP,GAAS,EACT,aAAa,EACb,YAAoB,EACpB,MAAM,EACN,UAAU,EACV,MAAM,GACP,EAAE,oBAAoB,+BAqItB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from "react";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { Pressable, Text, TextInput, View } from "react-native";
|
|
4
|
+
import { usePressRepeat } from "../../hooks/usePressRepeat.js";
|
|
4
5
|
import { useKitLocale } from "../../i18n/locale.js";
|
|
5
6
|
import { mergeSlotStyles } from "../../utils/helpers/mergeSlotStyles.js";
|
|
6
7
|
const defaultA11yLabels = {
|
|
@@ -49,12 +50,21 @@ const defaultStyles = {
|
|
|
49
50
|
padding: 0,
|
|
50
51
|
},
|
|
51
52
|
};
|
|
53
|
+
/** Keeps the digits, and a leading "-" when negatives are allowed. */
|
|
54
|
+
function sanitize(text, allowsNegative) {
|
|
55
|
+
const digits = text.replace(/\D/g, "");
|
|
56
|
+
return allowsNegative && text.trimStart().startsWith("-")
|
|
57
|
+
? `-${digits}`
|
|
58
|
+
: digits;
|
|
59
|
+
}
|
|
52
60
|
/**
|
|
53
61
|
* A whole-number counter from `min` (0) to `max`: −/+ buttons to adjust
|
|
54
|
-
* (optionally faster on long press), and a numeric
|
|
55
|
-
* type a large number in one go (
|
|
56
|
-
* clamped). A typed value
|
|
57
|
-
* `min
|
|
62
|
+
* (optionally faster on long press, or repeating while held), and a numeric
|
|
63
|
+
* field in the middle to type a large number in one go (other characters are
|
|
64
|
+
* dropped, the result is clamped). A typed value that is not a number yet or
|
|
65
|
+
* below `min` (an empty field, a lone "-", the "2" of "25" with `min={10}`)
|
|
66
|
+
* stays as typed until the field loses focus. A negative `min` allows
|
|
67
|
+
* negative values (penalties, defeat tokens…).
|
|
58
68
|
*
|
|
59
69
|
* @example
|
|
60
70
|
* ```tsx
|
|
@@ -62,40 +72,87 @@ const defaultStyles = {
|
|
|
62
72
|
*
|
|
63
73
|
* <QuantityStepper value={count} onChange={setCount} label="Croissants" />
|
|
64
74
|
* <QuantityStepper value={bans} onChange={setBans} label="Bannissements" min={1} max={40} longPressStep={5} />
|
|
75
|
+
* <QuantityStepper value={score} onChange={setScore} label="Points" min={-999} repeatOnHold />
|
|
65
76
|
* ```
|
|
66
77
|
*/
|
|
67
|
-
export function QuantityStepper({ value, onChange, label, min = 0, max = 999, longPressStep, locale, a11yLabels, styles, }) {
|
|
78
|
+
export function QuantityStepper({ value, onChange, label, min = 0, max = 999, longPressStep, repeatOnHold = false, locale, a11yLabels, styles, }) {
|
|
68
79
|
const labels = { ...defaultA11yLabels[useKitLocale(locale)], ...a11yLabels };
|
|
69
80
|
const merged = mergeSlotStyles(defaultStyles, styles);
|
|
70
81
|
const atMin = value <= min;
|
|
71
82
|
const atMax = value >= max;
|
|
83
|
+
const allowsNegative = min < 0;
|
|
72
84
|
// Field text not sent yet: clamping it to `min` on every keystroke would
|
|
73
85
|
// make any number starting below `min` impossible to type.
|
|
74
86
|
const [draft, setDraft] = useState(null);
|
|
87
|
+
const repeat = usePressRepeat();
|
|
88
|
+
// Value during a repeat, ahead of the `value` prop until the parent
|
|
89
|
+
// re-renders with each onChange.
|
|
90
|
+
const valueRef = useRef(value);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
valueRef.current = value;
|
|
93
|
+
}, [value]);
|
|
94
|
+
const clamp = (n) => Math.min(max, Math.max(min, n));
|
|
75
95
|
function set(next) {
|
|
76
96
|
setDraft(null);
|
|
77
|
-
onChange(
|
|
97
|
+
onChange(clamp(next));
|
|
98
|
+
}
|
|
99
|
+
function stepBy(delta) {
|
|
100
|
+
const next = clamp(valueRef.current + delta);
|
|
101
|
+
// A button disabled at its bound may never report the release.
|
|
102
|
+
if (next === valueRef.current) {
|
|
103
|
+
repeat.stop();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
valueRef.current = next;
|
|
107
|
+
set(next);
|
|
108
|
+
}
|
|
109
|
+
function startRepeat(delta) {
|
|
110
|
+
repeat.start(() => {
|
|
111
|
+
stepBy(delta);
|
|
112
|
+
});
|
|
78
113
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
114
|
+
// Held: repeat from press-in to press-out. Otherwise: one step per tap,
|
|
115
|
+
// and `longPressStep` on long press.
|
|
116
|
+
const tapsOnly = !repeatOnHold;
|
|
117
|
+
const jumps = tapsOnly && longPressStep !== undefined;
|
|
118
|
+
return (_jsxs(View, { style: merged.row, children: [_jsx(Pressable, { style: [merged.button, atMin && merged.buttonDisabled], onPress: tapsOnly
|
|
119
|
+
? () => {
|
|
120
|
+
set(value - 1);
|
|
121
|
+
}
|
|
122
|
+
: undefined, onLongPress: jumps
|
|
123
|
+
? () => {
|
|
84
124
|
set(value - longPressStep);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
125
|
+
}
|
|
126
|
+
: undefined, onPressIn: repeatOnHold
|
|
127
|
+
? () => {
|
|
128
|
+
startRepeat(-1);
|
|
129
|
+
}
|
|
130
|
+
: undefined, onPressOut: repeatOnHold ? repeat.stop : undefined, disabled: atMin, hitSlop: 6, accessibilityRole: "button", accessibilityLabel: labels.decrease(label), children: _jsx(Text, { style: merged.buttonText, children: "\u2212" }) }), _jsx(TextInput, { style: merged.input, value: draft ?? String(value), onChangeText: (text) => {
|
|
131
|
+
const cleaned = sanitize(text, allowsNegative);
|
|
132
|
+
const typed = Number(cleaned);
|
|
133
|
+
if (cleaned === "" || cleaned === "-" || typed < min) {
|
|
134
|
+
setDraft(cleaned);
|
|
135
|
+
}
|
|
89
136
|
else
|
|
90
|
-
set(
|
|
137
|
+
set(typed);
|
|
91
138
|
}, onBlur: () => {
|
|
92
|
-
if (draft
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
set(
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
|
|
139
|
+
if (draft === null)
|
|
140
|
+
return;
|
|
141
|
+
// Nothing typed yet: back to 0, or to the nearest bound.
|
|
142
|
+
set(draft === "" || draft === "-" ? 0 : Number(draft));
|
|
143
|
+
},
|
|
144
|
+
// "number-pad" has no "-" key on Android.
|
|
145
|
+
keyboardType: allowsNegative ? "numeric" : "number-pad", selectTextOnFocus: true, maxLength: Math.max(String(min).length, String(max).length), accessibilityLabel: labels.input(label) }), _jsx(Pressable, { style: [merged.button, atMax && merged.buttonDisabled], onPress: tapsOnly
|
|
146
|
+
? () => {
|
|
147
|
+
set(value + 1);
|
|
148
|
+
}
|
|
149
|
+
: undefined, onLongPress: jumps
|
|
150
|
+
? () => {
|
|
99
151
|
set(value + longPressStep);
|
|
100
|
-
}
|
|
152
|
+
}
|
|
153
|
+
: undefined, onPressIn: repeatOnHold
|
|
154
|
+
? () => {
|
|
155
|
+
startRepeat(1);
|
|
156
|
+
}
|
|
157
|
+
: undefined, onPressOut: repeatOnHold ? repeat.stop : undefined, disabled: atMax, hitSlop: 6, accessibilityRole: "button", accessibilityLabel: labels.increase(label), children: _jsx(Text, { style: merged.buttonText, children: "+" }) })] }));
|
|
101
158
|
}
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC"}
|
package/dist/hooks/index.js
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Timings of {@link usePressRepeat}, in milliseconds. */
|
|
2
|
+
export interface PressRepeatTimings {
|
|
3
|
+
/** Hold time before the first repeat. @defaultValue 450 */
|
|
4
|
+
initialDelay?: number;
|
|
5
|
+
/** Interval between repeats at first. @defaultValue 180 */
|
|
6
|
+
interval?: number;
|
|
7
|
+
/** Repeat time after which repeats speed up. @defaultValue 1200 */
|
|
8
|
+
accelerateAfter?: number;
|
|
9
|
+
/** Interval between repeats once sped up. @defaultValue 60 */
|
|
10
|
+
fastInterval?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Repeats an action while a button is held, faster and faster — the way a
|
|
14
|
+
* counter's −/+ reach large values quickly. `start` runs the action at once,
|
|
15
|
+
* then again every `interval` after `initialDelay`, every `fastInterval`
|
|
16
|
+
* after `accelerateAfter`; `stop` ends it (also done on unmount).
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* const repeat = usePressRepeat();
|
|
21
|
+
* <Pressable
|
|
22
|
+
* onPressIn={() => repeat.start(() => setCount((n) => n + 1))}
|
|
23
|
+
* onPressOut={repeat.stop}
|
|
24
|
+
* />
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function usePressRepeat({ initialDelay, interval, accelerateAfter, fastInterval, }?: PressRepeatTimings): {
|
|
28
|
+
start: (action: () => void) => void;
|
|
29
|
+
stop: () => void;
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=usePressRepeat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePressRepeat.d.ts","sourceRoot":"","sources":["../../src/hooks/usePressRepeat.ts"],"names":[],"mappings":"AAEA,0DAA0D;AAC1D,MAAM,WAAW,kBAAkB;IACjC,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAcD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,EAC7B,YAAkB,EAClB,QAAc,EACd,eAAsB,EACtB,YAAiB,GAClB,GAAE,kBAAuB;oBAeD,MAAM,IAAI;;EAclC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
function clearTimers(timers) {
|
|
3
|
+
clearTimeout(timers.delay);
|
|
4
|
+
clearInterval(timers.repeat);
|
|
5
|
+
clearTimeout(timers.accelerate);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Repeats an action while a button is held, faster and faster — the way a
|
|
9
|
+
* counter's −/+ reach large values quickly. `start` runs the action at once,
|
|
10
|
+
* then again every `interval` after `initialDelay`, every `fastInterval`
|
|
11
|
+
* after `accelerateAfter`; `stop` ends it (also done on unmount).
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const repeat = usePressRepeat();
|
|
16
|
+
* <Pressable
|
|
17
|
+
* onPressIn={() => repeat.start(() => setCount((n) => n + 1))}
|
|
18
|
+
* onPressOut={repeat.stop}
|
|
19
|
+
* />
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function usePressRepeat({ initialDelay = 450, interval = 180, accelerateAfter = 1200, fastInterval = 60, } = {}) {
|
|
23
|
+
const timersRef = useRef({});
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const timers = timersRef;
|
|
26
|
+
return () => {
|
|
27
|
+
clearTimers(timers.current);
|
|
28
|
+
};
|
|
29
|
+
}, []);
|
|
30
|
+
function stop() {
|
|
31
|
+
clearTimers(timersRef.current);
|
|
32
|
+
timersRef.current = {};
|
|
33
|
+
}
|
|
34
|
+
function start(action) {
|
|
35
|
+
stop();
|
|
36
|
+
action();
|
|
37
|
+
const timers = timersRef.current;
|
|
38
|
+
timers.delay = setTimeout(() => {
|
|
39
|
+
timers.repeat = setInterval(action, interval);
|
|
40
|
+
timers.accelerate = setTimeout(() => {
|
|
41
|
+
clearInterval(timers.repeat);
|
|
42
|
+
timers.repeat = setInterval(action, fastInterval);
|
|
43
|
+
}, accelerateAfter);
|
|
44
|
+
}, initialDelay);
|
|
45
|
+
}
|
|
46
|
+
return { start, stop };
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forthtilliath/react-native-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -103,8 +103,8 @@
|
|
|
103
103
|
"typescript": "^5.9.3",
|
|
104
104
|
"vitest": "^5.0.2",
|
|
105
105
|
"@forthtilliath/eslint-config": "0.9.0",
|
|
106
|
-
"@forthtilliath/
|
|
107
|
-
"@forthtilliath/
|
|
106
|
+
"@forthtilliath/expo-release-updates-ui": "^0.4.1",
|
|
107
|
+
"@forthtilliath/typescript-config": "0.1.1"
|
|
108
108
|
},
|
|
109
109
|
"size-limit": [
|
|
110
110
|
{
|