@ecohouse/ui 0.1.60 → 0.1.62
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/dist/index.cjs +345 -115
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -4
- package/dist/index.d.ts +16 -4
- package/dist/index.js +344 -117
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Checkbox/Checkbox.tsx +6 -0
- package/src/components/Counter/Counter.test.ts +38 -0
- package/src/components/Counter/Counter.tsx +7 -10
- package/src/components/Counter/Counter.utils.ts +28 -0
- package/src/components/DatePicker/DatePicker.tsx +5 -14
- package/src/components/Drawer/Drawer.tsx +15 -12
- package/src/components/Modal/Modal.tsx +31 -11
- package/src/components/RadioButton/RadioButton.tsx +14 -0
- package/src/components/Select/Select.tsx +16 -8
- package/src/components/Toggle/Toggle.tsx +6 -0
- package/src/components/VerificationCodeInput/VerificationCodeInput.test.ts +60 -12
- package/src/components/VerificationCodeInput/VerificationCodeInput.tsx +29 -24
- package/src/components/VerificationCodeInput/VerificationCodeInput.utils.ts +53 -13
- package/src/icons/PaymentCard/PaymentCardIcon.tsx +14 -0
- package/src/icons/PaymentCard/PaymentCardIcon.web.tsx +13 -0
- package/src/icons/PaymentCard/index.ts +1 -0
- package/src/icons/PaymentCard/paymentCardPath.ts +3 -0
- package/src/icons/Receipt/ReceiptIcon.tsx +14 -0
- package/src/icons/Receipt/ReceiptIcon.web.tsx +13 -0
- package/src/icons/Receipt/index.ts +1 -0
- package/src/icons/Receipt/receiptPath.ts +3 -0
- package/src/icons/index.ts +2 -0
- package/src/icons/registry.ts +4 -0
- package/src/index.ts +8 -1
- package/src/theme/typography.ts +2 -2
- package/src/utils/dateUtils.test.ts +72 -0
- package/src/utils/dateUtils.ts +30 -2
- package/src/utils/focusTrap.test.ts +26 -0
- package/src/utils/focusTrap.ts +39 -0
- package/src/utils/overlayStack.test.ts +175 -0
- package/src/utils/overlayStack.ts +105 -0
- package/src/utils/useHydrated.test.ts +14 -0
- package/src/utils/useHydrated.ts +14 -0
- package/src/utils/useOverlayLayer.ts +41 -0
- package/src/utils/useWebFocusTrap.ts +68 -0
- package/src/utils/webToggleKey.test.ts +49 -0
- package/src/utils/webToggleKey.ts +23 -0
|
@@ -22,7 +22,13 @@ import {
|
|
|
22
22
|
} from "react-native";
|
|
23
23
|
import { colors, fonts } from "../../theme";
|
|
24
24
|
import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
isVerificationCodeComplete,
|
|
27
|
+
reconcileVerificationCells,
|
|
28
|
+
toVerificationCells,
|
|
29
|
+
updateVerificationCells,
|
|
30
|
+
verificationCellsToCode,
|
|
31
|
+
} from "./VerificationCodeInput.utils";
|
|
26
32
|
|
|
27
33
|
const DEFAULT_LENGTH = 6;
|
|
28
34
|
const DESKTOP_CELL_WIDTH = 54.333335876464844;
|
|
@@ -85,17 +91,18 @@ export const VerificationCodeInput = forwardRef<
|
|
|
85
91
|
) {
|
|
86
92
|
const safeLength = Math.max(1, Math.floor(length));
|
|
87
93
|
const isControlled = typeof value === "string";
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
);
|
|
94
|
+
// Fixed-length cells, so clearing a middle cell leaves a gap instead of shifting digits.
|
|
95
|
+
const [cellsState, setCellsState] = useState(() => toVerificationCells(defaultValue, safeLength));
|
|
91
96
|
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
|
92
97
|
const { width: viewportWidth } = useWindowDimensions();
|
|
93
98
|
const rootRef = useRef<ComponentRef<typeof View>>(null);
|
|
94
99
|
const inputRefs = useRef<Array<ComponentRef<typeof TextInput> | null>>([]);
|
|
95
|
-
const
|
|
96
|
-
|
|
100
|
+
const cells = reconcileVerificationCells(
|
|
101
|
+
cellsState,
|
|
102
|
+
isControlled ? value : verificationCellsToCode(cellsState),
|
|
97
103
|
safeLength,
|
|
98
104
|
);
|
|
105
|
+
const currentCode = verificationCellsToCode(cells);
|
|
99
106
|
|
|
100
107
|
useApplyWebClassName(rootRef, className);
|
|
101
108
|
|
|
@@ -108,37 +115,35 @@ export const VerificationCodeInput = forwardRef<
|
|
|
108
115
|
);
|
|
109
116
|
|
|
110
117
|
const commit = useCallback(
|
|
111
|
-
(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
setUncontrolledValue(normalized);
|
|
116
|
-
}
|
|
118
|
+
(nextCells: string[]) => {
|
|
119
|
+
// Kept in controlled mode too, so gaps survive while the parent echoes the value back.
|
|
120
|
+
setCellsState(nextCells);
|
|
121
|
+
const code = verificationCellsToCode(nextCells);
|
|
117
122
|
|
|
118
|
-
onValueChange?.(
|
|
123
|
+
onValueChange?.(code);
|
|
119
124
|
|
|
120
|
-
if (
|
|
121
|
-
onComplete?.(
|
|
125
|
+
if (isVerificationCodeComplete(nextCells)) {
|
|
126
|
+
onComplete?.(code);
|
|
122
127
|
}
|
|
123
128
|
},
|
|
124
|
-
[
|
|
129
|
+
[onComplete, onValueChange],
|
|
125
130
|
);
|
|
126
131
|
|
|
127
132
|
useImperativeHandle(
|
|
128
133
|
forwardedRef,
|
|
129
134
|
() => ({
|
|
130
135
|
clear: () => {
|
|
131
|
-
commit("");
|
|
136
|
+
commit(toVerificationCells("", safeLength));
|
|
132
137
|
focus(0);
|
|
133
138
|
},
|
|
134
139
|
focus,
|
|
135
140
|
}),
|
|
136
|
-
[commit, focus],
|
|
141
|
+
[commit, focus, safeLength],
|
|
137
142
|
);
|
|
138
143
|
|
|
139
144
|
const handleChange = (index: number, input: string) => {
|
|
140
|
-
const update =
|
|
141
|
-
commit(update.
|
|
145
|
+
const update = updateVerificationCells(cells, index, input, safeLength);
|
|
146
|
+
commit(update.cells);
|
|
142
147
|
focus(update.nextFocus);
|
|
143
148
|
};
|
|
144
149
|
|
|
@@ -148,9 +153,9 @@ export const VerificationCodeInput = forwardRef<
|
|
|
148
153
|
) => {
|
|
149
154
|
const key = event.nativeEvent.key;
|
|
150
155
|
|
|
151
|
-
if (key === "Backspace" && !
|
|
152
|
-
const update =
|
|
153
|
-
commit(update.
|
|
156
|
+
if (key === "Backspace" && !cells[index] && index > 0) {
|
|
157
|
+
const update = updateVerificationCells(cells, index - 1, "", safeLength);
|
|
158
|
+
commit(update.cells);
|
|
154
159
|
focus(index - 1);
|
|
155
160
|
return;
|
|
156
161
|
}
|
|
@@ -223,7 +228,7 @@ export const VerificationCodeInput = forwardRef<
|
|
|
223
228
|
Platform.OS === "android" ? styles.cellAndroid : null,
|
|
224
229
|
inputStyle,
|
|
225
230
|
]}
|
|
226
|
-
value={
|
|
231
|
+
value={cells[index] ?? ""}
|
|
227
232
|
/>
|
|
228
233
|
);
|
|
229
234
|
})}
|
|
@@ -2,33 +2,73 @@ export function normalizeVerificationCode(value: string, length: number): string
|
|
|
2
2
|
return value.replace(/\D/g, "").slice(0, Math.max(1, length));
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/** Fills cells from the start of `code`; one entry per cell, `""` marks an empty cell. */
|
|
6
|
+
export function toVerificationCells(code: string, length: number): string[] {
|
|
7
|
+
const safeLength = Math.max(1, length);
|
|
8
|
+
const digits = normalizeVerificationCode(code, safeLength);
|
|
9
|
+
return Array.from({ length: safeLength }, (_, index) => digits[index] ?? "");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** The code reported to consumers: the entered digits in cell order, gaps skipped. */
|
|
13
|
+
export function verificationCellsToCode(cells: readonly string[]): string {
|
|
14
|
+
return cells.join("");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isVerificationCodeComplete(cells: readonly string[]): boolean {
|
|
18
|
+
return cells.length > 0 && cells.every((cell) => cell !== "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Keeps the displayed cells, including gaps, while they still produce `code`;
|
|
23
|
+
* otherwise (a new value from the parent, or a new length) lays `code` out again.
|
|
24
|
+
*/
|
|
25
|
+
export function reconcileVerificationCells(
|
|
26
|
+
cells: readonly string[],
|
|
27
|
+
code: string,
|
|
28
|
+
length: number,
|
|
29
|
+
): string[] {
|
|
30
|
+
const safeLength = Math.max(1, length);
|
|
31
|
+
const normalized = normalizeVerificationCode(code, safeLength);
|
|
32
|
+
if (cells.length === safeLength && verificationCellsToCode(cells) === normalized) {
|
|
33
|
+
return [...cells];
|
|
34
|
+
}
|
|
35
|
+
return toVerificationCells(normalized, safeLength);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface VerificationCellsUpdate {
|
|
39
|
+
cells: string[];
|
|
7
40
|
nextFocus: number;
|
|
8
41
|
}
|
|
9
42
|
|
|
10
|
-
|
|
11
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Applies input typed or pasted into cell `index`. Clearing a cell leaves a gap
|
|
45
|
+
* instead of shifting later digits left; pasted digits fill forward from `index`.
|
|
46
|
+
*/
|
|
47
|
+
export function updateVerificationCells(
|
|
48
|
+
currentCells: readonly string[],
|
|
12
49
|
index: number,
|
|
13
50
|
input: string,
|
|
14
51
|
length: number,
|
|
15
|
-
):
|
|
52
|
+
): VerificationCellsUpdate {
|
|
16
53
|
const safeLength = Math.max(1, length);
|
|
17
54
|
const safeIndex = Math.min(Math.max(0, index), safeLength - 1);
|
|
18
|
-
const
|
|
55
|
+
const cells = Array.from({ length: safeLength }, (_, cellIndex) => {
|
|
56
|
+
const cell = currentCells[cellIndex] ?? "";
|
|
57
|
+
return /^\d$/.test(cell) ? cell : "";
|
|
58
|
+
});
|
|
19
59
|
const insertedDigits = normalizeVerificationCode(input, safeLength - safeIndex);
|
|
20
60
|
|
|
21
61
|
if (!insertedDigits) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
nextFocus: safeIndex,
|
|
25
|
-
};
|
|
62
|
+
cells[safeIndex] = "";
|
|
63
|
+
return { cells, nextFocus: safeIndex };
|
|
26
64
|
}
|
|
27
65
|
|
|
66
|
+
Array.from(insertedDigits).forEach((digit, offset) => {
|
|
67
|
+
cells[safeIndex + offset] = digit;
|
|
68
|
+
});
|
|
69
|
+
|
|
28
70
|
return {
|
|
29
|
-
|
|
30
|
-
safeIndex + insertedDigits.length,
|
|
31
|
-
)}`.slice(0, safeLength),
|
|
71
|
+
cells,
|
|
32
72
|
nextFocus: Math.min(safeIndex + insertedDigits.length, safeLength - 1),
|
|
33
73
|
};
|
|
34
74
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import Svg, { Path } from "react-native-svg";
|
|
2
|
+
import { colors } from "../../theme";
|
|
3
|
+
import type { IconProps } from "../types";
|
|
4
|
+
import { PAYMENT_CARD_PATH } from "./paymentCardPath";
|
|
5
|
+
|
|
6
|
+
export function PaymentCardIcon({ size = 16, color = colors.grey100 }: IconProps) {
|
|
7
|
+
return (
|
|
8
|
+
<Svg width={size} height={size} viewBox="0 0 16 16" fill="none">
|
|
9
|
+
<Path d={PAYMENT_CARD_PATH} fill={color} />
|
|
10
|
+
</Svg>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { PAYMENT_CARD_PATH } from "./paymentCardPath";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { colors } from "../../theme";
|
|
2
|
+
import type { IconProps } from "../types";
|
|
3
|
+
import { PAYMENT_CARD_PATH } from "./paymentCardPath";
|
|
4
|
+
|
|
5
|
+
export function PaymentCardIcon({ size = 16, color = colors.grey100 }: IconProps) {
|
|
6
|
+
return (
|
|
7
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden>
|
|
8
|
+
<path d={PAYMENT_CARD_PATH} fill={color} />
|
|
9
|
+
</svg>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { PAYMENT_CARD_PATH } from "./paymentCardPath";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PaymentCardIcon, PAYMENT_CARD_PATH } from "./PaymentCardIcon";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Filled payment card icon path — 16×16 viewBox. */
|
|
2
|
+
export const PAYMENT_CARD_PATH =
|
|
3
|
+
"M4.66536 10H6.66536C6.84218 10 7.01174 9.9298 7.13677 9.80478C7.26179 9.67975 7.33203 9.51019 7.33203 9.33337C7.33203 9.15656 7.26179 8.98699 7.13677 8.86197C7.01174 8.73695 6.84218 8.66671 6.66536 8.66671H4.66536C4.48855 8.66671 4.31898 8.73695 4.19396 8.86197C4.06894 8.98699 3.9987 9.15656 3.9987 9.33337C3.9987 9.51019 4.06894 9.67975 4.19396 9.80478C4.31898 9.9298 4.48855 10 4.66536 10ZM12.6654 3.33337H3.33203C2.8016 3.33337 2.29289 3.54409 1.91782 3.91916C1.54274 4.29423 1.33203 4.80294 1.33203 5.33337V11.3334C1.33203 11.8638 1.54274 12.3725 1.91782 12.7476C2.29289 13.1227 2.8016 13.3334 3.33203 13.3334H12.6654C13.1958 13.3334 13.7045 13.1227 14.0796 12.7476C14.4546 12.3725 14.6654 11.8638 14.6654 11.3334V5.33337C14.6654 4.80294 14.4546 4.29423 14.0796 3.91916C13.7045 3.54409 13.1958 3.33337 12.6654 3.33337ZM13.332 11.3334C13.332 11.5102 13.2618 11.6798 13.1368 11.8048C13.0117 11.9298 12.8422 12 12.6654 12H3.33203C3.15522 12 2.98565 11.9298 2.86063 11.8048C2.7356 11.6798 2.66536 11.5102 2.66536 11.3334V7.33337H13.332V11.3334ZM13.332 6.00004H2.66536V5.33337C2.66536 5.15656 2.7356 4.98699 2.86063 4.86197C2.98565 4.73695 3.15522 4.66671 3.33203 4.66671H12.6654C12.8422 4.66671 13.0117 4.73695 13.1368 4.86197C13.2618 4.98699 13.332 5.15656 13.332 5.33337V6.00004Z";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import Svg, { Path } from "react-native-svg";
|
|
2
|
+
import { colors } from "../../theme";
|
|
3
|
+
import type { IconProps } from "../types";
|
|
4
|
+
import { RECEIPT_PATH } from "./receiptPath";
|
|
5
|
+
|
|
6
|
+
export function ReceiptIcon({ size = 16, color = colors.grey100 }: IconProps) {
|
|
7
|
+
return (
|
|
8
|
+
<Svg width={size} height={size} viewBox="0 0 16 16" fill="none">
|
|
9
|
+
<Path d={RECEIPT_PATH} fill={color} />
|
|
10
|
+
</Svg>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { RECEIPT_PATH } from "./receiptPath";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { colors } from "../../theme";
|
|
2
|
+
import type { IconProps } from "../types";
|
|
3
|
+
import { RECEIPT_PATH } from "./receiptPath";
|
|
4
|
+
|
|
5
|
+
export function ReceiptIcon({ size = 16, color = colors.grey100 }: IconProps) {
|
|
6
|
+
return (
|
|
7
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden>
|
|
8
|
+
<path d={RECEIPT_PATH} fill={color} />
|
|
9
|
+
</svg>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { RECEIPT_PATH } from "./receiptPath";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ReceiptIcon, RECEIPT_PATH } from "./ReceiptIcon";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Filled receipt icon path — 16×16 viewBox. */
|
|
2
|
+
export const RECEIPT_PATH =
|
|
3
|
+
"M6.66667 5.33337H10.6667C10.8435 5.33337 11.013 5.26314 11.1381 5.13811C11.2631 5.01309 11.3333 4.84352 11.3333 4.66671C11.3333 4.4899 11.2631 4.32033 11.1381 4.1953C11.013 4.07028 10.8435 4.00004 10.6667 4.00004H6.66667C6.48986 4.00004 6.32029 4.07028 6.19526 4.1953C6.07024 4.32033 6 4.4899 6 4.66671C6 4.84352 6.07024 5.01309 6.19526 5.13811C6.32029 5.26314 6.48986 5.33337 6.66667 5.33337ZM5.33333 8.00004H10.6667C10.8435 8.00004 11.013 7.9298 11.1381 7.80478C11.2631 7.67975 11.3333 7.51019 11.3333 7.33337C11.3333 7.15656 11.2631 6.98699 11.1381 6.86197C11.013 6.73695 10.8435 6.66671 10.6667 6.66671H5.33333C5.15652 6.66671 4.98695 6.73695 4.86193 6.86197C4.7369 6.98699 4.66667 7.15656 4.66667 7.33337C4.66667 7.51019 4.7369 7.67975 4.86193 7.80478C4.98695 7.9298 5.15652 8.00004 5.33333 8.00004ZM5.33333 10.6667H10.6667C10.8435 10.6667 11.013 10.5965 11.1381 10.4714C11.2631 10.3464 11.3333 10.1769 11.3333 10C11.3333 9.82323 11.2631 9.65366 11.1381 9.52864C11.013 9.40361 10.8435 9.33337 10.6667 9.33337H5.33333C5.15652 9.33337 4.98695 9.40361 4.86193 9.52864C4.7369 9.65366 4.66667 9.82323 4.66667 10C4.66667 10.1769 4.7369 10.3464 4.86193 10.4714C4.98695 10.5965 5.15652 10.6667 5.33333 10.6667ZM13.3333 1.33337H2.66667C2.48986 1.33337 2.32029 1.40361 2.19526 1.52864C2.07024 1.65366 2 1.82323 2 2.00004V14C2 14.1238 2.03448 14.2452 2.09957 14.3505C2.16466 14.4558 2.25779 14.541 2.36852 14.5963C2.47926 14.6517 2.60323 14.6751 2.72654 14.664C2.84984 14.6529 2.96762 14.6077 3.06667 14.5334L4.44667 13.5L5.82 14.5334C5.9354 14.6199 6.07575 14.6667 6.22 14.6667C6.36425 14.6667 6.5046 14.6199 6.62 14.5334L8 13.5L9.38 14.5334C9.4954 14.6199 9.63575 14.6667 9.78 14.6667C9.92425 14.6667 10.0646 14.6199 10.18 14.5334L11.5533 13.5L12.9333 14.5334C13.0329 14.6075 13.1511 14.6525 13.2748 14.6631C13.3985 14.6737 13.5226 14.6495 13.6333 14.5934C13.7433 14.538 13.8357 14.4532 13.9005 14.3485C13.9652 14.2438 13.9996 14.1232 14 14V2.00004C14 1.82323 13.9298 1.65366 13.8047 1.52864C13.6797 1.40361 13.5101 1.33337 13.3333 1.33337ZM12.6667 12.6667L11.9533 12.1334C11.8379 12.0468 11.6976 12 11.5533 12C11.4091 12 11.2687 12.0468 11.1533 12.1334L9.78 13.1667L8.4 12.1334C8.2846 12.0468 8.14425 12 8 12C7.85575 12 7.7154 12.0468 7.6 12.1334L6.22 13.1667L4.84667 12.1334C4.73127 12.0468 4.59091 12 4.44667 12C4.30242 12 4.16206 12.0468 4.04667 12.1334L3.33333 12.6667V2.66671H12.6667V12.6667Z";
|
package/src/icons/index.ts
CHANGED
|
@@ -102,9 +102,11 @@ export { NotificationQrCodeIcon } from "./NotificationQrCode";
|
|
|
102
102
|
export { NotificationSuccessIcon } from "./NotificationSuccess";
|
|
103
103
|
export { NotificationWarningIcon } from "./NotificationWarning";
|
|
104
104
|
export { PhoneIcon } from "./Phone";
|
|
105
|
+
export { PaymentCardIcon } from "./PaymentCard";
|
|
105
106
|
export { PlusIcon } from "./Plus";
|
|
106
107
|
export { MinusIcon } from "./Minus";
|
|
107
108
|
export { RefreshIcon } from "./Refresh";
|
|
109
|
+
export { ReceiptIcon } from "./Receipt";
|
|
108
110
|
export { RefundStatusIcon } from "./RefundStatus";
|
|
109
111
|
export { SearchIcon } from "./Search";
|
|
110
112
|
export { SettingsIcon } from "./Settings";
|
package/src/icons/registry.ts
CHANGED
|
@@ -90,8 +90,10 @@ import { NotificationQrCodeIcon } from "./NotificationQrCode";
|
|
|
90
90
|
import { NotificationSuccessIcon } from "./NotificationSuccess";
|
|
91
91
|
import { NotificationWarningIcon } from "./NotificationWarning";
|
|
92
92
|
import { PhoneIcon } from "./Phone";
|
|
93
|
+
import { PaymentCardIcon } from "./PaymentCard";
|
|
93
94
|
import { PlusIcon } from "./Plus";
|
|
94
95
|
import { RefreshIcon } from "./Refresh";
|
|
96
|
+
import { ReceiptIcon } from "./Receipt";
|
|
95
97
|
import { RefundStatusIcon } from "./RefundStatus";
|
|
96
98
|
import { SearchIcon } from "./Search";
|
|
97
99
|
import { SettingsIcon } from "./Settings";
|
|
@@ -191,12 +193,14 @@ export const icons = {
|
|
|
191
193
|
notificationSuccess: NotificationSuccessIcon,
|
|
192
194
|
notificationWarning: NotificationWarningIcon,
|
|
193
195
|
paw: PawIcon,
|
|
196
|
+
paymentCard: PaymentCardIcon,
|
|
194
197
|
phone: PhoneIcon,
|
|
195
198
|
qrCode: QrCodeIcon,
|
|
196
199
|
quietHours: QuietHoursIcon,
|
|
197
200
|
ratingStar: RatingStarIcon,
|
|
198
201
|
plus: PlusIcon,
|
|
199
202
|
refresh: RefreshIcon,
|
|
203
|
+
receipt: ReceiptIcon,
|
|
200
204
|
refundStatus: RefundStatusIcon,
|
|
201
205
|
search: SearchIcon,
|
|
202
206
|
saveHeart: SaveHeartIcon,
|
package/src/index.ts
CHANGED
|
@@ -225,8 +225,10 @@ export {
|
|
|
225
225
|
NotificationSuccessIcon,
|
|
226
226
|
NotificationWarningIcon,
|
|
227
227
|
PhoneIcon,
|
|
228
|
+
PaymentCardIcon,
|
|
228
229
|
PlusIcon,
|
|
229
230
|
RefreshIcon,
|
|
231
|
+
ReceiptIcon,
|
|
230
232
|
RefundStatusIcon,
|
|
231
233
|
SearchIcon,
|
|
232
234
|
SettingsIcon,
|
|
@@ -291,6 +293,11 @@ export {
|
|
|
291
293
|
} from "./theme";
|
|
292
294
|
export type { GreyStep } from "./theme";
|
|
293
295
|
|
|
294
|
-
export {
|
|
296
|
+
export {
|
|
297
|
+
abbreviateMonthName,
|
|
298
|
+
abbreviateMonthNames,
|
|
299
|
+
formatDate,
|
|
300
|
+
formatDateDayShortMonth,
|
|
301
|
+
} from "./utils/dateUtils";
|
|
295
302
|
export { resolvePopoverAlign } from "./utils/popoverAlign";
|
|
296
303
|
export type { PopoverAlign, ResolvePopoverAlignOptions } from "./utils/popoverAlign";
|
package/src/theme/typography.ts
CHANGED
|
@@ -47,12 +47,12 @@ export const segmentedToggleTypography = {
|
|
|
47
47
|
letterSpacing: 0,
|
|
48
48
|
} as const;
|
|
49
49
|
|
|
50
|
-
/** Language switcher trigger and option labels — SemiBold 14, tracking 3%. */
|
|
50
|
+
/** Language switcher trigger and option labels — SemiBold 14, 18px line-height, tracking 3%. */
|
|
51
51
|
export const languageSwitcherTypography = {
|
|
52
52
|
fontFamily: fonts.sans,
|
|
53
53
|
fontSize: 14,
|
|
54
54
|
fontWeight: "600" as const,
|
|
55
|
-
lineHeight:
|
|
55
|
+
lineHeight: 18,
|
|
56
56
|
letterSpacing: 0.42,
|
|
57
57
|
} as const;
|
|
58
58
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { abbreviateMonthNames, formatDateDayShortMonth } from "./dateUtils";
|
|
3
|
+
|
|
4
|
+
const ARMENIAN_MONTHS = [
|
|
5
|
+
"Հունվար",
|
|
6
|
+
"Փետրվար",
|
|
7
|
+
"Մարտ",
|
|
8
|
+
"Ապրիլ",
|
|
9
|
+
"Մայիս",
|
|
10
|
+
"Հունիս",
|
|
11
|
+
"Հուլիս",
|
|
12
|
+
"Օգոստոս",
|
|
13
|
+
"Սեպտեմբեր",
|
|
14
|
+
"Հոկտեմբեր",
|
|
15
|
+
"Նոյեմբեր",
|
|
16
|
+
"Դեկտեմբեր",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const ENGLISH_MONTHS = [
|
|
20
|
+
"January",
|
|
21
|
+
"February",
|
|
22
|
+
"March",
|
|
23
|
+
"April",
|
|
24
|
+
"May",
|
|
25
|
+
"June",
|
|
26
|
+
"July",
|
|
27
|
+
"August",
|
|
28
|
+
"September",
|
|
29
|
+
"October",
|
|
30
|
+
"November",
|
|
31
|
+
"December",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
describe("abbreviateMonthNames", () => {
|
|
35
|
+
it("gives Armenian January and June distinct abbreviations", () => {
|
|
36
|
+
const abbreviations = abbreviateMonthNames(ARMENIAN_MONTHS);
|
|
37
|
+
|
|
38
|
+
expect(abbreviations[0]).toBe("Հունվ");
|
|
39
|
+
expect(abbreviations[5]).toBe("Հունի");
|
|
40
|
+
expect(abbreviations[6]).toBe("Հուլ");
|
|
41
|
+
expect(new Set(abbreviations).size).toBe(12);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("keeps three letters when they are already unique", () => {
|
|
45
|
+
expect(abbreviateMonthNames(ENGLISH_MONTHS)).toEqual([
|
|
46
|
+
"Jan",
|
|
47
|
+
"Feb",
|
|
48
|
+
"Mar",
|
|
49
|
+
"Apr",
|
|
50
|
+
"May",
|
|
51
|
+
"Jun",
|
|
52
|
+
"Jul",
|
|
53
|
+
"Aug",
|
|
54
|
+
"Sep",
|
|
55
|
+
"Oct",
|
|
56
|
+
"Nov",
|
|
57
|
+
"Dec",
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("stops at the full name when two names are identical", () => {
|
|
62
|
+
expect(abbreviateMonthNames(["Same", "Same"])).toEqual(["Same", "Same"]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("formatDateDayShortMonth", () => {
|
|
67
|
+
it("formats Armenian January and June unambiguously", () => {
|
|
68
|
+
expect(formatDateDayShortMonth(new Date(2026, 0, 15), ARMENIAN_MONTHS)).toBe("15 Հունվ 2026");
|
|
69
|
+
expect(formatDateDayShortMonth(new Date(2026, 5, 15), ARMENIAN_MONTHS)).toBe("15 Հունի 2026");
|
|
70
|
+
expect(formatDateDayShortMonth(new Date(2026, 6, 15), ARMENIAN_MONTHS)).toBe("15 Հուլ 2026");
|
|
71
|
+
});
|
|
72
|
+
});
|
package/src/utils/dateUtils.ts
CHANGED
|
@@ -76,9 +76,37 @@ export function abbreviateMonthName(monthName: string, letterCount = 3): string
|
|
|
76
76
|
return chars.slice(0, end).join("");
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Abbreviates every month name to at least `letterCount` letters, adding letters
|
|
81
|
+
* where two months would otherwise share an abbreviation: Armenian
|
|
82
|
+
* «Հունվար» / «Հունիս» become «Հունվ» / «Հունի» instead of both «Հուն».
|
|
83
|
+
*/
|
|
84
|
+
export function abbreviateMonthNames(monthNames: readonly string[], letterCount = 3): string[] {
|
|
85
|
+
return monthNames.map((name, index) => {
|
|
86
|
+
const maxLetters = abbreviatedLetterCount(name);
|
|
87
|
+
let count = letterCount;
|
|
88
|
+
while (
|
|
89
|
+
count < maxLetters &&
|
|
90
|
+
monthNames.some(
|
|
91
|
+
(other, otherIndex) =>
|
|
92
|
+
otherIndex !== index &&
|
|
93
|
+
abbreviateMonthName(other, count) === abbreviateMonthName(name, count),
|
|
94
|
+
)
|
|
95
|
+
) {
|
|
96
|
+
count += 1;
|
|
97
|
+
}
|
|
98
|
+
return abbreviateMonthName(name, count);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Letter count of `monthName`, counting the Armenian digraph «ու» as one letter. */
|
|
103
|
+
function abbreviatedLetterCount(monthName: string): number {
|
|
104
|
+
return Array.from(monthName.replace(/ու/g, "u")).length;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** "15 Հուլ 2026" — day + unambiguous abbreviated month + year for range triggers. */
|
|
80
108
|
export function formatDateDayShortMonth(date: Date, monthNames: string[], letterCount = 3): string {
|
|
81
|
-
const month =
|
|
109
|
+
const month = abbreviateMonthNames(monthNames, letterCount)[date.getMonth()] ?? "";
|
|
82
110
|
return `${date.getDate()} ${month} ${date.getFullYear()}`;
|
|
83
111
|
}
|
|
84
112
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getWrappedFocusIndex } from "./focusTrap";
|
|
3
|
+
|
|
4
|
+
describe("getWrappedFocusIndex", () => {
|
|
5
|
+
it("wraps Tab from the last element to the first", () => {
|
|
6
|
+
expect(getWrappedFocusIndex(3, 2, false)).toBe(0);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("wraps Shift+Tab from the first element to the last", () => {
|
|
10
|
+
expect(getWrappedFocusIndex(3, 0, true)).toBe(2);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("leaves Tab between inner elements to the browser", () => {
|
|
14
|
+
expect(getWrappedFocusIndex(3, 1, false)).toBeNull();
|
|
15
|
+
expect(getWrappedFocusIndex(3, 1, true)).toBeNull();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("enters from the dialog container itself in either direction", () => {
|
|
19
|
+
expect(getWrappedFocusIndex(3, -1, false)).toBe(0);
|
|
20
|
+
expect(getWrappedFocusIndex(3, -1, true)).toBe(2);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("has nothing to move to without focusable content", () => {
|
|
24
|
+
expect(getWrappedFocusIndex(0, -1, false)).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const TABBABLE_SELECTOR = [
|
|
2
|
+
"a[href]",
|
|
3
|
+
"area[href]",
|
|
4
|
+
"button",
|
|
5
|
+
"input:not([type='hidden'])",
|
|
6
|
+
"select",
|
|
7
|
+
"textarea",
|
|
8
|
+
"iframe",
|
|
9
|
+
"[contenteditable='true']",
|
|
10
|
+
"[tabindex]",
|
|
11
|
+
].join(",");
|
|
12
|
+
|
|
13
|
+
/** Focusable elements inside `container` that Tab can reach, in DOM order. */
|
|
14
|
+
export function getTabbableElements(container: HTMLElement): HTMLElement[] {
|
|
15
|
+
return Array.from(container.querySelectorAll<HTMLElement>(TABBABLE_SELECTOR)).filter(
|
|
16
|
+
(element) =>
|
|
17
|
+
element.tabIndex >= 0 &&
|
|
18
|
+
!element.hasAttribute("disabled") &&
|
|
19
|
+
element.getAttribute("aria-hidden") !== "true" &&
|
|
20
|
+
element.getClientRects().length > 0,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Index to move focus to when Tab would leave the trap, or `null` when the
|
|
26
|
+
* browser's own Tab order already stays inside. `currentIndex` is `-1` when
|
|
27
|
+
* focus is on the container itself (or outside it).
|
|
28
|
+
*/
|
|
29
|
+
export function getWrappedFocusIndex(
|
|
30
|
+
count: number,
|
|
31
|
+
currentIndex: number,
|
|
32
|
+
backwards: boolean,
|
|
33
|
+
): number | null {
|
|
34
|
+
if (count === 0) return null;
|
|
35
|
+
if (currentIndex === -1) return backwards ? count - 1 : 0;
|
|
36
|
+
if (backwards && currentIndex === 0) return count - 1;
|
|
37
|
+
if (!backwards && currentIndex === count - 1) return 0;
|
|
38
|
+
return null;
|
|
39
|
+
}
|