@wordpress/components 28.0.3 → 28.2.0

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 (50) hide show
  1. package/CHANGELOG.md +133 -120
  2. package/build/custom-select-control-v2/custom-select.js +1 -3
  3. package/build/custom-select-control-v2/custom-select.js.map +1 -1
  4. package/build/custom-select-control-v2/legacy-component/index.js +16 -10
  5. package/build/custom-select-control-v2/legacy-component/index.js.map +1 -1
  6. package/build/custom-select-control-v2/styles.js +34 -19
  7. package/build/custom-select-control-v2/styles.js.map +1 -1
  8. package/build/custom-select-control-v2/types.js.map +1 -1
  9. package/build/drop-zone/index.js +16 -79
  10. package/build/drop-zone/index.js.map +1 -1
  11. package/build/radio-control/index.js +1 -1
  12. package/build/radio-control/index.js.map +1 -1
  13. package/build-module/custom-select-control-v2/custom-select.js +1 -3
  14. package/build-module/custom-select-control-v2/custom-select.js.map +1 -1
  15. package/build-module/custom-select-control-v2/legacy-component/index.js +16 -11
  16. package/build-module/custom-select-control-v2/legacy-component/index.js.map +1 -1
  17. package/build-module/custom-select-control-v2/styles.js +33 -18
  18. package/build-module/custom-select-control-v2/styles.js.map +1 -1
  19. package/build-module/custom-select-control-v2/types.js.map +1 -1
  20. package/build-module/drop-zone/index.js +17 -80
  21. package/build-module/drop-zone/index.js.map +1 -1
  22. package/build-module/radio-control/index.js +1 -1
  23. package/build-module/radio-control/index.js.map +1 -1
  24. package/build-style/style-rtl.css +30 -5
  25. package/build-style/style.css +30 -5
  26. package/build-types/checkbox-control/stories/index.story.d.ts.map +1 -1
  27. package/build-types/custom-select-control/stories/index.story.d.ts +15 -0
  28. package/build-types/custom-select-control/stories/index.story.d.ts.map +1 -1
  29. package/build-types/custom-select-control-v2/legacy-component/index.d.ts.map +1 -1
  30. package/build-types/custom-select-control-v2/styles.d.ts +4 -0
  31. package/build-types/custom-select-control-v2/styles.d.ts.map +1 -1
  32. package/build-types/custom-select-control-v2/types.d.ts +1 -0
  33. package/build-types/custom-select-control-v2/types.d.ts.map +1 -1
  34. package/build-types/drop-zone/index.d.ts +3 -0
  35. package/build-types/drop-zone/index.d.ts.map +1 -1
  36. package/package.json +19 -19
  37. package/src/checkbox-control/stories/index.story.tsx +3 -0
  38. package/src/checkbox-control/style.scss +4 -2
  39. package/src/custom-select-control/stories/index.story.tsx +32 -3
  40. package/src/custom-select-control/test/index.js +205 -22
  41. package/src/custom-select-control-v2/custom-select.tsx +1 -1
  42. package/src/custom-select-control-v2/legacy-component/index.tsx +18 -10
  43. package/src/custom-select-control-v2/legacy-component/test/index.tsx +220 -38
  44. package/src/custom-select-control-v2/styles.ts +22 -10
  45. package/src/custom-select-control-v2/types.ts +1 -0
  46. package/src/drop-zone/index.tsx +17 -76
  47. package/src/drop-zone/style.scss +51 -16
  48. package/src/radio-control/index.tsx +1 -1
  49. package/src/radio-control/style.scss +1 -1
  50. package/tsconfig.tsbuildinfo +1 -1
@@ -14,7 +14,11 @@ import { useState } from '@wordpress/element';
14
14
  */
15
15
  import UncontrolledCustomSelectControl from '..';
16
16
 
17
- const customClass = 'amber-skies';
17
+ const customClassName = 'amber-skies';
18
+ const customStyles = {
19
+ backgroundColor: 'rgb(127, 255, 212)',
20
+ rotate: '13deg',
21
+ };
18
22
 
