diff --git a/frui/src/field/RadioGroup.tsx b/frui/src/field/RadioGroup.tsx new file mode 100644 index 0000000..53c37ad --- /dev/null +++ b/frui/src/field/RadioGroup.tsx @@ -0,0 +1,176 @@ +// types +import type { ChangeEvent, CSSProperties, MouseEvent, ReactNode } from 'react'; +import type { InputConfig } from './Input'; +import type { HTMLInputProps } from '../types'; +// hooks +import { useState, useEffect } from 'react'; +// utils +import { Children, isValidElement, cloneElement } from 'react'; + +export type RadioGroupProps = { + children: ReactNode; + name: string; + defaultValue?: string | number; + onChange?: (value: string | number) => void; + onUpdate?: (value: string | number | undefined, checked: boolean) => void; + orientation?: 'row' | 'column'; + style?: CSSProperties; + className?: string; + color?: string; + error?: boolean; + disabled?: boolean; +}; + +export type RadioGroupItemProps = InputConfig & HTMLInputProps & { + label: string; + value: string | number; + color?: string; + error?: boolean; + disabled?: boolean; + onMouseOver?: (event: MouseEvent) => void; + onMouseOut?: (event: MouseEvent) => void; + onUpdate?: (value: string | number | undefined, checked: boolean) => void; +}; + +export function RadioGroupItem({ + label, + value, + name, + checked, + defaultChecked, + onChange, + color, + error, + disabled, + onUpdate, + onMouseOver, + onMouseOut, + ...rest +}: RadioGroupItemProps) { + const handleChange = () => { + if (disabled) return; + onChange?.({ target: { value } } as ChangeEvent); + onUpdate?.(value, true); + }; + + const inputStyle: CSSProperties = { + marginRight: '6px', + width: '18px', + height: '18px', + minWidth: '18px', + minHeight: '18px', + marginTop: '2px', + accentColor: error ? 'red' : color, + cursor: 'pointer', + }; + + const labelStyle: CSSProperties = { + color: error ? 'red' : 'inherit', + marginRight: '8px', + }; + + return ( + + ); +} + +export default function RadioGroup({ + children, + name, + defaultValue, + onChange, + onUpdate, + orientation = 'row', + style, + className, + color, + error, + disabled, +}: RadioGroupProps) { + const [selectedValue, setSelectedValue] = useState(defaultValue); + + // Set default selected value + useEffect(() => { + if (defaultValue !== undefined) { + setSelectedValue(defaultValue); + } + }, [defaultValue]); + + // If no defaultValue, check for item with defaultChecked + useEffect(() => { + if (defaultValue !== undefined) return; + + Children.forEach(children, (child) => { + if ( + isValidElement(child) && + child.props.defaultChecked + ) { + setSelectedValue(child.props.value); + } + }); + }, [children, defaultValue]); + + const handleChange = ( + newValue: string | number, + itemOnUpdate?: (value: string | number | undefined, checked: boolean) => void + ) => { + if (disabled) return; + setSelectedValue(newValue); + onChange?.(newValue); + itemOnUpdate?.(newValue, true); + onUpdate?.(newValue, true); + }; + + return ( +
+ + {Children.map(children, (child) => + isValidElement(child) + ? cloneElement(child, { + name, + checked: selectedValue === child.props.value, + onChange: (e: ChangeEvent) => + handleChange( + e.target.value, + child.props.onUpdate + ), + color: child.props.color || color, + error: child.props.error ?? error, + disabled: disabled || child.props.disabled, + }) + : child + )} +
+ ); +} \ No newline at end of file diff --git a/web/modules/theme/layouts/components/MainMenu.tsx b/web/modules/theme/layouts/components/MainMenu.tsx index ae69747..6ed4dcf 100644 --- a/web/modules/theme/layouts/components/MainMenu.tsx +++ b/web/modules/theme/layouts/components/MainMenu.tsx @@ -130,6 +130,9 @@ const MainMenu: React.FC<{ {_('Radio')} + + {_('Radio Group')} + {_('Select')} diff --git a/web/pages/field/index.tsx b/web/pages/field/index.tsx index e1866c3..62a0053 100644 --- a/web/pages/field/index.tsx +++ b/web/pages/field/index.tsx @@ -25,6 +25,7 @@ import Metadata from 'frui/field/Metadata'; import Number from 'frui/field/Number'; import Password from 'frui/field/Password'; import Radio from 'frui/field/Radio'; +import RadioGroup, { RadioGroupItem } from 'frui/field/RadioGroup' import Select from 'frui/field/Select'; import Slug from 'frui/field/Slug'; import Switch from 'frui/field/Switch'; @@ -334,6 +335,23 @@ export default function Home() { +
router.push('/field/radiogroup')} + > +
+
+ + + + + +
+

+ {_('Radio Group')} +

+
+
router.push('/field/select')} @@ -477,16 +495,6 @@ export default function Home() {
-
-
-
- Unlocks at 9,000 downloads -
-

- {_('Radio Group')} -

-
-
diff --git a/web/pages/field/radio.tsx b/web/pages/field/radio.tsx index 0fab43f..4ea0129 100644 --- a/web/pages/field/radio.tsx +++ b/web/pages/field/radio.tsx @@ -362,8 +362,8 @@ export default function Home() { {_('Password')}
- - {_('Select')} + + {_('Radio Group')}
diff --git a/web/pages/field/radiogroup.tsx b/web/pages/field/radiogroup.tsx new file mode 100644 index 0000000..269646a --- /dev/null +++ b/web/pages/field/radiogroup.tsx @@ -0,0 +1,533 @@ +//types +import type { Crumb } from 'modules/components/Crumbs'; +//hooks +import { useState } from 'react'; +import { useLanguage } from 'r22n'; +//components +import Link from 'next/link'; +import { Translate } from 'r22n'; +import RadioGroup, { RadioGroupItem } from 'frui/field/RadioGroup' +import Table, { Tcol, Thead, Trow } from 'frui/element/Table'; +import { LayoutPanel } from 'modules/theme'; +import Crumbs from 'modules/components/Crumbs'; +import Props from 'modules/components/Props'; +import Code, { InlineCode as C } from 'modules/components/Code'; +import React from 'react'; + +const codeBasic = ` + + + + +`.trim(); + +const codeDefault = ` + + + + +`.trim(); + +const codeChecked = ` + + + + +`.trim(); + +const codeRow = ` + + + + +`.trim(); + +const codeColumn = ` + + + + +`.trim(); + +const codeUpdate = ` +function Home() { + const [ value, setValue ] = useState('update1'); + return ( +
+ setValue(value as string)}> + + + + + ) +}`.trim(); + +const codeEvent = ` +function Home() { + const [selected, setSelected] = useState(); + return ( +
+ + + + + +
+

Selected: {selected || "None"}

+ ) +}`.trim(); + +const codeDisabled = ` + + + + +`.trim(); + +const codeErrors = ` + + + + +`.trim(); + +const codeColor = ` + + + + +`.trim(); + +const codeRadioColor = ` + + + + +`.trim(); + +export default function Home() { + //hooks + const { _ } = useLanguage(); + const [ , setValue ] = useState('update1'); + //variables + const crumbs: Crumb[] = [ + { icon: 'rectangle-list', label: 'Fields', href: '/field' }, + { label: 'Radio Group' } + ]; + const props = [ + [ _('checked'), _('boolean'), _('No'), _('Default checked state (Controlled)') ], + [ _('className'), _('string'), _('No'), _('Standard HTML class names') ], + [ _('color'), _('string'), _('No'), _('Changes the color of the radios') ], + [ _('defaultChecked'), _('string'), _('No'), _('Default checked state (Uncontrolled)') ], + [ _('defaultValue'), _('string'), _('No'), _('Default value (Uncontrolled)') ], + [ _('disabled'), _('boolean'), _('No'), _('Disables the radio group') ], + [ _('error'), _('boolean'), _('No'), _('Highlight the radio group red') ], + [ _('label'), _('string'), _('No'), _('Shows text to the right of radios') ], + [ _('name'), _('string'), _('Yes'), _('Used for react server components.') ], + [ _('onChange'), _('Function'), _('No'), _('Event handler when value has changed') ], + [ _('onUpdate'), _('Function'), _('No'), _('Update event handler') ], + [ _('orientation'), _('string'), _('No'), _('Set radio group layout (row or column)') ], + [ _('passRef'), _('LegacyRef'), _('No'), _('Passes ref to html input') ], + [ _('style'), _('CSS Object'), _('No'), _('Standard CSS object') ], + [ _('value'), _('string'), _('No'), _('Default value (Controlled)') ], + ]; + + const [selected, setSelected] = useState(); + + //render + return ( + +
+
+ +
+
+ +
+

+ {_('Radio Group')} +

+ + {`import RadioGroup, { RadioGroupItem } from 'frui/fields/RadioGroup';`} + + +

+ {_('Props')} +

+

+ + Radio Group accepts all props of a standard HTML input + element. See Moz for standard input attributes. + +

+ + +

+ {_('Basics')} +

+

+ + Radio Group wraps the HTML standard {'``'} element. Therefore, you can + use any input attributes as props. + +

+ +
+
+ + + + + +
+ + {codeBasic} + +
+ +

+ {_('Default Value')} +

+

+ + You can set the default selected value, using: . + +

+ +
+
+ + + + + +
+ + {codeDefault} + +
+ +

+ + You can set a radio as the default selected value: . + +

+
+
+ + + + + +
+ + {codeChecked} + +
+ +

+ {_('Orientation')} +

+ +

+ {_('Row')} +

+

+ + To change the radio group layout to horizontal (default), use: . + +

+
+
+ + + + + +
+ + {codeRow} + +
+ +

+ {_('Column')} +

+

+ + To change the radio group layout to vertical, use: . + +

+
+
+ + + + + +
+ + {codeColumn} + +
+ +

+ {_('Events')} +

+

+ + is like + except the value is passed instead of the change event. + +

+
+
+ setValue(value as string)}> + + + + +
+ + {codeUpdate} + +
+ +

+ + + handles changes whenever a user selects or unselects an item. + +

+
+
+ + + + + +
+
+

Selected: {selected || "None"}

+
+ + {codeEvent} + +
+ +

+ {_('On Change')} +

+

+ + The event is triggered when the + value has changed. The following arguments are passed + to the event handler: + +

+ + {_('Name')} + {_('Type')} + {_('Sample')} + + + {_('event')} + + + {_('Event Object')} + + + see: Change Event + + +
+ +

+ {_('On Update')} +

+

+ + The event is triggered when the + value has been updated. The following arguments are + passed to the event handler: + +

+ + {_('Name')} + {_('Type')} + {_('Sample')} + + + {_('value')} + + + {_('string')} + + + + + +
+ +

+ {_('Disabled')} +

+

+ + You can disable the radio group, preventing them from being selected: . + +

+
+
+ + + + + +
+ + {codeDisabled} + +
+ +

+ {_('Errors')} +

+

+ + You can pass the prop to highlight + the Radio Group field red. + +

+
+
+ + + + + +
+ + {codeErrors} + +
+ +

+ {_('Custom Styles')} +

+ +

+ {_('Colors')} +

+

+ + You can change the color of the radio group using: or . + +

+
+
+ + + + + +
+ + {codeColor} + +
+ +

+ + You may also change the individual color of the radios: + +

+
+
+ + + + + +
+ + {codeRadioColor} + +
+ +
+ + + {_('Radio')} + +
+ + {_('Select')} + + +
+
+
+
+
+ ); +} diff --git a/web/pages/field/select.tsx b/web/pages/field/select.tsx index 6340c7f..edd223f 100644 --- a/web/pages/field/select.tsx +++ b/web/pages/field/select.tsx @@ -333,9 +333,9 @@ export default function Home() {

- + - {_('Radio')} + {_('Radio Group')}