Skip to content

Commit 56d80d3

Browse files
feat: redo checkbox component (#3295)
1 parent cf60005 commit 56d80d3

File tree

14 files changed

+195
-77
lines changed

14 files changed

+195
-77
lines changed

apps/docs/components/content/_components/checkbox.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
#tab-1
66

7-
`SfCheckbox` is a wrapper around `<input type="checkbox">` with additional styles for different states . It can be used for forms or expressing consents.
7+
`SfCheckbox` is a wrapper around `<input type="checkbox">` with additional styles for different states . It can be used for forms or expressing consents.
88

99
The root element is an `<input>` so any attributes that can be used on an `<input>` can be used on `SfCheckbox`.
1010

@@ -75,18 +75,23 @@ It's focusable and can be toggled with `Space`.
7575

7676

7777
::vue-only
78-
| Prop name | Type | Default value | Possible values |
79-
| ------------ | ------------------------ | ------------- | -------------------------------------- |
78+
| Prop name | Type | Default value | Possible values |
79+
| -------------- | -------------------------- | --------------- | -------------------------------------- |
8080
| `modelValue` | `boolean | string[]` | `undefined` | |
8181
| `invalid` | `boolean` | `false` | |
82-
82+
| `indeterminate`| `boolean` | `false` | |
83+
| `wrapperTag` | `string` | `label` | Any tag name for checkbox wrapper |
84+
| `wrapperClass` | `string` | | |
8385
::
8486

8587
::react-only
86-
| Prop name | Type | Default value | Possible values |
87-
| ------------ | ------------------------ | ------------- | -------------------------------------- |
88-
| `className` | `string` | | |
89-
| `invalid` | `boolean` | `false` | |
88+
| Prop name | Type | Default value | Possible values |
89+
| ------------------ | ---------------------- | ------------- | -------------------------------------- |
90+
| `className` | `string` | | |
91+
| `invalid` | `boolean` | `false` | |
92+
| `indeterminate` | `boolean` | `false` | |
93+
| `wrapperAs` | `string` | `label` | Any tag name for c wrapper |
94+
| `wrapperClassName` | `string` | | |
9095
::
9196

9297
::vue-only

apps/docs/components/content/_components/input.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ If you need to make this field required, it is crucial to communicate this inten
3131
### Input with icons
3232

3333
::vue-only
34-
You can insert content before and after your input using the `prefix` and `suffix` slots.
34+
You can insert content before and after your input using the `prefix` and `suffix` slots.
3535
::
3636
::react-only
37-
You can insert content before and after your input using the `slotPrefix` and `slotSuffix` props.
37+
You can insert content before and after your input using the `slotPrefix` and `slotSuffix` props.
3838
::
3939

4040

@@ -114,10 +114,10 @@ This is an example of what `SfInput` might look like in your end code. It has a
114114
## Notes
115115

116116
::vue-only
117-
All non-prop attributes and styles added to `SfInput` component are passed directly to the native input element. This means that you can add all of the input attributes directly to `SfInput`. If you want to style the wrapper `div`, you can pass your classes via the `wrapperClass` prop.
117+
All non-prop attributes and styles added to `SfInput` component are passed directly to the native input element. This means that you can add all of the input attributes directly to `SfInput`. If you want to style the wrapper `div`, you can pass your classes via the `wrapperClass` prop.
118118
::
119119
::react-only
120-
All non-prop attributes and styles added to `SfInput` component are passed directly to the native input element. This means that you can add all of the input attributes directly to `SfInput`. If you want to style the wrapper `div`, you can pass your classes via the `wrapperClassName` prop.
120+
All non-prop attributes and styles added to `SfInput` component are passed directly to the native input element. This means that you can add all of the input attributes directly to `SfInput`. If you want to style the wrapper `div`, you can pass your classes via the `wrapperClassName` prop.
121121
::
122122

123123
Since, `size` is a specified prop of `SfInput`, you won't be able to pass the native `size` attribute to your input element. Instead, you can use the `width` property with `ch` unit instead (eg. `width: 10ch`).
@@ -149,7 +149,6 @@ Avoid adding `div` tags to slots. If an input element is wrapped in `label` tag
149149
| ------------ | -------- | ------------- | -------------------------------------- |
150150
| `size` | `SfInputSize` | `'base'` | `'sm'`, `'base'`, `'lg'` |
151151
| `invalid` | `boolean` | `false` | |
152-
| `wrapperTag` | `string` | `span` | Any tag name for input wrapper |
153152
| `wrapperAs` | `string` | `span` | Any tag name for input wrapper |
154153
| `className` | `string` | | |
155154
| `slotPrefix` | `ReactNode` | | |

apps/preview/next/components/utils/Controls.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,9 @@ export default function Controls<T extends { [k: string]: any }>({ controls, sta
217217
<td className="propType">
218218
<span>{control.propType}</span>
219219
</td>
220-
<td className="propDefaultValue">{control.propDefaultValue}</td>
220+
<td className="propDefaultValue">
221+
{control.propDefaultValue ?? (control.type === 'boolean' ? 'false' : '')}
222+
</td>
221223
<td className="required">{control?.isRequired?.toString()}</td>
222224
<td className="description">{control.description}</td>
223225
</tr>

apps/preview/next/pages/examples/SfCheckbox.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ function Example() {
2424
propType: 'string',
2525
description: '(not prop) example allows to add value attribute to input',
2626
},
27+
{
28+
type: 'text',
29+
modelName: 'wrapperAs',
30+
propDefaultValue: 'label',
31+
description: 'Change checkbox wrapper tag',
32+
},
33+
{
34+
type: 'text',
35+
modelName: 'wrapperClassName',
36+
propType: 'string',
37+
description: 'Change checkbox wrapper class',
38+
},
2739
{
2840
type: 'boolean',
2941
modelName: 'indeterminate',
@@ -48,6 +60,8 @@ function Example() {
4860
disabled: false,
4961
invalid: false,
5062
checkedValue: [],
63+
wrapperAs: 'label',
64+
wrapperClassName: '',
5165
},
5266
);
5367

@@ -67,6 +81,7 @@ function Example() {
6781
if (state.get.invalid) {
6882
checkboxRef.current.indeterminate = false;
6983
} else {
84+
console.log('state.get.indeterminate', state.get.indeterminate);
7085
checkboxRef.current.indeterminate = state.get.indeterminate;
7186
}
7287
}, [checkboxRef, state.get.indeterminate, state.get.invalid, state.get.disabled]);
@@ -78,10 +93,13 @@ function Example() {
7893
value={state.get.value}
7994
disabled={state.get.disabled}
8095
invalid={!state.get.disabled && state.get.invalid}
96+
indeterminate={state.get.indeterminate}
97+
wrapperAs={state.get.wrapperAs}
8198
ref={checkboxRef}
8299
onChange={onChange}
83100
className="peer"
84101
id="checkbox"
102+
wrapperClassName={state.get.wrapperClassName}
85103
/>
86104
<label
87105
htmlFor="checkbox"

apps/preview/next/pages/showcases.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ReactElement } from 'react';
22
import ShowcaseLayout from '../layouts/Showcases';
33

44
function ExamplePage() {
5-
return <div>This is page with examples of all available components</div>;
5+
return <div>This is page with showcases of all available components</div>;
66
}
77

88
export function ShowcasePageLayout(page: ReactElement) {

apps/preview/nuxt/components/utils/Controls.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@
111111
<span>{{ control.propType }}</span>
112112
</td>
113113
<td class="propDefaultValue">
114-
{{ (control.propDefaultValue ?? control.type === 'boolean') ? 'false' : '' }}
114+
{{ control.propDefaultValue ?? (control.type === 'boolean' ? 'false' : '') }}
115115
</td>
116116
<td class="required">{{ control?.isRequired?.toString() }}</td>
117117
<td class="description">{{ control.description }}</td>

apps/preview/nuxt/pages/examples/SfCheckbox.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
v-model="modelValue"
99
:invalid="!disabled && invalid"
1010
class="peer"
11+
:wrapper-class="wrapperClass"
1112
/>
1213
<label
1314
for="checkbox"
@@ -56,6 +57,18 @@ export default defineComponent({
5657
propType: 'string',
5758
description: '(not prop) example allows to add value attribute to input',
5859
},
60+
{
61+
type: 'text',
62+
propType: 'string',
63+
propDefaultValue: 'label',
64+
modelName: 'wrapperTag',
65+
},
66+
{
67+
type: 'text',
68+
modelName: 'wrapperClass',
69+
propType: 'string',
70+
description: 'Change checkbox wrapper class',
71+
},
5972
{
6073
type: 'boolean',
6174
modelName: 'indeterminate',
@@ -80,6 +93,8 @@ export default defineComponent({
8093
disabled: disabled,
8194
invalid: invalid,
8295
value: ref('label'),
96+
wrapperTag: ref(),
97+
wrapperClass: ref(''),
8398
},
8499
),
85100
checkboxRef,

apps/preview/nuxt/pages/showcases.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
<li v-if="groupValue.visible" class="flex flex-col select-none">
3131
<button
3232
type="button"
33-
class="text-left bg-gray-200 px-2 py-1 justify-between cursor-pointer"
33+
class="justify-between px-2 py-1 text-left bg-gray-200 cursor-pointer"
3434
@click="groupValue.open = !groupValue.open"
3535
>
3636
{{ groupKey }}<SfIconExpandMore :class="{ 'rotate-180': groupValue.open }" />

packages/config/tailwind/index.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -226,12 +226,6 @@ export const tailwindConfig: Config = {
226226
DEFAULT: '0px 1px 3px rgba(0, 0, 0, 0.1), 0px 1px 2px rgba(0, 0, 0, 0.06)',
227227
md: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -1px rgba(0, 0, 0, 0.06)',
228228
},
229-
backgroundImage: {
230-
'checked-checkbox-current':
231-
'linear-gradient(-45deg,transparent 65%, currentcolor 65.99%),linear-gradient(45deg,transparent 75%, currentcolor 75.99%),linear-gradient(-45deg, currentcolor 40%,transparent 40.99%),linear-gradient(45deg, currentcolor 30%, white 30.99%, white 40%,transparent 40.99%),linear-gradient(-45deg, white 50%, currentcolor 50.99%)',
232-
'indeterminate-checkbox-current':
233-
'linear-gradient(90deg,transparent 80%, currentcolor 80%),linear-gradient(-90deg,transparent 80%, currentcolor 80%),linear-gradient(0deg, currentcolor 43%, white 43%, white 57%, currentcolor 57%)',
234-
},
235229
colors: {
236230
brand: 'oklch(var(--colors-brand) / <alpha-value>)',
237231
neutral: {
@@ -371,7 +365,7 @@ export const tailwindConfig: Config = {
371365
'600': '0.443 0.016 152.174',
372366
'700': '0.365 0.016 156.314',
373367
'800': '0.282 0.011 156.383',
374-
'900': '0.211 0.011 151.165'
368+
'900': '0.211 0.011 151.165',
375369
},
376370
primary: {
377371
'50': '0.98 0.02 156.735',
@@ -383,7 +377,7 @@ export const tailwindConfig: Config = {
383377
'600': '0.525 0.041 162.018',
384378
'700': '0.466 0.035 162.976',
385379
'800': '0.365 0.026 164.592',
386-
'900': '0.27 0.017 163.365'
380+
'900': '0.27 0.017 163.365',
387381
},
388382
secondary: {
389383
'50': '0.982 0.027 157.322',
@@ -395,7 +389,7 @@ export const tailwindConfig: Config = {
395389
'600': '0.621 0.165 151.142',
396390
'700': '0.524 0.135 151.385',
397391
'800': '0.401 0.095 152.918',
398-
'900': '0.285 0.055 155.368'
392+
'900': '0.285 0.055 155.368',
399393
},
400394
positive: {
401395
'50': '0.982 0.027 157.322',
@@ -407,7 +401,7 @@ export const tailwindConfig: Config = {
407401
'600': '0.621 0.165 151.142',
408402
'700': '0.524 0.135 151.385',
409403
'800': '0.401 0.095 152.918',
410-
'900': '0.285 0.055 155.368'
404+
'900': '0.285 0.055 155.368',
411405
},
412406
negative: {
413407
'50': '0.978 0.011 3.577',
@@ -419,7 +413,7 @@ export const tailwindConfig: Config = {
419413
'600': '0.634 0.215 16.447',
420414
'700': '0.545 0.215 22.13',
421415
'800': '0.41 0.16 20.89',
422-
'900': '0.28 0.09 18.166'
416+
'900': '0.28 0.09 18.166',
423417
},
424418
warning: {
425419
'50': '0.979 0.016 79.212',
@@ -431,7 +425,7 @@ export const tailwindConfig: Config = {
431425
'600': '0.636 0.135 68.487',
432426
'700': '0.539 0.12 64.869',
433427
'800': '0.415 0.089 62.994',
434-
'900': '0.286 0.055 60.071'
428+
'900': '0.286 0.055 60.071',
435429
},
436430
disabled: {
437431
'50': '0.986 0.002 0',
@@ -443,7 +437,7 @@ export const tailwindConfig: Config = {
443437
'600': '0.443 0.016 152.174',
444438
'700': '0.365 0.016 156.314',
445439
'800': '0.282 0.011 156.383',
446-
'900': '0.211 0.011 151.165'
440+
'900': '0.211 0.011 151.165',
447441
},
448442
},
449443
},
Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,74 @@
11
import classNames from 'classnames';
2-
import { forwardRef } from 'react';
3-
import type { SfCheckboxProps } from '@storefront-ui/react';
2+
import { useRef, useEffect, useState } from 'react';
3+
import {
4+
mergeRefs,
5+
SfIconCheckBox,
6+
SfIconCheckBoxOutlineBlank,
7+
SfIconIndeterminateCheckBox,
8+
polymorphicForwardRef,
9+
type SfCheckboxProps,
10+
} from '@storefront-ui/react';
411

5-
const SfCheckbox = forwardRef<HTMLInputElement, SfCheckboxProps>(
6-
({ invalid, className, ...attributes }, ref): JSX.Element => (
7-
<input
8-
className={classNames(
9-
'h-[18px] min-w-[18px] border-2 rounded-sm appearance-none cursor-pointer text-neutral-500 hover:indeterminate:text-primary-800 enabled:active:checked:text-primary-900 checked:text-primary-700 checked:bg-checked-checkbox-current border-current indeterminate:bg-indeterminate-checkbox-current indeterminate:text-primary-700 disabled:text-disabled-500 hover:text-primary-800 disabled:cursor-not-allowed enabled:hover:border-primary-800 enabled:active:border-primary-900 enabled:hover:checked:text-primary-800 enabled:hover:indeterminate:text-primary-800 enabled:checked:text-primary-700 enabled:indeterminate:text-primary-700 enabled:focus-visible:outline enabled:focus-visible:outline-offset',
10-
{
11-
'border-negative-700 enabled:hover:border-negative-800 enabled:active:border-negative-900': invalid,
12-
},
13-
className,
14-
)}
15-
type="checkbox"
16-
ref={ref}
17-
data-testid="checkbox"
18-
{...attributes}
19-
/>
20-
),
12+
const defaultWrapperTag = 'label';
13+
14+
const SfCheckbox = polymorphicForwardRef<'input', SfCheckboxProps>(
15+
(
16+
{ wrapperAs, invalid, className, indeterminate: indeterminateProp, wrapperClassName, ...attributes },
17+
ref,
18+
): JSX.Element => {
19+
const inputRef = useRef<HTMLInputElement>(null);
20+
const WrapperTag = wrapperAs || defaultWrapperTag;
21+
const [isIndeterminate, setIsIndeterminate] = useState(indeterminateProp || false);
22+
const [isChecked, setIsChecked] = useState(attributes.checked || false);
23+
24+
useEffect(() => {
25+
if (inputRef.current) {
26+
inputRef.current.indeterminate = indeterminateProp || false;
27+
setIsIndeterminate(indeterminateProp || false);
28+
setIsChecked(inputRef.current.checked);
29+
}
30+
}, [indeterminateProp, attributes.checked]);
31+
32+
const handleInputChange = () => {
33+
if (inputRef.current) {
34+
setIsIndeterminate(inputRef.current.indeterminate);
35+
setIsChecked(inputRef.current.checked);
36+
}
37+
};
38+
39+
return (
40+
<WrapperTag
41+
className={classNames(
42+
'flex cursor-pointer focus-visible:outline-primary-700 focus-visible:outline focus-visible:outline-offset-2 rounded-md',
43+
{
44+
'text-neutral-500 hover:text-primary-800 active:text-primary-900': !invalid && !attributes.disabled,
45+
'text-negative-700 hover:text-negative-800 active:text-negative-900': invalid && !attributes.disabled,
46+
'text-disabled-500 hover:text-disabled-600 active:text-disabled-700': attributes.disabled,
47+
},
48+
wrapperClassName,
49+
)}
50+
data-testid="checkbox"
51+
>
52+
<input
53+
className={classNames('hidden', className)}
54+
type="checkbox"
55+
ref={mergeRefs([inputRef, ref])}
56+
{...attributes}
57+
onChange={(e) => {
58+
handleInputChange();
59+
attributes.onChange?.(e);
60+
}}
61+
/>
62+
{isIndeterminate ? (
63+
<SfIconIndeterminateCheckBox />
64+
) : isChecked ? (
65+
<SfIconCheckBox />
66+
) : (
67+
<SfIconCheckBoxOutlineBlank />
68+
)}
69+
</WrapperTag>
70+
);
71+
},
2172
);
2273

2374
export default SfCheckbox;

0 commit comments

Comments
 (0)