19
23
  const legacyProps = {
20
24
  label: 'label!',
@@ -26,7 +30,7 @@ const legacyProps = {
26
30
  {
27
31
  key: 'flower2',
28
32
  name: 'crimson clover',
29
- className: customClass,
33
+ className: customClassName,
30
34
  },
31
35
  {
32
36
  key: 'flower3',
@@ -35,33 +39,43 @@ const legacyProps = {
35
39
  {
36
40
  key: 'color1',
37
41
  name: 'amber',
38
- className: customClass,
42
+ className: customClassName,
39
43
  },
40
44
  {
41
45
  key: 'color2',
42
46
  name: 'aquamarine',
43
- style: {
44
- backgroundColor: 'rgb(127, 255, 212)',
45
- rotate: '13deg',
46
- },
47
+ style: customStyles,
48
+ },
49
+ {
50
+ key: 'color3',
51
+ name: 'tomato (with custom props)',
52
+ className: customClassName,
53
+ style: customStyles,
54
+ // try passing a valid HTML attribute
55
+ 'aria-label': 'test label',
56
+ // try adding a custom prop
57
+ customPropFoo: 'foo',
47
58
  },
48
59
  ],
49
60
  };
50
61
 
51
62
  const ControlledCustomSelectControl = ( {
52
63
  options,
53
- onChange,
64
+ onChange: onChangeProp,
54
65
  ...restProps
55
66
  }: React.ComponentProps< typeof UncontrolledCustomSelectControl > ) => {
56
- const [ value, setValue ] = useState( options[ 0 ] );
67
+ const [ value, setValue ] = useState( restProps.value ?? options[ 0 ] );
68
+
69
+ const onChange: typeof onChangeProp = ( changeObject ) => {
70
+ onChangeProp?.( changeObject );
71
+ setValue( changeObject.selectedItem );
72
+ };
73
+
57
74
  return (
58
75
  <UncontrolledCustomSelectControl
59
76
  { ...restProps }
60
77
  options={ options }
61
- onChange={ ( args: any ) => {
62
- onChange?.( args );
63
- setValue( args.selectedItem );
64
- } }
78
+ onChange={ onChange }
65
79
  value={ options.find(
66
80
  ( option: any ) => option.key === value.key
67
81
  ) }
@@ -69,12 +83,87 @@ const ControlledCustomSelectControl = ( {
69
83
  );
70
84
  };
71
85
 
86
+ it( 'Should apply external controlled updates', async () => {
87
+ const mockOnChange = jest.fn();
88
+ const { rerender } = render(
89
+ <UncontrolledCustomSelectControl
90
+ { ...legacyProps }
91
+ value={ legacyProps.options[ 0 ] }
92
+ onChange={ mockOnChange }
93
+ />
94
+ );
95
+
96
+ const currentSelectedItem = screen.getByRole( 'combobox', {
97
+ expanded: false,
98
+ } );
99
+
100
+ expect( currentSelectedItem ).toHaveTextContent(
101
+ legacyProps.options[ 0 ].name
102
+ );
103
+
104
+ expect( mockOnChange ).not.toHaveBeenCalled();
105
+
106
+ rerender(
107
+ <UncontrolledCustomSelectControl
108
+ { ...legacyProps }
109
+ value={ legacyProps.options[ 1 ] }
110
+ />
111
+ );
112
+
113
+ expect( currentSelectedItem ).toHaveTextContent(
114
+ legacyProps.options[ 1 ].name
115
+ );
116
+
117
+ // Necessary to wait for onChange to potentially fire
118
+ await sleep();
119
+
120
+ expect( mockOnChange ).not.toHaveBeenCalled();
121
+ } );
122
+
72
123
  describe.each( [
73
124
  [ 'Uncontrolled', UncontrolledCustomSelectControl ],
74
125
  [ 'Controlled', ControlledCustomSelectControl ],
75
126
  ] )( 'CustomSelectControl (%s)', ( ...modeAndComponent ) => {
76
127
  const [ , Component ] = modeAndComponent;
77
128
 
129
+ it( 'Should select the first option when no explicit initial value is passed without firing onChange', async () => {
130
+ const mockOnChange = jest.fn();
131
+ render( <Component { ...legacyProps } onChange={ mockOnChange } /> );
132
+
133
+ expect(
134
+ screen.getByRole( 'combobox', {
135
+ expanded: false,
136
+ } )
137
+ ).toHaveTextContent( legacyProps.options[ 0 ].name );
138
+
139
+ // Necessary to wait for onChange to potentially fire
140
+ await sleep();
141
+
142
+ expect( mockOnChange ).not.toHaveBeenCalled();
143
+ } );
144
+
145
+ it( 'Should pick the initially selected option if the value prop is passed without firing onChange', async () => {
146
+ const mockOnChange = jest.fn();
147
+ render(
148
+ <Component
149
+ { ...legacyProps }
150
+ onChange={ mockOnChange }
151
+ value={ legacyProps.options[ 3 ] }
152
+ />
153
+ );
154
+
155
+ expect(
156
+ screen.getByRole( 'combobox', {
157
+ expanded: false,
158
+ } )
159
+ ).toHaveTextContent( legacyProps.options[ 3 ].name );
160
+
161
+ // Necessary to wait for onChange to potentially fire
162
+ await sleep();
163
+
164
+ expect( mockOnChange ).not.toHaveBeenCalled();
165
+ } );
166
+
78
167
  it( 'Should replace the initial selection when a new item is selected', async () => {
79
168
  render( <Component { ...legacyProps } /> );
80
169
 
@@ -148,7 +237,7 @@ describe.each( [
148
237
  // assert against filtered array
149
238
  itemsWithClass.map( ( { name } ) =>
150
239
  expect( screen.getByRole( 'option', { name } ) ).toHaveClass(
151
- customClass
240
+ customClassName
152
241
  )
153
242
  );
154
243
 
@@ -160,15 +249,12 @@ describe.each( [
160
249
  // assert against filtered array
161
250
  itemsWithoutClass.map( ( { name } ) =>
162
251
  expect( screen.getByRole( 'option', { name } ) ).not.toHaveClass(
163
- customClass
252
+ customClassName
164
253
  )
165
254
  );
166
255
  } );
167
256
 
168
257
  it( 'Should apply styles only to options that have styles defined', async () => {
169
- const customStyles =
170
- 'background-color: rgb(127, 255, 212); rotate: 13deg;';
171
-
172
258
  render( <Component { ...legacyProps } /> );
173
259
 
174
260
  await click(
@@ -244,7 +330,7 @@ describe.each( [
244
330
  screen.getByRole( 'combobox', {
245
331
  expanded: false,
246
332
  } )
247
- ).toHaveTextContent( /hint/i )
333
+ ).toHaveTextContent( 'Hint' )
248
334
  );
249
335
  } );
250
336
 
@@ -281,39 +367,27 @@ describe.each( [
281
367
  } )
282
368
  );
283
369
 
284
- expect( mockOnChange ).toHaveBeenNthCalledWith(
285
- 1,
286
- expect.objectContaining( {
287
- inputValue: '',
288
- isOpen: false,
289
- selectedItem: { key: 'violets', name: 'violets' },
290
- type: '',
291
- } )
292
- );
293
-
294
370
  await click(
295
371
  screen.getByRole( 'option', {
296
372
  name: 'aquamarine',
297
373
  } )
298
374
  );
299
375
 
300
- expect( mockOnChange ).toHaveBeenNthCalledWith(
301
- 2,
376
+ expect( mockOnChange ).toHaveBeenCalledTimes( 1 );
377
+ expect( mockOnChange ).toHaveBeenLastCalledWith(
302
378
  expect.objectContaining( {
303
379
  inputValue: '',
304
380
  isOpen: false,
305
381
  selectedItem: expect.objectContaining( {
306
382
  name: 'aquamarine',
307
383
  } ),
308
- type: '',
384
+ type: expect.any( String ),
309
385
  } )
310
386
  );
311
387
  } );
312
388
 
313
389
  it( 'Should return selectedItem object when specified onChange', async () => {
314
- const mockOnChange = jest.fn(
315
- ( { selectedItem } ) => selectedItem.key
316
- );
390
+ const mockOnChange = jest.fn();
317
391
 
318
392
  render( <Component { ...legacyProps } onChange={ mockOnChange } /> );
319
393
 
@@ -328,10 +402,85 @@ describe.each( [
328
402
  await type( 'p' );
329
403
  await press.Enter();
330
404
 
331
- expect( mockOnChange ).toHaveReturnedWith( 'poppy' );
405
+ expect( mockOnChange ).toHaveBeenCalledTimes( 1 );
406
+ expect( mockOnChange ).toHaveBeenLastCalledWith(
407
+ expect.objectContaining( {
408
+ selectedItem: expect.objectContaining( {
409
+ key: 'flower3',
410
+ name: 'poppy',
411
+ } ),
412
+ } )
413
+ );
414
+ } );
415
+
416
+ it( "Should pass arbitrary props to onChange's selectedItem, but apply only style and className to DOM elements", async () => {
417
+ const onChangeMock = jest.fn();
418
+
419
+ render( <Component { ...legacyProps } onChange={ onChangeMock } /> );
420
+
421
+ const currentSelectedItem = screen.getByRole( 'combobox', {
422
+ expanded: false,
423
+ } );
424
+
425
+ await click( currentSelectedItem );
426
+
427
+ const optionWithCustomAttributes = screen.getByRole( 'option', {
428
+ name: 'tomato (with custom props)',
429
+ } );
430
+
431
+ // Assert that the option element does not have the custom attributes
432
+ expect( optionWithCustomAttributes ).not.toHaveAttribute(
433
+ 'customPropFoo'
434
+ );
435
+ expect( optionWithCustomAttributes ).not.toHaveAttribute(
436
+ 'aria-label'
437
+ );
438
+
439
+ await click( optionWithCustomAttributes );
440
+
441
+ expect( onChangeMock ).toHaveBeenCalledTimes( 1 );
442
+ expect( onChangeMock ).toHaveBeenCalledWith(
443
+ expect.objectContaining( {
444
+ selectedItem: expect.objectContaining( {
445
+ key: 'color3',
446
+ name: 'tomato (with custom props)',
447
+ className: customClassName,
448
+ style: customStyles,
449
+ 'aria-label': 'test label',
450
+ customPropFoo: 'foo',
451
+ } ),
452
+ } )
453
+ );
332
454
  } );
333
455
 
334
456
  describe( 'Keyboard behavior and accessibility', () => {
457
+ // skip reason: legacy v2 doesn't currently implement this behavior
458
+ it.skip( 'Captures the keypress event and does not let it propagate', async () => {
459
+ const onKeyDown = jest.fn();
460
+
461
+ render(
462
+ <div
463
+ // This role="none" is required to prevent an eslint warning about accessibility.
464
+ role="none"
465
+ onKeyDown={ onKeyDown }
466
+ >
467
+ <Component { ...legacyProps } />
468
+ </div>
469
+ );
470
+ const currentSelectedItem = screen.getByRole( 'combobox', {
471
+ expanded: false,
472
+ } );
473
+ await click( currentSelectedItem );
474
+
475
+ const customSelect = screen.getByRole( 'listbox', {
476
+ name: 'label!',
477
+ } );
478
+ expect( customSelect ).toHaveFocus();
479
+ await press.Enter();
480
+
481
+ expect( onKeyDown ).toHaveBeenCalledTimes( 0 );
482
+ } );
483
+
335
484
  it( 'Should be able to change selection using keyboard', async () => {
336
485
  render( <Component { ...legacyProps } /> );
337
486
 
@@ -353,7 +502,9 @@ describe.each( [
353
502
  await press.ArrowDown();
354
503
  await press.Enter();
355
504
 
356
- expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' );
505
+ expect( currentSelectedItem ).toHaveTextContent(
506
+ legacyProps.options[ 1 ].name
507
+ );
357
508
  } );
358
509
 
359
510
  it( 'Should be able to type characters to select matching options', async () => {
@@ -387,7 +538,9 @@ describe.each( [
387
538
  await sleep();
388
539
  await press.Tab();
389
540
  expect( currentSelectedItem ).toHaveFocus();
390
- expect( currentSelectedItem ).toHaveTextContent( 'violets' );
541
+ expect( currentSelectedItem ).toHaveTextContent(
542
+ legacyProps.options[ 0 ].name
543
+ );
391
544
 
392
545
  // Ideally we would test a multi-character typeahead, but anything more than a single character is flaky
393
546
  await type( 'a' );
@@ -456,5 +609,34 @@ describe.each( [
456
609
  } )
457
610
  ).toBeVisible();
458
611
  } );
612
+
613
+ it( 'Should call custom event handlers', async () => {
614
+ const onFocusMock = jest.fn();
615
+ const onBlurMock = jest.fn();
616
+
617
+ render(
618
+ <>
619
+ <Component
620
+ { ...legacyProps }
621
+ onFocus={ onFocusMock }
622
+ onBlur={ onBlurMock }
623
+ />
624
+ <button>Focus stop</button>
625
+ </>
626
+ );
627
+
628
+ const currentSelectedItem = screen.getByRole( 'combobox', {
629
+ expanded: false,
630
+ } );
631
+
632
+ await press.Tab();
633
+
634
+ expect( currentSelectedItem ).toHaveFocus();
635
+ expect( onFocusMock ).toHaveBeenCalledTimes( 1 );
636
+
637
+ await press.Tab();
638
+ expect( currentSelectedItem ).not.toHaveFocus();
639
+ expect( onBlurMock ).toHaveBeenCalledTimes( 1 );
640
+ } );
459
641
  } );
460
642
  } );
@@ -10,16 +10,27 @@ import styled from '@emotion/styled';
10
10
  */
11
11
  import { COLORS, CONFIG } from '../utils';
12
12
  import { space } from '../utils/space';
13
+ import { chevronIconSize } from '../select-control/styles/select-control-styles';
13
14
  import type { CustomSelectButtonSize } from './types';
14
15
 
15
16
  const ITEM_PADDING = space( 2 );
16
17
 
18
+ const truncateStyles = css`
19
+ overflow: hidden;
20
+ text-overflow: ellipsis;
21
+ white-space: nowrap;
22
+ `;
23
+
17
24
  export const WithHintWrapper = styled.div`
18
25
  display: flex;
19
26
  justify-content: space-between;
20
27
  flex: 1;
21
28
  `;
22
29
 
30
+ export const SelectedExperimentalHintWrapper = styled.div`
31
+ ${ truncateStyles }
32
+ `;
33
+
23
34
  export const SelectedExperimentalHintItem = styled.span`
24
35
  color: ${ COLORS.theme.gray[ 600 ] };
25
36
  margin-inline-start: ${ space( 2 ) };
@@ -55,19 +66,18 @@ export const Select = styled( Ariakit.Select, {
55
66
  const sizes = {
56
67
  compact: {
57
68
  [ heightProperty ]: 32,
58
- paddingInlineStart: space( 2 ),
59
- paddingInlineEnd: space( 1 ),
69
+ paddingInlineStart: 8,
70
+ paddingInlineEnd: 8 + chevronIconSize,
60
71
  },
61
72
  default: {
62
73
  [ heightProperty ]: 40,
63
- paddingInlineStart: space( 4 ),
64
- paddingInlineEnd: space( 3 ),
74
+ paddingInlineStart: 16,
75
+ paddingInlineEnd: 16 + chevronIconSize,
65
76
  },
66
77
  small: {
67
78
  [ heightProperty ]: 24,
68
- paddingInlineStart: space( 2 ),
69
- paddingInlineEnd: space( 1 ),
70
- fontSize: 11,
79
+ paddingInlineStart: 8,
80
+ paddingInlineEnd: 8 + chevronIconSize,
71
81
  },
72
82
  };
73
83
 
@@ -75,13 +85,14 @@ export const Select = styled( Ariakit.Select, {
75
85
  };
76
86
 
77
87
  return css`
78
- display: flex;
79
- align-items: center;
80
- justify-content: space-between;
88
+ display: block;
81
89
  background-color: ${ COLORS.theme.background };
82
90
  border: none;
91
+ color: ${ COLORS.theme.foreground };
83
92
  cursor: pointer;
93
+ font-family: inherit;
84
94
  font-size: ${ CONFIG.fontSize };
95
+ text-align: left;
85
96
  width: 100%;
86
97
 
87
98
  &[data-focus-visible] {
@@ -89,6 +100,7 @@ export const Select = styled( Ariakit.Select, {
89
100
  }
90
101
 
91
102
  ${ getSize() }
103
+ ${ ! hasCustomRenderProp && truncateStyles }
92
104
  `;
93
105
  } );
94
106
 
@@ -79,6 +79,7 @@ type LegacyOption = {
79
79
  style?: React.CSSProperties;
80
80
  className?: string;
81
81
  __experimentalHint?: string;
82
+ [ key: string ]: any;
82
83
  };
83
84
 
84
85
  /**
@@ -10,86 +10,14 @@ import { __ } from '@wordpress/i18n';
10
10
  import { useState } from '@wordpress/element';
11
11
  import { upload, Icon } from '@wordpress/icons';
12
12
  import { getFilesFromDataTransfer } from '@wordpress/dom';
13
- import {
14
- __experimentalUseDropZone as useDropZone,
15
- useReducedMotion,
16
- } from '@wordpress/compose';
13
+ import { __experimentalUseDropZone as useDropZone } from '@wordpress/compose';
17
14
 
18
15
  /**
19
16
  * Internal dependencies
20
17
  */
21
- import {
22
- __unstableMotion as motion,
23
- __unstableAnimatePresence as AnimatePresence,
24
- } from '../animation';
25
18
  import type { DropType, DropZoneProps } from './types';
26
19
  import type { WordPressComponentProps } from '../context';
27
20
 
28
- const backdrop = {
29
- hidden: { opacity: 0 },
30
- show: {
31
- opacity: 1,
32
- transition: {
33
- type: 'tween',
34
- duration: 0.2,
35
- delay: 0,
36
- delayChildren: 0.1,
37
- },
38
- },
39
- exit: {
40
- opacity: 0,
41
- transition: {
42
- duration: 0.2,
43
- delayChildren: 0,
44
- },
45
- },
46
- };
47
-
48
- const foreground = {
49
- hidden: { opacity: 0, scale: 0.9 },
50
- show: {
51
- opacity: 1,
52
- scale: 1,
53
- transition: {
54
- duration: 0.1,
55
- },
56
- },
57
- exit: { opacity: 0, scale: 0.9 },
58
- };
59
-
60
- function DropIndicator( { label }: { label?: string } ) {
61
- const disableMotion = useReducedMotion();
62
- const children = (
63
- <motion.div
64
- variants={ backdrop }
65
- initial={ disableMotion ? 'show' : 'hidden' }
66
- animate="show"
67
- exit={ disableMotion ? 'show' : 'exit' }
68
- className="components-drop-zone__content"
69
- // Without this, when this div is shown,
70
- // Safari calls a onDropZoneLeave causing a loop because of this bug
71
- // https://bugs.webkit.org/show_bug.cgi?id=66547
72
- style={ { pointerEvents: 'none' } }
73
- >
74
- <motion.div variants={ foreground }>
75
- <Icon
76
- icon={ upload }
77
- className="components-drop-zone__content-icon"
78
- />
79
- <span className="components-drop-zone__content-text">
80
- { label ? label : __( 'Drop files to upload' ) }
81
- </span>
82
- </motion.div>
83
- </motion.div>
84
- );
85
-
86
- if ( disableMotion ) {
87
- return children;
88
- }
89
-
90
- return <AnimatePresence>{ children }</AnimatePresence>;
91
- }
92
-
93
21
  /**
94
22
  * `DropZone` is a component creating a drop zone area taking the full size of its parent element. It supports dropping files, HTML content or any other HTML drop event.
95
23
  *
@@ -135,7 +63,7 @@ export function DropZoneComponent( {
135
63
 
136
64
  /**
137
65
  * From Windows Chrome 96, the `event.dataTransfer` returns both file object and HTML.
138
- * The order of the checks is important to recognise the HTML drop.
66
+ * The order of the checks is important to recognize the HTML drop.
139
67
  */
140
68
  if ( html && onHTMLDrop ) {
141
69
  onHTMLDrop( html );
@@ -152,7 +80,7 @@ export function DropZoneComponent( {
152
80
 
153
81
  /**
154
82
  * From Windows Chrome 96, the `event.dataTransfer` returns both file object and HTML.
155
- * The order of the checks is important to recognise the HTML drop.
83
+ * The order of the checks is important to recognize the HTML drop.
156
84
  */
157
85
  if ( event.dataTransfer?.types.includes( 'text/html' ) ) {
158
86
  _type = 'html';
@@ -181,12 +109,15 @@ export function DropZoneComponent( {
181
109
  setIsDraggingOverElement( false );
182
110
  },
183
111
  } );
112
+
184
113
  const classes = clsx( 'components-drop-zone', className, {
185
114
  'is-active':
186
115
  ( isDraggingOverDocument || isDraggingOverElement ) &&
187
116
  ( ( type === 'file' && onFilesDrop ) ||
188
117
  ( type === 'html' && onHTMLDrop ) ||
189
118
  ( type === 'default' && onDrop ) ),
119
+ 'has-dragged-out': ! isDraggingOverElement,
120
+ // Keeping the following classnames for legacy purposes
190
121
  'is-dragging-over-document': isDraggingOverDocument,
191
122
  'is-dragging-over-element': isDraggingOverElement,
192
123
  [ `is-dragging-${ type }` ]: !! type,
@@ -194,7 +125,17 @@ export function DropZoneComponent( {
194
125
 
195
126
  return (
196
127
  <div { ...restProps } ref={ ref } className={ classes }>
197
- { isDraggingOverElement && <DropIndicator label={ label } /> }
128
+ <div className="components-drop-zone__content">
129
+ <div className="components-drop-zone__content-inner">
130
+ <Icon
131
+ icon={ upload }
132
+ className="components-drop-zone__content-icon"
133
+ />
134
+ <span className="components-drop-zone__content-text">
135
+ { label ? label : __( 'Drop files to upload' ) }
136
+ </span>
137
+ </div>
138
+ </div>
198
139
  </div>
199
140
  );
200
141
  }
@@ -13,23 +13,58 @@
13
13
  opacity: 1;
14
14
  visibility: visible;
15
15
  }
16
- }
17
16
 
18
- .components-drop-zone__content {
19
- position: absolute;
20
- top: 0;
21
- bottom: 0;
22
- left: 0;
23
- right: 0;
24
- height: 100%;
25
- width: 100%;
26
- display: flex;
27
- background-color: $components-color-accent;
28
- align-items: center;
29
- justify-content: center;
30
- z-index: z-index(".components-drop-zone__content");
31
- text-align: center;
32
- color: $white;
17
+ .components-drop-zone__content {
18
+ position: absolute;
19
+ top: 0;
20
+ bottom: 0;
21
+ left: 0;
22
+ right: 0;
23
+ height: 100%;
24
+ width: 100%;
25
+ display: flex;
26
+ background-color: $components-color-accent;
27
+ align-items: center;
28
+ justify-content: center;
29
+ z-index: z-index(".components-drop-zone__content");
30
+ text-align: center;
31
+ color: $white;
32
+ opacity: 0;
33
+
34
+ // Without this, when this div is shown,
35
+ // Safari calls a onDropZoneLeave causing a loop because of this bug
36
+ // https://bugs.webkit.org/show_bug.cgi?id=66547
37
+ pointer-events: none;
38
+ }
39
+
40
+ .components-drop-zone__content-inner {
41
+ opacity: 0;
42
+ transform: scale(0.9);
43
+ }
44
+
45
+ &.is-active:not(.has-dragged-out) {
46
+ .components-drop-zone__content {
47
+ opacity: 1;
48
+
49
+ transition: opacity 0.2s ease-in-out;
50
+ @media (prefers-reduced-motion) {
51
+ transition: none;
52
+ }
53
+ }
54
+
55
+ .components-drop-zone__content-inner {
56
+ opacity: 1;
57
+ transform: scale(1);
58
+
59
+ transition:
60
+ opacity 0.1s ease-in-out 0.1s,
61
+ transform 0.1s ease-in-out 0.1s;
62
+
63
+ @media (prefers-reduced-motion) {
64
+ transition: none;
65
+ }
66
+ }
67
+ }
33
68
  }
34
69
 
35
70
  .components-drop-zone__content-icon,
@@ -73,7 +73,7 @@ export function RadioControl(
73
73
  help={ help }
74
74
  className={ clsx( className, 'components-radio-control' ) }
75
75
  >
76
- <VStack spacing={ 1 }>
76
+ <VStack spacing={ 3 }>
77
77
  { options.map( ( option, index ) => (
78
78
  <div
79
79
  key={ `${ id }-${ index }` }
@@ -7,7 +7,7 @@
7
7
  @include radio-control;
8
8
 
9
9
  display: inline-flex;
10
- margin: 0 6px 0 0;
10
+ margin: 0 $grid-unit-10 0 0;
11
11
  padding: 0;
12
12
  appearance: none;
13
13
  cursor: pointer;