Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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": "chore: use context passed handler",
"packageName": "@fluentui/react-button",
"email": "[email protected]",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "feat: add toolbar controlled state example for toggle buttons",
"packageName": "@fluentui/react-toolbar",
"email": "[email protected]",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,10 @@ export type ToggleButtonProps = ButtonProps & {
};

// @public (undocumented)
export type ToggleButtonState = ButtonState & Required<Pick<ToggleButtonProps, 'checked'>>;
export type ToggleButtonState = ButtonState & Required<Pick<ToggleButtonProps, 'checked'>> & {
name: string;
value: string;
};

// @public
export const useButton_unstable: (props: ButtonProps, ref: React_2.Ref<HTMLButtonElement | HTMLAnchorElement>) => ButtonState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ export type ToggleButtonProps = ButtonProps & {
checked?: boolean;
};

export type ToggleButtonState = ButtonState & Required<Pick<ToggleButtonProps, 'checked'>>;
export type ToggleButtonState = ButtonState &
Required<Pick<ToggleButtonProps, 'checked'>> & {
name: string;
value: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ export type ToolbarButtonState = ComponentState<Partial<ButtonSlots>> & ButtonSt
export const toolbarClassNames: SlotClassNames<ToolbarSlots>;

// @public (undocumented)
export type ToolbarContextValue = {
size: ToolbarProps['size'];
export type ToolbarContextValue = Pick<ToolbarProps, 'size'> & {
handleToggleButton?: ToggableHandler;
};

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

// @public
Expand Down Expand Up @@ -104,7 +107,9 @@ export type ToolbarSlots = {
};

// @public
export type ToolbarState = ComponentState<ToolbarSlots> & Required<Pick<ToolbarProps, 'size'>>;
export type ToolbarState = ComponentState<ToolbarSlots> & Required<Pick<ToolbarProps, 'size' | 'checkedValues'>> & Pick<ToolbarProps, 'defaultCheckedValues' | 'onCheckedValueChange'> & {
handleToggleButton: ToggableHandler;
};

// @public
export const ToolbarToggleButton: ForwardRefComponent<ToolbarToggleButtonProps>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import * as React from 'react';
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';

export type ToolbarSlots = {
root: Slot<'div'>;
};

export type ToolbarCheckedValueChangeData = {
/** The items for this value that are checked */
checkedItems: string[];
/** The name of the value */
name: string;
};

export type ToolbarCheckedValueChangeEvent = React.MouseEvent | React.KeyboardEvent;

/**
* Toolbar Props
*/
Expand All @@ -14,17 +24,52 @@ export type ToolbarProps = ComponentProps<ToolbarSlots> & {
* @default medium
*/
size?: 'small' | 'medium';

/**
* Map of all checked values
*/
checkedValues?: Record<string, string[]>;

/**
* Default values to be checked on mount
*/
defaultCheckedValues?: Record<string, string[]>;

/**
* Callback when checked items change for value with a name
*
* @param event - React's original SyntheticEvent
* @param data - A data object with relevant information
*/
onCheckedValueChange?: (e: ToolbarCheckedValueChangeEvent, data: ToolbarCheckedValueChangeData) => void;
};

/**
* State used in rendering Toolbar
*/
export type ToolbarState = ComponentState<ToolbarSlots> & Required<Pick<ToolbarProps, 'size'>>;
export type ToolbarState = ComponentState<ToolbarSlots> &
Required<Pick<ToolbarProps, 'size' | 'checkedValues'>> &
Pick<ToolbarProps, 'defaultCheckedValues' | 'onCheckedValueChange'> & {
/*
* Toggles the state of a ToggleButton item
*/
handleToggleButton: ToggableHandler;
};

export type ToolbarContextValue = {
size: ToolbarProps['size'];
export type ToolbarContextValue = Pick<ToolbarProps, 'size'> & {
handleToggleButton?: ToggableHandler;
};

export type ToolbarContextValues = {
toolbar: ToolbarContextValue;
};

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

export type ToggableHandler = (
e: React.MouseEvent | React.KeyboardEvent,
name: string,
value: string,
checked: boolean,
) => void;
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const ToolbarContext = React.createContext<ToolbarContextValue | undefine

const toolbarContextDefaultValue = {
size: 'medium' as 'medium',
handleToggleButton: () => null,
};

export const useToolbarContext = () => React.useContext(ToolbarContext) ?? toolbarContextDefaultValue;
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { useEventCallback, useControllableState } from '@fluentui/react-utilities';
import { getNativeElementProps } from '@fluentui/react-utilities';
import type { ToolbarProps, ToolbarState } from './Toolbar.types';
import type { ToggableHandler, ToolbarProps, ToolbarState, UninitializedToolbarState } from './Toolbar.types';
import { useArrowNavigationGroup } from '@fluentui/react-tabster';

/**
Expand All @@ -13,14 +14,14 @@ import { useArrowNavigationGroup } from '@fluentui/react-tabster';
* @param ref - reference to root HTMLElement of Toolbar
*/
export const useToolbar_unstable = (props: ToolbarProps, ref: React.Ref<HTMLElement>): ToolbarState => {
const { size = 'medium' } = props;

const arrowNavigationProps = useArrowNavigationGroup({
circular: true,
axis: 'horizontal',
});

const { size = 'medium' } = props;

return {
const initialState: UninitializedToolbarState = {
size,

// TODO add appropriate props/defaults
Expand All @@ -38,4 +39,33 @@ export const useToolbar_unstable = (props: ToolbarProps, ref: React.Ref<HTMLElem
}),
...props,
};

const [checkedValues, setCheckedValues] = useControllableState({
state: initialState.checkedValues,
defaultState: initialState.defaultCheckedValues,
initialState: {},
});

const { onCheckedValueChange } = initialState;

const handleToggleButton: ToggableHandler = useEventCallback(
(e: React.MouseEvent | React.KeyboardEvent, name: string, value: string, checked: boolean) => {
const checkedItems = checkedValues?.[name] || [];
const newCheckedItems = [...checkedItems];
if (checked) {
newCheckedItems.splice(newCheckedItems.indexOf(value), 1);
} else {
newCheckedItems.push(value);
}

onCheckedValueChange?.(e, { name, checkedItems: newCheckedItems });
setCheckedValues(s => ({ ...s, [name]: newCheckedItems }));
},
);

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

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

return { toolbar };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,23 @@ import { useToolbarContext } from '../Toolbar/ToolbarContext';
* ToolbarToggleButton component
*/
export const ToolbarToggleButton: ForwardRefComponent<ToolbarToggleButtonProps> = React.forwardRef((props, ref) => {
const { size } = useToolbarContext();

const { size, handleToggleButton } = useToolbarContext();
const { onClick: onClickOriginal } = props;
const state = useToggleButton_unstable({ size, ...props }, ref);
const handleOnClick = (
e: React.MouseEvent<HTMLButtonElement, MouseEvent> & React.MouseEvent<HTMLAnchorElement, MouseEvent>,
) => {
if (state.disabled) {
e.preventDefault();
e.stopPropagation();
return;
}

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

state.root.onClick = handleOnClick;

useToggleButtonStyles_unstable(state);
return renderToggleButton_unstable(state);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from 'react';
import { Toolbar, ToolbarToggleButton, ToolbarProps } from '@fluentui/react-toolbar';

export const ControlledToggleButton = () => {
const [checkedValues, setCheckedValues] = React.useState<Record<string, string[]>>({ edit: ['cut', 'paste'] });
const onChange: ToolbarProps['onCheckedValueChange'] = (e, { name, checkedItems }) => {
setCheckedValues(s => {
return s ? { ...s, [name]: checkedItems } : { [name]: checkedItems };
});
};

return (
<Toolbar checkedValues={checkedValues} onCheckedValueChange={onChange}>
<ToolbarToggleButton name="group">Enable Group</ToolbarToggleButton>
<ToolbarToggleButton name="group">Enable Group</ToolbarToggleButton>
</Toolbar>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { OverflowItems } from './ToolbarOverflow.stories';
export { WithTooltip } from './ToolbarWithTooltip.stories';
export { WithPopover } from './ToolbarWithPopover.stories';
export { Subtle } from './ToolbarSubtle.stories';
export { ControlledToggleButton } from './ToolbarControlledToggleButton.stories';

export default {
title: 'Preview Components/Toolbar',
Expand Down