Browse Source

refactor: switch component using radix-ui primitives

Nicolas Meienberger 2 years ago
parent
commit
98c931ed4c

+ 10 - 0
src/client/components/ui/Switch/Switch.module.scss

@@ -0,0 +1,10 @@
+.root[data-state='checked'] {
+  background-color: var(--tblr-primary);
+  background-position: right center;
+  --tblr-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23ffffff'/%3e%3c/svg%3e");
+
+  &:focus,
+  &:hover {
+    --tblr-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23ffffff'/%3e%3c/svg%3e");
+  }
+}

+ 9 - 11
src/client/components/ui/Switch/Switch.test.tsx

@@ -1,9 +1,7 @@
 import React from 'react';
 import React from 'react';
 
 
-import '@testing-library/jest-dom/extend-expect';
-
 import { Switch } from './Switch';
 import { Switch } from './Switch';
-import { fireEvent, render } from '../../../../../tests/test-utils';
+import { fireEvent, render, screen } from '../../../../../tests/test-utils';
 
 
 describe('Switch', () => {
 describe('Switch', () => {
   it('renders the label', () => {
   it('renders the label', () => {
@@ -22,16 +20,16 @@ describe('Switch', () => {
   });
   });
 
 
   it('renders the checked state', () => {
   it('renders the checked state', () => {
-    const { container } = render(<Switch checked onChange={jest.fn} />);
-    const checkbox = container.querySelector('input[type="checkbox"]');
+    render(<Switch checked onChange={jest.fn} />);
+    const checkbox = screen.getByRole('switch');
 
 
     expect(checkbox).toBeChecked();
     expect(checkbox).toBeChecked();
   });
   });
 
 
   it('triggers onChange event when clicked', () => {
   it('triggers onChange event when clicked', () => {
     const onChange = jest.fn();
     const onChange = jest.fn();
-    const { container } = render(<Switch onChange={onChange} />);
-    const checkbox = container.querySelector('input[type="checkbox"]') as Element;
+    render(<Switch onCheckedChange={onChange} />);
+    const checkbox = screen.getByRole('switch');
 
 
     fireEvent.click(checkbox);
     fireEvent.click(checkbox);
 
 
@@ -40,8 +38,8 @@ describe('Switch', () => {
 
 
   it('triggers onBlur event when blurred', () => {
   it('triggers onBlur event when blurred', () => {
     const onBlur = jest.fn();
     const onBlur = jest.fn();
-    const { container } = render(<Switch onBlur={onBlur} />);
-    const checkbox = container.querySelector('input[type="checkbox"]') as Element;
+    render(<Switch onBlur={onBlur} />);
+    const checkbox = screen.getByRole('switch');
 
 
     fireEvent.blur(checkbox);
     fireEvent.blur(checkbox);
 
 
@@ -49,8 +47,8 @@ describe('Switch', () => {
   });
   });
 
 
   it('should change the checked state when clicked', () => {
   it('should change the checked state when clicked', () => {
-    const { container } = render(<Switch onChange={jest.fn} />);
-    const checkbox = container.querySelector('input[type="checkbox"]') as Element;
+    render(<Switch onChange={jest.fn} />);
+    const checkbox = screen.getByRole('switch');
 
 
     fireEvent.click(checkbox);
     fireEvent.click(checkbox);
 
 

+ 19 - 16
src/client/components/ui/Switch/Switch.tsx

@@ -1,19 +1,22 @@
-import React from 'react';
+'use client';
 
 
-interface IProps {
-  label?: string;
-  className?: string;
-  checked?: boolean;
-  onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
-  name?: string;
-  onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
-}
+import * as React from 'react';
+import * as SwitchPrimitives from '@radix-ui/react-switch';
+import clsx from 'clsx';
+import classes from './Switch.module.scss';
 
 
-export const Switch = React.forwardRef<HTMLInputElement, IProps>(({ onChange, onBlur, name, label, checked, className }, ref) => (
-  <div className={className}>
-    <label htmlFor={name} aria-labelledby={name} className="form-check form-switch">
-      <input id={name} name={name} ref={ref} onChange={onChange} onBlur={onBlur} className="form-check-input" type="checkbox" checked={checked} />
-      <span className="form-check-label">{label}</span>
-    </label>
-  </div>
+type RootProps = typeof SwitchPrimitives.Root;
+
+const Switch = React.forwardRef<React.ElementRef<RootProps>, React.ComponentPropsWithoutRef<RootProps> & { label?: string }>(({ className, ...props }, ref) => (
+  <label htmlFor={props.name} aria-labelledby={props.name} className={clsx('form-check form-switch form-check-sigle', className)}>
+    <SwitchPrimitives.Root name={props.name} className={clsx('form-check-input', classes.root)} {...props} ref={ref}>
+      <SwitchPrimitives.Thumb />
+    </SwitchPrimitives.Root>
+    <span id={props.name} className="form-check-label text-muted">
+      {props.label}
+    </span>
+  </label>
 ));
 ));
+Switch.displayName = SwitchPrimitives.Root.displayName;
+
+export { Switch };

+ 8 - 2
src/client/modules/Apps/components/InstallForm/InstallForm.tsx

@@ -1,5 +1,5 @@
 import React, { useEffect } from 'react';
 import React, { useEffect } from 'react';
-import { useForm } from 'react-hook-form';
+import { Controller, useForm } from 'react-hook-form';
 
 
 import { Button } from '../../../../components/ui/Button';
 import { Button } from '../../../../components/ui/Button';
 import { Switch } from '../../../../components/ui/Switch';
 import { Switch } from '../../../../components/ui/Switch';
@@ -32,6 +32,7 @@ export const InstallForm: React.FC<IProps> = ({ formFields, onSubmit, initalValu
     setValue,
     setValue,
     watch,
     watch,
     setError,
     setError,
+    control,
   } = useForm<FormValues>({});
   } = useForm<FormValues>({});
   const watchExposed = watch('exposed', false);
   const watchExposed = watch('exposed', false);
 
 
@@ -57,7 +58,12 @@ export const InstallForm: React.FC<IProps> = ({ formFields, onSubmit, initalValu
 
 
   const renderExposeForm = () => (
   const renderExposeForm = () => (
     <>
     <>
-      <Switch className="mb-3" {...register('exposed')} label="Expose app" />
+      <Controller
+        control={control}
+        name="exposed"
+        defaultValue={false}
+        render={({ field: { onChange, value, ref, ...props } }) => <Switch className="mb-3" ref={ref} checked={value} onCheckedChange={onChange} {...props} label="Expose app" />}
+      />
       {watchExposed && (
       {watchExposed && (
         <div className="mb-3">
         <div className="mb-3">
           <Input {...register('domain')} label="Domain name" error={errors.domain?.message} disabled={loading} placeholder="Domain name" />
           <Input {...register('domain')} label="Domain name" error={errors.domain?.message} disabled={loading} placeholder="Domain name" />