Skip to content
Draft
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
7 changes: 7 additions & 0 deletions change/@fluentui-react-combobox-base-hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕵🏾‍♀️ visual changes to review in the Visual Change Report

vr-tests-react-components/Charts-DonutChart 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Charts-DonutChart.Dynamic.default.chromium.png 5581 Changed
vr-tests-react-components/Field 4 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Field.Combobox+horizontal.default.chromium.png 6024 Changed
vr-tests-react-components/Field.Dropdown+horizontal.default.chromium.png 6002 Changed
vr-tests-react-components/Field.Combobox.default.chromium.png 3608 Changed
vr-tests-react-components/Field.Dropdown.default.chromium.png 3582 Changed
vr-tests-react-components/Menu Converged - submenuIndicator slotted content 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default.submenus open.chromium.png 413 Changed
vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default - RTL.submenus open.chromium.png 404 Changed
vr-tests-react-components/Positioning 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png 620 Changed
vr-tests-react-components/Positioning.Positioning end.chromium.png 761 Changed
vr-tests-react-components/TagPicker 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/TagPicker.disabled.disabled input hover.chromium.png 677 Changed
vr-tests-react-components/TagPicker.disabled - RTL.disabled input hover.chromium.png 635 Changed

There were 2 duplicate changes discarded. Check the build logs for more information.

"type": "minor",
"comment": "feat: add useComboboxBase_unstable and useDropdownBase_unstable hooks",
"packageName": "@fluentui/react-combobox",
"email": "dmytrokirpa@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export type {
ActiveOptionChangeData,
ComboboxBaseHookProps,
ComboboxBaseHookState,
ComboboxContextValues,
ComboboxOpenChangeData,
ComboboxOpenEvents,
Expand All @@ -12,5 +14,6 @@ export {
comboboxClassNames,
renderCombobox_unstable,
useComboboxStyles_unstable,
useComboboxBase_unstable,
useCombobox_unstable,
} from './components/Combobox/index';
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export type {
ActiveOptionChangeData,
DropdownBaseHookProps,
DropdownBaseHookState,
DropdownContextValues,
DropdownOpenChangeData,
DropdownOpenEvents,
Expand All @@ -12,5 +14,6 @@ export {
dropdownClassNames,
renderDropdown_unstable,
useDropdownStyles_unstable,
useDropdownBase_unstable,
useDropdown_unstable,
} from './components/Dropdown/index';
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import type { ComponentProps, ComponentState, DistributiveOmit, Slot } from '@fluentui/react-utilities';
import type { ActiveDescendantImperativeRef } from '@fluentui/react-aria';
import type {
ActiveOptionChangeData as ComboboxBaseActiveOptionChangeData,
Expand Down Expand Up @@ -40,6 +40,11 @@ export type ComboboxProps = Omit<ComponentProps<Partial<ComboboxSlots>, 'input'>
children?: React.ReactNode;
};

/**
* Combobox Props without design-only props.
*/
export type ComboboxBaseHookProps = DistributiveOmit<ComboboxProps, 'appearance' | 'size'>;

/**
* State used in rendering Combobox
*/
Expand All @@ -49,6 +54,11 @@ export type ComboboxState = ComponentState<ComboboxSlots> &
activeDescendantController: ActiveDescendantImperativeRef;
};

/**
* State used in rendering Combobox, without design-only state.
*/
export type ComboboxBaseHookState = DistributiveOmit<ComboboxState, 'appearance' | 'size'>;

