@wordpress/components 19.1.5 → 19.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/build/font-size-picker/index.js +10 -9
  3. package/build/font-size-picker/index.js.map +1 -1
  4. package/build/font-size-picker/utils.js +19 -9
  5. package/build/font-size-picker/utils.js.map +1 -1
  6. package/build/palette-edit/index.js +31 -27
  7. package/build/palette-edit/index.js.map +1 -1
  8. package/build/palette-edit/styles.js +10 -10
  9. package/build/palette-edit/styles.js.map +1 -1
  10. package/build/tools-panel/tools-panel-item/hook.js +12 -6
  11. package/build/tools-panel/tools-panel-item/hook.js.map +1 -1
  12. package/build-module/font-size-picker/index.js +10 -9
  13. package/build-module/font-size-picker/index.js.map +1 -1
  14. package/build-module/font-size-picker/utils.js +19 -9
  15. package/build-module/font-size-picker/utils.js.map +1 -1
  16. package/build-module/palette-edit/index.js +30 -27
  17. package/build-module/palette-edit/index.js.map +1 -1
  18. package/build-module/palette-edit/styles.js +10 -10
  19. package/build-module/palette-edit/styles.js.map +1 -1
  20. package/build-module/tools-panel/tools-panel-item/hook.js +12 -6
  21. package/build-module/tools-panel/tools-panel-item/hook.js.map +1 -1
  22. package/build-types/tools-panel/tools-panel-item/hook.d.ts.map +1 -1
  23. package/package.json +4 -4
  24. package/src/font-size-picker/index.js +27 -13
  25. package/src/font-size-picker/stories/index.js +62 -0
  26. package/src/font-size-picker/test/index.js +87 -0
  27. package/src/font-size-picker/utils.js +22 -9
  28. package/src/palette-edit/index.js +106 -73
  29. package/src/palette-edit/styles.js +0 -2
  30. package/src/tools-panel/test/index.js +353 -3
  31. package/src/tools-panel/tools-panel/README.md +3 -2
  32. package/src/tools-panel/tools-panel-item/hook.ts +18 -6
  33. package/tsconfig.tsbuildinfo +1 -1
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { render, screen, fireEvent } from '@testing-library/react';
4
+ import { render, screen, fireEvent, within } from '@testing-library/react';
5
5
 
