Skip to content

Commit

Permalink
refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
prabhuignoto committed Jul 27, 2023
1 parent 283343c commit c28d5b7
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 94 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ exports[`Accordion > should render snapshot 1`] = `
class="_accordion_f93c7f _no-border_f93c7f"
>
<div
aria-controls="accordion-body-FE0OSUKNWtV-tMmJYMVd0"
aria-controls="accordion-body-6LlnNHQy5ZaWAV195KxJH"
aria-expanded="false"
class="_header_c2ceb5 _focusable_c2ceb5 _size_c2ceb5"
id="accordion-B2zg1DrRrGFAT7-1XjX7g"
id="accordion--yzse4L0aEQcS43-8698q"
role="heading"
style="--rc-accordion-header-height: 40px; outline: none; position: relative;"
tabindex="0"
Expand Down Expand Up @@ -40,9 +40,9 @@ exports[`Accordion > should render snapshot 1`] = `
/>
</div>
<div
aria-labelledby="accordion-B2zg1DrRrGFAT7-1XjX7g"
aria-labelledby="accordion--yzse4L0aEQcS43-8698q"
class="_body_f93c7f _animate_f93c7f _close_f93c7f"
id="accordion-body-FE0OSUKNWtV-tMmJYMVd0"
id="accordion-body-6LlnNHQy5ZaWAV195KxJH"
role="region"
style="--title-color: #000; --transition: cubic-bezier(0.19, 1, 0.22, 1); --max-height: 0px;"
>
Expand Down
55 changes: 41 additions & 14 deletions packages/lib/components/radio-group/radio-group.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
import classNames from 'classnames';
import { nanoid } from 'nanoid';
import React from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useFirstRender } from '../common/effects/useFirstRender';
import { Radio } from '../radio/radio';
import { RadioGroupItemProps, RadioGroupProps } from './radio-group-model';
import styles from './radio-group.module.scss';

