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

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.58",
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,49 +48,135 @@ 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
+ [max, min, numSteps]
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
+ [step, max, min]
115
+ );
62
116
 
63
117
  const currentLabel = useMemo(() => {
64
118
  if (!value) return undefined;
65
119
  return `${prefix}${formatter(value)}${suffix}`;
66
- }, [value]);
67
-
68
- const maxLabel = `${prefix}${formatter(String(max))}${suffix}`;
69
- const minLabel = `${prefix}${formatter(String(min))}${suffix}`;
70
-
71
- const classNames = {
72
- ...CLASS_NAMES,
73
- container: clsx(
74
- CLASS_NAMES.container,
75
- error ? "border-red-200" : "border-neutral-100"
76
- ),
77
- minLowerLabel: clsx(
78
- CLASS_NAMES.lowerLabel,
79
- highlightLabel === "min" && CLASS_NAMES.highlightedLowerLabel
80
- ),
81
- maxLowerLabel: clsx(
82
- CLASS_NAMES.lowerLabel,
83
- highlightLabel === "max" && CLASS_NAMES.highlightedLowerLabel
84
- ),
85
- };
120
+ }, [formatter, value, prefix, suffix]);
121
+
122
+ const maxLabel = useMemo(
123
+ () => `${prefix}${formatter(String(max))}${suffix}`,
124
+ [prefix, formatter, max, suffix]
125
+ );
126
+ const minLabel = useMemo(
127
+ () => `${prefix}${formatter(String(min))}${suffix}`,
128
+ [prefix, formatter, min, suffix]
129
+ );
130
+
131
+ const classNames = useMemo(
132
+ () => ({
133
+ ...CLASS_NAMES,
134
+ container: clsx(
135
+ CLASS_NAMES.container,
136
+ error ? "border-red-200" : "border-neutral-100"
137
+ ),
138
+ minLowerLabel: clsx(
139
+ CLASS_NAMES.lowerLabel,
140
+ highlightLabel === "min" && CLASS_NAMES.highlightedLowerLabel
141
+ ),
142
+ maxLowerLabel: clsx(
143
+ CLASS_NAMES.lowerLabel,
144
+ highlightLabel === "max" && CLASS_NAMES.highlightedLowerLabel
145
+ ),
146
+ }),
147
+ [error, highlightLabel]
148
+ );
149
+
150
+ const onSliderChange = useCallback(
151
+ (e: React.ChangeEvent<HTMLInputElement>) => {
152
+ const sliderValue = e.target.value;
153
+ setValue(sliderName, sliderValue);
154
+ },
155
+ [setValue, sliderName]
156
+ );
157
+
158
+ const getInitialSliderValue = useCallback(() => {
159
+ if (maxInput === "1") return maxInput;
160
+ if (!initialValue) return String(Math.ceil(parseInt(maxInput, 10) / 2));
161
+ const parsedMin = parseInt(min, 10);
162
+ const parsedMax = parseInt(max, 10);
163
+ const parsedVal = parseInt(initialValue, 10);
164
+ if (parsedVal >= parsedMax) {
165
+ setValue(name, String(parsedMax));
166
+ return "100";
167
+ }
168
+ if (parsedVal <= parsedMin) {
169
+ setValue(name, String(parsedMin));
170
+ return "0";
171
+ }
172
+ if (tieStepsTo === "max") {
173
+ const initDiff = (parseInt(max, 10) - parseInt(initialValue, 10)) / step;
174
+ return String(Math.round(parseInt(maxInput, 10) - initDiff));
175
+ }
176
+ return String(
177
+ Math.round((parseInt(initialValue, 10) - parseInt(min, 10)) / step)
178
+ );
179
+ }, [initialValue, maxInput, setValue, name, min, max, step]);
86
180
 
87
181
  useEffect(() => {
88
182
  if (!ref.current) return;
@@ -98,6 +192,22 @@ export default function useRangeInputProps(
98
192
  input.style.background = BG_COLOR.replaceAll("$PCT", String(pct));
99
193
  }, [value, min, max]);
100
194
 
195
+ const initRef = useRef<boolean>(false);
196
+ useEffect(() => {
197
+ if (initRef.current) return;
198
+ const initValue = getInitialSliderValue();
199
+ setValue(sliderName, initValue);
200
+ setTimeout(() => {
201
+ initRef.current = true;
202
+ }, 10);
203
+ }, [setValue, sliderName, getInitialSliderValue]);
204
+
205
+ useEffect(() => {
206
+ if (!initRef.current) return;
207
+ const newValue = tieStepsTo === "max" ? tieMax(rawValue) : tieMin(rawValue);
208
+ setValue(name, newValue);
209
+ }, [rawValue, tieStepsTo, setValue, tieMax, tieMin, name]);
210
+
101
211
  return {
102
212
  ref,
103
213
  inputProps,
@@ -105,5 +215,9 @@ export default function useRangeInputProps(
105
215
  minLabel,
106
216
  currentLabel,
107
217
  classNames,
218
+ onSliderChange,
219
+ sliderName,
220
+ value: value || "",
221
+ rawValue: rawValue || "0",
108
222
  };
109
223
  }