Skip to content

Commit

Permalink
Prevent accidental miss-clicks outside multi-select checkboxes
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed Sep 6, 2024
1 parent 5976906 commit 883b318
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 32 deletions.
7 changes: 6 additions & 1 deletion src/components/input/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { JSX } from 'preact';
import type { JSX, Ref } from 'preact';
import { useState } from 'preact/hooks';

import type { CompositeProps, IconComponent } from '../../types';
Expand All @@ -20,6 +20,11 @@ type ComponentProps = {
checkedIcon?: IconComponent;
/** type is always `checkbox` */
type?: never;

/** Optional extra CSS classes appended to the container's className */
containerClasses?: string | string[];
/** Ref associated with the components container */
containerRef?: Ref<HTMLLabelElement | undefined>;
};

export type CheckboxProps = CompositeProps &
Expand Down
7 changes: 6 additions & 1 deletion src/components/input/RadioButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { JSX } from 'preact';
import type { JSX, Ref } from 'preact';

import type { CompositeProps, IconComponent } from '../../types';
import { RadioCheckedIcon, RadioIcon } from '../icons';
Expand All @@ -13,6 +13,11 @@ type ComponentProps = {
checkedIcon?: IconComponent;
/** type is always `radio` */
type?: never;

/** Optional extra CSS classes appended to the container's className */
containerClasses?: string | string[];
/** Ref associated with the components container */
containerRef?: Ref<HTMLLabelElement | undefined>;
};

export type RadioButtonProps = CompositeProps &
Expand Down
63 changes: 38 additions & 25 deletions src/components/input/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ function SelectOption<T>({
elementRef,
}: SelectOptionProps<T>) {
const checkboxRef = useRef<HTMLElement | null>(null);
const checkboxContainerRef = useRef<HTMLLabelElement | null>(null);
const optionRef = useSyncedRef(elementRef);

const selectContext = useContext(SelectContext);
Expand Down Expand Up @@ -150,9 +151,13 @@ function SelectOption<T>({
classes,
)}
onClick={e => {
// Do not invoke callback if clicked element is the checkbox, as it has
// its own event handler.
if (!disabled && e.target !== checkboxRef.current) {
if (
!disabled &&
// Do not invoke callback if clicked element is the checkbox or its
// container, as it has its own event handler.
e.target !== checkboxRef.current &&
e.target !== checkboxContainerRef.current
) {
selectOneValue();
}
}}
Expand All @@ -163,9 +168,10 @@ function SelectOption<T>({

if (
['Enter', ' '].includes(e.key) &&
// Do not invoke callback if event triggers in checkbox, as it has its
// own event handler.
e.target !== checkboxRef.current
// Do not invoke callback if event triggers in the checkbox or its
// container, as it has its own event handler.
e.target !== checkboxRef.current &&
e.target !== checkboxContainerRef.current
) {
e.preventDefault();
selectOneValue();
Expand Down Expand Up @@ -203,25 +209,32 @@ function SelectOption<T>({
/>
)}
{multiple && (
<div
className={classnames('scale-125', {
'text-grey-6': selected,
'text-grey-3 hover:text-grey-6': !selected,
})}
>
<Checkbox
checked={selected}
checkedIcon={CheckboxCheckedFilledIcon}
elementRef={checkboxRef}
onChange={toggleValue}
onKeyDown={e => {
if (e.key === 'ArrowLeft') {
e.preventDefault();
optionRef.current?.focus();
}
}}
/>
</div>
<Checkbox
containerClasses={classnames(
// Add negative margin, padding and invisible border to match the
// container's, preventing accidental miss-clicks outside the
// checkbox that mess the whole selection.
'-m-2.5 p-2.5 border border-transparent',
// The checkbox is sized based on the container's font size. Make
// it a bit larger.
'text-xl',
{
'text-grey-6': selected,
'text-grey-3 hover:text-grey-6': !selected,
},
)}
checked={selected}
checkedIcon={CheckboxCheckedFilledIcon}
elementRef={checkboxRef}
containerRef={checkboxContainerRef}
onChange={toggleValue}
onKeyDown={e => {
if (e.key === 'ArrowLeft') {
e.preventDefault();
optionRef.current?.focus();
}
}}
/>
)}
</div>
</li>
Expand Down
22 changes: 17 additions & 5 deletions src/components/input/ToggleInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import classnames from 'classnames';
import type { JSX } from 'preact';
import type { JSX, Ref } from 'preact';

import type { CompositeProps, IconComponent } from '../../types';
import { downcastRef } from '../../util/typing';
Expand All @@ -13,6 +13,11 @@ type ComponentProps = {
checkedIcon: IconComponent;

type: 'checkbox' | 'radio';

/** Optional extra CSS classes appended to the container's className */
containerClasses?: string | string[];
/** Ref associated with the components container */
containerRef?: Ref<HTMLLabelElement | undefined>;
};

export type ToggleInputProps = CompositeProps &
Expand All @@ -27,6 +32,7 @@ export type ToggleInputProps = CompositeProps &
export default function ToggleInput({
children,
elementRef,
containerRef,

checked,
icon: UncheckedIcon,
Expand All @@ -36,20 +42,26 @@ export default function ToggleInput({
onChange,
id,
type,
containerClasses,
...htmlAttributes
}: ToggleInputProps) {
const Icon = checked ? CheckedIcon : UncheckedIcon;

return (
<label
className={classnames('relative flex items-center gap-x-1.5', {
'cursor-pointer': !disabled,
'opacity-70': disabled,
})}
className={classnames(
'relative flex items-center gap-x-1.5',
{
'cursor-pointer': !disabled,
'opacity-70': disabled,
},
containerClasses,
)}
htmlFor={id}
data-composite-component={
type === 'checkbox' ? 'Checkbox' : 'RadioButton'
}
ref={downcastRef(containerRef)}
>
<input
{...htmlAttributes}
Expand Down

0 comments on commit 883b318

Please sign in to comment.