/**
* RadioGroup Component
* @property {Array} items - An array of objects containing radio item properties.
* @property {boolean} disabled - Whether the entire group is disabled (default: false).
* @property {Function} onSelected - Callback function called when the selection changes.
* @property {Object} style - Additional style to be applied to the radio group element.
* @property {string} layout - The layout direction of the radio group ('column' or 'row', default: 'column').
* @property {boolean} RTL - Whether the layout is right-to-left (default: false).
* @property {boolean} focusable - Whether the radio group is focusable (default: true).
* @property {string} size - The size of the radio buttons in the group (default: 'sm').
* @returns {JSX.Element} The RadioGroup component.
*/
const RadioGroup: React.FunctionComponent<RadioGroupProps> = ({
items,
disabled,
Expand All @@ -17,6 +34,7 @@ const RadioGroup: React.FunctionComponent<RadioGroupProps> = ({
focusable = true,
size = 'sm',
}) => {
// State to manage the list of radio group items
const [_items, setItems] = useState<RadioGroupItemProps<string>[]>(
Array.isArray(items)
? items.map(item => ({
Expand All @@ -26,9 +44,14 @@ const RadioGroup: React.FunctionComponent<RadioGroupProps> = ({
}))
: []
);

// State to track changes in the radio group items
const [changeTracker, setChangeTracker] = useState<number>();

// Reference to the currently active radio item
const active = useRef<string>();

// Function to handle changes in the radio group selection
const handleChange = useCallback(
({ id }: { checked?: boolean; id?: string }) => {
if (active.current !== id) {
Expand All @@ -46,28 +69,31 @@ const RadioGroup: React.FunctionComponent<RadioGroupProps> = ({
[]
);

useEffect(() => {
if (!isFirstRender.current) {
const foundItem = _items.find(item => item.checked);
const value = foundItem ? foundItem.value : undefined;

if (value && onSelected) {
onSelected(value);
}
}
}, [changeTracker]);

// Detect if it's the first render of the component
const isFirstRender = useFirstRender();

// Calculate the class for the radio group container based on layout
const radioGroupClass = useMemo(
() =>
classNames(styles.radio_group, {
[styles.column]: layout === 'column',
[styles.row]: layout === 'row',
}),
[]
[layout]
);

// useEffect to trigger onSelected when a radio item is changed (excluding the first render)
useEffect(() => {
if (!isFirstRender.current) {
const foundItem = _items.find(item => item.checked);
const value = foundItem ? foundItem.value : undefined;

if (value && onSelected) {
onSelected(value);
}
}
}, [changeTracker, _items, onSelected]);

return (
<ul className={radioGroupClass} role="radiogroup" style={style}>
{_items.map(({ id, disabled, label, checked }) => (
Expand All @@ -90,6 +116,7 @@ const RadioGroup: React.FunctionComponent<RadioGroupProps> = ({
);
};

// Set a display name for the component (useful for debugging)
RadioGroup.displayName = 'RadioGroup';

export { RadioGroup };
60 changes: 47 additions & 13 deletions packages/lib/components/radio/radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ import { isDark } from '../common/utils';
import { RadioProps } from './radio-model';
import styles from './radio.module.scss';

/**
* Radio Component
* @property {boolean} disabled - Whether the radio is disabled (default: false).
* @property {string} id - An optional ID for the radio element.
* @property {boolean} isChecked - Whether the radio is checked (default: false).
* @property {string} label - The label text for the radio button.
* @property {Function} onChange - Callback function called when the radio state changes.
* @property {any} value - The value associated with the radio button.
* @property {string} size - The size of the radio button (default: 'sm').
* @property {Object} style - Additional style to be applied to the radio element.
* @property {boolean} focusable - Whether the radio is focusable (default: true).
* @property {boolean} withGroup - Whether the radio is part of a group (default: false).
* @property {boolean} fullWidth - Whether the radio button should occupy full width (default: true).
* @property {boolean} RTL - Whether the layout is right-to-left (default: false).
* @returns {JSX.Element} The Radio component.
*/
const Radio: React.FunctionComponent<RadioProps> = React.memo(
({
disabled,
Expand All @@ -28,42 +44,53 @@ const Radio: React.FunctionComponent<RadioProps> = React.memo(
fullWidth = true,
RTL = false,
}: RadioProps) => {
// Generate a unique ID for the radio if not provided
const idRef = useRef<string>(id || nanoid());

// Reference to the label element for accessibility
const labelID = useRef(`label-${idRef.current}`);

// Reference to the radio container element for focus handling
const radioRef = useRef<HTMLLIElement | null>(null);

// State to manage the checked state of the radio
const [checked, setChecked] = useState<boolean | null>(isChecked);

// Detect if it's the first render of the component
const isFirstRender = useFirstRender();

// Determine whether the radio can be toggled (not disabled)
const canToggle = useMemo(() => !disabled, [disabled]);

// Detect if the theme is dark mode
const isDarkMode = useMemo(() => isDark(), []);

// Function to toggle the checked state of the radio
const toggleCheck = useCallback(() => {
if (canToggle) {
if (!withGroup) {
setChecked(prev => !prev);
} else {
setChecked(true);
}
onChange &&
onChange({
id,
selected: !withGroup ? !checked : true,
value,
});
onChange?.({
id,
selected: !withGroup ? !checked : true,
value,
});
}
}, [canToggle, checked]);
}, [canToggle, checked, onChange, value, withGroup]);

// Determine if the radio is focusable (not disabled and focusable)
const canFocus = useMemo(
() => focusable && !disabled,
[disabled, focusable]
);

// Set up focus management using custom hook
useFocusNew(canFocus ? radioRef : null, canFocus ? toggleCheck : null);

// Calculate classes for the radio wrapper element
const radioWrapperClass = useMemo(() => {
return cls(styles.wrapper, {
[styles[size]]: true,
Expand All @@ -72,40 +99,45 @@ const Radio: React.FunctionComponent<RadioProps> = React.memo(
[styles.rtl]: RTL,
[styles.dark]: isDarkMode,
});
}, [disabled, fullWidth]);
}, [disabled, fullWidth, RTL, size, isDarkMode]);

// Calculate classes for the radio input element
const radioClass = useMemo(
() =>
cls(styles.radio, {
[styles.ico_checked]: checked,
[styles.disabled]: disabled,
[styles[size]]: true,
}),
[checked, disabled]
[checked, disabled, size]
);

// Calculate classes for the radio icon element
const radioIconClass = useMemo(() => {
return cls(styles.icon, {
[styles.ico_checked]: checked,
[styles.ico_unchecked]: !isFirstRender.current && !checked,
[styles.dark]: isDarkMode,
});
}, [checked]);
}, [checked, isFirstRender, isDarkMode]);

// Calculate classes for the radio label element
const radioLabelClass = useMemo(() => {
return cls([styles.label, styles[`label_${size}`]], {
[styles.disabled]: disabled,
[styles.rtl]: RTL,
[styles.dark]: isDarkMode,
});
}, [size, disabled]);
}, [size, disabled, RTL, isDarkMode]);

// Handle updates to the isChecked prop after the first render
useEffect(() => {
if (!isFirstRender.current) {
setChecked(isChecked);
}
}, [isChecked]);
}, [isChecked, isFirstRender]);

// Calculate HTML attributes for the radio input element
const htmlAttrs = useMemo(
() => ({
'aria-checked': !!checked,
Expand All @@ -114,7 +146,7 @@ const Radio: React.FunctionComponent<RadioProps> = React.memo(
role: 'radio',
tabIndex: canFocus ? 0 : -1,
}),
[checked, disabled, canFocus]
[canFocus, checked, toggleCheck]
);

return (
Expand All @@ -133,6 +165,7 @@ const Radio: React.FunctionComponent<RadioProps> = React.memo(
</li>
);
},
// Memoization function for React.memo to optimize rendering
(prevProps, nextProps) => {
return (
prevProps.disabled === nextProps.disabled &&
Expand All @@ -141,6 +174,7 @@ const Radio: React.FunctionComponent<RadioProps> = React.memo(
}
);

// Set a display name for the component (useful for debugging)
Radio.displayName = 'Radio';

export { Radio };
29 changes: 25 additions & 4 deletions packages/lib/components/rate/rate-item.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import { RateIcon } from '@icons';
import classNames from 'classnames';
import React, { useMemo } from 'react';
import React, { useMemo, useRef } from 'react';
import useFocusNew from '../common/effects/useFocusNew';
import { isDark } from '../common/utils';
import { RateItemViewProps } from './rate-model';
import styles from './rate.module.scss';

/**
* RateItem Component
* @property {boolean} active - Whether the rate item is currently active (checked).
* @property {boolean} disabled - Whether the rate item is disabled.
* @property {boolean} focusable - Whether the rate item is focusable.
* @property {boolean} hovered - Whether the rate item is currently being hovered.
* @property {React.ReactElement} icon - The custom icon to use for the rate item.
* @property {string} id - The unique ID of the rate item.
* @property {number} index - The index of the rate item within the rate component.
* @property {Function} onMouseOver - Callback function called when the rate item is hovered.
* @property {Function} onSelect - Callback function called when the rate item is selected.
* @property {string} size - The size of the rate item ('sm', 'md', or 'lg', default: 'sm').
* @returns {JSX.Element} The RateItem component.
*/
const RateItem: React.FunctionComponent<RateItemViewProps> = ({
active,
disabled,
Expand All @@ -18,14 +32,18 @@ const RateItem: React.FunctionComponent<RateItemViewProps> = ({
onSelect,
size = 'sm',
}: RateItemViewProps) => {
const ref = React.useRef<HTMLLIElement | null>(null);
const ref = useRef<HTMLLIElement | null>(null);

// Check if the dark mode is enabled
const isDarkMode = useMemo(() => isDark(), []);

// Hook to manage focus and keyboard navigation for the rate item
useFocusNew(
focusable && !disabled ? ref : null,
focusable ? () => onSelect(index) : null
);

// Calculate the class for the rate item based on its state (active, hovered, disabled, size, dark mode)
const rateItemClass = useMemo(
() =>
classNames(styles.item, {
Expand All @@ -35,18 +53,20 @@ const RateItem: React.FunctionComponent<RateItemViewProps> = ({
[styles.hovered]: hovered,
[styles.dark]: isDarkMode,
}),
[active, hovered, disabled]
[active, hovered, disabled, size, isDarkMode]
);

// Generate the props for focusable elements
const focusableProps = useMemo(
() =>
focusable && {
ref,
tabIndex: 0,
},
[]
[focusable]
);

// Generate the props for disabled elements
const disabledProps = useMemo(
() =>
disabled && {
Expand All @@ -65,6 +85,7 @@ const RateItem: React.FunctionComponent<RateItemViewProps> = ({
{...focusableProps}
{...disabledProps}
>
{/* The icon representing the rate item */}
<span role="img" onClick={() => onSelect(index)} aria-label="star">
{icon || <RateIcon />}
</span>
Expand Down
Loading

1 comment on commit c28d5b7

@vercel
Copy link

@vercel vercel bot commented on c28d5b7 Jul 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.