blob: cb5300e88c5023cc485be8dbbf344df0d1b3d5a0 [file]
// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import './radio_group.scss';
import m from 'mithril';
import {classForIntent, type HTMLAttrs, Intent} from './common';
import {createContext} from '../base/mithril_utils';
import {classNames} from '../base/classnames';
import {Icon} from './icon';
// A group of mutually-exclusive buttons (like radio buttons).
//
// Supports both controlled and uncontrolled usage.
//
// Uncontrolled — the widget tracks its own selection state internally:
//
// m(RadioGroup, {intent: Intent.Primary}, [
// m(RadioGroup.Button, {value: 'day'}, 'Day'),
// m(RadioGroup.Button, {value: 'week'}, 'Week'),
// m(RadioGroup.Button, {value: 'month'}, 'Month'),
// ]);
//
// Controlled — the parent owns the selected value and updates it in the
// callback:
//
// m(RadioGroup, {
// selectedValue: this.view,
// onValueChange: (value) => (this.view = value),
// }, [
// m(RadioGroup.Button, {value: 'top-down', icon: 'arrow_downward'}, 'Top Down'),
// m(RadioGroup.Button, {value: 'bottom-up', icon: 'arrow_upward'}, 'Bottom Up'),
// ]);
export interface RadioGroupAttrs extends HTMLAttrs {
readonly initialValue?: string;
// The value of the selected button. Omit for uncontrolled mode.
readonly selectedValue?: string;
// Called when the user picks an option.
readonly onValueChange?: (value: string) => void;
// Disables all buttons when true.
// false by default.
readonly disabled?: boolean;
// Whether the buttons should stretch to fill the container width.
// false by default.
readonly fillWidth?: boolean;
// What color to make the selected button.
readonly intent?: Intent;
}
interface RadioGroupState {
readonly selectedValue?: string;
readonly onValueChange?: (value: string) => void;
}
const {Consumer, Provider} = createContext<RadioGroupState>({});
export function RadioGroup({
attrs,
}: m.Vnode<RadioGroupAttrs>): m.Component<RadioGroupAttrs> {
let selectedValueInternal: string | undefined = attrs.initialValue;
return {
view({attrs, children}: m.Vnode<RadioGroupAttrs>) {
const {
disabled,
fillWidth,
intent = Intent.None,
selectedValue = selectedValueInternal,
onValueChange = (value: string) => (selectedValueInternal = value),
...htmlAttrs
} = attrs;
return m(
'.pf-radio-group',
{
...htmlAttrs,
'role': 'radiogroup',
'disabled': disabled ? true : undefined,
'aria-disabled': disabled ? 'true' : undefined,
'className': classNames(
fillWidth && 'pf-radio-group--fill-width',
classForIntent(intent),
),
},
m(Provider, {value: {selectedValue, onValueChange}}, children),
);
},
};
}
export namespace RadioGroup {
export interface ButtonAttrs extends HTMLAttrs {
// Unique value for this button, used to identify the selected one.
readonly value: string;
// The icon of the button.
readonly icon?: string;
}
export const Button: m.Component<ButtonAttrs> = {
view({attrs, children}: m.CVnode<ButtonAttrs>) {
const {value, icon, className, ...htmlAttrs} = attrs;
return m(Consumer, ({selectedValue, onValueChange}) => {
const isSelected = selectedValue === value;
return m(
'.pf-radio-group__button',
{
...htmlAttrs,
'role': 'radio',
'aria-checked': isSelected ? 'true' : 'false',
'onclick': () => onValueChange?.(value),
'onkeydown': (e: KeyboardEvent) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
onValueChange?.(value);
}
},
'className': classNames(className, isSelected && 'pf-active'),
},
icon && m(Icon, {icon}),
children,
);
});
},
};
}