6
6
  /**
7
7
  * Internal dependencies
@@ -11,6 +11,7 @@ import { createSlotFill, Provider as SlotFillProvider } from '../../slot-fill';
11
11
 
12
12
  const { Fill: ToolsPanelItems, Slot } = createSlotFill( 'ToolsPanelSlot' );
13
13
  const resetAll = jest.fn();
14
+ const noop = () => undefined;
14
15
 
15
16
  // Default props for the tools panel.
16
17
  const defaultProps = {
@@ -86,6 +87,23 @@ const GroupedItems = ( {
86
87
  );
87
88
  };
88
89
 
90
+ // This context object is used to help simulate different scenarios in which
91
+ // `ToolsPanelItem` registration or deregistration requires testing.
92
+ const panelContext = {
93
+ panelId: '1234',
94
+ menuItems: {
95
+ default: {},
96
+ optional: { [ altControlProps.label ]: true },
97
+ },
98
+ hasMenuItems: false,
99
+ isResetting: false,
100
+ shouldRenderPlaceholderItems: false,
101
+ registerPanelItem: jest.fn(),
102
+ deregisterPanelItem: jest.fn(),
103
+ flagItemCustomization: noop,
104
+ areAllOptionalControlsHidden: true,
105
+ };
106
+
89
107
  // Renders a tools panel including panel items that have been grouped within
90
108
  // a custom component.
91
109
  const renderGroupedItemsInPanel = () => {
@@ -348,6 +366,287 @@ describe( 'ToolsPanel', () => {
348
366
  // there.
349
367
  expect( optionalItem ).not.toBeInTheDocument();
350
368
  } );
369
+
370
+ it( 'should render default controls with conditional isShownByDefault', async () => {
371
+ const linkedControlProps = {
372
+ attributes: { value: false },
373
+ hasValue: jest.fn().mockImplementation( () => {
374
+ return !! linkedControlProps.attributes.value;
375
+ } ),
376
+ label: 'Linked',
377
+ onDeselect: jest.fn(),
378
+ onSelect: jest.fn(),
379
+ };
380
+
381
+ const TestPanel = () => (
382
+ <ToolsPanel { ...defaultProps }>
383
+ <ToolsPanelItem
384
+ { ...altControlProps }
385
+ isShownByDefault={ true }
386
+ >
387
+ <div>Default control</div>
388
+ </ToolsPanelItem>
389
+ <ToolsPanelItem
390
+ { ...linkedControlProps }
391
+ isShownByDefault={ !! altControlProps.attributes.value }
392
+ >
393
+ <div>Linked control</div>
394
+ </ToolsPanelItem>
395
+ </ToolsPanel>
396
+ );
397
+
398
+ const { rerender } = render( <TestPanel /> );
399
+
400
+ // The linked control should start out as an optional control and is
401
+ // not rendered because it does not have a value.
402
+ let linkedItem = screen.queryByText( 'Linked control' );
403
+ expect( linkedItem ).not.toBeInTheDocument();
404
+
405
+ openDropdownMenu();
406
+
407
+ // The linked control should initially appear in the optional controls
408
+ // menu group. There should be three menu groups: default controls,
409
+ // optional controls, and the group to reset all options.
410
+ let menuGroups = screen.getAllByRole( 'group' );
411
+ expect( menuGroups.length ).toEqual( 3 );
412
+
413
+ // The linked control should be in the second group, of optional controls.
414
+ let optionalItem = within( menuGroups[ 1 ] ).getByText( 'Linked' );
415
+ expect( optionalItem ).toBeInTheDocument();
416
+
417
+ // Simulate the main control having a value set which should
418
+ // trigger the linked control becoming a default control via the
419
+ // conditional `isShownByDefault` prop.
420
+ altControlProps.attributes.value = true;
421
+
422
+ rerender( <TestPanel /> );
423
+
424
+ // The linked control should now be a default control and rendered
425
+ // despite not having a value.
426
+ linkedItem = screen.getByText( 'Linked control' );
427
+ expect( linkedItem ).toBeInTheDocument();
428
+
429
+ // The linked control should now appear in the default controls
430
+ // menu group and have been removed from the optional group.
431
+ menuGroups = screen.getAllByRole( 'group' );
432
+
433
+ // There should now only be two groups. The default controls and
434
+ // and the group for the reset all option.
435
+ expect( menuGroups.length ).toEqual( 2 );
436
+
437
+ // The new default control item for the Linked control should be
438
+ // within the first menu group.
439
+ const defaultItem = within( menuGroups[ 0 ] ).getByText( 'Linked' );
440
+ expect( defaultItem ).toBeInTheDocument();
441
+
442
+ // Optional controls have an additional aria-label. This can be used
443
+ // to confirm the conditional default control has been removed from
444
+ // the optional menu item group.
445
+ optionalItem = screen.queryByRole( 'menuitemcheckbox', {
446
+ name: 'Show Linked',
447
+ } );
448
+ expect( optionalItem ).not.toBeInTheDocument();
449
+ } );
450
+
451
+ it( 'should handle conditionally rendered default control', async () => {
452
+ const conditionalControlProps = {
453
+ attributes: { value: false },
454
+ hasValue: jest.fn().mockImplementation( () => {
455
+ return !! conditionalControlProps.attributes.value;
456
+ } ),
457
+ label: 'Conditional',
458
+ onDeselect: jest.fn(),
459
+ onSelect: jest.fn(),
460
+ };
461
+
462
+ const TestPanel = () => (
463
+ <ToolsPanel { ...defaultProps }>
464
+ <ToolsPanelItem
465
+ { ...altControlProps }
466
+ isShownByDefault={ true }
467
+ >
468
+ <div>Default control</div>
469
+ </ToolsPanelItem>
470
+ { !! altControlProps.attributes.value && (
471
+ <ToolsPanelItem
472
+ { ...conditionalControlProps }
473
+ isShownByDefault={ true }
474
+ >
475
+ <div>Conditional control</div>
476
+ </ToolsPanelItem>
477
+ ) }
478
+ </ToolsPanel>
479
+ );
480
+
481
+ const { rerender } = render( <TestPanel /> );
482
+
483
+ // The conditional control should not yet be rendered.
484
+ let conditionalItem = screen.queryByText( 'Conditional control' );
485
+ expect( conditionalItem ).not.toBeInTheDocument();
486
+
487
+ // The conditional control should not yet appear in the default controls
488
+ // menu group.
489
+ openDropdownMenu();
490
+ let menuGroups = screen.getAllByRole( 'group' );
491
+ let defaultItem = within( menuGroups[ 0 ] ).queryByText(
492
+ 'Conditional'
493
+ );
494
+ expect( defaultItem ).not.toBeInTheDocument();
495
+
496
+ // Simulate the main control having a value set which will now
497
+ // render the new default control into the ToolsPanel.
498
+ altControlProps.attributes.value = true;
499
+
500
+ rerender( <TestPanel /> );
501
+
502
+ // The conditional control should now be rendered and included in
503
+ // the panel's menu.
504
+ conditionalItem = screen.getByText( 'Conditional control' );
505
+ expect( conditionalItem ).toBeInTheDocument();
506
+
507
+ // The conditional control should now appear in the default controls
508
+ // menu group.
509
+ menuGroups = screen.getAllByRole( 'group' );
510
+
511
+ // The new default control item for the Conditional control should
512
+ // be within the first menu group.
513
+ defaultItem = within( menuGroups[ 0 ] ).getByText( 'Conditional' );
514
+ expect( defaultItem ).toBeInTheDocument();
515
+ } );
516
+ } );
517
+
518
+ describe( 'registration of panel items', () => {
519
+ beforeEach( () => {
520
+ jest.clearAllMocks();
521
+ } );
522
+
523
+ it( 'should register and deregister items when panelId changes', () => {
524
+ // This test simulates switching block selection, which causes the
525
+ // `ToolsPanel` to rerender with a new panelId, necessitating the
526
+ // registration and deregistration of appropriate `ToolsPanelItem`
527
+ // children.
528
+ //
529
+ // When the `panelId` changes, only items matching the new ID register
530
+ // themselves, while those for the old panelId deregister.
531
+ //
532
+ // See: https://github.com/WordPress/gutenberg/pull/36588
533
+ const context = { ...panelContext };
534
+ const TestPanel = () => (
535
+ <ToolsPanelContext.Provider value={ context }>
536
+ <ToolsPanelItem { ...altControlProps } panelId="1234">
537
+ <div>Item</div>
538
+ </ToolsPanelItem>
539
+ </ToolsPanelContext.Provider>
540
+ );
541
+
542
+ // On the initial render of the panel, the ToolsPanelItem should
543
+ // be registered.
544
+ const { rerender } = render( <TestPanel /> );
545
+
546
+ expect( context.registerPanelItem ).toHaveBeenCalledWith(
547
+ expect.objectContaining( {
548
+ label: altControlProps.label,
549
+ panelId: '1234',
550
+ } )
551
+ );
552
+ expect( context.deregisterPanelItem ).not.toHaveBeenCalled();
553
+
554
+ // Simulate a change in panel, e.g. a switch of block selection.
555
+ context.panelId = '4321';
556
+ context.menuItems.optional[ altControlProps.label ] = false;
557
+
558
+ // Rerender the panel item. Because we have a new panelId, this
559
+ // panelItem should NOT be registered, but it SHOULD be
560
+ // deregistered.
561
+ rerender( <TestPanel /> );
562
+
563
+ // registerPanelItem has still only been called once.
564
+ expect( context.registerPanelItem ).toHaveBeenCalledTimes( 1 );
565
+ // deregisterPanelItem is called, given that we have switched panels.
566
+ expect( context.deregisterPanelItem ).toBeCalledWith(
567
+ altControlProps.label
568
+ );
569
+
570
+ // Simulate switching back to the original panelId, e.g. by selecting
571
+ // the original block again.
572
+ context.panelId = '1234';
573
+ context.menuItems.optional[ altControlProps.label ] = true;
574
+
575
+ // Rerender the panel and ensure that the panelItem is registered
576
+ // again, and it is not de-registered.
577
+ rerender( <TestPanel /> );
578
+
579
+ expect( context.registerPanelItem ).toHaveBeenCalledWith(
580
+ expect.objectContaining( {
581
+ label: altControlProps.label,
582
+ panelId: '1234',
583
+ } )
584
+ );
585
+ expect( context.registerPanelItem ).toHaveBeenCalledTimes( 2 );
586
+ // deregisterPanelItem has still only been called once.
587
+ expect( context.deregisterPanelItem ).toHaveBeenCalledTimes( 1 );
588
+ } );
589
+
590
+ it( 'should register items when ToolsPanel panelId is null', () => {
591
+ // This test simulates when a panel spans multiple block selections.
592
+ // Multi-selection means a panel can't have a single id to match
593
+ // against the item's. Instead the panel gets an id of `null` and
594
+ // individual items should still render themselves in this case.
595
+ //
596
+ // See: https://github.com/WordPress/gutenberg/pull/37216
597
+ const context = { ...panelContext, panelId: null };
598
+ const TestPanel = () => (
599
+ <ToolsPanelContext.Provider value={ context }>
600
+ <ToolsPanelItem { ...altControlProps } panelId="1234">
601
+ <div>Item</div>
602
+ </ToolsPanelItem>
603
+ </ToolsPanelContext.Provider>
604
+ );
605
+
606
+ // On the initial render of the panel, the ToolsPanelItem should
607
+ // be registered.
608
+ const { rerender, unmount } = render( <TestPanel /> );
609
+
610
+ expect( context.registerPanelItem ).toHaveBeenCalledWith(
611
+ expect.objectContaining( {
612
+ label: altControlProps.label,
613
+ panelId: '1234',
614
+ } )
615
+ );
616
+ expect( context.deregisterPanelItem ).not.toHaveBeenCalled();
617
+
618
+ // Simulate a further block selection being added to the
619
+ // multi-selection. The panelId will remain `null` in this case.
620
+ rerender( <TestPanel /> );
621
+ expect( context.registerPanelItem ).toHaveBeenCalledTimes( 1 );
622
+ expect( context.deregisterPanelItem ).not.toHaveBeenCalled();
623
+
624
+ // Simulate a change in panel back to single block selection for
625
+ // which the item matches panelId.
626
+ context.panelId = '1234';
627
+ rerender( <TestPanel /> );
628
+ expect( context.registerPanelItem ).toHaveBeenCalledTimes( 1 );
629
+ expect( context.deregisterPanelItem ).not.toHaveBeenCalled();
630
+
631
+ // Simulate another multi-selection where the panelId is `null`.
632
+ // Item should re-register itself after it deregistered as the
633
+ // multi-selection occurred.
634
+ context.panelId = null;
635
+ rerender( <TestPanel /> );
636
+ expect( context.registerPanelItem ).toHaveBeenCalledTimes( 2 );
637
+ expect( context.deregisterPanelItem ).toHaveBeenCalledTimes( 1 );
638
+
639
+ // Simulate a change in panel e.g. back to a single block selection
640
+ // Where the item's panelId is not a match.
641
+ context.panelId = '4321';
642
+ rerender( <TestPanel /> );
643
+
644
+ // As the item no longer matches the panelId it should not have
645
+ // registered again but instead deregistered.
646
+ unmount();
647
+ expect( context.registerPanelItem ).toHaveBeenCalledTimes( 2 );
648
+ expect( context.deregisterPanelItem ).toHaveBeenCalledTimes( 2 );
649
+ } );
351
650
  } );
352
651
 
353
652
  describe( 'callbacks on menu item selection', () => {
@@ -542,8 +841,6 @@ describe( 'ToolsPanel', () => {
542
841
  // This test simulates this issue by rendering an item within a
543
842
  // contrived `ToolsPanelContext` to reflect the changes the panel
544
843
  // item needs to protect against.
545
-
546
- const noop = () => undefined;
547
844
  const context = {
548
845
  panelId: '1234',
549
846
  menuItems: {
@@ -588,6 +885,59 @@ describe( 'ToolsPanel', () => {
588
885
 
589
886
  expect( altControlProps.onDeselect ).not.toHaveBeenCalled();
590
887
  } );
888
+
889
+ it( 'should not contain orphaned menu items when panelId changes', async () => {
890
+ // As fills and the panel can update independently this aims to
891
+ // test that no orphaned items appear registered in the panel menu.
892
+ //
893
+ // See: https://github.com/WordPress/gutenberg/pull/34085
894
+ const TestSlotFillPanel = ( { panelId } ) => (
895
+ <SlotFillProvider>
896
+ <ToolsPanelItems>
897
+ <ToolsPanelItem { ...altControlProps } panelId="1234">
898
+ <div>Item 1</div>
899
+ </ToolsPanelItem>
900
+ </ToolsPanelItems>
901
+ <ToolsPanelItems>
902
+ <ToolsPanelItem { ...controlProps } panelId="9999">
903
+ <div>Item 2</div>
904
+ </ToolsPanelItem>
905
+ </ToolsPanelItems>
906
+ <ToolsPanel { ...defaultProps } panelId={ panelId }>
907
+ <Slot />
908
+ </ToolsPanel>
909
+ </SlotFillProvider>
910
+ );
911
+
912
+ const { rerender } = render( <TestSlotFillPanel panelId="1234" /> );
913
+ await openDropdownMenu();
914
+
915
+ // Only the item matching the panelId should have been registered
916
+ // and appear in the panel menu.
917
+ let altMenuItem = screen.getByRole( 'menuitemcheckbox', {
918
+ name: 'Show Alt',
919
+ } );
920
+ let exampleMenuItem = screen.queryByRole( 'menuitemcheckbox', {
921
+ name: 'Hide and reset Example',
922
+ } );
923
+
924
+ expect( altMenuItem ).toBeInTheDocument();
925
+ expect( exampleMenuItem ).not.toBeInTheDocument();
926
+
927
+ // Re-render the panel with different panelID simulating a block
928
+ // selection change.
929
+ rerender( <TestSlotFillPanel panelId="9999" /> );
930
+
931
+ altMenuItem = screen.queryByRole( 'menuitemcheckbox', {
932
+ name: 'Show Alt',
933
+ } );
934
+ exampleMenuItem = screen.getByRole( 'menuitemcheckbox', {
935
+ name: 'Hide and reset Example',
936
+ } );
937
+
938
+ expect( altMenuItem ).not.toBeInTheDocument();
939
+ expect( exampleMenuItem ).toBeInTheDocument();
940
+ } );
591
941
  } );
592
942
 
593
943
  describe( 'panel header icon toggle', () => {
@@ -84,8 +84,9 @@ panel's dropdown menu.
84
84
  ### `panelId`: `string`
85
85
 
86
86
  If a `panelId` is set, it is passed through the `ToolsPanelContext` and used
87
- to restrict panel items. Only items with a matching `panelId` will be able
88
- to register themselves with this panel.
87
+ to restrict panel items. When a `panelId` is set, items can only register
88
+ themselves if the `panelId` is explicitly `null` or the item's `panelId` matches
89
+ exactly.
89
90
 
90
91
  - Required: No
91
92
 
@@ -40,11 +40,15 @@ export function useToolsPanelItem(
40
40
 
41
41
  const hasValueCallback = useCallback( hasValue, [ panelId ] );
42
42
  const resetAllFilterCallback = useCallback( resetAllFilter, [ panelId ] );
43
+ const previousPanelId = usePrevious( currentPanelId );
44
+
45
+ const hasMatchingPanel =
46
+ currentPanelId === panelId || currentPanelId === null;
43
47
 
44
48
  // Registering the panel item allows the panel to include it in its
45
49
  // automatically generated menu and determine its initial checked status.
46
50
  useEffect( () => {
47
- if ( currentPanelId === panelId ) {
51
+ if ( hasMatchingPanel && previousPanelId !== null ) {
48
52
  registerPanelItem( {
49
53
  hasValue: hasValueCallback,
50
54
  isShownByDefault,
@@ -54,13 +58,22 @@ export function useToolsPanelItem(
54
58
  } );
55
59
  }
56
60
 
57
- return () => deregisterPanelItem( label );
61
+ return () => {
62
+ if (
63
+ ( previousPanelId === null && !! currentPanelId ) ||
64
+ currentPanelId === panelId
65
+ ) {
66
+ deregisterPanelItem( label );
67
+ }
68
+ };
58
69
  }, [
59
70
  currentPanelId,
60
- panelId,
71
+ hasMatchingPanel,
61
72
  isShownByDefault,
62
73
  label,
63
74
  hasValueCallback,
75
+ panelId,
76
+ previousPanelId,
64
77
  resetAllFilterCallback,
65
78
  ] );
66
79
 
@@ -84,7 +97,7 @@ export function useToolsPanelItem(
84
97
  // Determine if the panel item's corresponding menu is being toggled and
85
98
  // trigger appropriate callback if it is.
86
99
  useEffect( () => {
87
- if ( isResetting || currentPanelId !== panelId ) {
100
+ if ( isResetting || ! hasMatchingPanel ) {
88
101
  return;
89
102
  }
90
103
 
@@ -96,11 +109,10 @@ export function useToolsPanelItem(
96
109
  onDeselect?.();
97
110
  }
98
111
  }, [
99
- currentPanelId,
112
+ hasMatchingPanel,
100
113
  isMenuItemChecked,
101
114
  isResetting,
102
115
  isValueSet,
103
- panelId,
104
116
  wasMenuItemChecked,
105
117
  ] );
106
118