@kanda-libs/ks-component-ts 0.3.56 → 0.3.57

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kanda-libs/ks-component-ts",
3
- "version": "0.3.56",
3
+ "version": "0.3.57",
4
4
  "description": "Kanda form component library",
5
5
  "main": "dist/index.esm.js",
6
6
  "module": "dist/index.esm.js",
@@ -4,6 +4,8 @@ import { DefaultFormFieldProps } from "~/field/types";
4
4
  import useRangeInputProps from "./useRangeInputProps";
5
5
  import { defaultFormatter } from "./helpers";
6
6
 
7
+ export type TieStepsTo = "min" | "max";
8
+
7
9
  export interface RangeInputUncontrolledProps
8
10
  extends InputHTMLAttributes<HTMLInputElement> {
9
11
  min?: string;
@@ -14,6 +16,7 @@ export interface RangeInputUncontrolledProps
14
16
  highlightLabel?: "min" | "max";
15
17
  showCurrentValueLabel?: boolean;
16
18
  suffix?: string;
19
+ tieStepsTo?: TieStepsTo;
17
20
  }
18
21
 
19
22
  const RangeInputUncontrolled: FunctionComponent<
@@ -30,22 +33,34 @@ const RangeInputUncontrolled: FunctionComponent<
30
33
  prefix = "",
31
34
  suffix = "",
32
35
  name = "",
36
+ tieStepsTo = "max",
33
37
  highlightLabel,
34
38
  showCurrentValueLabel = true,
35
39
  ...restProps
36
40
  }) {
37
- const { ref, inputProps, minLabel, maxLabel, currentLabel, classNames } =
38
- useRangeInputProps(
39
- name,
40
- min,
41
- max,
42
- steps,
43
- formatter,
44
- prefix,
45
- suffix,
46
- error,
47
- highlightLabel
48
- );
41
+ const {
42
+ ref,
43
+ inputProps,
44
+ minLabel,
45
+ maxLabel,
46
+ currentLabel,
47
+ classNames,
48
+ sliderName,
49
+ onSliderChange,
50
+ rawValue,
51
+ value,
52
+ } = useRangeInputProps(
53
+ name,
54
+ min,
55
+ max,
56
+ steps,
57
+ tieStepsTo,
58
+ formatter,
59
+ prefix,
60
+ suffix,
61
+ error,
62
+ highlightLabel
63
+ );
49
64
 
50
65
  return (
51
66
  <div className={classNames.container}>
@@ -66,11 +81,22 @@ const RangeInputUncontrolled: FunctionComponent<
66
81
  type="range"
67
82
  ref={forwardRef}
68
83
  onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
69
- if (onChange) onChange(e);
84
+ onSliderChange(e);
70
85
  }}
71
- name={name}
86
+ name={sliderName}
72
87
  {...restProps}
73
88
  {...inputProps}
89
+ value={rawValue}
90
+ />
91
+ <input
92
+ name={name}
93
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
94
+ if (onChange) onChange(e);
95
+ }}
96
+ style={{
97
+ display: "none",
98
+ }}
99
+ value={value}
74
100
  />
75
101
  <div className={classNames.cap} />
76
102
  </div>
@@ -14,3 +14,9 @@ export const CLASS_NAMES = {
14
14
  rangeWrapper: "flex flex-row relative",
15
15
  cap: "w-2.5 h-2.5 bg-green-600 rounded-full border border-neutral-000 absolute z-0 top-[5px] first:left-0 last:right-0",
16
16
  };
17
+
18
+ export const BASE_INPUT_PROPS = {
19
+ min: "0",
20
+ max: "100",
21
+ step: "1",
22
+ };
@@ -1,11 +1,15 @@
1
- import { useEffect, useMemo, useRef } from "react";
2
- import { useWatch } from "react-hook-form";
1
+ import { useCallback, useEffect, useMemo, useRef } from "react";
2
+ import { useFormContext, useWatch } from "react-hook-form";
3
3
  import clsx from "clsx";
4
4
 
5
5
  import { BG_COLOR, CLASS_NAMES } from "./constants";
6
6
 
7
7
  import { ErrorMessage } from "~/field/types";
8
- import { RangeInputUncontrolledProps } from "./RangeInputUncontrolled";
8
+ import {
9
+ RangeInputUncontrolledProps,
10
+ TieStepsTo,
11
+ } from "./RangeInputUncontrolled";
12
+ import { chainStateK } from "fp-ts/lib/FromState";
9
13
 
