Skip to content

Added Color Picker and Rating fields 8d #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frui/frui.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@import url('./styles/globals.css');
@import url('./styles/alert.css');
@import url('./styles/accordion.css');
@import url('./styles/badge.css');
@import url('./styles/button.css');
@import url('./styles/control.css');
Expand All @@ -18,6 +19,7 @@

@import url('./styles/fields/autocomplete.css');
@import url('./styles/fields/date.css');
@import url('./styles/fields/colorpicker.css');
@import url('./styles/fields/datetime.css');
@import url('./styles/fields/file.css');
@import url('./styles/fields/filelist.css');
Expand All @@ -29,6 +31,7 @@
@import url('./styles/fields/multiselect.css');
@import url('./styles/fields/option.css');
@import url('./styles/fields/password.css');
@import url('./styles/fields/rating.css');
@import url('./styles/fields/select.css');
@import url('./styles/fields/switch.css');
@import url('./styles/fields/taglist.css');
Expand Down
165 changes: 165 additions & 0 deletions frui/src/element/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import React, {
createContext,
useContext,
useState,
useCallback,
useMemo,
ReactNode,
HTMLAttributes,
CSSProperties,
ButtonHTMLAttributes,
useId,
} from 'react';

interface AccordionContextProps {
isOpen: boolean;
detailsId: string;
summaryId: string;
disabled: boolean;
toggle: (event: React.SyntheticEvent) => void;
}

export type AccordionProps = Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> & {
children: ReactNode;
id?: string; // Optional ID - will be generated if missing
expanded?: boolean; // Controlled state
defaultExpanded?: boolean; // Uncontrolled state
disabled?: boolean;
onChange?: (event: React.SyntheticEvent, isExpanded: boolean) => void;
className?: string;
style?: CSSProperties;
};

export type AccordionSummaryProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> & {
children: ReactNode;
expandIcon?: ReactNode;
className?: string;
style?: CSSProperties;
};

export type AccordionDetailsProps = HTMLAttributes<HTMLDivElement> & {
children: ReactNode;
className?: string;
style?: CSSProperties;
};

const AccordionContext = createContext<AccordionContextProps | undefined>(undefined);

const useAccordionContext = () => {
const context = useContext(AccordionContext);
if (!context) {
throw new Error('Accordion components must be used within an Accordion');
}
return context;
};


/**
* Accordion Component
*/
export function Accordion({
children,
id: providedId,
expanded: controlledExpanded,
defaultExpanded = false,
disabled = false,
onChange,
className,
style,
...attributes
}: AccordionProps) {
const [uncontrolledExpanded, setUncontrolledExpanded] = useState(defaultExpanded);
const generatedId = useId(); // Generate a unique ID if none is provided
const id = providedId || generatedId; // Use provided ID or generated one

const isControlled = controlledExpanded !== undefined;
const isOpen = isControlled ? controlledExpanded : uncontrolledExpanded;

const summaryId = `${id}-summary`;
const detailsId = `${id}-details`;

const toggle = useCallback((event: React.SyntheticEvent) => {
if (disabled) return;
const newState = !isOpen;
if (!isControlled) {
setUncontrolledExpanded(newState);
}
onChange?.(event, newState);
}, [isControlled, isOpen, onChange, disabled]);

const contextValue = useMemo(() => ({
isOpen,
detailsId,
summaryId,
disabled,
toggle,
}), [isOpen, detailsId, summaryId, disabled, toggle]);

const accordionClassName = `frui-accordion ${disabled ? 'frui-accordion-disabled' : ''} ${isOpen ? 'frui-accordion-open' : ''} ${className || ''}`;

return (
<AccordionContext.Provider value={contextValue}>
<div className={accordionClassName} style={style} {...attributes}>
{children}
</div>
</AccordionContext.Provider>
);
}

/**
* Accordion Summary Component
*/
export function AccordionSummary({
children,
expandIcon,
className,
style,
...attributes
}: AccordionSummaryProps) {
const { isOpen, detailsId, summaryId, disabled, toggle } = useAccordionContext();
const summaryClassName = `frui-accordion-button ${disabled ? 'frui-accordion-button-disabled' : ''} ${className || ''}`;
const iconClassName = `frui-accordion-icon ${isOpen ? 'frui-accordion-icon-rotate' : ''}`;

return (
<button
id={summaryId}
className={summaryClassName}
style={style}
onClick={toggle}
disabled={disabled}
aria-expanded={isOpen}
aria-controls={detailsId}
type="button"
{...attributes}
>
{children}
{expandIcon && <span className={iconClassName} aria-hidden="true">{expandIcon}</span>}
</button>
);
}

/**
* Accordion Details Component
*/
export function AccordionDetails({
children,
className,
style,
...attributes
}: AccordionDetailsProps) {
const { isOpen, detailsId, summaryId } = useAccordionContext();
const detailsClassName = `frui-accordion-content ${isOpen ? 'frui-accordion-content-open' : ''} ${className || ''}`;

return (
<div
id={detailsId}
className={detailsClassName}
style={style}
role="region"
aria-labelledby={summaryId}
{...attributes}
>
{children}
</div>
);
}
Loading