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

CheckboxControl: Support indeterminate state #39462

Merged
merged 8 commits into from
Mar 18, 2022

Conversation

Mamaduka
Copy link
Member

What?

Closes #39434.

PR adds indeterminate state support to the CheckboxControl.

Why?

Previously, we could achieve this by manually setting aria-checked="mixed" on the component. However, Spec provides a "native" way to handle this state, so I think this should be the preferred method.

How?

The implementation is pretty simple. While I included a few safeguards for the impossibles states, the parent component is responsible for correctly setting both indeterminate and checked props.

Testing Instructions

Tested locally using Storybook

Screenshots or screencast

CleanShot.2022-03-15.at.17.32.32.mp4

@Mamaduka Mamaduka self-assigned this Mar 15, 2022
@Mamaduka Mamaduka added [Type] Enhancement A suggestion for improvement. [Package] Components /packages/components labels Mar 15, 2022
@github-actions
Copy link

github-actions bot commented Mar 15, 2022

Size Change: +154 B (0%)

Total Size: 1.16 MB

Filename Size Change
build/components/index.min.js 218 kB +103 B (0%)
build/components/style-rtl.css 15.6 kB +26 B (0%)
build/components/style.css 15.6 kB +25 B (0%)
ℹ️ View Unchanged
Filename Size
build/a11y/index.min.js 993 B
build/admin-manifest/index.min.js 1.24 kB
build/annotations/index.min.js 2.77 kB
build/api-fetch/index.min.js 2.27 kB
build/autop/index.min.js 2.15 kB
build/blob/index.min.js 487 B
build/block-directory/index.min.js 6.49 kB
build/block-directory/style-rtl.css 1.01 kB
build/block-directory/style.css 1.01 kB
build/block-editor/default-editor-styles-rtl.css 378 B
build/block-editor/default-editor-styles.css 378 B
build/block-editor/index.min.js 145 kB
build/block-editor/style-rtl.css 15 kB
build/block-editor/style.css 15 kB
build/block-library/blocks/archives/editor-rtl.css 61 B
build/block-library/blocks/archives/editor.css 60 B
build/block-library/blocks/archives/style-rtl.css 65 B
build/block-library/blocks/archives/style.css 65 B
build/block-library/blocks/audio/editor-rtl.css 150 B
build/block-library/blocks/audio/editor.css 150 B
build/block-library/blocks/audio/style-rtl.css 111 B
build/block-library/blocks/audio/style.css 111 B
build/block-library/blocks/audio/theme-rtl.css 125 B
build/block-library/blocks/audio/theme.css 125 B
build/block-library/blocks/block/editor-rtl.css 161 B
build/block-library/blocks/block/editor.css 161 B
build/block-library/blocks/button/editor-rtl.css 445 B
build/block-library/blocks/button/editor.css 445 B
build/block-library/blocks/button/style-rtl.css 560 B
build/block-library/blocks/button/style.css 560 B
build/block-library/blocks/buttons/editor-rtl.css 292 B
build/block-library/blocks/buttons/editor.css 292 B
build/block-library/blocks/buttons/style-rtl.css 275 B
build/block-library/blocks/buttons/style.css 275 B
build/block-library/blocks/calendar/style-rtl.css 207 B
build/block-library/blocks/calendar/style.css 207 B
build/block-library/blocks/categories/editor-rtl.css 84 B
build/block-library/blocks/categories/editor.css 83 B
build/block-library/blocks/categories/style-rtl.css 79 B
build/block-library/blocks/categories/style.css 79 B
build/block-library/blocks/code/style-rtl.css 103 B
build/block-library/blocks/code/style.css 103 B
build/block-library/blocks/code/theme-rtl.css 124 B
build/block-library/blocks/code/theme.css 124 B
build/block-library/blocks/columns/editor-rtl.css 108 B
build/block-library/blocks/columns/editor.css 108 B
build/block-library/blocks/columns/style-rtl.css 406 B
build/block-library/blocks/columns/style.css 406 B
build/block-library/blocks/comment-author-avatar/editor-rtl.css 125 B
build/block-library/blocks/comment-author-avatar/editor.css 125 B
build/block-library/blocks/comment-template/style-rtl.css 127 B
build/block-library/blocks/comment-template/style.css 127 B
build/block-library/blocks/comments-pagination-numbers/editor-rtl.css 123 B
build/block-library/blocks/comments-pagination-numbers/editor.css 121 B
build/block-library/blocks/comments-pagination/editor-rtl.css 222 B
build/block-library/blocks/comments-pagination/editor.css 209 B
build/block-library/blocks/comments-pagination/style-rtl.css 235 B
build/block-library/blocks/comments-pagination/style.css 231 B
build/block-library/blocks/comments-query-loop/editor-rtl.css 95 B
build/block-library/blocks/comments-query-loop/editor.css 95 B
build/block-library/blocks/cover/editor-rtl.css 546 B
build/block-library/blocks/cover/editor.css 547 B
build/block-library/blocks/cover/style-rtl.css 1.56 kB
build/block-library/blocks/cover/style.css 1.56 kB
build/block-library/blocks/embed/editor-rtl.css 293 B
build/block-library/blocks/embed/editor.css 293 B
build/block-library/blocks/embed/style-rtl.css 417 B
build/block-library/blocks/embed/style.css 417 B
build/block-library/blocks/embed/theme-rtl.css 124 B
build/block-library/blocks/embed/theme.css 124 B
build/block-library/blocks/file/editor-rtl.css 300 B
build/block-library/blocks/file/editor.css 300 B
build/block-library/blocks/file/style-rtl.css 255 B
build/block-library/blocks/file/style.css 255 B
build/block-library/blocks/file/view.min.js 353 B
build/block-library/blocks/freeform/editor-rtl.css 2.44 kB
build/block-library/blocks/freeform/editor.css 2.44 kB
build/block-library/blocks/gallery/editor-rtl.css 961 B
build/block-library/blocks/gallery/editor.css 964 B
build/block-library/blocks/gallery/style-rtl.css 1.51 kB
build/block-library/blocks/gallery/style.css 1.51 kB
build/block-library/blocks/gallery/theme-rtl.css 122 B
build/block-library/blocks/gallery/theme.css 122 B
build/block-library/blocks/group/editor-rtl.css 159 B
build/block-library/blocks/group/editor.css 159 B
build/block-library/blocks/group/style-rtl.css 57 B
build/block-library/blocks/group/style.css 57 B
build/block-library/blocks/group/theme-rtl.css 78 B
build/block-library/blocks/group/theme.css 78 B
build/block-library/blocks/heading/style-rtl.css 114 B
build/block-library/blocks/heading/style.css 114 B
build/block-library/blocks/html/editor-rtl.css 332 B
build/block-library/blocks/html/editor.css 333 B
build/block-library/blocks/image/editor-rtl.css 731 B
build/block-library/blocks/image/editor.css 730 B
build/block-library/blocks/image/style-rtl.css 529 B
build/block-library/blocks/image/style.css 535 B
build/block-library/blocks/image/theme-rtl.css 124 B
build/block-library/blocks/image/theme.css 124 B
build/block-library/blocks/latest-comments/style-rtl.css 284 B
build/block-library/blocks/latest-comments/style.css 284 B
build/block-library/blocks/latest-posts/editor-rtl.css 199 B
build/block-library/blocks/latest-posts/editor.css 198 B
build/block-library/blocks/latest-posts/style-rtl.css 447 B
build/block-library/blocks/latest-posts/style.css 446 B
build/block-library/blocks/list/style-rtl.css 94 B
build/block-library/blocks/list/style.css 94 B
build/block-library/blocks/media-text/editor-rtl.css 266 B
build/block-library/blocks/media-text/editor.css 263 B
build/block-library/blocks/media-text/style-rtl.css 493 B
build/block-library/blocks/media-text/style.css 490 B
build/block-library/blocks/more/editor-rtl.css 431 B
build/block-library/blocks/more/editor.css 431 B
build/block-library/blocks/navigation-link/editor-rtl.css 708 B
build/block-library/blocks/navigation-link/editor.css 706 B
build/block-library/blocks/navigation-link/style-rtl.css 94 B
build/block-library/blocks/navigation-link/style.css 94 B
build/block-library/blocks/navigation-submenu/editor-rtl.css 299 B
build/block-library/blocks/navigation-submenu/editor.css 299 B
build/block-library/blocks/navigation-submenu/view.min.js 375 B
build/block-library/blocks/navigation/editor-rtl.css 2.03 kB
build/block-library/blocks/navigation/editor.css 2.04 kB
build/block-library/blocks/navigation/style-rtl.css 1.89 kB
build/block-library/blocks/navigation/style.css 1.88 kB
build/block-library/blocks/navigation/view.min.js 2.85 kB
build/block-library/blocks/nextpage/editor-rtl.css 395 B
build/block-library/blocks/nextpage/editor.css 395 B
build/block-library/blocks/page-list/editor-rtl.css 363 B
build/block-library/blocks/page-list/editor.css 363 B
build/block-library/blocks/page-list/style-rtl.css 175 B
build/block-library/blocks/page-list/style.css 175 B
build/block-library/blocks/paragraph/editor-rtl.css 157 B
build/block-library/blocks/paragraph/editor.css 157 B
build/block-library/blocks/paragraph/style-rtl.css 273 B
build/block-library/blocks/paragraph/style.css 273 B
build/block-library/blocks/post-author/style-rtl.css 175 B
build/block-library/blocks/post-author/style.css 176 B
build/block-library/blocks/post-comments-form/style-rtl.css 446 B
build/block-library/blocks/post-comments-form/style.css 446 B
build/block-library/blocks/post-comments/style-rtl.css 521 B
build/block-library/blocks/post-comments/style.css 521 B
build/block-library/blocks/post-excerpt/editor-rtl.css 73 B
build/block-library/blocks/post-excerpt/editor.css 73 B
build/block-library/blocks/post-excerpt/style-rtl.css 69 B
build/block-library/blocks/post-excerpt/style.css 69 B
build/block-library/blocks/post-featured-image/editor-rtl.css 721 B
build/block-library/blocks/post-featured-image/editor.css 721 B
build/block-library/blocks/post-featured-image/style-rtl.css 153 B
build/block-library/blocks/post-featured-image/style.css 153 B
build/block-library/blocks/post-template/editor-rtl.css 99 B
build/block-library/blocks/post-template/editor.css 98 B
build/block-library/blocks/post-template/style-rtl.css 323 B
build/block-library/blocks/post-template/style.css 323 B
build/block-library/blocks/post-terms/style-rtl.css 73 B
build/block-library/blocks/post-terms/style.css 73 B
build/block-library/blocks/post-title/style-rtl.css 80 B
build/block-library/blocks/post-title/style.css 80 B
build/block-library/blocks/preformatted/style-rtl.css 103 B
build/block-library/blocks/preformatted/style.css 103 B
build/block-library/blocks/pullquote/editor-rtl.css 198 B
build/block-library/blocks/pullquote/editor.css 198 B
build/block-library/blocks/pullquote/style-rtl.css 389 B
build/block-library/blocks/pullquote/style.css 388 B
build/block-library/blocks/pullquote/theme-rtl.css 167 B
build/block-library/blocks/pullquote/theme.css 167 B
build/block-library/blocks/query-pagination-numbers/editor-rtl.css 122 B
build/block-library/blocks/query-pagination-numbers/editor.css 121 B
build/block-library/blocks/query-pagination/editor-rtl.css 221 B
build/block-library/blocks/query-pagination/editor.css 211 B
build/block-library/blocks/query-pagination/style-rtl.css 234 B
build/block-library/blocks/query-pagination/style.css 231 B
build/block-library/blocks/query/editor-rtl.css 131 B
build/block-library/blocks/query/editor.css 132 B
build/block-library/blocks/quote/style-rtl.css 201 B
build/block-library/blocks/quote/style.css 201 B
build/block-library/blocks/quote/theme-rtl.css 223 B
build/block-library/blocks/quote/theme.css 226 B
build/block-library/blocks/read-more/style-rtl.css 132 B
build/block-library/blocks/read-more/style.css 132 B
build/block-library/blocks/rss/editor-rtl.css 202 B
build/block-library/blocks/rss/editor.css 204 B
build/block-library/blocks/rss/style-rtl.css 289 B
build/block-library/blocks/rss/style.css 288 B
build/block-library/blocks/search/editor-rtl.css 165 B
build/block-library/blocks/search/editor.css 165 B
build/block-library/blocks/search/style-rtl.css 397 B
build/block-library/blocks/search/style.css 398 B
build/block-library/blocks/search/theme-rtl.css 64 B
build/block-library/blocks/search/theme.css 64 B
build/block-library/blocks/separator/editor-rtl.css 99 B
build/block-library/blocks/separator/editor.css 99 B
build/block-library/blocks/separator/style-rtl.css 233 B
build/block-library/blocks/separator/style.css 233 B
build/block-library/blocks/separator/theme-rtl.css 172 B
build/block-library/blocks/separator/theme.css 172 B
build/block-library/blocks/shortcode/editor-rtl.css 474 B
build/block-library/blocks/shortcode/editor.css 474 B
build/block-library/blocks/site-logo/editor-rtl.css 744 B
build/block-library/blocks/site-logo/editor.css 744 B
build/block-library/blocks/site-logo/style-rtl.css 181 B
build/block-library/blocks/site-logo/style.css 181 B
build/block-library/blocks/site-tagline/editor-rtl.css 86 B
build/block-library/blocks/site-tagline/editor.css 86 B
build/block-library/blocks/site-title/editor-rtl.css 84 B
build/block-library/blocks/site-title/editor.css 84 B
build/block-library/blocks/social-link/editor-rtl.css 177 B
build/block-library/blocks/social-link/editor.css 177 B
build/block-library/blocks/social-links/editor-rtl.css 674 B
build/block-library/blocks/social-links/editor.css 673 B
build/block-library/blocks/social-links/style-rtl.css 1.37 kB
build/block-library/blocks/social-links/style.css 1.36 kB
build/block-library/blocks/spacer/editor-rtl.css 332 B
build/block-library/blocks/spacer/editor.css 332 B
build/block-library/blocks/spacer/style-rtl.css 48 B
build/block-library/blocks/spacer/style.css 48 B
build/block-library/blocks/table/editor-rtl.css 471 B
build/block-library/blocks/table/editor.css 472 B
build/block-library/blocks/table/style-rtl.css 481 B
build/block-library/blocks/table/style.css 481 B
build/block-library/blocks/table/theme-rtl.css 188 B
build/block-library/blocks/table/theme.css 188 B
build/block-library/blocks/tag-cloud/style-rtl.css 226 B
build/block-library/blocks/tag-cloud/style.css 227 B
build/block-library/blocks/template-part/editor-rtl.css 235 B
build/block-library/blocks/template-part/editor.css 235 B
build/block-library/blocks/template-part/theme-rtl.css 101 B
build/block-library/blocks/template-part/theme.css 101 B
build/block-library/blocks/text-columns/editor-rtl.css 95 B
build/block-library/blocks/text-columns/editor.css 95 B
build/block-library/blocks/text-columns/style-rtl.css 166 B
build/block-library/blocks/text-columns/style.css 166 B
build/block-library/blocks/verse/style-rtl.css 87 B
build/block-library/blocks/verse/style.css 87 B
build/block-library/blocks/video/editor-rtl.css 571 B
build/block-library/blocks/video/editor.css 572 B
build/block-library/blocks/video/style-rtl.css 173 B
build/block-library/blocks/video/style.css 173 B
build/block-library/blocks/video/theme-rtl.css 124 B
build/block-library/blocks/video/theme.css 124 B
build/block-library/common-rtl.css 934 B
build/block-library/common.css 932 B
build/block-library/editor-rtl.css 9.96 kB
build/block-library/editor.css 9.96 kB
build/block-library/index.min.js 169 kB
build/block-library/reset-rtl.css 474 B
build/block-library/reset.css 474 B
build/block-library/style-rtl.css 11.2 kB
build/block-library/style.css 11.3 kB
build/block-library/theme-rtl.css 665 B
build/block-library/theme.css 670 B
build/block-serialization-default-parser/index.min.js 1.12 kB
build/block-serialization-spec-parser/index.min.js 2.83 kB
build/blocks/index.min.js 46.8 kB
build/compose/index.min.js 11.2 kB
build/core-data/index.min.js 14.3 kB
build/customize-widgets/index.min.js 11.2 kB
build/customize-widgets/style-rtl.css 1.39 kB
build/customize-widgets/style.css 1.39 kB
build/data-controls/index.min.js 663 B
build/data/index.min.js 8.19 kB
build/date/index.min.js 31.9 kB
build/deprecated/index.min.js 518 B
build/dom-ready/index.min.js 336 B
build/dom/index.min.js 4.53 kB
build/edit-navigation/index.min.js 16.1 kB
build/edit-navigation/style-rtl.css 4.04 kB
build/edit-navigation/style.css 4.05 kB
build/edit-post/classic-rtl.css 546 B
build/edit-post/classic.css 547 B
build/edit-post/index.min.js 29.8 kB
build/edit-post/style-rtl.css 7.07 kB
build/edit-post/style.css 7.07 kB
build/edit-site/index.min.js 43.8 kB
build/edit-site/style-rtl.css 7.44 kB
build/edit-site/style.css 7.42 kB
build/edit-widgets/index.min.js 16.5 kB
build/edit-widgets/style-rtl.css 4.39 kB
build/edit-widgets/style.css 4.39 kB
build/editor/index.min.js 38.4 kB
build/editor/style-rtl.css 3.71 kB
build/editor/style.css 3.71 kB
build/element/index.min.js 4.29 kB
build/escape-html/index.min.js 548 B
build/format-library/index.min.js 6.62 kB
build/format-library/style-rtl.css 571 B
build/format-library/style.css 571 B
build/hooks/index.min.js 1.66 kB
build/html-entities/index.min.js 454 B
build/i18n/index.min.js 3.79 kB
build/is-shallow-equal/index.min.js 535 B
build/keyboard-shortcuts/index.min.js 1.83 kB
build/keycodes/index.min.js 1.41 kB
build/list-reusable-blocks/index.min.js 1.75 kB
build/list-reusable-blocks/style-rtl.css 838 B
build/list-reusable-blocks/style.css 838 B
build/media-utils/index.min.js 2.94 kB
build/notices/index.min.js 957 B
build/nux/index.min.js 2.12 kB
build/nux/style-rtl.css 751 B
build/nux/style.css 749 B
build/plugins/index.min.js 1.98 kB
build/preferences/index.min.js 1.2 kB
build/primitives/index.min.js 949 B
build/priority-queue/index.min.js 611 B
build/react-i18n/index.min.js 704 B
build/react-refresh-entry/index.min.js 8.44 kB
build/react-refresh-runtime/index.min.js 7.31 kB
build/redux-routine/index.min.js 2.69 kB
build/reusable-blocks/index.min.js 2.24 kB
build/reusable-blocks/style-rtl.css 256 B
build/reusable-blocks/style.css 256 B
build/rich-text/index.min.js 11.1 kB
build/server-side-render/index.min.js 1.61 kB
build/shortcode/index.min.js 1.52 kB
build/token-list/index.min.js 668 B
build/url/index.min.js 1.99 kB
build/viewport/index.min.js 1.08 kB
build/warning/index.min.js 280 B
build/widgets/index.min.js 7.21 kB
build/widgets/style-rtl.css 1.16 kB
build/widgets/style.css 1.16 kB
build/wordcount/index.min.js 1.07 kB

