Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
07b16f9
Add autoexpand on touch to align with the desired behavior
jspurlin Mar 12, 2018
3502f3a
update to use pointer events for comboBox and splitButton
jspurlin Mar 14, 2018
1deccdd
update snapshot
jspurlin Mar 14, 2018
4f36e14
remove unused variable
jspurlin Mar 14, 2018
06806a1
Add preventDefault and stopImmediatePropagation so that IE/Edge will …
jspurlin Mar 14, 2018
e6e0276
test change
Mar 22, 2018
78ee211
removed test push
Mar 22, 2018
aea2641
resolved merge conflict
Mar 23, 2018
020ed81
added change file
Mar 23, 2018
c60caca
update bundle size
Mar 23, 2018
8b888d8
temp combo box changes
Mar 27, 2018
fbb8857
added touch start event and added test cases
Mar 30, 2018
4c5720e
updated tests with else case
Apr 3, 2018
90c6ac1
resolved merge
Apr 3, 2018
b8b07c9
removed unneeded comment
Apr 3, 2018
fab1029
updated change list
Apr 3, 2018
0293d75
updated code to deal with async race conditions
Apr 3, 2018
fa7671e
removed stopping evnet handling that stops firefox from opening the g…
Apr 4, 2018
c284b76
cleaned up the code; made sure to cancel actions when they're finished
Apr 4, 2018
d6be2f4
fixed minor nitpick on style
Apr 4, 2018
30a2933
added if case to deal with touch event if there is no onclick; remove…
Apr 4, 2018
1fe7db7
removed unnecessary imports to reduce file size
Apr 5, 2018
ad563fe
fixed build problems; added constants
Apr 5, 2018
1702de4
changed readonly to const
Apr 5, 2018
f20265a
solved build problem with string types
Apr 5, 2018
ec0bd53
fixed missing const that's causing build break with defined type
Apr 5, 2018
51e730f
resolved merge
Apr 9, 2018
f8fea3b
resolved merge; moved reference of splitbutton container to component…
chang47 Apr 11, 2018
dd096d8
Merge https://github.com/OfficeDev/office-ui-fabric-react into jspurl…
chang47 Apr 11, 2018
acce342
added comment for time out
chang47 Apr 11, 2018
dbaa322
increased max size limit of bundle
chang47 Apr 11, 2018
e3ebdf7
Merge branch 'master' into jspurlin/ComboBoxExpandOnTouch
dzearing Apr 12, 2018
e57e6e9
resolve merge conflict
Apr 12, 2018
dd13382
updated contextual menu to address comments
Apr 12, 2018
089233c
added issue regarding dynamically generating a primary action into th…
Apr 12, 2018
a94b7c5
increased the size limit
Apr 12, 2018
71ade9a
updated with better comments and grammatical fixes
Apr 12, 2018
b68bf62
pulled latest from fabric
chang47 Apr 30, 2018
16a88a7
fixed edge async menu close bug
chang47 Apr 30, 2018
cec47d0
update test
chang47 Apr 30, 2018
6d3810e
bumped bundle size
chang47 Apr 30, 2018
1f9fd86
fixed gammar and moved some code around
chang47 Apr 30, 2018
8ce9bc7
made better callback name
chang47 Apr 30, 2018
9b45fef
added pointer and touch event
chang47 Apr 30, 2018
fb63fb8
fixed semicolon
chang47 Apr 30, 2018
e7d0229
updated bundle size
May 1, 2018
9d247be
resovled merge
May 2, 2018
d3b1d13
fixed bundle size to include keytip change
May 2, 2018
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,11 @@
{
"changes": [
{
"packageName": "office-ui-fabric-react",
"comment": "SplitButton/ComboBox: added onTouch support for menu expansion.",
"type": "minor"
}
],
"packageName": "office-ui-fabric-react",
"email": "[email protected]"
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export interface IBaseButtonState {
menuProps?: IContextualMenuProps | null;
}

const TouchIdleDelay = 500; /* ms */

export class BaseButton extends BaseComponent<IBaseButtonProps, IBaseButtonState> implements IButton {

private get _isSplitButton(): boolean {
Expand All @@ -52,6 +54,8 @@ export class BaseButton extends BaseComponent<IBaseButtonProps, IBaseButtonState
private _descriptionId: string;
private _ariaDescriptionId: string;
private _classNames: IButtonClassNames;
private _processingTouch: boolean;
private _lastTouchTimeoutId: number | undefined;

constructor(props: IBaseButtonProps, rootClassName: string) {
super(props);
Expand Down Expand Up @@ -200,6 +204,15 @@ export class BaseButton extends BaseComponent<IBaseButtonProps, IBaseButtonState
return this._onRenderContent(tag, buttonProps);
}

public componentDidMount() {
// For split buttons, touching anywhere in the button should drop the dropdown, which should contain the primary action.
// This gives more hit target space for touch environments. We're setting the onpointerdown here, because React
// does not support Pointer events yet.
if (this._isSplitButton && this._splitButtonContainer.value && 'onpointerdown' in this._splitButtonContainer.value) {
this._events.on(this._splitButtonContainer.value, 'pointerdown', this._onPointerDown, true);
}
}

public componentDidUpdate(prevProps: IBaseButtonProps, prevState: IBaseButtonState) {
// If Button's menu was closed, run onAfterMenuDismiss
if (this.props.onAfterMenuDismiss && prevState.menuProps && !this.state.menuProps) {
Expand Down Expand Up @@ -508,6 +521,7 @@ export class BaseButton extends BaseComponent<IBaseButtonProps, IBaseButtonState
aria-describedby={ ariaDescribedBy + (keytipAttributes['aria-describedby'] || '') }
className={ classNames && classNames.splitButtonContainer }
onKeyDown={ this._onSplitButtonContainerKeyDown }
onTouchStart={ this._onTouchStart }
ref={ this._splitButtonContainer }
data-is-focusable={ true }
onClick={ !disabled && !primaryDisabled ? this._onSplitButtonPrimaryClick : undefined }
Expand All @@ -530,8 +544,11 @@ export class BaseButton extends BaseComponent<IBaseButtonProps, IBaseButtonState
if (this._isExpanded) {
this._dismissMenu();
}
if (this.props.onClick) {

if (!this._processingTouch && this.props.onClick) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Now if we are not processingTouch but there also isn't a this.porps.onClick we will call this._onMenuClick where as before we would not call this.onMenuClick, is that expected?

Copy link
Contributor Author

@chang47 chang47 Apr 4, 2018

Choose a reason for hiding this comment

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

You're correct, I've changed it to an else if to only do a menu click if we're processingTouch

this.props.onClick(ev);
} else if (this._processingTouch) {
this._onMenuClick(ev);
}
}

Expand Down Expand Up @@ -624,6 +641,36 @@ export class BaseButton extends BaseComponent<IBaseButtonProps, IBaseButtonState
}
}

private _onTouchStart: () => void = () => {
if (this._isSplitButton && this._splitButtonContainer.value && !('onpointerdown' in this._splitButtonContainer.value)) {
this._handleTouchAndPointerEvent();
}
}

private _onPointerDown(ev: PointerEvent) {
if (ev.pointerType === 'touch') {
this._handleTouchAndPointerEvent();

ev.preventDefault();
ev.stopImmediatePropagation();
}
}

private _handleTouchAndPointerEvent() {
// If we already have an existing timeeout from a previous touch and pointer event
// cancel that timeout so we can set a nwe one.
if (this._lastTouchTimeoutId !== undefined) {
this._async.clearTimeout(this._lastTouchTimeoutId);
this._lastTouchTimeoutId = undefined;
}
this._processingTouch = true;

this._lastTouchTimeoutId = this._async.setTimeout(() => {
this._processingTouch = false;
this._lastTouchTimeoutId = undefined;
}, TouchIdleDelay);
}

/**
* Returns if the user hits a valid keyboard key to open the menu
* @param ev - the keyboard event
Expand All @@ -637,7 +684,7 @@ export class BaseButton extends BaseComponent<IBaseButtonProps, IBaseButtonState
}
}

private _onMenuClick = (ev: React.MouseEvent<HTMLAnchorElement>) => {
private _onMenuClick = (ev: React.MouseEvent<HTMLDivElement | HTMLAnchorElement>) => {
const { onMenuClick } = this.props;
if (onMenuClick) {
onMenuClick(ev, this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,40 @@ describe('Button', () => {
expect(renderedDOM.getAttribute('aria-expanded')).toEqual('true');
});

it('Touch Start on primary button of SplitButton expands menu', () => {
const button = ReactTestUtils.renderIntoDocument<any>(
<DefaultButton
data-automation-id='test'
text='Create account'
split={ true }
onClick={ alertClicked }
menuProps={ {
items: [
{
key: 'emailMessage',
name: 'Email message',
icon: 'Mail'
},
{
key: 'calendarEvent',
name: 'Calendar event',
icon: 'Calendar'
}
]
} }
/>
);
const renderedDOM = ReactDOM.findDOMNode(button as React.ReactInstance) as Element;
const primaryButtonDOM: HTMLButtonElement = renderedDOM.getElementsByTagName('button')[0] as HTMLButtonElement;

// in a normal scenario, when we do a touchstart we would also cause a
// click event to fire. This doesn't happen in the simulator so we're
// manually adding this in.
ReactTestUtils.Simulate.touchStart(primaryButtonDOM);
ReactTestUtils.Simulate.click(primaryButtonDOM);
expect(renderedDOM.getAttribute('aria-expanded')).toEqual('true');
});

it('If menu trigger is disabled, pressing down does not trigger menu', () => {
const button = ReactTestUtils.renderIntoDocument<any>(
<DefaultButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,32 @@ describe('ComboBox', () => {
expect(returnUndefined.mock.calls.length).toBe(1);
});

it('Call onMenuOpened when touch start on the input', () => {
let comboBoxRoot;
let inputElement;
const returnUndefined = jest.fn();

const wrapper = mount(
<ComboBox
label='testgroup'
defaultSelectedKey='1'
options={ DEFAULT_OPTIONS2 }
onMenuOpen={ returnUndefined }
allowFreeform={ true }
/>);
comboBoxRoot = wrapper.find('.ms-ComboBox');

inputElement = comboBoxRoot.find('input');

// in a normal scenario, when we do a touchstart we would also cause a
// click event to fire. This doesn't happen in the simulator so we're
// manually adding this in.
inputElement.simulate('touchstart');
inputElement.simulate('click');

expect(wrapper.find('.is-open').length).toEqual(1);
});

it('Can type a complete option with autocomplete and allowFreeform on and submit it', () => {
let updatedOption;
let updatedIndex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ enum HoverStatus {
default = -1
}

const ScrollIdleDelay = 250 /* ms */;
const TouchIdleDelay = 500; /* ms */

// This is used to clear any pending autocomplete
// text (used when autocomplete is true and allowFreeform is false)
const ReadOnlyPendingAutoCompleteTimeout = 1000 /* ms */;
interface IComboBoxOptionWrapperProps extends IComboBoxOption {
// True if the option is currently selected
isSelected: boolean;
Expand Down Expand Up @@ -127,10 +133,6 @@ export class ComboBox extends BaseComponent<IComboBoxProps, IComboBoxState> {
// The base id for the comboBox
private _id: string;

// This is used to clear any pending autocomplete
// text (used when autocomplete is true and allowFreeform is false)
private readonly _readOnlyPendingAutoCompleteTimeout: number = 1000 /* ms */;

// After a character is inserted when autocomplete is true and
// allowFreeform is false, remember the task that will clear
// the pending string of characters
Expand All @@ -148,10 +150,12 @@ export class ComboBox extends BaseComponent<IComboBoxProps, IComboBoxState> {

private _hasPendingValue: boolean;

private readonly _scrollIdleDelay: number = 250 /* ms */;

private _scrollIdleTimeoutId: number | undefined;

private _processingTouch: boolean;

private _lastTouchTimeoutId: number | undefined;

// Determines if we should be setting
// focus back to the input when the menu closes.
// The general rule of thumb is if the menu was launched
Expand All @@ -175,6 +179,7 @@ export class ComboBox extends BaseComponent<IComboBoxProps, IComboBoxState> {
const selectedKeys: (string | number)[] = this._getSelectedKeys(props.defaultSelectedKey, props.selectedKey);

this._isScrollIdle = true;
this._processingTouch = false;

const initialSelectedIndices: number[] = this._getSelectedIndices(props.options, selectedKeys);

Expand All @@ -191,8 +196,16 @@ export class ComboBox extends BaseComponent<IComboBoxProps, IComboBoxState> {
}

public componentDidMount(): void {
// hook up resolving the options if needed on focus
this._events.on(this._comboBoxWrapper.current, 'focus', this._onResolveOptions, true);
if (this._comboBoxWrapper.current) {
// hook up resolving the options if needed on focus
this._events.on(this._comboBoxWrapper.current, 'focus', this._onResolveOptions, true);
if ('onpointerdown' in this._comboBoxWrapper.current) {
// For ComboBoxes, touching anywhere in the combo box should drop the dropdown, including the input element.
// This gives more hit target space for touch environments. We're setting the onpointerdown here, because React
// does not support Pointer events yet.
this._events.on(this._comboBoxWrapper.value, 'pointerdown', this._onPointerDown, true);
}
}
}

public componentWillReceiveProps(newProps: IComboBoxProps): void {
Expand Down Expand Up @@ -356,6 +369,7 @@ export class ComboBox extends BaseComponent<IComboBoxProps, IComboBoxState> {
onKeyDown={ this._onInputKeyDown }
onKeyUp={ this._onInputKeyUp }
onClick={ this._onAutofillClick }
onTouchStart={ this._onTouchStart }
onInputValueChange={ this._onInputChange }
aria-expanded={ isOpen }
aria-autocomplete={ this._getAriaAutoCompleteValue() }
Expand Down Expand Up @@ -692,7 +706,7 @@ export class ComboBox extends BaseComponent<IComboBoxProps, IComboBoxState> {
this._lastReadOnlyAutoCompleteChangeTimeoutId =
this._async.setTimeout(
() => { this._lastReadOnlyAutoCompleteChangeTimeoutId = undefined; },
this._readOnlyPendingAutoCompleteTimeout
ReadOnlyPendingAutoCompleteTimeout
);
return;
}
Expand Down Expand Up @@ -1191,7 +1205,7 @@ export class ComboBox extends BaseComponent<IComboBoxProps, IComboBoxState> {
this._isScrollIdle = false;
}

this._scrollIdleTimeoutId = this._async.setTimeout(() => { this._isScrollIdle = true; }, this._scrollIdleDelay);
this._scrollIdleTimeoutId = this._async.setTimeout(() => { this._isScrollIdle = true; }, ScrollIdleDelay);
}

/**
Expand Down Expand Up @@ -1729,12 +1743,42 @@ export class ComboBox extends BaseComponent<IComboBoxProps, IComboBoxState> {
*/
private _onAutofillClick = (): void => {
if (this.props.allowFreeform) {
this.focus(this.state.isOpen);
this.focus(this.state.isOpen || this._processingTouch);
} else {
this._onComboBoxClick();
}
}

private _onTouchStart: () => void = () => {
if (this._comboBoxWrapper.value && !('onpointerdown' in this._comboBoxWrapper)) {
this._handleTouchAndPointerEvent();
}
}

private _onPointerDown = (ev: PointerEvent): void => {
if (ev.pointerType === 'touch') {
this._handleTouchAndPointerEvent();

ev.preventDefault();
ev.stopImmediatePropagation();
}
}

private _handleTouchAndPointerEvent() {
// If we already have an existing timeeout from a previous touch and pointer event
// cancel that timeout so we can set a nwe one.
if (this._lastTouchTimeoutId !== undefined) {
this._async.clearTimeout(this._lastTouchTimeoutId);
this._lastTouchTimeoutId = undefined;
}
this._processingTouch = true;

this._lastTouchTimeoutId = this._async.setTimeout(() => {
this._processingTouch = false;
this._lastTouchTimeoutId = undefined;
}, TouchIdleDelay);
}

/**
* Get the styles for the current option.
* @param item Item props for the current option
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ exports[`ComboBox Renders ComboBox correctly 1`] = `
onInput={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onTouchStart={[Function]}
readOnly={true}
role="combobox"
spellCheck={false}
Expand Down Expand Up @@ -422,6 +423,7 @@ exports[`ComboBox renders a ComboBox with a Keytip correctly 1`] = `
onInput={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onTouchStart={[Function]}
readOnly={true}
role="combobox"
spellCheck={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,42 @@ describe('ContextualMenu', () => {
expect(document.querySelector('.SubMenuClass')).toBeDefined();
});

it('opens a splitbutton submenu item on touch start', () => {
const items: IContextualMenuItem[] = [
{
name: 'TestText 1',
key: 'TestKey1',
split: true,
onClick: () => { alert('test'); },
subMenuProps: {
items: [
{
name: 'SubmenuText 1',
key: 'SubmenuKey1',
className: 'SubMenuClass'
}
]
}
},
];

ReactTestUtils.renderIntoDocument<ContextualMenu>(
<ContextualMenu
items={ items }
/>
);

const menuItem = document.getElementsByName('TestText 1')[0] as HTMLButtonElement;

// in a normal scenario, when we do a touchstart we would also cause a
// click event to fire. This doesn't happen in the simulator so we're
// manually adding this in.
ReactTestUtils.Simulate.touchStart(menuItem);
ReactTestUtils.Simulate.click(menuItem);

expect(document.querySelector('.is-expanded')).toBeDefined();
});

it('sets the correct aria-owns attribute for the submenu', () => {
const submenuId = 'testSubmenuId';
const items: IContextualMenuItem[] = [
Expand Down
Loading