Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "feat: replace ToolbarRadio implementation by usage of toggle button as radio",
"packageName": "@fluentui/react-toolbar",
"email": "chassunc@microsoft.com",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,12 @@ import type { ComponentState } from '@fluentui/react-utilities';
import { DividerSlots } from '@fluentui/react-divider';
import { DividerState } from '@fluentui/react-divider';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
import { RadioGroupProps } from '@fluentui/react-radio';
import { RadioGroupState } from '@fluentui/react-radio';
import { RadioProps } from '@fluentui/react-radio';
import { RadioState } from '@fluentui/react-radio';
import * as React_2 from 'react';
import type { Slot } from '@fluentui/react-utilities';
import { SlotClassNames } from '@fluentui/react-utilities';
import { ToggleButtonProps } from '@fluentui/react-button';
import { ToggleButtonState } from '@fluentui/react-button';

// @public (undocumented)
export type RadioGroupContextValue = Pick<RadioGroupProps, 'name' | 'value' | 'defaultValue' | 'disabled' | 'layout' | 'required'>;

// @public (undocumented)
export type RadioGroupContextValues = {
radioGroup: RadioGroupContextValue;
};

// @public
export const renderToolbar_unstable: (state: ToolbarState, contextValues: ToolbarContextValues) => JSX.Element;

Expand All @@ -55,6 +43,7 @@ export const toolbarClassNames: SlotClassNames<ToolbarSlots>;
// @public (undocumented)
export type ToolbarContextValue = Pick<ToolbarState, 'size' | 'vertical' | 'checkedValues'> & {
handleToggleButton?: ToggableHandler;
handleRadio?: ToggableHandler;
};

// @public (undocumented)
Expand All @@ -77,32 +66,23 @@ export type ToolbarDividerState = ComponentState<Partial<DividerSlots>> & Divide
export type ToolbarProps = ComponentProps<ToolbarSlots> & {
size?: 'small' | 'medium';
vertical?: boolean;
checkedValues?: Record<string, string[]>;
defaultCheckedValues?: Record<string, string[]>;
checkedValues?: Record<string, string[] | string>;
defaultCheckedValues?: Record<string, string[] | string>;
onCheckedValueChange?: (e: ToolbarCheckedValueChangeEvent, data: ToolbarCheckedValueChangeData) => void;
};

// @public
export const ToolbarRadio: ForwardRefComponent<ToolbarRadioProps>;

// @public
export const ToolbarRadioGroup: ForwardRefComponent<ToolbarRadioGroupProps>;

// @public
export type ToolbarRadioGroupProps = RadioGroupProps;

// @public
export type ToolbarRadioGroupState = RadioGroupState;

