@seed-design/react-field-button 1.0.0 → 1.0.2

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.
@@ -4,6 +4,7 @@ import { composeRefs } from '@radix-ui/react-compose-refs';
4
4
  import { elementProps, dataAttr, buttonProps, inputProps, ariaAttr, mergeProps } from '@seed-design/dom-utils';
5
5
  import { Primitive } from '@seed-design/react-primitive';
6
6
  import { useId, useCallback, useState, createContext, useContext, forwardRef } from 'react';
7
+ import { useSupports } from '@seed-design/react-supports';
7
8
 
8
9
  const getDescriptionId = (id)=>`field-button:${id}:description`;
9
10
  const getErrorMessageId = (id)=>`field-button:${id}:error-message`;
@@ -42,9 +43,9 @@ function useFieldButtonState({ values = [], onValuesChange = ()=>{} }) {
42
43
  setIsFocusVisible
43
44
  };
44
45
  }
45
- function useFieldButton(props) {
46
+ function useFieldButton({ values: propValues, onValuesChange, disabled = false, invalid = false, readOnly = false, name }) {
46
47
  const id = useId();
47
- const { values: propValues, onValuesChange, disabled = false, invalid = false, name } = props;
48
+ const isFocusVisibleSupported = useSupports("selector(:focus-visible)");
48
49
  const { values: stateValues, isHovered, isActive, isFocused, isFocusVisible, refs, renderedElements, setValues, setIsHovered, setIsActive, setIsFocused, setIsFocusVisible } = useFieldButtonState({
49
50
  values: propValues,
50
51
  onValuesChange
@@ -59,13 +60,16 @@ function useFieldButton(props) {
59
60
  "data-focus": dataAttr(isFocused),
60
61
  "data-focus-visible": dataAttr(isFocusVisible),
61
62
  "data-disabled": dataAttr(disabled),
62
- "data-invalid": dataAttr(invalid)
63
+ "data-invalid": dataAttr(invalid),
64
+ "data-readonly": dataAttr(readOnly)
63
65
  });
64
66
  return {
65
67
  values: stateValues,
66
68
  active: isActive,
67
69
  focused: isFocused,
68
70
  invalid,
71
+ disabled,
72
+ readOnly,
69
73
  setIsFocused,
70
74
  setIsFocusVisible,
71
75
  refs,
@@ -83,8 +87,8 @@ function useFieldButton(props) {
83
87
  buttonProps: buttonProps({
84
88
  ...stateProps,
85
89
  type: "button",
86
- disabled,
87
- "aria-disabled": ariaAttr(disabled),
90
+ disabled: disabled || readOnly,
91
+ "aria-disabled": ariaAttr(disabled || readOnly),
88
92
  "aria-describedby": ariaDescribedBy,
89
93
  // note that pointerdown and pointerup are attached to the button, not the root
90
94
  // this is for preventing setting isActive to true when the clear button is pressed
@@ -96,21 +100,24 @@ function useFieldButton(props) {
96
100
  },
97
101
  onBlur () {
98
102
  setIsFocused(false);
99
- setIsFocusVisible(false);
103
+ if (isFocusVisibleSupported) {
104
+ setIsFocusVisible(false);
105
+ }
100
106
  },
101
107
  onFocus (event) {
102
108
  setIsFocused(true);
103
- setIsFocusVisible(event.target.matches(":focus-visible"));
109
+ if (isFocusVisibleSupported) {
110
+ setIsFocusVisible(event.target.matches(":focus-visible"));
111
+ }
104
112
  }
105
113
  }),
106
114
  clearButtonProps: buttonProps({
107
- ...stateProps,
108
115
  type: "button",
109
- disabled,
116
+ disabled: disabled || readOnly,
110
117
  onClick: useCallback(()=>setValues([]), [
111
118
  setValues
112
119
  ]),
113
- hidden: disabled
120
+ hidden: disabled || readOnly
114
121
  }),
115
122
  getHiddenInputProps: useCallback((index)=>{
116
123
  const value = stateValues[index];
@@ -118,12 +125,14 @@ function useFieldButton(props) {
118
125
  return inputProps({
119
126
  type: "hidden",
120
127
  value,
128
+ disabled,
121
129
  name: name || id
122
130
  });
123
131
  }, [
124
132
  stateValues,
125
133
  name,
126
- id
134
+ id,
135
+ disabled
127
136
  ]),
128
137
  descriptionProps: elementProps({
129
138
  ...stateProps,
@@ -147,9 +156,10 @@ function useFieldButtonContext({ strict = true } = {}) {
147
156
  return context;
148
157
  }
149
158
 
150
- const FieldButtonRoot = /*#__PURE__*/ forwardRef(({ disabled, invalid, name, values, onValuesChange, ...otherProps }, ref)=>{
159
+ const FieldButtonRoot = /*#__PURE__*/ forwardRef(({ disabled, readOnly, invalid, name, values, onValuesChange, ...otherProps }, ref)=>{
151
160
  const api = useFieldButton({
152
161
  disabled,
162
+ readOnly,
153
163
  invalid,
154
164
  name,
155
165
  values,
@@ -4,6 +4,7 @@ var reactComposeRefs = require('@radix-ui/react-compose-refs');
4
4
  var domUtils = require('@seed-design/dom-utils');
5
5
  var reactPrimitive = require('@seed-design/react-primitive');
6
6
  var react = require('react');
7
+ var reactSupports = require('@seed-design/react-supports');
7
8
 
8
9
  const getDescriptionId = (id)=>`field-button:${id}:description`;
9
10
  const getErrorMessageId = (id)=>`field-button:${id}:error-message`;
@@ -42,9 +43,9 @@ function useFieldButtonState({ values = [], onValuesChange = ()=>{} }) {
42
43
  setIsFocusVisible
43
44
  };
44
45
  }
45
- function useFieldButton(props) {
46
+ function useFieldButton({ values: propValues, onValuesChange, disabled = false, invalid = false, readOnly = false, name }) {
46
47
  const id = react.useId();
47
- const { values: propValues, onValuesChange, disabled = false, invalid = false, name } = props;
48
+ const isFocusVisibleSupported = reactSupports.useSupports("selector(:focus-visible)");
48
49
  const { values: stateValues, isHovered, isActive, isFocused, isFocusVisible, refs, renderedElements, setValues, setIsHovered, setIsActive, setIsFocused, setIsFocusVisible } = useFieldButtonState({
49
50
  values: propValues,
50
51
  onValuesChange
@@ -59,13 +60,16 @@ function useFieldButton(props) {
59
60
  "data-focus": domUtils.dataAttr(isFocused),
60
61
  "data-focus-visible": domUtils.dataAttr(isFocusVisible),
61
62
  "data-disabled": domUtils.dataAttr(disabled),
62
- "data-invalid": domUtils.dataAttr(invalid)
63
+ "data-invalid": domUtils.dataAttr(invalid),
64
+ "data-readonly": domUtils.dataAttr(readOnly)
63
65
  });
64
66
  return {
65
67
  values: stateValues,
66
68
  active: isActive,
67
69
  focused: isFocused,
68
70
  invalid,
71
+ disabled,
72
+ readOnly,
69
73
  setIsFocused,
70
74
  setIsFocusVisible,
71
75
  refs,
@@ -83,8 +87,8 @@ function useFieldButton(props) {
83
87
  buttonProps: domUtils.buttonProps({
84
88
  ...stateProps,
85
89
  type: "button",
86
- disabled,
87
- "aria-disabled": domUtils.ariaAttr(disabled),
90
+ disabled: disabled || readOnly,
91
+ "aria-disabled": domUtils.ariaAttr(disabled || readOnly),
88
92
  "aria-describedby": ariaDescribedBy,
89
93
  // note that pointerdown and pointerup are attached to the button, not the root
90
94
  // this is for preventing setting isActive to true when the clear button is pressed
@@ -96,21 +100,24 @@ function useFieldButton(props) {
96
100
  },
97
101
  onBlur () {
98
102
  setIsFocused(false);
99
- setIsFocusVisible(false);
103
+ if (isFocusVisibleSupported) {
104
+ setIsFocusVisible(false);
105
+ }
100
106
  },
101
107
  onFocus (event) {
102
108
  setIsFocused(true);
103
- setIsFocusVisible(event.target.matches(":focus-visible"));
109
+ if (isFocusVisibleSupported) {
110
+ setIsFocusVisible(event.target.matches(":focus-visible"));
111
+ }
104
112
  }
105
113
  }),
106
114
  clearButtonProps: domUtils.buttonProps({
107
- ...stateProps,
108
115
  type: "button",
109
- disabled,
116
+ disabled: disabled || readOnly,
110
117
  onClick: react.useCallback(()=>setValues([]), [
111
118
  setValues
112
119
  ]),
113
- hidden: disabled
120
+ hidden: disabled || readOnly
114
121
  }),
115
122
  getHiddenInputProps: react.useCallback((index)=>{
116
123
  const value = stateValues[index];
@@ -118,12 +125,14 @@ function useFieldButton(props) {
118
125
  return domUtils.inputProps({
119
126
  type: "hidden",
120
127
  value,
128
+ disabled,
121
129
  name: name || id
122
130
  });
123
131
  }, [
124
132
  stateValues,
125
133
  name,
126
- id
134
+ id,
135
+ disabled
127
136
  ]),
128
137
  descriptionProps: domUtils.elementProps({
129
138
  ...stateProps,
@@ -147,9 +156,10 @@ function useFieldButtonContext({ strict = true } = {}) {
147
156
  return context;
148
157
  }
149
158
 
150
- const FieldButtonRoot = /*#__PURE__*/ react.forwardRef(({ disabled, invalid, name, values, onValuesChange, ...otherProps }, ref)=>{
159
+ const FieldButtonRoot = /*#__PURE__*/ react.forwardRef(({ disabled, readOnly, invalid, name, values, onValuesChange, ...otherProps }, ref)=>{
151
160
  const api = useFieldButton({
152
161
  disabled,
162
+ readOnly,
153
163
  invalid,
154
164
  name,
155
165
  values,
package/lib/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- var FieldButton12s = require('./FieldButton-12s-CF9DP1xP.cjs');
1
+ var FieldButton12s = require('./FieldButton-12s-CG6rqNcy.cjs');
2
2
 
3
3
  var FieldButton_namespace = {
4
4
  __proto__: null,
package/lib/index.d.ts CHANGED
@@ -15,14 +15,20 @@ interface UseFieldButtonProps extends UseFieldButtonStateProps {
15
15
  * @default false
16
16
  */
17
17
  invalid?: boolean;
18
+ /**
19
+ * @default false
20
+ */
21
+ readOnly?: boolean;
18
22
  name?: string;
19
23
  }
20
24
  type UseFieldButtonReturn = ReturnType<typeof useFieldButton>;
21
- declare function useFieldButton(props: UseFieldButtonProps): {
25
+ declare function useFieldButton({ values: propValues, onValuesChange, disabled, invalid, readOnly, name, }: UseFieldButtonProps): {
22
26
  values: string[];
23
27
  active: boolean;
24
28
  focused: boolean;
25
29
  invalid: boolean;
30
+ disabled: boolean;
31
+ readOnly: boolean;
26
32
  setIsFocused: react.Dispatch<react.SetStateAction<boolean>>;
27
33
  setIsFocusVisible: react.Dispatch<react.SetStateAction<boolean>>;
28
34
  refs: {
@@ -1179,6 +1185,7 @@ declare function useFieldButton(props: UseFieldButtonProps): {
1179
1185
  };
1180
1186
  getHiddenInputProps: (index: number) => {
1181
1187
  disabled?: boolean | undefined | undefined;
1188
+ readOnly?: boolean | undefined | undefined;
1182
1189
  name?: string | undefined | undefined;
1183
1190
  defaultChecked?: boolean | undefined | undefined;
1184
1191
  defaultValue?: string | number | readonly string[] | undefined;
@@ -1482,7 +1489,6 @@ declare function useFieldButton(props: UseFieldButtonProps): {
1482
1489
  multiple?: boolean | undefined | undefined;
1483
1490
  pattern?: string | undefined | undefined;
1484
1491
  placeholder?: string | undefined | undefined;
1485
- readOnly?: boolean | undefined | undefined;
1486
1492
  required?: boolean | undefined | undefined;
1487
1493
  size?: number | undefined | undefined;
1488
1494
  src?: string | undefined | undefined;
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { F as FieldButtonButton, a as FieldButtonClearButton, b as FieldButtonDescription, c as FieldButtonErrorMessage, d as FieldButtonHiddenInput, e as FieldButtonRoot } from './FieldButton-12s-nUvzy79W.js';
2
- export { u as useFieldButtonContext } from './FieldButton-12s-nUvzy79W.js';
1
+ import { F as FieldButtonButton, a as FieldButtonClearButton, b as FieldButtonDescription, c as FieldButtonErrorMessage, d as FieldButtonHiddenInput, e as FieldButtonRoot } from './FieldButton-12s-BzczO8x6.js';
2
+ export { u as useFieldButtonContext } from './FieldButton-12s-BzczO8x6.js';
3
3
 
4
4
  var FieldButton_namespace = {
5
5
  __proto__: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seed-design/react-field-button",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/daangn/seed-design.git",
@@ -30,7 +30,8 @@
30
30
  "@radix-ui/react-compose-refs": "^1.1.2",
31
31
  "@radix-ui/react-use-controllable-state": "^1.2.2",
32
32
  "@seed-design/dom-utils": "1.0.0",
33
- "@seed-design/react-primitive": "1.0.0"
33
+ "@seed-design/react-primitive": "1.0.0",
34
+ "@seed-design/react-supports": "0.0.1"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/react": "^19.1.6",
@@ -13,8 +13,8 @@ export interface FieldButtonRootProps
13
13
  HTMLAttributes<HTMLDivElement> {}
14
14
 
15
15
  export const FieldButtonRoot = forwardRef<HTMLDivElement, FieldButtonRootProps>(
16
- ({ disabled, invalid, name, values, onValuesChange, ...otherProps }, ref) => {
17
- const api = useFieldButton({ disabled, invalid, name, values, onValuesChange });
16
+ ({ disabled, readOnly, invalid, name, values, onValuesChange, ...otherProps }, ref) => {
17
+ const api = useFieldButton({ disabled, readOnly, invalid, name, values, onValuesChange });
18
18
  const mergedProps = mergeProps(api.rootProps, otherProps);
19
19
 
20
20
  return (
@@ -1,7 +1,6 @@
1
- import "@testing-library/jest-dom/vitest";
2
- import { cleanup, fireEvent, render } from "@testing-library/react";
1
+ import { fireEvent, render } from "@testing-library/react";
3
2
  import userEvent from "@testing-library/user-event";
4
- import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { describe, expect, it, mock } from "bun:test";
5
4
 
6
5
  import type { ReactElement } from "react";
7
6
 
@@ -15,8 +14,6 @@ import {
15
14
  type FieldButtonRootProps,
16
15
  } from "./FieldButton";
17
16
 
18
- afterEach(cleanup);
19
-
20
17
  function setUp(jsx: ReactElement) {
21
18
  return {
22
19
  user: userEvent.setup(),
@@ -103,6 +100,15 @@ describe("useFieldButton", () => {
103
100
  expect(button).toHaveAttribute("aria-disabled", "true");
104
101
  });
105
102
 
103
+ it("should have readOnly state when readOnly prop is true", () => {
104
+ const { getByRole } = setUp(<FieldButton readOnly />);
105
+ const button = getByRole("button", { name: "Click me" });
106
+
107
+ expect(button).toBeDisabled();
108
+ expect(button).toHaveAttribute("data-readonly");
109
+ expect(button).toHaveAttribute("aria-disabled", "true");
110
+ });
111
+
106
112
  it("should have invalid state when invalid prop is true", () => {
107
113
  const { getByRole } = setUp(<FieldButton invalid />);
108
114
  const button = getByRole("button", { name: "Click me" });
@@ -158,6 +164,26 @@ describe("useFieldButton", () => {
158
164
  expect(button).not.toHaveAttribute("data-active");
159
165
  });
160
166
 
167
+ it("should not disable hidden inputs when readOnly (unlike disabled)", () => {
168
+ const { container } = setUp(<FieldButton readOnly values={["value1", "value2"]} />);
169
+ const hiddenInputs = container.querySelectorAll('input[type="hidden"]');
170
+
171
+ expect(hiddenInputs).toHaveLength(2);
172
+ expect(hiddenInputs[0]).not.toBeDisabled();
173
+ expect(hiddenInputs[1]).not.toBeDisabled();
174
+ expect(hiddenInputs[0]).toHaveValue("value1");
175
+ expect(hiddenInputs[1]).toHaveValue("value2");
176
+ });
177
+
178
+ it("should disable hidden inputs when disabled", () => {
179
+ const { container } = setUp(<FieldButton disabled values={["value1", "value2"]} />);
180
+ const hiddenInputs = container.querySelectorAll('input[type="hidden"]');
181
+
182
+ expect(hiddenInputs).toHaveLength(2);
183
+ expect(hiddenInputs[0]).toBeDisabled();
184
+ expect(hiddenInputs[1]).toBeDisabled();
185
+ });
186
+
161
187
  it("should render without description and errorMessage", () => {
162
188
  function MinimalFieldButton(props: FieldButtonRootProps) {
163
189
  return (
@@ -199,7 +225,7 @@ describe("useFieldButton", () => {
199
225
 
200
226
  describe("clear button", () => {
201
227
  it("should call onValuesChange when clear button is clicked", async () => {
202
- const handleValuesChange = vi.fn();
228
+ const handleValuesChange = mock(() => {});
203
229
  const { getByRole, user } = setUp(
204
230
  <FieldButton values={["value1"]} onValuesChange={handleValuesChange} />,
205
231
  );
@@ -210,7 +236,7 @@ describe("useFieldButton", () => {
210
236
  });
211
237
 
212
238
  it("should hide clear button when disabled", async () => {
213
- const handleValuesChange = vi.fn();
239
+ const handleValuesChange = mock(() => {});
214
240
  const { getByText, user } = setUp(
215
241
  <FieldButton disabled values={["value1"]} onValuesChange={handleValuesChange} />,
216
242
  );
@@ -222,6 +248,19 @@ describe("useFieldButton", () => {
222
248
  expect(handleValuesChange).not.toHaveBeenCalled();
223
249
  });
224
250
 
251
+ it("should hide clear button when readOnly", async () => {
252
+ const handleValuesChange = mock(() => {});
253
+ const { getByText, user } = setUp(
254
+ <FieldButton readOnly values={["value1"]} onValuesChange={handleValuesChange} />,
255
+ );
256
+ const clearButton = getByText("Clear");
257
+
258
+ expect(clearButton).toHaveAttribute("hidden");
259
+
260
+ await user.click(clearButton);
261
+ expect(handleValuesChange).not.toHaveBeenCalled();
262
+ });
263
+
225
264
  it("should not have active state when clear button is pressed", () => {
226
265
  const { getByRole } = setUp(<FieldButton />);
227
266
  const button = getByRole("button", { name: "Click me" });
@@ -1,6 +1,7 @@
1
1
  import { ariaAttr, buttonProps, dataAttr, elementProps, inputProps } from "@seed-design/dom-utils";
2
2
  import { useCallback, useId, useState } from "react";
3
3
  import { getDescriptionId, getErrorMessageId } from "./dom";
4
+ import { useSupports } from "@seed-design/react-supports";
4
5
 
5
6
  interface UseFieldButtonStateProps {
6
7
  values?: string[];
@@ -57,14 +58,26 @@ export interface UseFieldButtonProps extends UseFieldButtonStateProps {
57
58
  */
58
59
  invalid?: boolean;
59
60
 
61
+ /**
62
+ * @default false
63
+ */
64
+ readOnly?: boolean;
65
+
60
66
  name?: string;
61
67
  }
62
68
 
63
69
  export type UseFieldButtonReturn = ReturnType<typeof useFieldButton>;
64
70
 
65
- export function useFieldButton(props: UseFieldButtonProps) {
71
+ export function useFieldButton({
72
+ values: propValues,
73
+ onValuesChange,
74
+ disabled = false,
75
+ invalid = false,
76
+ readOnly = false,
77
+ name,
78
+ }: UseFieldButtonProps) {
66
79
  const id = useId();
67
- const { values: propValues, onValuesChange, disabled = false, invalid = false, name } = props;
80
+ const isFocusVisibleSupported = useSupports("selector(:focus-visible)");
68
81
 
69
82
  const {
70
83
  values: stateValues,
@@ -96,6 +109,7 @@ export function useFieldButton(props: UseFieldButtonProps) {
96
109
  "data-focus-visible": dataAttr(isFocusVisible),
97
110
  "data-disabled": dataAttr(disabled),
98
111
  "data-invalid": dataAttr(invalid),
112
+ "data-readonly": dataAttr(readOnly),
99
113
  });
100
114
 
101
115
  return {
@@ -103,6 +117,8 @@ export function useFieldButton(props: UseFieldButtonProps) {
103
117
  active: isActive,
104
118
  focused: isFocused,
105
119
  invalid,
120
+ disabled,
121
+ readOnly,
106
122
 
107
123
  setIsFocused,
108
124
  setIsFocusVisible,
@@ -127,9 +143,9 @@ export function useFieldButton(props: UseFieldButtonProps) {
127
143
  ...stateProps,
128
144
 
129
145
  type: "button",
130
- disabled,
146
+ disabled: disabled || readOnly,
131
147
 
132
- "aria-disabled": ariaAttr(disabled),
148
+ "aria-disabled": ariaAttr(disabled || readOnly),
133
149
  "aria-describedby": ariaDescribedBy,
134
150
 
135
151
  // note that pointerdown and pointerup are attached to the button, not the root
@@ -143,23 +159,25 @@ export function useFieldButton(props: UseFieldButtonProps) {
143
159
 
144
160
  onBlur() {
145
161
  setIsFocused(false);
146
- setIsFocusVisible(false);
162
+ if (isFocusVisibleSupported) {
163
+ setIsFocusVisible(false);
164
+ }
147
165
  },
148
166
  onFocus(event) {
149
167
  setIsFocused(true);
150
- setIsFocusVisible(event.target.matches(":focus-visible"));
168
+ if (isFocusVisibleSupported) {
169
+ setIsFocusVisible(event.target.matches(":focus-visible"));
170
+ }
151
171
  },
152
172
  }),
153
173
 
154
174
  clearButtonProps: buttonProps({
155
- ...stateProps,
156
-
157
175
  type: "button",
158
- disabled,
176
+ disabled: disabled || readOnly,
159
177
 
160
178
  onClick: useCallback(() => setValues([]), [setValues]),
161
179
 
162
- hidden: disabled,
180
+ hidden: disabled || readOnly,
163
181
  }),
164
182
 
165
183
  getHiddenInputProps: useCallback(
@@ -171,10 +189,11 @@ export function useFieldButton(props: UseFieldButtonProps) {
171
189
  return inputProps({
172
190
  type: "hidden",
173
191
  value,
192
+ disabled, // disabled field button should not submit values, while readonly field button should submit values
174
193
  name: name || id,
175
194
  });
176
195
  },
177
- [stateValues, name, id],
196
+ [stateValues, name, id, disabled],
178
197
  ),
179
198
 
180
199
  descriptionProps: elementProps({