-
Notifications
You must be signed in to change notification settings - Fork 1.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
[ResourceList-BulkAction][Checkbox] Handle focus on bulk actions and converts checkbox into a functional component. #2138
Conversation
src/components/Checkbox/Checkbox.tsx
Outdated
const {onChange, id, disabled} = this.props; | ||
|
||
if (onChange == null || this.inputNode.current == null || disabled) { | ||
const ImperitaveCheckbox: RefForwardingComponent< |
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 think we'd be able to sidestep this explicit typing stuff if we define this inline rather than a separate function
export const Checkbox = React.forwardRef(function Checkbox({
/* Props here */
}) {
/* Blah */
});
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 couldn't get this to work for some reason. I'll give it another go.
src/components/Checkbox/Checkbox.tsx
Outdated
const inputNode = useRef<HTMLInputElement>(null); | ||
|
||
const generatedId = useRef(getUniqueID()); | ||
const id = idProp || generatedId.current; |
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.
use useUniqueId
instead of createUniqueIDFactory and ref dancing
52e20ab
to
693b755
Compare
After a discussion wit @AndrewMusgrave and @BPScott regarding this being a breaking change we agreed that in our case we wouldn't consider this a breaking change because even though the type for Checkbox is changing, we did not expose any public API or forwardRef on Checkbox, therefore, this shouldn't affect anything we support today. |
693b755
to
e326ded
Compare
src/components/Checkbox/Checkbox.tsx
Outdated
if (onChange == null || inputNode.current == null || disabled) { | ||
return; | ||
} | ||
onChange(!inputNode.current.checked, id as any); |
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.
It looks like id
is now typed to be string
so do you need that as any
?
e326ded
to
a387db2
Compare
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.
Looks like we might be able to infer the imperative handler type with one of these
@@ -27,6 +27,8 @@ const MAX_PROMOTED_ACTIONS = 2; | |||
export interface BulkActionsProps { | |||
/** Visually hidden text for screen readers */ | |||
accessibilityLabel?: string; | |||
/** Whether to render the small screen BulkActions or not */ | |||
smallScreen?: 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.
Rather than adding this to the public API - should we use #2117 ?
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.
In this case smallScreen is const random number for the resourceList. I could duplicate the code or add it to context, but I opted for a prop. What do you think?
@@ -363,11 +366,12 @@ class BulkActions extends React.PureComponent<CombinedProps, State> { | |||
disabled, | |||
}; | |||
|
|||
const smallScreenGroup = ( | |||
const smallScreenGroup = smallScreen ? ( |
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.
Love how we're only rendering these when we need to 😍
const bulkActions = mountWithAppProvider( | ||
<BulkActions {...bulkActionProps} selectMode />, | ||
); | ||
const smallGroup = findByTestID(bulkActions, 'smallGroup'); |
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.
An alternative to testID's could be find(Transition)
since you were worried about using them
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 wanted to use findWhere and use the key, but the keys get striped. Just using Find doesn't guarantee us which will be found.
const largeGroup = findByTestID(bulkActions, 'largeGroup'); | ||
|
||
expect(smallGroup.exists()).toBe(false); | ||
expect(largeGroup.exists()).toBe(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.
expect(bulkActions ).toContainReactComponentTimes(Transition, 1);
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.
Same as above. No guarantee which was found.
'input[type="checkbox"]', | ||
); | ||
|
||
expect(checkBox.instance()).toBe(document.activeElement); |
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.
instance
returns a react component, however, getDOMNode
returns the dom element
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.
Interesting how this did pass though.
/>, | ||
); | ||
|
||
const deselectAllCheckableButton = resourceList.findWhere( |
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.
With react-testing you can automatically filter by props in the matcher
button: CheckableButtonNode, | ||
) => { | ||
const {checkableButtons} = this.state; | ||
checkableButtons.set(key, button); |
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.
Uh-oh we're mutating state here 🚫
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.
🙈
15d8a4d
to
33059ae
Compare
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.
Conversion of Checkbox looks good. I don't know enough about ResourceList to be able to give a decent review on that.
One small note inline about note about not making CheckboxHandles public but other than that I'm happy
src/components/index.ts
Outdated
@@ -28,7 +28,7 @@ export {Caption, CaptionProps} from './Caption'; | |||
|
|||
export {Card, CardProps} from './Card'; | |||
|
|||
export {Checkbox, CheckboxProps} from './Checkbox'; | |||
export {Checkbox, CheckboxProps, CheckboxHandles} from './Checkbox'; |
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 like to avoid exporting this publicly, people shouldn't need it
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.
fair enough. I had to disable strict-component-boundaries in the types file to do this.
c8a6364
to
7d79ac3
Compare
7d79ac3
to
6c6ac69
Compare
👋 @BPScott and @AndrewMusgrave, any chance you've taken a look at this yet? |
4fe585d
to
dcdb251
Compare
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.
Using the keyboard it looks like the correct checkbox has focus however it doesn't show visually (https://cl.ly/12e433c1102a). I'm using spacebar to toggle. Left a few comments with thoughts/questions, no strong feelings so do with it if you please 😄 Awesome job! This looks amazing 🎉
); | ||
|
||
const selectAllCheckableButton = resourceList.findWhere( | ||
(wrap) => wrap.is(CheckableButton) && wrap.prop('plain') === 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.
No strong feelings, we usually coerce these types as values rather than compare against a boolean but I'll leave the choice up to you
(wrap) => wrap.is(CheckableButton) && wrap.prop('plain') === true, | |
(wrap) => wrap.is(CheckableButton) && wrap.prop('plain'), |
.findWhere( | ||
(wrap) => wrap.is(CheckableButton) && !wrap.prop('plain'), | ||
) | ||
.find('input[type="checkbox"]'); |
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 curious why you didn't include this in findWhere
or before the findWhere
(both should reduce the number of operations, although probably by a negligible amount)(not asking for a code change, just curious)
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.
🤔 the findWhere will return the wrap
based on the filtering condition. I'm after the checkbox inside the wrap. Putting the find
before would not allow me to findWhere
based on parents asaik. In any case, this has been simplified, because as you suggested I made the findWhere into a function.
/>, | ||
); | ||
|
||
const deselectAllCheckableButton = resourceList.findWhere( |
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 we use this logic a lot, might be worth bringing it out into a function
function deselectAllCheckableButtons(wrapper) {}
| function selectedAllCheckableButtons(wrapper)
|
||
const selectAllCheckableCheckbox = resourceList | ||
.findWhere( | ||
(wrap) => wrap.is(CheckableButton) && wrap.prop('plain') === 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.
Same comment as a few . above
(wrap) => wrap.is(CheckableButton) && wrap.prop('plain') === true, | |
(wrap) => wrap.is(CheckableButton) && wrap.prop('plain'), |
@@ -9,6 +14,7 @@ export interface CheckableButtonProps { | |||
label?: string; | |||
selected?: boolean | 'indeterminate'; | |||
selectMode?: boolean; | |||
smallScreen?: 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.
Should we create an issue to add RL smallScreen
to media query provider and remove this 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.
If you feel like we should we could do that. I actually don't mind the prop here making this a controllable dumb component.
|
||
let currentKey: CheckableButtonKey = 'plain'; | ||
|
||
if (plain) { |
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.
Curious what you think about a switch statement here?
case plain:
break;
case smallScreen:
currentKey = 'bulkSm';
break;
default:
currentKey = 'bulkLg';
or if there's a condition that lets us remove currentKey = 'plain';
or default currentKey
to undefined. Just some written thoughts, don't let this block you from 🐑 🇮🇹
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.
in this case, we're switching on 2 different props. I can simply by defaulting to the else
instead.
src/utilities/resource-list/types.ts
Outdated
export type ResourceListSelectedItems = string[] | 'All'; | ||
|
||
export type CheckableButtonNode = CheckboxHandles; |
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.
Do we need this type?
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.
Not really I guess. I wasn't thinking it made sense just because of the naming, but I can just us the CheckboxHandles. I'll remove it since it brings up the question.
dcdb251
to
78cd5f2
Compare
Not sure why this is, everything checks out and the input does get focus. For some reason with the keyboard it doesn't trigger the visual change on the checkbox. Tried forcing a reflow and it doesn't work either. Sadly the only thing that does work is The rest of the suggestions have been applied. |
0aba4d3
to
d5636b3
Compare
@dleroux I suspect this is because the actual checkbox is visually hidden, and the visually mocked on is getting updated when the user unchecks it. Focus is on the "old" version of the visual checkbox. If we can avoid hiding the actual checkbox visually, this issue should go away. |
Is this ready for another round of reviews? |
Yes it is. |
if (onSelectionChange) { | ||
onSelectionChange(newlySelectedItems); | ||
} | ||
|
||
// setTimeout ensures execution after the Transition on BulkActions | ||
setTimeout(() => { |
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.
cc/ @beefchimi similar to what we were looking at yesterday
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.
Similar, except this is able to get away with a 0ms
Timeout 😄
In this case, is requestAnimationFrame
an alternative solution the problem? Also... is this needed specifically because of being nested in react-transition-group
?
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, it is needed because of react-transition-group
. They trigger their transition using setTimeout
. So using setTimeout
here ensures that theirs is called first. requestAnimationFrame
was working, but because they are using setTimeout
we felt it was better here.
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.
🎩 looked ✅
d5636b3
to
a65d5d3
Compare
WHY are these changes introduced?
Fixes #792
WHAT is this pull request doing?
I believe this is the last of the ResourceList a11y issues outline in [#792].
In order to switch focus between the plain
CheckableButton
checkbox and the ones inside the BulkActions, we needed to keep track of the button that was clicked and which of the 3 possible buttons that would receive focus.handleToggleAll
is only called when the bulk action or plain button are clicked. From there, depending on screen size we focus the right button.How to 🎩
🖥 Local development instructions
🗒 General tophatting guidelines
📄 Changelog guidelines
Copy-paste this code in
playground/Playground.tsx
:🎩 checklist
README.md
with documentation changes