// @public
export type ToolbarRadioProps = RadioProps & {
size?: 'small' | 'medium';
export type ToolbarRadioProps = ComponentProps<ButtonSlots> & Partial<Pick<ToggleButtonProps, 'disabled' | 'disabledFocusable' | 'size'>> & {
appearance?: 'primary' | 'subtle';
name: string;
value: string;
};

// @public
export type ToolbarRadioState = RadioState & {
size?: 'small' | 'medium';
};
export type ToolbarRadioState = ComponentState<Partial<ButtonSlots>> & ToggleButtonState & Required<Pick<ToggleButtonProps, 'checked'>> & Pick<ToolbarRadioProps, 'name' | 'value'>;

// @public (undocumented)
export type ToolbarSlots = {
Expand All @@ -112,6 +92,7 @@ export type ToolbarSlots = {
// @public
export type ToolbarState = ComponentState<ToolbarSlots> & Required<Pick<ToolbarProps, 'size' | 'checkedValues' | 'vertical'>> & Pick<ToolbarProps, 'defaultCheckedValues' | 'onCheckedValueChange'> & {
handleToggleButton: ToggableHandler;
handleRadio: ToggableHandler;
};

// @public
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type ToolbarSlots = {

export type ToolbarCheckedValueChangeData = {
/** The items for this value that are checked */
checkedItems: string[];
checkedItems: string[] | string;
/** The name of the value */
name: string;
};
Expand All @@ -34,12 +34,12 @@ export type ToolbarProps = ComponentProps<ToolbarSlots> & {
/**
* Map of all checked values
*/
checkedValues?: Record<string, string[]>;
checkedValues?: Record<string, string[] | string>;

/**
* Default values to be checked on mount
*/
defaultCheckedValues?: Record<string, string[]>;
defaultCheckedValues?: Record<string, string[] | string>;
Comment thread
chpalac marked this conversation as resolved.
Outdated

/**
* Callback when checked items change for value with a name
Expand All @@ -60,17 +60,22 @@ export type ToolbarState = ComponentState<ToolbarSlots> &
* Toggles the state of a ToggleButton item
*/
handleToggleButton: ToggableHandler;
/*
* Toggles the state of a ToggleButton item
*/
handleRadio: ToggableHandler;
};

export type ToolbarContextValue = Pick<ToolbarState, 'size' | 'vertical' | 'checkedValues'> & {
handleToggleButton?: ToggableHandler;
handleRadio?: ToggableHandler;
};

export type ToolbarContextValues = {
toolbar: ToolbarContextValue;
};

export type UninitializedToolbarState = Omit<ToolbarState, 'checkedValues' | 'handleToggleButton'> &
export type UninitializedToolbarState = Omit<ToolbarState, 'checkedValues' | 'handleToggleButton' | 'handleRadio'> &
Partial<Pick<ToolbarState, 'checkedValues'>>;

export type ToggableHandler = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const ToolbarContext = createContext<ToolbarContextValue | undefined>(und
const toolbarContextDefaultValue: ToolbarContextValue = {
size: 'medium' as 'medium',
handleToggleButton: () => null,
handleRadio: () => null,
vertical: false,
checkedValues: {},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,23 @@ export const useToolbar_unstable = (props: ToolbarProps, ref: React.Ref<HTMLElem
},
);

const handleRadio: ToggableHandler = useEventCallback(
(e: React.MouseEvent | React.KeyboardEvent, name?: string, value?: string, checked?: boolean) => {
Comment thread
chpalac marked this conversation as resolved.
Outdated
if (name && value) {
checkedValues?.[name];
Comment thread
chpalac marked this conversation as resolved.
Outdated
onCheckedValueChange?.(e, {
name,
checkedItems: checkedValues?.[name],
});
setCheckedValues(s => ({ ...s, [name]: value }));
}
},
);

return {
...initialState,
handleToggleButton,
handleRadio,
checkedValues: checkedValues ?? {},
};
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { ToolbarContextValue, ToolbarContextValues, ToolbarState } from './Toolbar.types';

export function useToolbarContextValues_unstable(state: ToolbarState): ToolbarContextValues {
const { size, handleToggleButton, vertical, checkedValues } = state;
const { size, handleToggleButton, vertical, checkedValues, handleRadio } = state;
// This context is created with "@fluentui/react-context-selector", these is no sense to memoize it
const toolbar: ToolbarContextValue = {
size,
vertical,
handleToggleButton,
handleRadio,
checkedValues,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ import * as React from 'react';
import { render } from '@testing-library/react';
import { ToolbarRadio } from './ToolbarRadio';
import { isConformant } from '../../common/isConformant';
import { ToggleButtonProps } from '@fluentui/react-button';

describe('ToolbarRadio', () => {
isConformant({
Component: ToolbarRadio,
Component: ToolbarRadio as React.FunctionComponent<ToggleButtonProps>,
displayName: 'ToolbarRadio',
primarySlot: 'input',
disabledTests: ['component-has-static-classnames-object'],
});

// TODO add more tests here, and create visual regression tests in /apps/vr-tests

it('renders a default state', () => {
const result = render(<ToolbarRadio />);
const result = render(
<ToolbarRadio name="name" value="value">
Default ToolbarRadio
</ToolbarRadio>,
);
expect(result.container).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import * as React from 'react';
import type { ToolbarRadioProps } from './ToolbarRadio.types';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
import { useRadio_unstable, renderRadio_unstable } from '@fluentui/react-radio';
import { renderToggleButton_unstable } from '@fluentui/react-button';
import { useToolbarRadio_unstable } from './useToolbarRadio';
import { useToolbarRadioStyles_unstable } from './useToolbarRadioStyles';
import { useToolbarContext_unstable } from '../Toolbar/ToolbarContext';

/**
* ToolbarRadio component is a Radio to be used inside Toolbar
* ToolbarToggleButton component
*/
export const ToolbarRadio: ForwardRefComponent<ToolbarRadioProps> = React.forwardRef((props, ref) => {
Comment thread
chpalac marked this conversation as resolved.
Outdated
const size = useToolbarContext_unstable(ctx => ctx.size);
const state = useRadio_unstable(props, ref);
useToolbarRadioStyles_unstable({ size, ...state });
return renderRadio_unstable(state);
const state = useToolbarRadio_unstable(props, ref);

useToolbarRadioStyles_unstable(state);
return renderToggleButton_unstable(state);
}) as ForwardRefComponent<ToolbarRadioProps>;

ToolbarRadio.displayName = 'ToolbarRadio';
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { RadioState, RadioProps } from '@fluentui/react-radio';
import type { ComponentProps, ComponentState } from '@fluentui/react-utilities';
import { ToggleButtonProps, ButtonSlots, ToggleButtonState } from '@fluentui/react-button';

/**
* ToolbarRadio Props
* ToolbarToggleButton Props
Comment thread
chpalac marked this conversation as resolved.
Outdated
*/
export type ToolbarRadioProps = RadioProps & {
size?: 'small' | 'medium';
};
export type ToolbarRadioProps = ComponentProps<ButtonSlots> &
Partial<Pick<ToggleButtonProps, 'disabled' | 'disabledFocusable' | 'size'>> & {
appearance?: 'primary' | 'subtle';
name: string;
value: string;
};

/**
* State used in rendering ToolbarRadio
* State used in rendering ToolbarToggleButton
*/
export type ToolbarRadioState = RadioState & {
size?: 'small' | 'medium';
};
export type ToolbarRadioState = ComponentState<Partial<ButtonSlots>> &
ToggleButtonState &
Required<Pick<ToggleButtonProps, 'checked'>> &
Pick<ToolbarRadioProps, 'name' | 'value'>;

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as React from 'react';
import { useToggleButton_unstable } from '@fluentui/react-button';
import { useToolbarContext_unstable } from '../Toolbar/ToolbarContext';
import { ToolbarRadioProps, ToolbarRadioState } from './ToolbarRadio.types';

/**
* Given user props, defines default props for the ToggleButton, calls useButtonState and useChecked, and returns
* processed state.
* @param props - User provided props to the ToggleButton component.
* @param ref - User provided ref to be passed to the ToggleButton component.
*/
export const useToolbarRadio_unstable = (
props: ToolbarRadioProps,
ref: React.Ref<HTMLButtonElement | HTMLAnchorElement>,
): ToolbarRadioState => {
const handleRadio = useToolbarContext_unstable(ctx => ctx.handleRadio);
const checked = useToolbarContext_unstable(ctx => !!ctx.checkedValues[props.name]?.includes(props.value));
const size = useToolbarContext_unstable(ctx => ctx.size);

const { onClick: onClickOriginal } = props;
const toggleButtonState = useToggleButton_unstable({ size, checked, ...props }, ref);
const state: ToolbarRadioState = {
...toggleButtonState,
name: props.name,
value: props.value,
};

const handleOnClick = (
Comment thread
chpalac marked this conversation as resolved.
Outdated
e: React.MouseEvent<HTMLButtonElement, MouseEvent> & React.MouseEvent<HTMLAnchorElement, MouseEvent>,
) => {
if (state.disabled) {
e.preventDefault();
e.stopPropagation();
return;
}

handleRadio?.(e, state.name, state.value, state.checked);
onClickOriginal?.(e);
};

state.root.onClick = handleOnClick;
return state;
};
Original file line number Diff line number Diff line change
@@ -1,38 +1,20 @@
import { makeStyles, mergeClasses, shorthands } from '@griffel/react';
import { useRadioStyles_unstable } from '@fluentui/react-radio';
import { tokens } from '@fluentui/react-theme';
import { makeStyles, mergeClasses } from '@griffel/react';
import { useToggleButtonStyles_unstable } from '@fluentui/react-button';
import { ToolbarRadioState } from './ToolbarRadio.types';

const useBaseStyles = makeStyles({
root: {
...shorthands.padding('0px'),
},
});

const useSmallStyles = makeStyles({
label: {
fontSize: 'var(--fontSizeBase200)',
},
root: {
columnGap: '8px',
selected: {
color: tokens.colorBrandForeground1,
},
});

/**
* Apply styling to the ToolbarRadio slots based on the state
* Apply styling to the ToolbarToggleButton slots based on the state
*/
export const useToolbarRadioStyles_unstable = (state: ToolbarRadioState) => {
useRadioStyles_unstable(state);
const baseToolbarRadioStyles = useBaseStyles();
const toolbarRadioSmallStyles = useSmallStyles();
if (state.label) {
state.label.className = mergeClasses(
state.label.className,
state.size === 'small' && toolbarRadioSmallStyles.label,
);
}
state.root.className = mergeClasses(
state.root.className,
baseToolbarRadioStyles.root,
state.size === 'small' && toolbarRadioSmallStyles.root,
);
useToggleButtonStyles_unstable(state);
const toggleButtonStyles = useBaseStyles();

state.root.className = mergeClasses(state.root.className, state.checked && toggleButtonStyles.selected);
};
Loading