From abe6966a7d724f86294e5441b551cdcc3641f8ed Mon Sep 17 00:00:00 2001 From: Paolo Infante Date: Tue, 13 Jan 2026 12:51:39 +0100 Subject: [PATCH] Refactor RLInput and RLDatePicker components to use Shoelace inputs and update event handling --- src/components/RLDatePicker/RLDatePicker.tsx | 40 +++--- src/components/RLInput/RLInput.tsx | 136 ++++++++---------- src/components/RLInput/types.ts | 12 +- .../RLNumberInput/RLNumberInput.tsx | 87 ++++++----- src/stories/components/RLInput.stories.tsx | 12 ++ .../components/RLNumberInput.stories.tsx | 14 +- 6 files changed, 149 insertions(+), 152 deletions(-) diff --git a/src/components/RLDatePicker/RLDatePicker.tsx b/src/components/RLDatePicker/RLDatePicker.tsx index 79535e2..3c36bf1 100644 --- a/src/components/RLDatePicker/RLDatePicker.tsx +++ b/src/components/RLDatePicker/RLDatePicker.tsx @@ -1,20 +1,12 @@ import { forwardRef, useImperativeHandle, useCallback, useState, useMemo, useEffect } from 'react' import { Calendar } from 'primereact/calendar' +import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown/index.js' +import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button/index.js' import type { RLDatePickerProps, RLDatePickerRef } from './types' import { ErrorMessage } from '../utils/ErrorMessage' import { useValidation } from '../../hooks/useValidation' import { RLIcon } from '../RLIcon' - -declare global { - namespace JSX { - interface IntrinsicElements { - 'sl-icon-button': React.DetailedHTMLProps, HTMLElement> & { - library?: string - name?: string - } - } - } -} +import { RLInput } from '../RLInput' export const RLDatePicker = forwardRef( ( @@ -111,28 +103,28 @@ export const RLDatePicker = forwardRef( [disabled] ) - const handleShow = useCallback((event: Event) => { + const handleShow = useCallback((event: CustomEvent) => { event.stopPropagation() setIsDropdownOpen(true) }, []) - const handleHide = useCallback((event: Event) => { + const handleHide = useCallback((event: CustomEvent) => { event.stopPropagation() setIsDropdownOpen(false) }, []) return ( - +
-
( className={`flex items-center justify-end gap-2 ${disabled ? 'cursor-not-allowed' : ''}`} > {clearable && value && ( - + )}
-
+ {errorMessage && {errorMessage}}
( showTime={withTime} panelClassName="min-w-min !inline" /> -
+ ) } ) diff --git a/src/components/RLInput/RLInput.tsx b/src/components/RLInput/RLInput.tsx index 7458c25..4008b96 100644 --- a/src/components/RLInput/RLInput.tsx +++ b/src/components/RLInput/RLInput.tsx @@ -1,45 +1,10 @@ import { forwardRef, useImperativeHandle, useCallback, useEffect } from 'react' +import SlInput from '@shoelace-style/shoelace/dist/react/input/index.js' +import type SlInputElement from '@shoelace-style/shoelace/dist/components/input/input.js' import type { RLInputProps, RLInputRef } from './types' -import type { SlChangeEvent } from '../utils/types' import { ErrorMessage } from '../utils/ErrorMessage' import { useValidation } from '../../hooks/useValidation' -declare global { - namespace JSX { - interface IntrinsicElements { - 'sl-input': React.DetailedHTMLProps, HTMLElement> & { - type?: string - name?: string - value?: string - defaultValue?: string - size?: string - filled?: boolean - pill?: boolean - label?: string - 'help-text'?: string - clearable?: boolean - disabled?: boolean - placeholder?: string - readonly?: boolean - 'password-toggle'?: boolean - form?: string - required?: boolean - autocapitalize?: string - autocomplete?: string - autocorrect?: string - autofocus?: boolean - spellcheck?: boolean - inputmode?: string - min?: number - max?: number - step?: number | 'any' - title?: string - class?: string - } - } - } -} - export const RLInput = forwardRef( ( { @@ -58,6 +23,7 @@ export const RLInput = forwardRef( placeholder, readonly, passwordToggle, + noSpinButtons, form, required, autocapitalize = 'off', @@ -66,9 +32,14 @@ export const RLInput = forwardRef( autofocus, spellcheck, inputmode = 'text', + min, + max, + step, rules = [], error, title, + className, + onClick, onFocus, onBlur, onInput, @@ -93,87 +64,96 @@ export const RLInput = forwardRef( })) const handleChange = useCallback( - (event: Event) => { - const evt = event as unknown as SlChangeEvent - const target = evt.target as HTMLInputElement + (event: CustomEvent) => { + const target = event.target as SlInputElement const newValue = target?.value ?? '' + validate(newValue) onChange?.(newValue) - onSlChange?.(evt) + onSlChange?.(event) }, - [onChange, onSlChange] + [onChange, onSlChange, validate] ) const handleBlur = useCallback( - (event: Event) => { - onBlur?.(event as unknown as Parameters>[0]) + (event: CustomEvent) => { + onBlur?.(event) }, [onBlur] ) const handleFocus = useCallback( - (event: Event) => { - onFocus?.(event as unknown as Parameters>[0]) + (event: CustomEvent) => { + onFocus?.(event) }, [onFocus] ) const handleInput = useCallback( - (event: Event) => { - onInput?.(event as unknown as Parameters>[0]) + (event: CustomEvent) => { + onInput?.(event) }, [onInput] ) const handleClear = useCallback( - (event: Event) => { - onClear?.(event as unknown as Parameters>[0]) + (event: CustomEvent) => { + onClear?.(event) }, [onClear] ) const handleInvalid = useCallback( - (event: Event) => { - onInvalid?.(event as unknown as Parameters>[0]) + (event: CustomEvent) => { + onInvalid?.(event) }, [onInvalid] ) + const combinedClassName = [className, errorMessage ? 'error' : undefined] + .filter(Boolean) + .join(' ') || undefined + return (
- {children} - + {errorMessage && {errorMessage}}
) diff --git a/src/components/RLInput/types.ts b/src/components/RLInput/types.ts index e154cdc..b3eea13 100644 --- a/src/components/RLInput/types.ts +++ b/src/components/RLInput/types.ts @@ -12,19 +12,20 @@ import type { export interface RLInputProps { value?: string onChange?: (value: string) => void - type?: 'password' | 'text' | 'email' + type?: 'password' | 'text' | 'email' | 'number' name?: string defaultValue?: string size?: 'small' | 'medium' | 'large' filled?: boolean pill?: boolean - label: string + label?: string helpText?: string clearable?: boolean disabled?: boolean placeholder?: string readonly?: boolean passwordToggle?: boolean + noSpinButtons?: boolean form?: string required?: boolean autocapitalize?: 'off' | 'none' | 'on' | 'sentences' | 'words' | 'characters' @@ -32,10 +33,15 @@ export interface RLInputProps { autocorrect?: 'off' | 'on' autofocus?: boolean spellcheck?: boolean - inputmode?: 'none' | 'text' | 'email' + inputmode?: 'none' | 'text' | 'email' | 'numeric' | 'decimal' + min?: number + max?: number + step?: number | 'any' rules?: RLInputRuleType[] error?: string title?: string + className?: string + onClick?: (e: React.MouseEvent) => void onFocus?: (e: SlFocusEvent) => void onBlur?: (e: SlBlurEvent) => void onInput?: (e: SlInputEvent) => void diff --git a/src/components/RLNumberInput/RLNumberInput.tsx b/src/components/RLNumberInput/RLNumberInput.tsx index 5af6b51..9384521 100644 --- a/src/components/RLNumberInput/RLNumberInput.tsx +++ b/src/components/RLNumberInput/RLNumberInput.tsx @@ -1,6 +1,7 @@ -import { forwardRef, useImperativeHandle, useCallback, useEffect, useRef } from 'react' +import { forwardRef, useImperativeHandle, useCallback, useEffect } from 'react' +import SlInput from '@shoelace-style/shoelace/dist/react/input/index.js' +import type SlInputElement from '@shoelace-style/shoelace/dist/components/input/input.js' import type { RLNumberInputProps, RLNumberInputRef } from './types' -import type { SlChangeEvent } from '../utils/types' import { ErrorMessage } from '../utils/ErrorMessage' import { useValidation } from '../../hooks/useValidation' @@ -41,7 +42,6 @@ export const RLNumberInput = forwardRef( }, ref ) => { - const inputRef = useRef(null) const { errorMessage, isValid, validate } = useValidation({ rules, externalError: error }) useEffect(() => { @@ -76,93 +76,88 @@ export const RLNumberInput = forwardRef( ) const handleChange = useCallback( - (event: Event) => { - const evt = event as unknown as SlChangeEvent - const target = evt.target as HTMLInputElement - const validValue = checkMinMax(target?.value ?? '') + (event: CustomEvent) => { + const target = event.target as SlInputElement + const newValue = target?.value ?? '' + const validValue = checkMinMax(newValue) + validate(validValue) onChange?.(validValue) - onSlChange?.(evt) - - // Update input display if value was clamped - if (inputRef.current && validValue !== null) { - (inputRef.current as unknown as HTMLInputElement).value = validValue.toString() - } + onSlChange?.(event) }, - [checkMinMax, onChange, onSlChange] + [checkMinMax, onChange, onSlChange, validate] ) const handleBlur = useCallback( - (event: Event) => { - onBlur?.(event as unknown as Parameters>[0]) + (event: CustomEvent) => { + onBlur?.(event) }, [onBlur] ) const handleFocus = useCallback( - (event: Event) => { - onFocus?.(event as unknown as Parameters>[0]) + (event: CustomEvent) => { + onFocus?.(event) }, [onFocus] ) const handleInput = useCallback( - (event: Event) => { - onInput?.(event as unknown as Parameters>[0]) + (event: CustomEvent) => { + onInput?.(event) }, [onInput] ) const handleClear = useCallback( - (event: Event) => { + (event: CustomEvent) => { onChange?.(null) - onClear?.(event as unknown as Parameters>[0]) + onClear?.(event) }, [onChange, onClear] ) const handleInvalid = useCallback( - (event: Event) => { - onInvalid?.(event as unknown as Parameters>[0]) + (event: CustomEvent) => { + onInvalid?.(event) }, [onInvalid] ) return (
- {children} - + {errorMessage && {errorMessage}}
) diff --git a/src/stories/components/RLInput.stories.tsx b/src/stories/components/RLInput.stories.tsx index 3a27f7d..0e5ae46 100644 --- a/src/stories/components/RLInput.stories.tsx +++ b/src/stories/components/RLInput.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react' import { RLInput } from '../../components' +import { useState } from 'react' const meta = { title: 'Components/Input', @@ -14,6 +15,17 @@ const meta = { }, autocorrect: { control: 'select', options: ['off', 'on', undefined] }, inputmode: { control: 'select', options: ['none', 'text', 'email', undefined] } + }, + render(args) { + const [inputValue, setInputValue] = useState(undefined) + + return ( + + ) } } satisfies Meta diff --git a/src/stories/components/RLNumberInput.stories.tsx b/src/stories/components/RLNumberInput.stories.tsx index f2b8328..f4ba92b 100644 --- a/src/stories/components/RLNumberInput.stories.tsx +++ b/src/stories/components/RLNumberInput.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react' import { RLNumberInput } from '../../components' +import { useState } from 'react' const meta = { title: 'Components/Number input', @@ -8,7 +9,18 @@ const meta = { argTypes: { size: { control: 'select', options: ['small', 'medium', 'large', undefined] } }, - args: {} + args: {}, + render(args) { + const [numberValue, setNumberValue] = useState(null) + + return ( + + ) + } } satisfies Meta export default meta