-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
[Table] Deselect cells on cmd+click only #1665
Conversation
Little cleanupsPreview: documentation | table |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feels amazing 💯
* Whether the meta key should be pressed to enable deselection on click. | ||
* @default false | ||
*/ | ||
requireMetaKeyToDeselect?: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now true
in every context (body cells and header cells). Shall I get rid of it and make the true
path the default behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes please
ignoredSelectors.some((selector: string) => element.closest(selector) != null) | ||
); | ||
} | ||
|
||
// Update logic | ||
// ============ | ||
|
||
/** | ||
* Returns true if the component should ignore subsequent drag-move's. | ||
*/ | ||
private handleUpdateExistingSelection = (selectedRegionIndex: number, event: MouseEvent) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we get rid of the requireMetaKeyToDeselect
prop, I can also promote the two logic paths in this function to the top-level handleActivate
function (with new, nicely named helper functions to manage each logic path). That would obviate the need for a return value here.
if (event.metaKey) { | ||
// if the meta key is pressed, we want to forcefully ignore reordering | ||
// interactions and prioritize drag-selection interactions (e.g. to make | ||
// it possible to deselect a row). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i don't think meta
is the ideal key here for all platforms. meta
means cmd
on mac and windows-key
on windows. typically i use what the hotkeys code calls mod
which is cmd
on mac and ctrl
on windows. i think the windows-key
accesses OS resources like the start menu so it's not usually used in a key combo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe holding any modifier key should disable the drag?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems like this would likely conflict with other key combos
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed by using DragEvents.isAdditive(event)
packages/table/src/tableBody.tsx
Outdated
@@ -85,6 +85,7 @@ export class TableBody extends React.Component<ITableBodyProps, {}> { | |||
onFocus={this.props.onFocus} | |||
onSelection={this.props.onSelection} | |||
onSelectionEnd={this.handleSelectionEnd} | |||
requireMetaKeyToDeselect={true} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
non-configurable? why even have a prop?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because I added it at the beginning of this work. And then realized by the end of it that I didn't need it to be configurable. Will remove.
public state: IHeaderState = { | ||
hasSelectionEnded: false, | ||
}; | ||
public state: IHeaderState; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove this if defining in constructor.
// a selection is already defined, so enable reordering interactions | ||
// right away if other criteria are satisfied too. | ||
this.state = { | ||
hasSelectionEnded: props.selectedRegions != null && props.selectedRegions.length > 0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this logic need to appear in componentWillReceiveProps
too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, already does.
if (event.metaKey) { | ||
// if the meta key is pressed, we want to forcefully ignore reordering | ||
// interactions and prioritize drag-selection interactions (e.g. to make | ||
// it possible to deselect a row). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe holding any modifier key should disable the drag?
* Whether the meta key should be pressed to enable deselection on click. | ||
* @default false | ||
*/ | ||
requireMetaKeyToDeselect?: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes please
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(to prevent accidental merge after recent reviews)
Use DragEvents.isAdditive, fix testsPreview: documentation | table |
Write tests for PlatformUtilsPreview: documentation | table |
@themadcreator @giladgray @adidahiya @gscshoyru The situation:
I tried using <Hotkey
label="???"
combo="mod"
onKeyDown={() => (this.isModKeyPressed = true)}
onKeyUp={() => (this.isModKeyPressed = false)}
/> And then do something on click like But this has some problems:
So I need some kind of platform detection. We're already doing this via the For now, I've copied the ^ Thoughts on this approach? I'd rather not copy code between packages, but nothing else seemed workable. |
|
||
it("returns true if CTRL key pressed", () => { | ||
const fakeEvent: any = { metaKey: false, ctrlKey: true }; | ||
expect(PlatformUtils.isModKeyPressed(fakeEvent, PLATFORM)).to.be.true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can't do const isMac = sinon.stub(PlatformUtils, "isMac")
and expect the tests to pass, because under the hood, isModKeyPressed
invokes isMac
as its own free-floating function, not as a child of some PlatformUtils
construct.
Thus, a platformOverride
parameter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that seems wrong... you should be able to stub the function at the module where it's exported (but not at some other module which re-exports the symbol). did you try stubbing it with a specific implementation (specify its return value)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd love to be wrong, but it wouldn't work with my normal strategies. I tried stubbing the return value within the describe
, but that actually stubs the isMac
function before the describe("isMac")
block runs, which causes all those tests to fail. When I did the stubbing in a before
or in a beforeEach
, it didn't work. Maybe I was just tired and missing something yesterday - I'll try some other stuff right now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this test fails when run on a Mac; isModKeyPressed
still invokes the real isMac
function in its same file:
import * as PlatformUtils from "../../../src/common/internal/platformUtils";
it("returns true if CTRL key pressed on a non-Mac", () => {
const isMac = sinon.stub(PlatformUtils, "isMac").returns(false);
const fakeEvent: any = { metaKey: false, ctrlKey: true };
expect(PlatformUtils.isModKeyPressed(fakeEvent)).to.be.true;
isMac.restore();
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if the signature was isModKeyPressed(event, isMac = isMac)
(callback param, defaults to real impl in the file)?
then you could simply pass a fake isMac
impl () => false
instead of trying to stub some module export.
also then your actual isMac
impl wouldn't need to support platformOverride
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would I test the logic in the real isMac
without a platformOverride
param? I'd like to just stub navigator.platform
, but you can't because it's not a function.
} else { | ||
this.setState({ hasSelectionEnded: false }); | ||
} | ||
this.setState({ hasSelectionEnded: this.isSelectedRegionsControlledAndNonEmpty(nextProps) }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if
clause?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, that's intended. For row headers, hasSelectionEnded
needs to be true
before reordering is allowed. When you pass in selectedRegions
in a controlled fashion, we want the flag to be true from the get-go so that reordering is immediately enabled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we should rename this since the boolean does not indicate anything about the selection "ending", but rather if it is a valid complete selection.
maybe hasValidSelection
or hasReorderableSelection
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to hasValidSelection
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just some nits
} else { | ||
this.setState({ hasSelectionEnded: false }); | ||
} | ||
this.setState({ hasSelectionEnded: this.isSelectedRegionsControlledAndNonEmpty(nextProps) }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we should rename this since the boolean does not indicate anything about the selection "ending", but rather if it is a valid complete selection.
maybe hasValidSelection
or hasReorderableSelection
private wrapInDragReorderable( | ||
index: number, | ||
children: JSX.Element, | ||
disabled: boolean | ((event: MouseEvent) => boolean) = false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
this method will only ever be called with the function type right?
also, it's another private method, so you could just omit this param and use this.isDragReorderableDisabled
directly (though maybe i missed something hidden in folded code)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually this function is used twice: in one case, it falls back to the default false
value, in the other, shown in this diff, the invoking function passes this.isDragReorderableDisabled
directly.
For clarity, I'll remove the default in the first case and passed in the false
value explicitly.
const element = event.target as HTMLElement; | ||
|
||
const isLeftClick = Utils.isLeftClick(event); | ||
const isContextMenuTrigger = isLeftClick && event.ctrlKey; | ||
const isContextMenuTrigger = isLeftClick && event.ctrlKey && PlatformUtils.isMac(); | ||
const isDisabled = CoreUtils.isFunction(disabled) ? CoreUtils.safeInvoke(disabled, event) : disabled; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: seems like a good opportunity to DRY. maybe a templatized CoreUtils method that resolves a constant or functional result and has a default constant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added CoreUtils.safeInvokeOrValue
. 👍
Add CoreUtils.safeInvokeOrValuePreview: documentation | table |
* Returns `true` if (1) the platform is Mac and the keypress includes the `cmd` | ||
* key, or (2) the platform is non-Mac and the keypress includes the `ctrl` key. | ||
*/ | ||
export const isModKeyPressed = (event: KeyboardEvent, platformOverride?: string) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...there's no way to do this via feature discovery, huh? Drat.
|
||
private isDragSelectableDisabled = (event: MouseEvent) => { | ||
if (DragEvents.isAdditive(event)) { | ||
// if the meta/crtl key was pressed, we want to forcefully ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: ctrl, not crtl :P
Nit: 'crtl' => 'ctrl'Preview: documentation | table |
Fixes #1073, Addresses #1079
Checklist
Changes proposed in this pull request:
A morning surprise for @llorca.
Table
no longer toggles selection on click; now, you mustcmd
+ click to deselect cells, columns, or rows.Table
ctrl
+ click now works on Windows again.Reviewers should focus on:
DragReorderable
component would swallow all clicks (evencmd
+ clicks meant for deselecting). The fix required a refactor of thedisabled?
prop to optionally be a function that accepts the currentMouseEvent
, so it can check if themetaKey
is pressed.DragSelectable
layer.