Skip to content

Commit

Permalink
chore(react-tag-picker): ensure navigation between TagPickerGroup and…
Browse files Browse the repository at this point in the history
… TagPickerInput (#31085)
  • Loading branch information
bsunderhus committed Apr 18, 2024
1 parent 9550c4c commit 5c4fa80
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: ensure navigation between TagPickerGroup and TagPickerInput",
"packageName": "@fluentui/react-tag-picker-preview",
"email": "bernardo.sunderhus@gmail.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -219,14 +219,26 @@ describe('TagPicker', () => {
cy.get('[data-testid="tag-picker-input"]').focus().realPress(['Shift', 'Tab']);
cy.get(`[data-testid="tag--${options[options.length - 1]}"]`).should('be.focused');
});
it('should navigate circularly between tags with Arrow key press', () => {
it('should not navigate circularly between tags with Arrow key press', () => {
mount(<TagPickerControlled defaultSelectedOptions={options} />);
cy.get(`[data-testid="tag--${options[0]}"]`).focus().realPress('ArrowRight');
cy.get(`[data-testid="tag--${options[1]}"]`).should('be.focused').realPress('ArrowDown');
cy.get(`[data-testid="tag--${options[2]}"]`).should('be.focused').realPress('ArrowLeft');
cy.get(`[data-testid="tag--${options[1]}"]`).should('be.focused').realPress('ArrowUp');
cy.get(`[data-testid="tag--${options[0]}"]`).should('be.focused').realPress('ArrowUp');
cy.get(`[data-testid="tag--${options[options.length - 1]}"]`).should('be.focused');
cy.get(`[data-testid="tag--${options[0]}"]`).should('be.focused');
cy.get(`[data-testid="tag--${options[options.length - 1]}"]`)
.focus()
.realPress('ArrowRight');
cy.get(`[data-testid="tag--${options[0]}"]`).should('not.be.focused');
});
it('should navigate from tags to input and back with Arrow key press', () => {
mount(<TagPickerControlled defaultSelectedOptions={options} />);
cy.get(`[data-testid="tag-picker-input"]`).focus().realPress('ArrowLeft');
cy.get(`[data-testid="tag--${options[options.length - 1]}"]`)
.should('be.focused')
.realPress('ArrowRight');
cy.get(`[data-testid="tag-picker-input"]`).should('be.focused');
});
it('should memorize last focused tag while switching focus between tags and input', () => {
mount(<TagPickerControlled defaultSelectedOptions={options} />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import * as React from 'react';
import type { TagPickerGroupProps, TagPickerGroupState } from './TagPickerGroup.types';
import { useTagGroup_unstable } from '@fluentui/react-tags';
import { useTagPickerContext_unstable } from '../../contexts/TagPickerContext';
import { useEventCallback, useMergedRefs } from '@fluentui/react-utilities';
import { isHTMLElement, useEventCallback, useMergedRefs } from '@fluentui/react-utilities';
import { tagPickerAppearanceToTagAppearance, tagPickerSizeToTagSize } from '../../utils/tagPicker2Tag';
import { useArrowNavigationGroup } from '@fluentui/react-tabster';
import { ArrowRight } from '@fluentui/keyboard-keys';

/**
* Create the state required to render TagPickerGroup.
Expand All @@ -18,20 +20,33 @@ export const useTagPickerGroup_unstable = (
props: TagPickerGroupProps,
ref: React.Ref<HTMLDivElement>,
): TagPickerGroupState => {
const selectedOptions = useTagPickerContext_unstable(ctx => ctx.selectedOptions);
const hasSelectedOptions = useTagPickerContext_unstable(ctx => ctx.selectedOptions.length > 0);
const hasOneSelectedOption = useTagPickerContext_unstable(ctx => ctx.selectedOptions.length === 1);
const triggerRef = useTagPickerContext_unstable(ctx => ctx.triggerRef);
const tagPickerGroupRef = useTagPickerContext_unstable(ctx => ctx.tagPickerGroupRef);
const selectOption = useTagPickerContext_unstable(ctx => ctx.selectOption);
const size = useTagPickerContext_unstable(ctx => tagPickerSizeToTagSize(ctx.size));
const appearance = useTagPickerContext_unstable(ctx => ctx.appearance);

const arrowNavigationProps = useArrowNavigationGroup({
circular: false,
axis: 'both',
memorizeCurrent: true,
});

const state = useTagGroup_unstable(
{
role: 'listbox',
...props,
...arrowNavigationProps,
size,
appearance: tagPickerAppearanceToTagAppearance(appearance),
dismissible: true,
onKeyDown: useEventCallback(event => {
if (isHTMLElement(event.target) && event.key === ArrowRight) {
triggerRef.current?.focus();
}
}),
onDismiss: useEventCallback((event, data) => {
selectOption(event as React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>, {
value: data.value,
Expand All @@ -50,6 +65,6 @@ export const useTagPickerGroup_unstable = (

return {
...state,
hasSelectedOptions: !!selectedOptions.length,
hasSelectedOptions,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import {
useEventCallback,
useIsomorphicLayoutEffect,
} from '@fluentui/react-utilities';
import { Backspace, Enter, Space } from '@fluentui/keyboard-keys';
import { ArrowLeft, Backspace, Enter, Space } from '@fluentui/keyboard-keys';
import { useInputTriggerSlot } from '@fluentui/react-combobox';
import { useFieldControlProps_unstable } from '@fluentui/react-field';
import { tagPickerInputCSSRules } from '../../utils/tokens';
import { useFocusFinders } from '@fluentui/react-tabster';

/**
* Create the state required to render TagPickerInput.
Expand All @@ -32,6 +33,7 @@ export const useTagPickerInput_unstable = (
const size = useTagPickerContext_unstable(ctx => ctx.size);
const freeform = useTagPickerContext_unstable(ctx => ctx.freeform);
const contextDisabled = useTagPickerContext_unstable(ctx => ctx.disabled);
const tagPickerGroupRef = useTagPickerContext_unstable(ctx => ctx.tagPickerGroupRef);
const {
triggerRef,
clearSelection,
Expand All @@ -57,9 +59,7 @@ export const useTagPickerInput_unstable = (
useIsomorphicLayoutEffect(() => {
if (triggerRef.current) {
const input = triggerRef.current;
const cb = () => {
setTagPickerInputStretchStyle(input);
};
const cb = () => setTagPickerInputStretchStyle(input);
input.addEventListener('input', cb);
return () => {
input.removeEventListener('input', cb);
Expand All @@ -68,6 +68,7 @@ export const useTagPickerInput_unstable = (
}, [triggerRef]);

const { value = contextValue, disabled = contextDisabled } = props;
const { findLastFocusable } = useFocusFinders();

const root = useInputTriggerSlot(
{
Expand All @@ -78,6 +79,10 @@ export const useTagPickerInput_unstable = (
...getIntrinsicElementProps('input', props),
onKeyDown: useEventCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
props.onKeyDown?.(event);
if (event.key === ArrowLeft && event.currentTarget.selectionStart === 0 && tagPickerGroupRef.current) {
findLastFocusable(tagPickerGroupRef.current)?.focus();
return;
}
if (event.key === Space && open) {
setOpen(event, false);
return;
Expand All @@ -91,8 +96,8 @@ export const useTagPickerInput_unstable = (
} else {
setOpen(event, true);
}
return;
}

if (event.key === Backspace && value?.length === 0 && selectedOptions.length) {
const toDismiss = selectedOptions[selectedOptions.length - 1];
selectOption(event, {
Expand All @@ -102,6 +107,7 @@ export const useTagPickerInput_unstable = (
id: 'ERROR_DO_NOT_USE',
text: 'ERROR_DO_NOT_USE',
});
return;
}
}),
},
Expand Down

0 comments on commit 5c4fa80

Please sign in to comment.