/* Export types defined in ComboboxBase */
export type ComboboxContextValues = ComboboxBaseContextValues;
export type ComboboxOpenChangeData = ComboboxBaseOpenChangeData;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export { Combobox } from './Combobox';
export type {
ActiveOptionChangeData,
ComboboxBaseHookProps,
ComboboxBaseHookState,
ComboboxContextValues,
ComboboxOpenChangeData,
ComboboxOpenEvents,
Expand All @@ -9,5 +11,5 @@ export type {
ComboboxState,
} from './Combobox.types';
export { renderCombobox_unstable } from './renderCombobox';
export { useCombobox_unstable } from './useCombobox';
export { useComboboxBase_unstable, useCombobox_unstable } from './useCombobox';
export { comboboxClassNames, useComboboxStyles_unstable } from './useComboboxStyles.styles';
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,40 @@ import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts
import { useComboboxBaseState } from '../../utils/useComboboxBaseState';
import { useComboboxPositioning } from '../../utils/useComboboxPositioning';
import { Listbox } from '../Listbox/Listbox';
import type { ComboboxOpenEvents, ComboboxProps, ComboboxState } from './Combobox.types';
import type {
ComboboxBaseHookProps,
ComboboxBaseHookState,
ComboboxOpenEvents,
ComboboxProps,
ComboboxState,
} from './Combobox.types';
import { useListboxSlot } from '../../utils/useListboxSlot';
import { useInputTriggerSlot } from './useInputTriggerSlot';
import { optionClassNames } from '../Option/useOptionStyles.styles';

/**
* Create the state required to render Combobox.
*
* The returned state can be modified with hooks such as useComboboxStyles_unstable,
* before being passed to renderCombobox_unstable.
* Create the base state required to render Combobox, without design-only props.
*
* @param props - props from this instance of Combobox
* @param ref - reference to root HTMLElement of Combobox
* @param props - props from this instance of Combobox (without appearance and size)
* @param ref - reference to root HTMLInputElement of Combobox
*/
export const useCombobox_unstable = (props: ComboboxProps, ref: React.Ref<HTMLInputElement>): ComboboxState => {
export const useComboboxBase_unstable = (
props: ComboboxBaseHookProps,
ref: React.Ref<HTMLInputElement>,
): ComboboxBaseHookState => {
'use no memo';

// Merge props from surrounding <Field>, if any
props = useFieldControlProps_unstable(props, { supportsLabelFor: true, supportsRequired: true, supportsSize: true });
props = useFieldControlProps_unstable(props, { supportsLabelFor: true, supportsRequired: true });
const {
listboxRef: activeDescendantListboxRef,
activeParentRef,
controller: activeDescendantController,
} = useActiveDescendant<HTMLInputElement, HTMLDivElement>({
matchOption: el => el.classList.contains(optionClassNames.root),
});
const baseState = useComboboxBaseState({ ...props, editable: true, activeDescendantController });
const comboboxInternalState = useComboboxBaseState({ ...props, editable: true, activeDescendantController });
const { appearance: _appearance, size: _size, ...baseState } = comboboxInternalState;

const { clearable, clearSelection, disabled, multiselect, open, selectedOptions, setOpen, value, hasFocus } =
baseState;
Expand All @@ -60,7 +67,7 @@ export const useCombobox_unstable = (props: ComboboxProps, ref: React.Ref<HTMLIn
const triggerRef = React.useRef<HTMLInputElement>(null);

const listbox = useListboxSlot(props.listbox, useMergedRefs(comboboxPopupRef, activeDescendantListboxRef), {
state: baseState,
state: comboboxInternalState,
triggerRef,
defaultProps: {
children: props.children,
Expand All @@ -69,7 +76,7 @@ export const useCombobox_unstable = (props: ComboboxProps, ref: React.Ref<HTMLIn
});

const triggerSlot = useInputTriggerSlot(props.input ?? {}, useMergedRefs(triggerRef, activeParentRef, ref), {
state: baseState,
state: comboboxInternalState,
freeform,
defaultProps: {
type: 'text',
Expand All @@ -90,15 +97,14 @@ export const useCombobox_unstable = (props: ComboboxProps, ref: React.Ref<HTMLIn
rootSlot.ref = useMergedRefs(rootSlot.ref, comboboxTargetRef);

const showClearIcon = selectedOptions.length > 0 && !disabled && clearable && !multiselect;
const state: ComboboxState = {
const state: ComboboxBaseHookState = {
components: { root: 'div', input: 'input', expandIcon: 'span', listbox: Listbox, clearIcon: 'span' },
root: rootSlot,
input: triggerSlot,
listbox: open || hasFocus ? listbox : undefined,
clearIcon: slot.optional(props.clearIcon, {
defaultProps: {
'aria-hidden': 'true',
children: <DismissIcon />,
},
elementType: 'span',
renderByDefault: true,
Expand All @@ -108,7 +114,6 @@ export const useCombobox_unstable = (props: ComboboxProps, ref: React.Ref<HTMLIn
defaultProps: {
'aria-disabled': disabled ? 'true' : undefined,
'aria-expanded': open,
children: <ChevronDownIcon />,
role: 'button',
},
elementType: 'span',
Expand Down Expand Up @@ -198,3 +203,31 @@ export const useCombobox_unstable = (props: ComboboxProps, ref: React.Ref<HTMLIn

return state;
};

/**
* Create the state required to render Combobox.
*
* The returned state can be modified with hooks such as useComboboxStyles_unstable,
* before being passed to renderCombobox_unstable.
*
* @param props - props from this instance of Combobox
* @param ref - reference to root HTMLElement of Combobox
*/
export const useCombobox_unstable = (props: ComboboxProps, ref: React.Ref<HTMLInputElement>): ComboboxState => {
'use no memo';

const { appearance = 'outline', size = 'medium' } = props;
const baseState = useComboboxBase_unstable(props, ref);

return {
...baseState,
appearance,
size,
clearIcon: baseState.clearIcon
? { ...baseState.clearIcon, children: baseState.clearIcon.children ?? <DismissIcon /> }
: baseState.clearIcon,
expandIcon: baseState.expandIcon
? { ...baseState.expandIcon, children: baseState.expandIcon.children ?? <ChevronDownIcon /> }
: baseState.expandIcon,
};
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import type { ComponentProps, ComponentState, DistributiveOmit, Slot } from '@fluentui/react-utilities';
import type { ActiveDescendantImperativeRef } from '@fluentui/react-aria';
import type {
ActiveOptionChangeData as ComboboxBaseActiveOptionChangeData,
Expand Down Expand Up @@ -32,6 +32,11 @@ export type DropdownSlots = {
*/
export type DropdownProps = ComponentProps<Partial<DropdownSlots>, 'button'> & ComboboxBaseProps;

/**
* Dropdown Props without design-only props.
*/
export type DropdownBaseHookProps = DistributiveOmit<DropdownProps, 'appearance' | 'size'>;

/**
* State used in rendering Dropdown
*/
Expand All @@ -45,6 +50,11 @@ export type DropdownState = ComponentState<DropdownSlots> &
activeDescendantController: ActiveDescendantImperativeRef;
};

/**
* State used in rendering Dropdown, without design-only state.
*/
export type DropdownBaseHookState = DistributiveOmit<DropdownState, 'appearance' | 'size'>;

/* Export types defined in ComboboxBase */
export type DropdownContextValues = ComboboxBaseContextValues;
export type DropdownOpenEvents = ComboboxBaseOpenEvents;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export { Dropdown } from './Dropdown';
export type {
ActiveOptionChangeData,
DropdownBaseHookProps,
DropdownBaseHookState,
DropdownContextValues,
DropdownOpenChangeData,
DropdownOpenEvents,
Expand All @@ -9,5 +11,5 @@ export type {
DropdownState,
} from './Dropdown.types';
export { renderDropdown_unstable } from './renderDropdown';
export { useDropdown_unstable } from './useDropdown';
export { useDropdownBase_unstable, useDropdown_unstable } from './useDropdown';
export { dropdownClassNames, useDropdownStyles_unstable } from './useDropdownStyles.styles';
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,26 @@ import {
import { useComboboxBaseState } from '../../utils/useComboboxBaseState';
import { useComboboxPositioning } from '../../utils/useComboboxPositioning';
import { Listbox } from '../Listbox/Listbox';
import type { DropdownProps, DropdownState } from './Dropdown.types';
import type { DropdownBaseHookProps, DropdownBaseHookState, DropdownProps, DropdownState } from './Dropdown.types';
import { useListboxSlot } from '../../utils/useListboxSlot';
import { useButtonTriggerSlot } from './useButtonTriggerSlot';
import { optionClassNames } from '../Option/useOptionStyles.styles';
import type { ComboboxOpenEvents } from '../Combobox/Combobox.types';

/**
* Create the state required to render Dropdown.
*
* The returned state can be modified with hooks such as useDropdownStyles_unstable,
* before being passed to renderDropdown_unstable.
* Create the base state required to render Dropdown, without design-only props.
*
* @param props - props from this instance of Dropdown
* @param ref - reference to root HTMLElement of Dropdown
* @param props - props from this instance of Dropdown (without appearance and size)
* @param ref - reference to root HTMLButtonElement of Dropdown
*/
export const useDropdown_unstable = (props: DropdownProps, ref: React.Ref<HTMLButtonElement>): DropdownState => {
export const useDropdownBase_unstable = (
props: DropdownBaseHookProps,
ref: React.Ref<HTMLButtonElement>,
): DropdownBaseHookState => {
'use no memo';

// Merge props from surrounding <Field>, if any
props = useFieldControlProps_unstable(props, { supportsLabelFor: true, supportsSize: true });
props = useFieldControlProps_unstable(props, { supportsLabelFor: true });
const {
listboxRef: activeDescendantListboxRef,
activeParentRef,
Expand All @@ -44,7 +44,8 @@ export const useDropdown_unstable = (props: DropdownProps, ref: React.Ref<HTMLBu
matchOption: el => el.classList.contains(optionClassNames.root),
});

const baseState = useComboboxBaseState({ ...props, activeDescendantController, freeform: false });
const dropdownInternalState = useComboboxBaseState({ ...props, activeDescendantController, freeform: false });
const { appearance: _appearance, size: _size, freeform: _freeform, ...baseState } = dropdownInternalState;
const { clearable, clearSelection, disabled, hasFocus, multiselect, open, selectedOptions, setOpen } = baseState;

const { primary: triggerNativeProps, root: rootNativeProps } = getPartitionedNativeProps({
Expand All @@ -57,7 +58,7 @@ export const useDropdown_unstable = (props: DropdownProps, ref: React.Ref<HTMLBu

const triggerRef = React.useRef<HTMLButtonElement>(null);
const listbox = useListboxSlot(props.listbox, useMergedRefs(comboboxPopupRef, activeDescendantListboxRef), {
state: baseState,
state: dropdownInternalState,
triggerRef,
defaultProps: {
children: props.children,
Expand All @@ -74,7 +75,7 @@ export const useDropdown_unstable = (props: DropdownProps, ref: React.Ref<HTMLBu
});

const trigger = useButtonTriggerSlot(props.button ?? {}, useMergedRefs(triggerRef, activeParentRef, ref), {
state: baseState,
state: dropdownInternalState,
defaultProps: {
type: 'button',
// tabster navigation breaks if the button is disabled and tabIndex is 0
Expand All @@ -97,15 +98,14 @@ export const useDropdown_unstable = (props: DropdownProps, ref: React.Ref<HTMLBu
rootSlot.ref = useMergedRefs(rootSlot.ref, comboboxTargetRef);

const showClearButton = selectedOptions.length > 0 && !disabled && clearable && !multiselect;
const state: DropdownState = {
const state: DropdownBaseHookState = {
components: { root: 'div', button: 'button', clearButton: 'button', expandIcon: 'span', listbox: Listbox },
root: rootSlot,
button: trigger,
listbox: open || hasFocus ? listbox : undefined,
clearButton: slot.optional(props.clearButton, {
defaultProps: {
'aria-label': 'Clear selection',
children: <DismissIcon />,
// Safari doesn't allow to focus an element with this
// when the element is not visible (display: none) we need to remove it to avoid tabster issues
tabIndex: showClearButton ? 0 : undefined,
Expand All @@ -116,9 +116,6 @@ export const useDropdown_unstable = (props: DropdownProps, ref: React.Ref<HTMLBu
}),
expandIcon: slot.optional(props.expandIcon, {
renderByDefault: true,
defaultProps: {
children: <ChevronDownIcon />,
},
elementType: 'span',
}),
placeholderVisible: !baseState.value && !!props.placeholder,
Expand Down Expand Up @@ -155,3 +152,31 @@ export const useDropdown_unstable = (props: DropdownProps, ref: React.Ref<HTMLBu

return state;
};

/**
* Create the state required to render Dropdown.
*
* The returned state can be modified with hooks such as useDropdownStyles_unstable,
* before being passed to renderDropdown_unstable.
*
* @param props - props from this instance of Dropdown
* @param ref - reference to root HTMLElement of Dropdown
*/
export const useDropdown_unstable = (props: DropdownProps, ref: React.Ref<HTMLButtonElement>): DropdownState => {
'use no memo';

const { appearance = 'outline', size = 'medium' } = props;
const baseState = useDropdownBase_unstable(props, ref);

return {
...baseState,
appearance,
size,
clearButton: baseState.clearButton
? { ...baseState.clearButton, children: baseState.clearButton.children ?? <DismissIcon /> }
: baseState.clearButton,
expandIcon: baseState.expandIcon
? { ...baseState.expandIcon, children: baseState.expandIcon.children ?? <ChevronDownIcon /> }
: baseState.expandIcon,
};
};
Loading
Loading