compressed-size-action

Copy link
Contributor

@ciampo ciampo left a comment

Choose a reason for hiding this comment

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

Thank you for working on this, @Mamaduka! Also appreciate the addition of a Storybook example!

The implementation is pretty simple. While I included a few safeguards for the impossibles states, the parent component is responsible for correctly setting both indeterminate and checked props.

I tested the behaviour of a native input type="checkbox" with respect to the indeterminate and checked properties:

  • if indeterminate is true, the checkbox is computed as indeterminate (regardless of the value of checked)
  • if indeterminate is false, then the value of checked is used to determine the checkbox's status
checkbox-indeterminate-checked.mp4

In the light of the above, I wonder if we should just rely more on the native browser behavior:

  • avoid "computing" the checked prop passed to the input element (i.e. ! indeterminate && checked), and instead just pass checked directly
  • use the css :checked and :indeterminate pseudo-classes in CSS to show/hide icons
Example
diff --git a/packages/components/src/checkbox-control/index.js b/packages/components/src/checkbox-control/index.js
index 332db9214a..5341b999e2 100644
--- a/packages/components/src/checkbox-control/index.js
+++ b/packages/components/src/checkbox-control/index.js
@@ -57,24 +57,22 @@ export default function CheckboxControl( {
 					type="checkbox"
 					value="1"
 					onChange={ onChangeValue }
-					checked={ ! indeterminate && checked }
+					checked={ checked }
 					aria-describedby={ !! help ? id + '__help' : undefined }
 					{ ...props }
 				/>
-				{ indeterminate && ! checked ? (
-					<Icon
-						icon={ reset }
-						className="components-checkbox-control__indeterminate"
-						role="presentation"
-					/>
-				) : null }
-				{ checked ? (
-					<Icon
-						icon={ check }
-						className="components-checkbox-control__checked"
-						role="presentation"
-					/>
-				) : null }
+				{ /* Shown/hidden via CSS rules */ }
+				<Icon
+					icon={ reset }
+					className="components-checkbox-control__indeterminate"
+					role="presentation"
+				/>
+				{ /* Shown/hidden via CSS rules */ }
+				<Icon
+					icon={ check }
+					className="components-checkbox-control__checked"
+					role="presentation"
+				/>
 			</span>
 			<label
 				className="components-checkbox-control__label"
diff --git a/packages/components/src/checkbox-control/style.scss b/packages/components/src/checkbox-control/style.scss
index a9fac4381f..fbcc49e16a 100644
--- a/packages/components/src/checkbox-control/style.scss
+++ b/packages/components/src/checkbox-control/style.scss
@@ -65,6 +67,7 @@ $checkbox-input-size-sm: 24px; // Width & height for small viewports.
 
 svg.components-checkbox-control__checked,
 svg.components-checkbox-control__indeterminate {
+	display: none;
 	fill: $white;
 	cursor: pointer;
 	position: absolute;
@@ -80,3 +83,13 @@ svg.components-checkbox-control__indeterminate {
 	user-select: none;
 	pointer-events: none;
 }
+
+.components-checkbox-control__input[type='checkbox']:checked
+	~ svg.components-checkbox-control__checked {
+	display: block;
+}
+
+.components-checkbox-control__input[type='checkbox']:indeterminate
+	~ svg.components-checkbox-control__indeterminate {
+	display: block;
+}

Finally, it'd be great if you also could:

  • (not blocking, can be done in a follow-up) add some unit tests (just noticing this component doesn't have any!), especially around the indeterminate status
  • add a CHANGELOG entry

packages/components/src/checkbox-control/index.js Outdated Show resolved Hide resolved
@Mamaduka
Copy link
Member Author

Thanks for the feedback, @ciampo.

I will push updates shortly.

use the css :checked and :indeterminate pseudo-classes in CSS to show/hide icons

Any reason for preferring this method instead of conditionally rendering elements?

@ciampo
Copy link
Contributor

ciampo commented Mar 16, 2022

Any reason for preferring this method instead of conditionally rendering elements?

It's mainly to make sure that the behaviour is consistent with the browser's logic, rather than reimplementing the logic ourselves in JS.

This was my suggestion, of course I'm open to hear what everyone thinks too!

@Mamaduka
Copy link
Member Author

I've no strong opinions on this one 😄 However, I remember that we try to avoid rendering extra DOM elements when possible.

cc @jasmussen

@jasmussen
Copy link
Contributor

No strong opinion here either! Though I do appreciate us being able to use the icon from the icon library as-is, outside of any CSS background shenanigans we might need to get into otherwise.

@ciampo
Copy link
Contributor

ciampo commented Mar 16, 2022

What about using the matches function to avoid rendering extra DOM elements, while still using the browser's logic?

Something like

{ ref.matches(':checked') && ( <CheckedIcon /> ) }
{ ref.matches(':indeterminate') && ( <IndeterminateIcon /> ) }

@Mamaduka
Copy link
Member Author

I just tried that and got some unexpected results 😄

CleanShot.2022-03-16.at.18.54.28.mp4

@ciampo
Copy link
Contributor

ciampo commented Mar 16, 2022

Hey @Mamaduka , I just pushed an update to the PR (hope you don't mind!) that should make the suggested approach work — basically the main issue, previously, was that the component wouldn't re-render when the ref changed from null to the correct input's reference.

Using useRefEffect allows us to run a callback everytime ref (and any other dependency that we list) change, which should solve the issue you were encountering!

@Mamaduka
Copy link
Member Author

Thank you, @ciampo.

I've going to rebase and resolve the merge conflict.

@Mamaduka Mamaduka force-pushed the try/checkbox-control-indeterminate branch from 365ea61 to 27b7940 Compare March 16, 2022 18:16
@alexstine
Copy link
Contributor

@Mamaduka Should I be able to test this by checking the Options > Lock from a block or does this have to be done via Storybook?

If via Storybook, how can I check locally? Never used Storybook with Gutenberg before.

@Mamaduka
Copy link
Member Author

Hi, @alexstine

Currently, it's only possible to test via Storybook. After this is merged, I'm going to update components that use an "indeterminate" state.

You can run the following command, and it will open Storybook locally:

npm run storybook:dev

@alexstine
Copy link
Contributor

@Mamaduka The screen reader is not announcing the state difference, very odd.

I am going to ask for @MarcoZehe opinion on this because I am not sure how to make this work for screen readers. The aria-checked attribute did not help us and now this new state isn't either. I hope I haven't asked you to make this for no good reason if we've got a lack of support in browsers or screen readers. I know I've heard my screen reader announce a checkbox as mixed before, but can't remember where or how it was implemented.

Let's see if we can get some guidance...

@MarcoZehe
Copy link
Contributor

The aria-checked property takes one of three possible values:

  • true if the checkbox is checked.
  • false if it is unchecked
  • mixed if the state is indeterminate.
  • So, a role of "checkbox" needs the aria-checked property set at all times to one of these three defined values.
    I just checked, and a simple testcase definitely exposes the mixed state to screen readers in Firefox and Chrome. I didn't see a mention of aria-checked or such in the patch at all. So am wondering if this is even set.

@ciampo
Copy link
Contributor

ciampo commented Mar 17, 2022

Thank you

The aria-checked property takes one of three possible values:

  • true if the checkbox is checked.
  • false if it is unchecked
  • mixed if the state is indeterminate.
  • So, a role of "checkbox" needs the aria-checked property set at all times to one of these three defined values.
    I just checked, and a simple testcase definitely exposes the mixed state to screen readers in Firefox and Chrome. I didn't see a mention of aria-checked or such in the patch at all. So am wondering if this is even set.

Thank you both, @alexstine and @MarcoZehe !

@Mamaduka , would you mind adding aria-checked to the underlying input ? Thank you!

@Mamaduka
Copy link
Member Author

@ciampo Sure. The current implementation in Block Lock modal uses aria-checked, but @alexstine was experiencing the same issue.

let ariaChecked;
if ( isAllChecked ) {
ariaChecked = 'true';
} else if ( Object.values( lock ).some( Boolean ) ) {
ariaChecked = 'mixed';
} else {
ariaChecked = 'false';
}

I thought inputs with type "checkbox" and "radio" don't require aria attributes because of built-in semantics.

Happy to make necessary changes, but it would be great to add inline comments on why those are needed to avoid regressions.

@ciampo
Copy link
Contributor

ciampo commented Mar 17, 2022

Thank you!

The current implementation in Block Lock modal uses aria-checked, but @alexstine was experiencing the same issue.
...
I thought inputs with type "checkbox" and "radio" don't require aria attributes because of built-in semantics.

That's interesting — I personally would assume the same when using standard HTML element. I say, let's apply those changes and test again.

Happy to make necessary changes, but it would be great to add inline comments on why those are needed to avoid regressions.

Comments are definitely needed here to explain better what's going on in the code!

Although the better way to avoid regressions would be to add unit tests, and test specifically for roles, properties (e.g. checked and indeterminate) and attributes (aria-checked).

@MarcoZehe
Copy link
Contributor

Perhaps this StackOverflow question and answer explain some of this bettzer. In essence, there is no way from HTML to set the indeterminate checkbox state, only from JavaScript. I tested the JSFiddle that is linked to from the answer, and Chrome does indeed expose the indeterminate checkbox, and when I toggle it, NVDA also gets notified of state to unchecked to checked. But the indeterminate state can only be set via the JS property, not via markup alone.

@ciampo
Copy link
Contributor

ciampo commented Mar 17, 2022

In essence, there is no way from HTML to set the indeterminate checkbox state, only from JavaScript

Our current implementation in this PR should already take care of that, as it sets indeterminate as a JS property:

https://github.com/WordPress/gutenberg/blob/27b794000e206462fc97513e31cedc6f4c5bff12/packages/components/src/checkbox-control/index.js#L43-L55

Hopefully this approach, together with aria-checked, will be enough.

@Mamaduka Mamaduka force-pushed the try/checkbox-control-indeterminate branch from 27b7940 to dec8b06 Compare March 17, 2022 11:44
@MarcoZehe
Copy link
Contributor

If indeterminate is set this way already, then aria-checked should indeed not be needed. I don't fully understand the issue at hand. Are there exact steps with something that contains this PR to see the problem @alexstine is having?

@Mamaduka
Copy link
Member Author

Thanks for the confirmation, @MarcoZehe.

@alexstine, can you give us more details on how we can reproduce the issue?

@MarcoZehe
Copy link
Contributor

I can definitely say that the JSFiddle that is linked from the Stackoverflow entry works fine in Chrome, but not Firefox. Firefox doesn't expose the indeterminate state to NVDA properly in the JSFiddle. That seems to be a browser bug.

@alexstine
Copy link
Contributor

Firefox doesn't expose the indeterminate state to NVDA properly in the JSFiddle. That seems to be a browser bug.

Makes sense why it isn't working then. I only tested in Storybook with Firefox. I will try Chrome this afternoon. If all checks out, I'll review the code and hopefully we can get this in.

Sorry about the confusion all.

Copy link
Contributor

@alexstine alexstine left a comment

Choose a reason for hiding this comment

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

This works great in Google Chrome.

Maybe want to add some component tests?

Code looks solid enough. Giving it my approval (excluding visuals).

@Mamaduka
Copy link
Member Author

Thanks, everyone for testing 🙇

@ciampo, I can do a test coverage follow-up next week + update two components to use new props.

@ciampo
Copy link
Contributor

ciampo commented Mar 17, 2022

Thanks, everyone for testing 🙇

@ciampo, I can do a test coverage follow-up next week + update two components to use new props.

Sounds good to me — let's get this merged once tests pass.

I just wanted to thank everyone involved in this PR, it's been an excellent example of open source collaboration.

@Mamaduka Mamaduka merged commit a42bba4 into trunk Mar 18, 2022
@Mamaduka Mamaduka deleted the try/checkbox-control-indeterminate branch March 18, 2022 04:39
@github-actions github-actions bot added this to the Gutenberg 12.9 milestone Mar 18, 2022
jostnes pushed a commit to jostnes/gutenberg that referenced this pull request Mar 23, 2022
* CheckboxControl: Support indeterminate state

* Add story

* Feedback

* Update changelog

* Use `useRefEffect` and `element.matches` to show/hide icons

* Fix changelog

* Set aria-checked

* Revert aria-checked for testing

Co-authored-by: Marco Ciampini <marco.ciampo@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Components /packages/components [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

CheckboxControl: Add indeterminate support
5 participants