Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(react-tag-picker): ensure navigation between TagPickerGroup and TagPickerInput #31085

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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