Skip to content

Commit

Permalink
Merge pull request #157 from fictoan/option-card-improvements
Browse files Browse the repository at this point in the history
v1.11.9-alpha-0
  • Loading branch information
sujan-s authored Dec 30, 2024
2 parents 570e05c + 8e763ee commit 5425c5a
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 97 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fictoan-react",
"version": "1.11.8",
"version": "1.11.9-alpha.0",
"private": false,
"description": "A full-featured, designer-friendly, yet performant framework with plain-English props and focus on rapid iteration.",
"repository": {
Expand Down
205 changes: 109 additions & 96 deletions src/components/OptionCard/OptionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface OptionCardsProviderProps {
showTickIcon ? : boolean;
tickPosition ? : TickPosition;
onSelectionChange ? : (selectedIds: Set<string>) => void;
selectionLimit ? : number;
selectionLimit ? : number;
}

export interface OptionCardProps extends CardProps {
Expand All @@ -41,6 +41,11 @@ export interface OptionCardProps extends CardProps {
disabled ? : boolean;
}

export interface OptionCardsGroupRef {
selectAllOptions: () => void;
clearAllOptions: () => void;
}

interface OptionCardsContextType {
isSelected : (id: string) => boolean;
toggleSelection : (id: string) => void;
Expand All @@ -64,103 +69,111 @@ const OptionCardsContext = createContext<OptionCardsContextType>({
});

// COMPONENT ///////////////////////////////////////////////////////////////////////////////////////////////////////////
export const OptionCardsGroup: React.FC<OptionCardsProviderProps> = (
{
children,
allowMultipleSelections = false,
showTickIcon,
onSelectionChange,
tickPosition = "top-right",
selectionLimit,
...props
},
) => {
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const availableOptionsRef = useRef<Map<string, boolean>>(new Map()); // id -> disabled

const registerOption = useCallback((id: string, disabled: boolean) => {
availableOptionsRef.current.set(id, disabled);
}, []);

const unregisterOption = useCallback((id: string) => {
availableOptionsRef.current.delete(id);
}, []);

const toggleSelection = useCallback((id: string) => {
setSelectedIds(prevSelectedIds => {
const newSelectedIds = new Set(prevSelectedIds);
if (allowMultipleSelections) {
if (newSelectedIds.has(id)) {
newSelectedIds.delete(id);
} else {
if (selectionLimit && newSelectedIds.size >= selectionLimit) {
return prevSelectedIds;
export const OptionCardsGroup = React.forwardRef<OptionCardsGroupRef, OptionCardsProviderProps>(
(
{
children,
allowMultipleSelections = false,
showTickIcon,
onSelectionChange,
tickPosition = "top-right",
selectionLimit,
...props
},
ref
) => {
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const availableOptionsRef = useRef<Map<string, boolean>>(new Map()); // id -> disabled

const registerOption = useCallback((id: string, disabled: boolean) => {
availableOptionsRef.current.set(id, disabled);
}, []);

const unregisterOption = useCallback((id: string) => {
availableOptionsRef.current.delete(id);
}, []);

const toggleSelection = useCallback((id: string) => {
setSelectedIds(prevSelectedIds => {
const newSelectedIds = new Set(prevSelectedIds);
if (allowMultipleSelections) {
if (newSelectedIds.has(id)) {
newSelectedIds.delete(id);
} else {
if (selectionLimit && newSelectedIds.size >= selectionLimit) {
return prevSelectedIds;
}
newSelectedIds.add(id);
}
newSelectedIds.add(id);
}
} else {
if (newSelectedIds.has(id) && prevSelectedIds.size === 1) {
newSelectedIds.clear();
} else {
newSelectedIds.clear();
newSelectedIds.add(id);
if (newSelectedIds.has(id) && prevSelectedIds.size === 1) {
newSelectedIds.clear();
} else {
newSelectedIds.clear();
newSelectedIds.add(id);
}
}
}
onSelectionChange?.(newSelectedIds);
return newSelectedIds;
});
}, [allowMultipleSelections, onSelectionChange, selectionLimit]);

const selectAllOptions = useCallback(() => {
if (!allowMultipleSelections) return;

setSelectedIds(prevSelectedIds => {
const newSelectedIds = new Set(prevSelectedIds);

// Get all enabled options
const enabledOptions = Array.from(availableOptionsRef.current.entries())
.filter(([_, disabled]) => !disabled)
.map(([id]) => id);

// Respect selection limit if set
const optionsToAdd = selectionLimit
? enabledOptions.slice(0, selectionLimit)
: enabledOptions;

optionsToAdd.forEach(id => newSelectedIds.add(id));
onSelectionChange?.(newSelectedIds);
return newSelectedIds;
});
}, [allowMultipleSelections, selectionLimit, onSelectionChange]);

const clearAllOptions = useCallback(() => {
setSelectedIds(new Set());
onSelectionChange?.(new Set());
}, [onSelectionChange]);

const isSelected = useCallback((id: string) => {
return selectedIds.has(id);
}, [selectedIds]);

const contextValue = {
isSelected,
toggleSelection,
showTickIcon,
tickPosition,
selectAllOptions,
clearAllOptions,
registerOption,
unregisterOption,
};

return (
<OptionCardsContext.Provider value={contextValue}>
<Div data-option-cards-group className={`tick-${tickPosition}`}>
{children}
</Div>
</OptionCardsContext.Provider>
);
};
onSelectionChange?.(newSelectedIds);
return newSelectedIds;
});
}, [allowMultipleSelections, onSelectionChange, selectionLimit]);

const selectAllOptions = useCallback(() => {
if (!allowMultipleSelections) return;

setSelectedIds(prevSelectedIds => {
const newSelectedIds = new Set(prevSelectedIds);

// Get all enabled options
const enabledOptions = Array.from(availableOptionsRef.current.entries())
.filter(([_, disabled]) => !disabled)
.map(([id]) => id);

// Respect selection limit if set
const optionsToAdd = selectionLimit
? enabledOptions.slice(0, selectionLimit)
: enabledOptions;

optionsToAdd.forEach(id => newSelectedIds.add(id));
onSelectionChange?.(newSelectedIds);
return newSelectedIds;
});
}, [allowMultipleSelections, selectionLimit, onSelectionChange]);

const clearAllOptions = useCallback(() => {
setSelectedIds(new Set());
onSelectionChange?.(new Set());
}, [onSelectionChange]);

const isSelected = useCallback((id: string) => {
return selectedIds.has(id);
}, [selectedIds]);

React.useImperativeHandle(ref, () => ({
selectAllOptions,
clearAllOptions
}));

const contextValue = {
isSelected,
toggleSelection,
showTickIcon,
tickPosition,
selectAllOptions,
clearAllOptions,
registerOption,
unregisterOption,
};

return (
<OptionCardsContext.Provider value={contextValue}>
<Div data-option-cards-group className={`tick-${tickPosition}`}>
{children}
</Div>
</OptionCardsContext.Provider>
);
}
);

export const useOptionCard = (id: string) => {
const context = useContext(OptionCardsContext);
Expand All @@ -173,7 +186,7 @@ export const useOptionCard = (id: string) => {

export const useOptionCards = () => {
const { selectAllOptions, clearAllOptions } = useContext(OptionCardsContext);
return { selectAllOptions, clearAllOptions }; // Shorter version, same functionality
return { selectAllOptions, clearAllOptions };
};

export const OptionCard: React.FC<OptionCardProps> = ({ id, children, disabled = false, ...props }) => {
Expand Down

0 comments on commit 5425c5a

Please sign in to comment.