Skip to content
Merged
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
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 @@ -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,18 +107,23 @@ 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>;

// @public
export type ToolbarToggleButtonProps = ComponentProps<ButtonSlots> & Partial<Pick<ToggleButtonProps, 'disabled' | 'disabledFocusable'>> & {
export type ToolbarToggleButtonProps = ComponentProps<ButtonSlots> & Partial<Pick<ToggleButtonProps, 'disabled' | 'disabledFocusable' | 'size'>> & {
appearance?: 'primary' | 'subtle';
};

// @public
export type ToolbarToggleButtonState = ComponentState<Partial<ButtonSlots>> & ToggleButtonState;
export type ToolbarToggleButtonState = ComponentState<Partial<ButtonSlots>> & ToggleButtonState & Required<Pick<ToggleButtonProps, 'checked'>> & {
name?: string;
value?: string;
};

// @public
export const useToolbar_unstable: (props: ToolbarProps, ref: React_2.Ref<HTMLElement>) => ToolbarState;
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 @@ -36,6 +37,36 @@ export const useToolbar_unstable = (props: ToolbarProps, ref: React.Ref<HTMLElem
...arrowNavigationProps,
...props,
}),
...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) => {
if (name && value) {
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
@@ -1,20 +1,14 @@
import * as React from 'react';
import type { ToolbarToggleButtonProps } from './ToolbarToggleButton.types';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
import {
renderToggleButton_unstable,
useToggleButtonStyles_unstable,
useToggleButton_unstable,
} from '@fluentui/react-button';
import { useToolbarContext } from '../Toolbar/ToolbarContext';
import { renderToggleButton_unstable, useToggleButtonStyles_unstable } from '@fluentui/react-button';
import { useToolbarToggleButton_unstable } from './useToolbarToggleButton';

/**
* ToolbarToggleButton component
*/
export const ToolbarToggleButton: ForwardRefComponent<ToolbarToggleButtonProps> = React.forwardRef((props, ref) => {
const { size } = useToolbarContext();

const state = useToggleButton_unstable({ size, ...props }, ref);
const state = useToolbarToggleButton_unstable(props, ref);

useToggleButtonStyles_unstable(state);
return renderToggleButton_unstable(state);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ import { ToggleButtonProps, ButtonSlots, ToggleButtonState } from '@fluentui/rea
* ToolbarToggleButton Props
*/
export type ToolbarToggleButtonProps = ComponentProps<ButtonSlots> &
Partial<Pick<ToggleButtonProps, 'disabled' | 'disabledFocusable'>> & {
Partial<Pick<ToggleButtonProps, 'disabled' | 'disabledFocusable' | 'size'>> & {
appearance?: 'primary' | 'subtle';
};

/**
* State used in rendering ToolbarToggleButton
*/
export type ToolbarToggleButtonState = ComponentState<Partial<ButtonSlots>> & ToggleButtonState;
export type ToolbarToggleButtonState = ComponentState<Partial<ButtonSlots>> &
ToggleButtonState &
Required<Pick<ToggleButtonProps, 'checked'>> & {
name?: string;
value?: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as React from 'react';
import { useToggleButton_unstable } from '@fluentui/react-button';
import { useToolbarContext } from '../Toolbar/ToolbarContext';
import { ToolbarToggleButtonProps, ToolbarToggleButtonState } from './ToolbarToggleButton.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 useToolbarToggleButton_unstable = (
props: ToolbarToggleButtonProps,
ref: React.Ref<HTMLButtonElement | HTMLAnchorElement>,
): ToolbarToggleButtonState => {
const { handleToggleButton, size } = useToolbarContext();
const { onClick: onClickOriginal } = props;
const state = useToggleButton_unstable({ size, ...props }, ref) as ToolbarToggleButtonState;

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;
return state;
};
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