Skip to content

Commit

Permalink
improve slot resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
ling1726 committed Dec 18, 2023
1 parent 84ee4bf commit 344b397
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
useMergedRefs,
slot,
} from '@fluentui/react-utilities';
import type { Slot } from '@fluentui/react-utilities';
import { useComboboxBaseState } from '../../utils/useComboboxBaseState';
import { useComboboxPositioning } from '../../utils/useComboboxPositioning';
import { Listbox } from '../Listbox/Listbox';
Expand Down Expand Up @@ -63,29 +62,24 @@ export const useCombobox_unstable = (props: ComboboxProps, ref: React.Ref<HTMLIn
};

const triggerRef = React.useRef<HTMLInputElement>(null);
let listbox = slot.optional(props.listbox, {
renderByDefault: true,
defaultProps: { children: props.children },
elementType: Listbox,
});

const listboxRef = useMergedRefs(listbox?.ref, comboboxPopupRef);
if (listbox) {
listbox.ref = listboxRef;
}

listbox = useListboxSlot(baseState, listbox, triggerRef) as typeof listbox;
const listbox = useListboxSlot(props.listbox, comboboxPopupRef, {
state: baseState,
triggerRef,
defaultProps: {
children: props.children,
},
});

let triggerSlot: Slot<'input'> = slot.always(props.input, {
const triggerSlot = useInputTriggerSlot(props.input ?? {}, useMergedRefs(triggerRef, ref), {
state: baseState,
freeform,
defaultProps: {
type: 'text',
value: value ?? '',
...triggerNativeProps,
},
elementType: 'input',
});
triggerSlot.ref = useMergedRefs(triggerSlot.ref, triggerRef, ref);
triggerSlot = useInputTriggerSlot(baseState, freeform, triggerSlot);

const rootSlot = slot.always(props.root, {
defaultProps: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,50 @@
import * as React from 'react';
import { mergeCallbacks, useEventCallback } from '@fluentui/react-utilities';
import type { ExtractSlotProps, Slot } from '@fluentui/react-utilities';
import type { ExtractSlotProps, Slot, SlotComponentType } from '@fluentui/react-utilities';
import { ArrowLeft, ArrowRight } from '@fluentui/keyboard-keys';
import { useTriggerSlot, UseTriggerSlotState } from '../../utils/useTriggerSlot';
import { ComboboxState } from './Combobox.types';
import { ComboboxProps, ComboboxState } from './Combobox.types';
import { OptionValue } from '../../utils/OptionCollection.types';
import { getDropdownActionFromKey } from '../../utils/dropdownKeyActions';

type UsedComboboxState = UseTriggerSlotState &
Pick<ComboboxState, 'value' | 'setValue' | 'selectedOptions' | 'clearSelection' | 'getOptionsMatchingText'>;

type UseInputTriggerSlotOptions = {
state: UsedComboboxState;
freeform: boolean | undefined;
defaultProps: Partial<ComboboxProps>;
};

/*
* useInputTriggerSlot returns a tuple of trigger/listbox shorthand,
* with the semantics and event handlers needed for the Combobox and Dropdown components.
* The element type of the ref should always match the element type used in the trigger shorthand.
*/
export function useInputTriggerSlot(
state: UsedComboboxState,
freeform: boolean | undefined,
triggerFromProps?: ExtractSlotProps<Slot<'input'>>,
): ExtractSlotProps<Slot<'input'>> {
triggerFromProps: NonNullable<Slot<'input'>>,
ref: React.Ref<HTMLInputElement>,
options: UseInputTriggerSlotOptions,
): SlotComponentType<ExtractSlotProps<Slot<'input'>>> {
const {
open,
value,
activeOption,
selectOption,
setValue,
setActiveOption,
setFocusVisible,
multiselect,
selectedOptions,
clearSelection,
getOptionsMatchingText,
getIndexOfId,
setOpen,
} = state;
state: {
open,
value,
activeOption,
selectOption,
setValue,
setActiveOption,
setFocusVisible,
multiselect,
selectedOptions,
clearSelection,
getOptionsMatchingText,
getIndexOfId,
setOpen,
},
freeform,
defaultProps,
} = options;

const onBlur = (ev: React.FocusEvent<HTMLInputElement>) => {
// handle selection and updating value if freeform is false
Expand Down Expand Up @@ -87,7 +97,12 @@ export function useInputTriggerSlot(
}
};

const trigger = useTriggerSlot(state, triggerFromProps);
const trigger = useTriggerSlot(triggerFromProps, ref, {
state: options.state,
defaultProps,
elementType: 'input',
});

trigger.onChange = mergeCallbacks(trigger.onChange, onChange);
trigger.onBlur = mergeCallbacks(trigger.onBlur, onBlur);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import * as React from 'react';
import { useTimeout, mergeCallbacks } from '@fluentui/react-utilities';
import type { Slot, ExtractSlotProps } from '@fluentui/react-utilities';
import type { Slot, ExtractSlotProps, SlotComponentType } from '@fluentui/react-utilities';
import { useTriggerSlot, UseTriggerSlotState } from '../../utils/useTriggerSlot';
import { OptionValue } from '../../utils/OptionCollection.types';
import { getDropdownActionFromKey } from '../../utils/dropdownKeyActions';
import { DropdownState } from './Dropdown.types';

type UsedDropdownState = UseTriggerSlotState & Pick<DropdownState, 'getOptionsMatchingText'>;
type UseButtonTriggerSlotOptions = {
state: UsedDropdownState;
defaultProps: unknown;
};

/*
* useButtonTriggerSlot returns a tuple of trigger/listbox shorthand,
* with the semantics and event handlers needed for the Combobox and Dropdown components.
* The element type of the ref should always match the element type used in the trigger shorthand.
*/
export function useButtonTriggerSlot(
state: UsedDropdownState,
triggerFromProps?: ExtractSlotProps<Slot<'button'>>,
): ExtractSlotProps<Slot<'button'>> {
const { open, activeOption, setOpen, getOptionsMatchingText, getIndexOfId, setActiveOption, setFocusVisible } = state;
triggerFromProps: NonNullable<Slot<'button'>>,
ref: React.Ref<HTMLButtonElement>,
options: UseButtonTriggerSlotOptions,
): SlotComponentType<ExtractSlotProps<Slot<'button'>>> {
const {
state: { open, activeOption, setOpen, getOptionsMatchingText, getIndexOfId, setActiveOption, setFocusVisible },
defaultProps,
} = options;

// jump to matching option based on typing
const searchString = React.useRef('');
Expand Down Expand Up @@ -80,7 +88,7 @@ export function useButtonTriggerSlot(
}
};

const trigger = useTriggerSlot(state, triggerFromProps);
const trigger = useTriggerSlot(triggerFromProps, ref, { state: options.state, defaultProps, elementType: 'button' });
trigger.onKeyDown = mergeCallbacks(onTriggerKeyDown, trigger.onKeyDown);

return trigger;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as React from 'react';
import { useFieldControlProps_unstable } from '@fluentui/react-field';
import { ChevronDownRegular as ChevronDownIcon } from '@fluentui/react-icons';
import { getPartitionedNativeProps, useMergedRefs, slot } from '@fluentui/react-utilities';
import type { Slot } from '@fluentui/react-utilities';
import { useComboboxBaseState } from '../../utils/useComboboxBaseState';
import { useComboboxPositioning } from '../../utils/useComboboxPositioning';
import { Listbox } from '../Listbox/Listbox';
Expand Down Expand Up @@ -35,28 +34,23 @@ export const useDropdown_unstable = (props: DropdownProps, ref: React.Ref<HTMLBu
const [comboboxPopupRef, comboboxTargetRef] = useComboboxPositioning(props);

const triggerRef = React.useRef<HTMLButtonElement>(null);
let listbox = slot.optional(props.listbox, {
renderByDefault: true,
defaultProps: { children: props.children },
elementType: Listbox,
const listbox = useListboxSlot(props.listbox, comboboxPopupRef, {
state: baseState,
triggerRef,
defaultProps: {
children: props.children,
},
});
const listboxRef = useMergedRefs(listbox?.ref, comboboxPopupRef);
if (listbox) {
listbox.ref = listboxRef;
}
listbox = useListboxSlot(baseState, listbox, triggerRef) as typeof listbox;

let trigger: Slot<'button'> = slot.always(props.button, {
const trigger = useButtonTriggerSlot(props.button ?? {}, useMergedRefs(triggerRef, ref), {
state: baseState,
defaultProps: {
type: 'button',
tabIndex: 0, // Safari doesn't focus the button on click without this
tabIndex: 0,
children: baseState.value || props.placeholder,
...triggerNativeProps,
},
elementType: 'button',
});
trigger.ref = useMergedRefs(trigger.ref, triggerRef, ref);
trigger = useButtonTriggerSlot(baseState, trigger);

const rootSlot = slot.always(props.root, {
defaultProps: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,6 @@ export const useComboboxBaseState = (
setValue,
size,
value,
multiselect,
};
};
Original file line number Diff line number Diff line change
@@ -1,20 +1,54 @@
import * as React from 'react';
import { mergeCallbacks, useId, useEventCallback } from '@fluentui/react-utilities';
import type { ExtractSlotProps, Slot } from '@fluentui/react-utilities';
import { Listbox } from '../components/Listbox/Listbox';
import {
mergeCallbacks,
useId,
useEventCallback,
slot,
isResolvedShorthand,
useMergedRefs,
} from '@fluentui/react-utilities';
import type { ExtractSlotProps, Slot, SlotComponentType } from '@fluentui/react-utilities';
import type { ComboboxBaseState } from './ComboboxBase.types';
import { Listbox } from '../Listbox';
import { ListboxProps } from '../Listbox';

export type UseTriggerSlotState = Pick<ComboboxBaseState, 'multiselect'>;

type UseListboxSlotOptions = {
state: ComboboxBaseState;
triggerRef: React.RefObject<HTMLInputElement> | React.RefObject<HTMLButtonElement>;
defaultProps?: Partial<ListboxProps>;
};

/**
* @returns listbox slot with desired behaviour and props
*/
export function useListboxSlot(
state: ComboboxBaseState,
listboxSlot: ExtractSlotProps<Slot<typeof Listbox>> | undefined,
triggerRef: React.RefObject<HTMLInputElement> | React.RefObject<HTMLButtonElement>,
): ExtractSlotProps<Slot<typeof Listbox>> {
const { multiselect } = state;
listboxSlotFromProp: Slot<typeof Listbox> | undefined,
ref: React.Ref<HTMLDivElement>,
options: UseListboxSlotOptions,
): SlotComponentType<ExtractSlotProps<Slot<typeof Listbox>>> | undefined {
const {
state: { multiselect },
triggerRef,
defaultProps,
} = options;

const listboxId = useId(
'fluent-listbox',
isResolvedShorthand(listboxSlotFromProp) ? listboxSlotFromProp.id : undefined,
);

const listboxSlot = slot.optional(listboxSlotFromProp, {
renderByDefault: true,
elementType: Listbox,
defaultProps: {
id: listboxId,
multiselect,
tabIndex: undefined,
...defaultProps,
},
});

/**
* Clicking on the listbox should never blur the trigger
Expand All @@ -33,15 +67,12 @@ export function useListboxSlot(
}, listboxSlot?.onClick),
);

const listboxId = useId('fluent-listbox', listboxSlot?.id);
const listbox: typeof listboxSlot = {
id: listboxId,
multiselect,
tabIndex: undefined,
...listboxSlot,
onMouseDown,
onClick,
};

return listbox;
const listboxRef = useMergedRefs(listboxSlot?.ref, ref);
if (listboxSlot) {
listboxSlot.ref = listboxRef;
listboxSlot.onMouseDown = onMouseDown;
listboxSlot.onClick = onClick;
}

return listboxSlot;
}
Loading

0 comments on commit 344b397

Please sign in to comment.