feat: create Select component with radix-ui primitives

This commit is contained in:
Nicolas Meienberger 2023-04-15 01:59:21 +02:00 committed by Nicolas Meienberger
parent 84ac086678
commit fc7f4b8358
7 changed files with 222 additions and 5 deletions

View file

@ -35,6 +35,7 @@
"@otplib/plugin-thirty-two": "^12.0.1",
"@prisma/client": "^4.12.0",
"@radix-ui/react-dialog": "^1.0.3",
"@radix-ui/react-select": "^1.2.1",
"@radix-ui/react-switch": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.3",
"@runtipi/postgres-migrations": "^5.3.0",
@ -85,6 +86,7 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@total-typescript/shoehorn": "^0.1.0",
"@total-typescript/ts-reset": "^0.4.2",
"@types/express": "^4.17.13",
"@types/fs-extra": "^11.0.1",

View file

@ -19,6 +19,9 @@ dependencies:
'@radix-ui/react-dialog':
specifier: ^1.0.3
version: 1.0.3(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-select':
specifier: ^1.2.1
version: 1.2.1(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-switch':
specifier: ^1.0.2
version: 1.0.2(react-dom@18.2.0)(react@18.2.0)
@ -165,6 +168,9 @@ devDependencies:
'@testing-library/user-event':
specifier: ^14.4.3
version: 14.4.3(@testing-library/dom@9.0.1)
'@total-typescript/shoehorn':
specifier: ^0.1.0
version: 0.1.0
'@total-typescript/ts-reset':
specifier: ^0.4.2
version: 0.4.2
@ -992,16 +998,40 @@ packages:
engines: {node: '>=14.0.0', npm: '>=6.0.0'}
dev: true
/@floating-ui/core@0.7.3:
resolution: {integrity: sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==}
dev: false
/@floating-ui/core@1.2.1:
resolution: {integrity: sha512-LSqwPZkK3rYfD7GKoIeExXOyYx6Q1O4iqZWwIehDNuv3Dv425FIAE8PRwtAx1imEolFTHgBEcoFHm9MDnYgPCg==}
dev: false
/@floating-ui/dom@0.5.4:
resolution: {integrity: sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==}
dependencies:
'@floating-ui/core': 0.7.3
dev: false
/@floating-ui/dom@1.2.1:
resolution: {integrity: sha512-Rt45SmRiV8eU+xXSB9t0uMYiQ/ZWGE/jumse2o3i5RGlyvcbqOF4q+1qBnzLE2kZ5JGhq0iMkcGXUKbFe7MpTA==}
dependencies:
'@floating-ui/core': 1.2.1
dev: false
/@floating-ui/react-dom@0.7.2(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@floating-ui/dom': 0.5.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
use-isomorphic-layout-effect: 1.1.2(@types/react@18.0.28)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
/@hookform/resolvers@2.9.11(react-hook-form@7.43.7):
resolution: {integrity: sha512-bA3aZ79UgcHj7tFV7RlgThzwSSHZgvfbt2wprldRkYBcMopdMvHyO17Wwp/twcJasNFischFfS7oz8Katz8DdQ==}
peerDependencies:
@ -1557,12 +1587,30 @@ packages:
resolution: {integrity: sha512-0alKtnxhNB5hYU+ymESBlGI4b9XrGGSdv7Ud+8TE/fBNOEhIud0XQsAR+TrvUZgS4na5czubiMsODw0TUrgkIA==}
requiresBuild: true
/@radix-ui/number@1.0.0:
resolution: {integrity: sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA==}
dependencies:
'@babel/runtime': 7.20.13
dev: false
/@radix-ui/primitive@1.0.0:
resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==}
dependencies:
'@babel/runtime': 7.20.13
dev: false
/@radix-ui/react-arrow@1.0.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-collection@1.0.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-s8WdQQ6wNXpaxdZ308KSr8fEWGrg4un8i4r/w7fhiS4ElRNjk5rRcl0/C6TANG2LvLOGIxtzo/jAg6Qf73TEBw==}
peerDependencies:
@ -1681,6 +1729,29 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-popper@1.1.1(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.13
'@floating-ui/react-dom': 0.7.2(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-arrow': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-context': 1.0.0(react@18.2.0)
'@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.0.0(react@18.2.0)
'@radix-ui/react-use-rect': 1.0.0(react@18.2.0)
'@radix-ui/react-use-size': 1.0.0(react@18.2.0)
'@radix-ui/rect': 1.0.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
/@radix-ui/react-portal@1.0.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-swu32idoCW7KA2VEiUZGBSu9nB6qwGdV6k6HYhUoOo3M1FFpD+VgLzUqtt3mwL1ssz7r2x8MggpLSQach2Xy/Q==}
peerDependencies:
@ -1738,6 +1809,40 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-select@1.2.1(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-GULRMITaOHNj79BZvQs3iZO0+f2IgI8g5HDhMi7Bnc13t7IlG86NFtOCfTLme4PNZdEtU+no+oGgcl6IFiphpQ==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/number': 1.0.0
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-collection': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-context': 1.0.0(react@18.2.0)
'@radix-ui/react-direction': 1.0.0(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.0.3(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-focus-guards': 1.0.0(react@18.2.0)
'@radix-ui/react-focus-scope': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-id': 1.0.0(react@18.2.0)
'@radix-ui/react-popper': 1.1.1(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-portal': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.1(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.0(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.0.0(react@18.2.0)
'@radix-ui/react-use-previous': 1.0.0(react@18.2.0)
'@radix-ui/react-visually-hidden': 1.0.2(react-dom@18.2.0)(react@18.2.0)
aria-hidden: 1.2.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.5.5(@types/react@18.0.28)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
/@radix-ui/react-slot@1.0.1(react@18.2.0):
resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==}
peerDependencies:
@ -1832,6 +1937,16 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-use-rect@1.0.0(react@18.2.0):
resolution: {integrity: sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/rect': 1.0.0
react: 18.2.0
dev: false
/@radix-ui/react-use-size@1.0.0(react@18.2.0):
resolution: {integrity: sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==}
peerDependencies:
@ -1842,6 +1957,24 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-visually-hidden@1.0.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-qirnJxtYn73HEk1rXL12/mXnu2rwsNHDID10th2JGtdK25T9wX+mxRmGt7iPSahw512GbZOc0syZX1nLQGoEOg==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/rect@1.0.0:
resolution: {integrity: sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg==}
dependencies:
'@babel/runtime': 7.20.13
dev: false
/@redis/bloom@1.2.0(@redis/client@1.5.6):
resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==}
peerDependencies:
@ -2097,6 +2230,10 @@ packages:
engines: {node: '>= 10'}
dev: true
/@total-typescript/shoehorn@0.1.0:
resolution: {integrity: sha512-XKig6hXxWnUh0fsW3LR2vxpxwLlPFokfOSR0riHKA9uXvdHDfwOOPdAOi4U/YNKLmgYUu/plUfnF3yiAAz1+Zg==}
dev: true
/@total-typescript/ts-reset@0.4.2:
resolution: {integrity: sha512-vqd7ZUDSrXFVT1n8b2kc3LnklncDQFPvR58yUS1kEP23/nHPAO9l1lMjUfnPrXYYk4Hj54rrLKMW5ipwk7k09A==}
dev: true

View file

@ -21,9 +21,6 @@ const DialogPortal = ({ className, children, ...props }: DialogPrimitive.DialogP
<div className={clsx('modal modal-sm d-block', styles.dimmedBackground)}>
<div className={clsx(`modal-dialog modal-dialog-centered modal-${props.size || 'lg'}`, styles.zoomIn)}>
<div className="shadow modal-content">
<DialogPrimitive.Close className="btn-close mt-1">
<button data-testid="modal-close-button" type="button" className="btn-close" aria-label="Close" />
</DialogPrimitive.Close>
<div data-testid="modal-status" className={clsx('modal-status', { [`bg-${props.type}`]: Boolean(props.type), 'd-none': !props.type })} />
{children}
</div>
@ -41,8 +38,12 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Content>, React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & ModalProps>(
({ className, children, ...props }, ref) => (
<DialogPortal type={props.type} size={props.size}>
<DialogOverlay />
<DialogPrimitive.Content ref={ref} className={clsx('modal-content mt-1', className)} {...props}>
{children}
<DialogPrimitive.Close className="btn-close">
<button data-testid="modal-close-button" type="button" className="btn-close" aria-label="Close" />
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
),

View file

@ -4,7 +4,7 @@ import clsx from 'clsx';
interface IProps {
placeholder?: string;
error?: string;
label?: string;
label?: string | React.ReactNode;
className?: string;
isInvalid?: boolean;
type?: HTMLInputElement['type'];

View file

@ -0,0 +1,76 @@
'use client';
import * as React from 'react';
import * as SelectPrimitive from '@radix-ui/react-select';
import { IconCheck, IconChevronDown, IconChevronUp } from '@tabler/icons-react';
import clsx from 'clsx';
type TriggerProps = {
label?: string | React.ReactNode;
error?: string;
};
const Select: React.FC<React.ComponentPropsWithoutRef<typeof SelectPrimitive.Root> & { label?: string; error?: string; className?: string }> = ({ children, error, className, ...props }) => {
return <SelectPrimitive.Root {...props}>{children}</SelectPrimitive.Root>;
};
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
// Button
const SelectTrigger = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Trigger>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & TriggerProps>(
({ className, error, label, children, ...props }, ref) => (
<label htmlFor={props.name} aria-labelledby={props.name} className={clsx('w-100', className)}>
{Boolean(label) && (
<span id={props.name} className="form-label">
{label}
</span>
)}
<SelectPrimitive.Trigger ref={ref} className={clsx('d-flex w-100 align-items-center justify-content-between form-select', { 'is-invalid is-invalid-lite': error })} {...props}>
{children}
</SelectPrimitive.Trigger>
{error && <div className="invalid-feedback">{error}</div>}
</label>
),
);
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectContent = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Content>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content ref={ref} style={{ zIndex: 2000 }} className={clsx('overflow-hidden dropdown-menu', className)} {...props}>
<SelectPrimitive.ScrollUpButton className="d-flex align-items-center justify-content-center">
<IconChevronUp />
</SelectPrimitive.ScrollUpButton>
<SelectPrimitive.Viewport className="p-1">{children}</SelectPrimitive.Viewport>
<SelectPrimitive.ScrollDownButton className="d-flex align-items-center justify-content-center">
<IconChevronDown />
</SelectPrimitive.ScrollDownButton>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Label>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>>(({ className, ...props }, ref) => (
<SelectPrimitive.Label ref={ref} className={clsx('', className)} {...props} />
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item ref={ref} className={clsx('ps-8 position-relative d-flex align-items-center dropdown-item', className)} {...props}>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
<span style={{ right: 8 }} className="position-absolute d-flex align-items-center justify-content-center">
<SelectPrimitive.ItemIndicator>
<IconCheck size={20} style={{ marginBottom: 3 }} />
</SelectPrimitive.ItemIndicator>
</span>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Separator>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator ref={ref} className={clsx('', className)} {...props} />
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator };

View file

@ -0,0 +1 @@
export { Select, SelectItem, SelectContent, SelectTrigger, SelectValue, SelectGroup, SelectLabel, SelectSeparator } from './Select';

View file

@ -7,7 +7,7 @@ import classes from './Switch.module.scss';
type RootProps = typeof SwitchPrimitives.Root;
const Switch = React.forwardRef<React.ElementRef<RootProps>, React.ComponentPropsWithoutRef<RootProps> & { label?: string }>(({ className, ...props }, ref) => (
const Switch = React.forwardRef<React.ElementRef<RootProps>, React.ComponentPropsWithoutRef<RootProps> & { label?: string | React.ReactNode }>(({ 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 />