10
14
  interface RangeClassNames {
11
15
  container: string;
@@ -33,6 +37,10 @@ export interface Hook {
33
37
  minLabel: string;
34
38
  currentLabel?: string;
35
39
  classNames: RangeClassNames;
40
+ onSliderChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
41
+ sliderName: string;
42
+ rawValue: string;
43
+ value: string;
36
44
  }
37
45
 
38
46
  export default function useRangeInputProps(
@@ -40,25 +48,71 @@ export default function useRangeInputProps(
40
48
  min: string,
41
49
  max: string,
42
50
  steps: string,
51
+ tieStepsTo: TieStepsTo,
43
52
  formatter: (input: string) => string,
44
53
  prefix: string,
45
54
  suffix: string,
46
55
  error?: string | ErrorMessage,
47
56
  highlightLabel?: RangeInputUncontrolledProps["highlightLabel"]
48
57
  ): Hook {
49
- const value = useWatch({ name });
58
+ const sliderName = useMemo(() => `${name}__slider`, [name]);
59
+ const [value, rawValue] = useWatch({ name: [name, sliderName] });
60
+ const {
61
+ setValue,
62
+ formState: { defaultValues },
63
+ } = useFormContext();
64
+
65
+ const initialValue = useMemo(
66
+ () => defaultValues?.[name],
67
+ [defaultValues, name]
68
+ );
50
69
 
51
70
  const ref = useRef<HTMLInputElement>(null);
52
71
 
53
- const step = String(
54
- Math.ceil((parseInt(max, 10) - parseInt(min, 10)) / parseInt(steps, 10))
72
+ const diff = useMemo(() => parseInt(max, 10) - parseInt(min, 10), [max, min]);
73
+
74
+ const numSteps = useMemo(
75
+ () => Math.min(diff, parseInt(steps, 10)),
76
+ [diff, steps]
55
77
  );
56
78
 
57
- const inputProps = {
58
- min,
59
- max,
60
- step,
61
- };
79
+ const step = useMemo(
80
+ () => Math.ceil((parseInt(max, 10) - parseInt(min, 10)) / numSteps),
81
+ []
82
+ );
83
+
84
+ const maxInput = useMemo(() => {
85
+ if (diff < 0) return "1";
86
+ if (diff > 100) return "100";
87
+ return String(diff);
88
+ }, [diff]);
89
+
90
+ const inputProps = useMemo(
91
+ () => ({
92
+ min: "0",
93
+ max: maxInput,
94
+ step: "1",
95
+ }),
96
+ [maxInput]
97
+ );
98
+
99
+ const tieMax = useCallback(
100
+ (rawInputValue: string) => {
101
+ const rawDiff = parseInt(maxInput, 10) - parseInt(rawInputValue, 10);
102
+ const increment = rawDiff * step;
103
+ return String(Math.max(parseInt(max, 10) - increment, parseInt(min, 10)));
104
+ },
105
+ [maxInput, step, max, min]
106
+ );
107
+
108
+ const tieMin = useCallback(
109
+ (rawInputValue: string) => {
110
+ const rawDiff = parseInt(rawInputValue, 10);
111
+ const increment = rawDiff * step;
112
+ return String(Math.min(parseInt(min, 10) + increment, parseInt(max, 10)));
113
+ },
114
+ [maxInput, step, max]
115
+ );
62
116
 
63
117
  const currentLabel = useMemo(() => {
64
118
  if (!value) return undefined;
@@ -84,6 +138,37 @@ export default function useRangeInputProps(
84
138
  ),
85
139
  };
86
140
 
141
+ const onSliderChange = useCallback(
142
+ (e: React.ChangeEvent<HTMLInputElement>) => {
143
+ const sliderValue = e.target.value;
144
+ setValue(sliderName, sliderValue);
145
+ },
146
+ [setValue, sliderName]
147
+ );
148
+
149
+ const getInitialSliderValue = useCallback(() => {
150
+ if (maxInput === "1") return maxInput;
151
+ if (!initialValue) return String(Math.ceil(parseInt(maxInput, 10) / 2));
152
+ const parsedMin = parseInt(min, 10);
153
+ const parsedMax = parseInt(max, 10);
154
+ const parsedVal = parseInt(initialValue, 10);
155
+ if (parsedVal >= parsedMax) {
156
+ setValue(name, String(parsedMax));
157
+ return "100";
158
+ }
159
+ if (parsedVal <= parsedMin) {
160
+ setValue(name, String(parsedMin));
161
+ return "0";
162
+ }
163
+ if (tieStepsTo === "max") {
164
+ const initDiff = (parseInt(max, 10) - parseInt(initialValue, 10)) / step;
165
+ return String(Math.round(parseInt(maxInput, 10) - initDiff));
166
+ }
167
+ return String(
168
+ Math.round((parseInt(initialValue, 10) - parseInt(min, 10)) / step)
169
+ );
170
+ }, [initialValue, maxInput, setValue, name, min, max, step]);
171
+
87
172
  useEffect(() => {
88
173
  if (!ref.current) return;
89
174
  const inputArr = Array.from(ref.current.children).filter(
@@ -98,6 +183,22 @@ export default function useRangeInputProps(
98
183
  input.style.background = BG_COLOR.replaceAll("$PCT", String(pct));
99
184
  }, [value, min, max]);
100
185
 
186
+ const initRef = useRef<boolean>(false);
187
+ useEffect(() => {
188
+ if (initRef.current) return;
189
+ const initValue = getInitialSliderValue();
190
+ setValue(sliderName, initValue);
191
+ setTimeout(() => {
192
+ initRef.current = true;
193
+ }, 10);
194
+ }, [setValue, sliderName, getInitialSliderValue]);
195
+
196
+ useEffect(() => {
197
+ if (!initRef.current) return;
198
+ const newValue = tieStepsTo === "max" ? tieMax(rawValue) : tieMin(rawValue);
199
+ setValue(name, newValue);
200
+ }, [rawValue, tieStepsTo, setValue]);
201
+
101
202
  return {
102
203
  ref,
103
204
  inputProps,
@@ -105,5 +206,9 @@ export default function useRangeInputProps(
105
206
  minLabel,
106
207
  currentLabel,
107
208
  classNames,
209
+ onSliderChange,
210
+ sliderName,
211
+ value: value || "",
212
+ rawValue: rawValue || "0",
108
213
  };
109
214
  }