@wordpress/components 32.5.2-next.v.202604091042.0 → 32.6.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 (175) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/build/alignment-matrix-control/cell.cjs +3 -3
  3. package/build/alignment-matrix-control/cell.cjs.map +2 -2
  4. package/build/alignment-matrix-control/index.cjs +3 -3
  5. package/build/alignment-matrix-control/index.cjs.map +2 -2
  6. package/build/custom-gradient-picker/index.cjs.map +2 -2
  7. package/build/date-time/{date → date-picker}/index.cjs +6 -6
  8. package/build/date-time/{date → date-picker}/index.cjs.map +2 -2
  9. package/build/date-time/{date → date-picker}/styles.cjs +17 -17
  10. package/build/date-time/{date → date-picker}/styles.cjs.map +2 -2
  11. package/build/date-time/{date → date-picker}/use-lilius/index.cjs +1 -1
  12. package/build/date-time/{date → date-picker}/use-lilius/index.cjs.map +1 -1
  13. package/build/date-time/date-time/index.cjs +6 -6
  14. package/build/date-time/date-time/index.cjs.map +2 -2
  15. package/build/date-time/index.cjs +4 -4
  16. package/build/date-time/index.cjs.map +2 -2
  17. package/build/date-time/{time → time-picker}/index.cjs +6 -6
  18. package/build/date-time/time-picker/index.cjs.map +7 -0
  19. package/build/date-time/{time → time-picker}/styles.cjs +21 -21
  20. package/build/date-time/{time → time-picker}/styles.cjs.map +2 -2
  21. package/build/date-time/{time → time-picker}/time-input/index.cjs +1 -1
  22. package/build/date-time/{time → time-picker}/time-input/index.cjs.map +1 -1
  23. package/build/date-time/{time → time-picker}/timezone.cjs +1 -1
  24. package/build/date-time/{time → time-picker}/timezone.cjs.map +1 -1
  25. package/build/modal/index.cjs.map +2 -2
  26. package/build/palette-edit/index.cjs.map +2 -2
  27. package/build/radio-control/index.cjs +2 -0
  28. package/build/radio-control/index.cjs.map +2 -2
  29. package/build/sandbox/index.cjs +125 -1
  30. package/build/sandbox/index.cjs.map +2 -2
  31. package/build/textarea-control/styles/textarea-control-styles.cjs +3 -3
  32. package/build/textarea-control/styles/textarea-control-styles.cjs.map +2 -2
  33. package/build-module/alignment-matrix-control/cell.mjs +3 -3
  34. package/build-module/alignment-matrix-control/cell.mjs.map +2 -2
  35. package/build-module/alignment-matrix-control/index.mjs +3 -3
  36. package/build-module/alignment-matrix-control/index.mjs.map +2 -2
  37. package/build-module/custom-gradient-picker/index.mjs.map +2 -2
  38. package/build-module/date-time/{date → date-picker}/index.mjs +3 -3
  39. package/build-module/date-time/{date → date-picker}/index.mjs.map +2 -2
  40. package/build-module/date-time/{date → date-picker}/styles.mjs +17 -17
  41. package/build-module/date-time/{date → date-picker}/styles.mjs.map +2 -2
  42. package/build-module/date-time/{date → date-picker}/use-lilius/index.mjs +1 -1
  43. package/build-module/date-time/{date → date-picker}/use-lilius/index.mjs.map +1 -1
  44. package/build-module/date-time/date-time/index.mjs +2 -2
  45. package/build-module/date-time/date-time/index.mjs.map +1 -1
  46. package/build-module/date-time/index.mjs +2 -2
  47. package/build-module/date-time/index.mjs.map +1 -1
  48. package/build-module/date-time/{time → time-picker}/index.mjs +3 -3
  49. package/build-module/date-time/time-picker/index.mjs.map +7 -0
  50. package/build-module/date-time/{time → time-picker}/styles.mjs +21 -21
  51. package/build-module/date-time/{time → time-picker}/styles.mjs.map +2 -2
  52. package/build-module/date-time/{time → time-picker}/time-input/index.mjs +1 -1
  53. package/build-module/date-time/{time → time-picker}/time-input/index.mjs.map +1 -1
  54. package/build-module/date-time/{time → time-picker}/timezone.mjs +1 -1
  55. package/build-module/date-time/{time → time-picker}/timezone.mjs.map +1 -1
  56. package/build-module/modal/index.mjs.map +2 -2
  57. package/build-module/palette-edit/index.mjs.map +2 -2
  58. package/build-module/radio-control/index.mjs +2 -0
  59. package/build-module/radio-control/index.mjs.map +2 -2
  60. package/build-module/sandbox/index.mjs +126 -2
  61. package/build-module/sandbox/index.mjs.map +2 -2
  62. package/build-module/textarea-control/styles/textarea-control-styles.mjs +3 -3
  63. package/build-module/textarea-control/styles/textarea-control-styles.mjs.map +2 -2
  64. package/build-style/style-rtl.css +83 -23
  65. package/build-style/style.css +83 -23
  66. package/build-types/checkbox-control/types.d.ts +4 -0
  67. package/build-types/checkbox-control/types.d.ts.map +1 -1
  68. package/build-types/custom-gradient-picker/index.d.ts.map +1 -1
  69. package/build-types/date-time/date-picker/index.d.ts.map +1 -0
  70. package/build-types/date-time/date-picker/styles.d.ts.map +1 -0
  71. package/build-types/date-time/date-picker/test/index.d.ts.map +1 -0
  72. package/build-types/date-time/date-picker/test/use-lilius.d.ts.map +1 -0
  73. package/build-types/date-time/date-picker/use-lilius/index.d.ts.map +1 -0
  74. package/build-types/date-time/date-time/index.d.ts +2 -2
  75. package/build-types/date-time/date-time/index.d.ts.map +1 -1
  76. package/build-types/date-time/index.d.ts +2 -2
  77. package/build-types/date-time/index.d.ts.map +1 -1
  78. package/build-types/date-time/stories/date.story.d.ts +1 -1
  79. package/build-types/date-time/stories/date.story.d.ts.map +1 -1
  80. package/build-types/date-time/stories/time.story.d.ts +1 -1
  81. package/build-types/date-time/stories/time.story.d.ts.map +1 -1
  82. package/build-types/date-time/{time → time-picker}/index.d.ts +1 -1
  83. package/build-types/date-time/time-picker/index.d.ts.map +1 -0
  84. package/build-types/date-time/time-picker/styles.d.ts.map +1 -0
  85. package/build-types/date-time/time-picker/test/index.d.ts.map +1 -0
  86. package/build-types/date-time/time-picker/time-input/index.d.ts.map +1 -0
  87. package/build-types/date-time/time-picker/time-input/test/index.d.ts.map +1 -0
  88. package/build-types/date-time/time-picker/timezone.d.ts.map +1 -0
  89. package/build-types/date-time/types.d.ts +1 -1
  90. package/build-types/date-time/types.d.ts.map +1 -1
  91. package/build-types/modal/index.d.ts.map +1 -1
  92. package/build-types/palette-edit/index.d.ts.map +1 -1
  93. package/build-types/radio-control/index.d.ts.map +1 -1
  94. package/build-types/radio-control/types.d.ts +6 -0
  95. package/build-types/radio-control/types.d.ts.map +1 -1
  96. package/build-types/sandbox/index.d.ts +1 -1
  97. package/build-types/sandbox/index.d.ts.map +1 -1
  98. package/build-types/sandbox/types.d.ts +11 -0
  99. package/build-types/sandbox/types.d.ts.map +1 -1
  100. package/build-types/textarea-control/stories/index.story.d.ts.map +1 -1
  101. package/build-types/textarea-control/styles/textarea-control-styles.d.ts.map +1 -1
  102. package/build-types/validated-form-controls/components/checkbox-control.d.ts +2 -1
  103. package/build-types/validated-form-controls/components/checkbox-control.d.ts.map +1 -1
  104. package/build-types/validated-form-controls/components/radio-control.d.ts +2 -1
  105. package/build-types/validated-form-controls/components/radio-control.d.ts.map +1 -1
  106. package/package.json +21 -21
  107. package/src/alignment-matrix-control/style.module.scss +1 -1
  108. package/src/button/style.scss +1 -1
  109. package/src/calendar/style.scss +3 -3
  110. package/src/checkbox-control/style.scss +17 -5
  111. package/src/checkbox-control/types.ts +4 -0
  112. package/src/circular-option-picker/style.scss +1 -1
  113. package/src/color-palette/style.scss +1 -1
  114. package/src/css.d.ts +1 -0
  115. package/src/custom-gradient-picker/index.tsx +1 -0
  116. package/src/date-time/README.md +3 -3
  117. package/src/date-time/date-picker/README.md +65 -0
  118. package/src/date-time/date-time/index.tsx +2 -2
  119. package/src/date-time/index.ts +2 -2
  120. package/src/date-time/stories/date.story.tsx +1 -1
  121. package/src/date-time/stories/time.story.tsx +1 -1
  122. package/src/date-time/time-picker/README.md +119 -0
  123. package/src/date-time/{time → time-picker}/index.tsx +1 -1
  124. package/src/date-time/types.ts +1 -1
  125. package/src/dropdown-menu/style.scss +1 -1
  126. package/src/form-toggle/style.scss +35 -2
  127. package/src/form-token-field/style.scss +12 -3
  128. package/src/modal/index.tsx +1 -0
  129. package/src/palette-edit/index.tsx +1 -0
  130. package/src/radio-control/index.tsx +2 -0
  131. package/src/radio-control/style.scss +21 -2
  132. package/src/radio-control/test/index.tsx +10 -0
  133. package/src/radio-control/types.ts +6 -0
  134. package/src/sandbox/index.tsx +189 -9
  135. package/src/sandbox/test/index.tsx +65 -24
  136. package/src/sandbox/types.ts +11 -0
  137. package/src/snackbar/style.scss +2 -2
  138. package/src/tab-panel/style.scss +1 -1
  139. package/src/textarea-control/stories/index.story.tsx +3 -0
  140. package/src/textarea-control/styles/textarea-control-styles.ts +6 -0
  141. package/src/toggle-control/style.scss +1 -1
  142. package/src/toggle-control/test/index.tsx +8 -2
  143. package/build/date-time/time/index.cjs.map +0 -7
  144. package/build-module/date-time/time/index.mjs.map +0 -7
  145. package/build-types/date-time/date/index.d.ts.map +0 -1
  146. package/build-types/date-time/date/styles.d.ts.map +0 -1
  147. package/build-types/date-time/date/test/index.d.ts.map +0 -1
  148. package/build-types/date-time/date/test/use-lilius.d.ts.map +0 -1
  149. package/build-types/date-time/date/use-lilius/index.d.ts.map +0 -1
  150. package/build-types/date-time/time/index.d.ts.map +0 -1
  151. package/build-types/date-time/time/styles.d.ts.map +0 -1
  152. package/build-types/date-time/time/test/index.d.ts.map +0 -1
  153. package/build-types/date-time/time/time-input/index.d.ts.map +0 -1
  154. package/build-types/date-time/time/time-input/test/index.d.ts.map +0 -1
  155. package/build-types/date-time/time/timezone.d.ts.map +0 -1
  156. /package/build-types/date-time/{date → date-picker}/index.d.ts +0 -0
  157. /package/build-types/date-time/{date → date-picker}/styles.d.ts +0 -0
  158. /package/build-types/date-time/{date → date-picker}/test/index.d.ts +0 -0
  159. /package/build-types/date-time/{date → date-picker}/test/use-lilius.d.ts +0 -0
  160. /package/build-types/date-time/{date → date-picker}/use-lilius/index.d.ts +0 -0
  161. /package/build-types/date-time/{time → time-picker}/styles.d.ts +0 -0
  162. /package/build-types/date-time/{time → time-picker}/test/index.d.ts +0 -0
  163. /package/build-types/date-time/{time → time-picker}/time-input/index.d.ts +0 -0
  164. /package/build-types/date-time/{time → time-picker}/time-input/test/index.d.ts +0 -0
  165. /package/build-types/date-time/{time → time-picker}/timezone.d.ts +0 -0
  166. /package/src/date-time/{date → date-picker}/index.tsx +0 -0
  167. /package/src/date-time/{date → date-picker}/styles.ts +0 -0
  168. /package/src/date-time/{date → date-picker}/test/index.tsx +0 -0
  169. /package/src/date-time/{date → date-picker}/test/use-lilius.ts +0 -0
  170. /package/src/date-time/{date → date-picker}/use-lilius/index.ts +0 -0
  171. /package/src/date-time/{time → time-picker}/styles.ts +0 -0
  172. /package/src/date-time/{time → time-picker}/test/index.tsx +0 -0
  173. /package/src/date-time/{time → time-picker}/time-input/index.tsx +0 -0
  174. /package/src/date-time/{time → time-picker}/time-input/test/index.tsx +0 -0
  175. /package/src/date-time/{time → time-picker}/timezone.tsx +0 -0
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Internal dependencies
3
3
  */
4
- import type { MinutesInput } from './time/styles';
4
+ import type { MinutesInput } from './time-picker/styles';
5
5
 
6
6
  export type TimePickerProps = {
7
7
  /**
@@ -16,7 +16,7 @@
16
16
  width: 100%;
17
17
  padding: 6px;
18
18
  outline: none;
19
- cursor: pointer;
19
+ cursor: var(--wpds-cursor-control);
20
20
  white-space: nowrap;
21
21
  font-weight: $font-weight-regular;
22
22
 
@@ -104,7 +104,40 @@ $transition-duration: 0.2s;
104
104
  // Disabled state:
105
105
  &.is-disabled,
106
106
  .components-disabled & {
107
- opacity: 0.3;
107
+ .components-form-toggle__track {
108
+ background-color: $components-color-gray-100;
109
+ border-color: $components-color-gray-300;
110
+
111
+ @media ( forced-colors: active ) {
112
+ border-color: GrayText;
113
+ }
114
+ }
115
+
116
+ .components-form-toggle__thumb {
117
+ background-color: $components-color-gray-400;
118
+ box-shadow: none;
119
+
120
+ @media ( forced-colors: active ) {
121
+ border-color: GrayText;
122
+ }
123
+ }
124
+
125
+ &.is-checked .components-form-toggle__track {
126
+ background-color: $components-color-gray-400;
127
+ border-color: $components-color-gray-400;
128
+
129
+ @media ( forced-colors: active ) {
130
+ border-color: GrayText;
131
+
132
+ &::after {
133
+ border-top-color: GrayText;
134
+ }
135
+ }
136
+ }
137
+
138
+ &.is-checked .components-form-toggle__thumb {
139
+ background-color: $white;
140
+ }
108
141
  }
109
142
  }
110
143
 
@@ -132,6 +165,6 @@ $transition-duration: 0.2s;
132
165
  }
133
166
 
134
167
  &:not(:disabled, [aria-disabled="true"]) {
135
- cursor: pointer;
168
+ cursor: var(--wpds-cursor-control);
136
169
  }
137
170
  }
@@ -11,8 +11,9 @@
11
11
  cursor: text;
12
12
 
13
13
  &.is-disabled {
14
- background: $gray-300;
15
- border-color: $gray-300;
14
+ background: $components-color-gray-100;
15
+ border-color: $components-color-gray-400;
16
+ cursor: default;
16
17
  }
17
18
 
18
19
  &.is-active {
@@ -82,6 +83,14 @@
82
83
  }
83
84
  }
84
85
 
86
+ &.is-disabled {
87
+ .components-form-token-field__token-text,
88
+ .components-form-token-field__remove-token.components-button {
89
+ background: $components-color-gray-100;
90
+ color: $components-color-gray-600;
91
+ }
92
+ }
93
+
85
94
  &.is-borderless {
86
95
  position: relative;
87
96
  padding: 0 24px 0 0;
@@ -198,6 +207,6 @@
198
207
  }
199
208
 
200
209
  &:not(.is-empty) {
201
- cursor: pointer;
210
+ cursor: var(--wpds-cursor-control);
202
211
  }
203
212
  }
@@ -121,6 +121,7 @@ function UnforwardedModal(
121
121
  const onRequestCloseRef =
122
122
  useRef< ModalProps[ 'onRequestClose' ] >( undefined );
123
123
  useEffect( () => {
124
+ // eslint-disable-next-line react-compiler/react-compiler -- false positive, see https://github.com/facebook/react/issues/29196
124
125
  onRequestCloseRef.current = onRequestClose;
125
126
  }, [ onRequestClose ] );
126
127
 
@@ -299,6 +299,7 @@ function PaletteEditListView< T extends PaletteElement >( {
299
299
  // When unmounting the component if there are empty elements (the user did not complete the insertion) clean them.
300
300
  const elementsReferenceRef = useRef< T[] >( undefined );
301
301
  useEffect( () => {
302
+ // eslint-disable-next-line react-compiler/react-compiler -- false positive, see https://github.com/facebook/react/issues/29196
302
303
  elementsReferenceRef.current = elements;
303
304
  }, [ elements ] );
304
305
 
@@ -67,6 +67,7 @@ export function RadioControl(
67
67
  onChange,
68
68
  onClick,
69
69
  hideLabelFromVision,
70
+ disabled,
70
71
  options = [],
71
72
  id: preferredId,
72
73
  ...additionalProps
@@ -89,6 +90,7 @@ export function RadioControl(
89
90
  id={ id }
90
91
  role="radiogroup"
91
92
  className={ clsx( className, 'components-radio-control' ) }
93
+ disabled={ disabled }
92
94
  aria-describedby={ !! help ? generateHelpId( id ) : undefined }
93
95
  >
94
96
  { hideLabelFromVision ? (
@@ -10,6 +10,7 @@
10
10
 
11
11
  font-family: $default-font;
12
12
  font-size: $default-font-size;
13
+
13
14
  }
14
15
 
15
16
  .components-radio-control__group-wrapper.has-help {
@@ -34,7 +35,10 @@
34
35
  margin: 0;
35
36
  padding: 0;
36
37
  appearance: none;
37
- cursor: pointer;
38
+
39
+ &:not(:disabled) {
40
+ cursor: var(--wpds-cursor-control);
41
+ }
38
42
 
39
43
  &:focus {
40
44
  @include button-style-outset__focus(var(--wp-admin-theme-color));
@@ -49,13 +53,28 @@
49
53
  border-radius: $radius-round;
50
54
  }
51
55
  }
56
+
57
+ &:disabled {
58
+ background: $components-color-gray-100;
59
+ border: 1px solid $components-color-gray-300;
60
+ opacity: 1; // Override style from wp-admin forms.css.
61
+
62
+ &:checked::before {
63
+ border-color: $components-color-gray-400;
64
+ opacity: 1; // Override style from wp-admin forms.css.
65
+
66
+ }
67
+ }
52
68
  }
53
69
 
54
70
  .components-radio-control__label {
55
71
  grid-column: 2;
56
72
  grid-row: 1;
57
73
 
58
- cursor: pointer;
74
+ .components-radio-control:not(:disabled) & {
75
+ cursor: var(--wpds-cursor-control);
76
+ }
77
+
59
78
  line-height: $radio-input-size-sm;
60
79
 
61
80
  @include break-small() {
@@ -82,6 +82,16 @@ describe.each( [
82
82
  ).toBeVisible();
83
83
  } );
84
84
 
85
+ it( 'should disable the radio group when `disabled` is true', () => {
86
+ render(
87
+ <Component { ...defaultProps } disabled onChange={ () => {} } />
88
+ );
89
+
90
+ expect(
91
+ screen.getByRole( 'radiogroup', { name: defaultProps.label } )
92
+ ).toBeDisabled();
93
+ } );
94
+
85
95
  it( 'should describe the radio group with the help text', () => {
86
96
  const onChangeSpy = jest.fn();
87
97
  render(
@@ -7,6 +7,12 @@ export type RadioControlProps = Pick<
7
7
  BaseControlProps,
8
8
  'label' | 'help' | 'hideLabelFromVision'
9
9
  > & {
10
+ /**
11
+ * Whether the radio group should be disabled.
12
+ *
13
+ * @default false
14
+ */
15
+ disabled?: boolean;
10
16
  /**
11
17
  * A function that receives the value of the new option that is being
12
18
  * selected as input.
@@ -6,6 +6,7 @@ import {
6
6
  useRef,
7
7
  useState,
8
8
  useEffect,
9
+ useMemo,
9
10
  } from '@wordpress/element';
10
11
  import { useFocusableIframe, useMergeRefs } from '@wordpress/compose';
11
12
 
@@ -14,6 +15,8 @@ import { useFocusableIframe, useMergeRefs } from '@wordpress/compose';
14
15
  */
15
16
  import type { SandBoxProps } from './types';
16
17
 
18
+ type SandBoxContentProps = Omit< SandBoxProps, 'allowSameOrigin' >;
19
+
17
20
  const observeAndResizeJS = function () {
18
21
  const { MutationObserver } = window;
19
22
 
@@ -115,17 +118,176 @@ const style = `
115
118
  `;
116
119
 
117
120
  /**
118
- * This component provides an isolated environment for arbitrary HTML via iframes.
121
+ * Builds the full HTML document string for the sandbox iframe content.
122
+ */
123
+ function buildSandBoxDocument( {
124
+ html,
125
+ title,
126
+ type,
127
+ styles,
128
+ scripts,
129
+ }: {
130
+ html: string;
131
+ title: string;
132
+ type?: string;
133
+ styles: string[];
134
+ scripts: string[];
135
+ } ): string {
136
+ const htmlDoc = (
137
+ <html lang={ document.documentElement.lang } className={ type }>
138
+ <head>
139
+ <title>{ title }</title>
140
+ <style dangerouslySetInnerHTML={ { __html: style } } />
141
+ { styles.map( ( rules, i ) => (
142
+ <style
143
+ key={ i }
144
+ dangerouslySetInnerHTML={ { __html: rules } }
145
+ />
146
+ ) ) }
147
+ </head>
148
+ <body
149
+ data-resizable-iframe-connected="data-resizable-iframe-connected"
150
+ className={ type }
151
+ >
152
+ <div dangerouslySetInnerHTML={ { __html: html } } />
153
+ <script
154
+ type="text/javascript"
155
+ dangerouslySetInnerHTML={ {
156
+ __html: `(${ observeAndResizeJS.toString() })();`,
157
+ } }
158
+ />
159
+ { scripts.map( ( src ) => (
160
+ <script key={ src } src={ src } />
161
+ ) ) }
162
+ </body>
163
+ </html>
164
+ );
165
+
166
+ return '<!DOCTYPE html>' + renderToString( htmlDoc );
167
+ }
168
+
169
+ /**
170
+ * Isolated sandbox that uses the `srcdoc` attribute to render content
171
+ * without `allow-same-origin`. This is the default for user-controlled
172
+ * content (e.g., the HTML block) where same-origin access would be a
173
+ * security risk.
119
174
  *
120
- * ```jsx
121
- * import { SandBox } from '@wordpress/components';
175
+ * Because `srcdoc` is a declarative attribute, the browser automatically
176
+ * re-renders the content when the iframe is moved in the DOM (e.g.,
177
+ * block reordering), so no `load` event listener is needed.
178
+ * The `message` listener is re-synced on every `load` so
179
+ * it follows the iframe if it's reparented into a different document.
180
+ */
181
+ function IsolatedSandBox( {
182
+ html = '',
183
+ title = '',
184
+ type,
185
+ styles = [],
186
+ scripts = [],
187
+ onFocus,
188
+ tabIndex,
189
+ }: SandBoxContentProps ) {
190
+ const ref = useRef< HTMLIFrameElement >( null );
191
+ const [ width, setWidth ] = useState( 0 );
192
+ const [ height, setHeight ] = useState( 0 );
193
+
194
+ const srcDoc = useMemo(
195
+ () => buildSandBoxDocument( { html, title, type, styles, scripts } ),
196
+ [ html, title, type, styles, scripts ]
197
+ );
198
+
199
+ useEffect( () => {
200
+ const iframe = ref.current;
201
+ if ( ! iframe ) {
202
+ return;
203
+ }
204
+
205
+ function checkMessageForResize( event: MessageEvent ) {
206
+ // Verify that the mounted element is the source of the message.
207
+ // iframe.contentWindow is accessible cross-origin as a
208
+ // WindowProxy reference, so this check still works without
209
+ // allow-same-origin.
210
+ if ( ! iframe || iframe.contentWindow !== event.source ) {
211
+ return;
212
+ }
213
+
214
+ // Attempt to parse the message data as JSON if passed as string.
215
+ let data = event.data || {};
216
+
217
+ if ( 'string' === typeof data ) {
218
+ try {
219
+ data = JSON.parse( data );
220
+ } catch {}
221
+ }
222
+
223
+ // Update the state only if the message is formatted as we expect,
224
+ // i.e. as an object with a 'resize' action.
225
+ if ( 'resize' !== data.action ) {
226
+ return;
227
+ }
228
+
229
+ setWidth( data.width );
230
+ setHeight( data.height );
231
+ }
232
+
233
+ let currentView: Window | null = null;
234
+ function syncListener() {
235
+ const view = iframe?.ownerDocument?.defaultView ?? null;
236
+ if ( view === currentView ) {
237
+ return;
238
+ }
239
+
240
+ currentView?.removeEventListener(
241
+ 'message',
242
+ checkMessageForResize
243
+ );
244
+
245
+ currentView = view;
246
+ currentView?.addEventListener( 'message', checkMessageForResize );
247
+ }
248
+
249
+ syncListener();
250
+ iframe.addEventListener( 'load', syncListener );
251
+
252
+ return () => {
253
+ iframe.removeEventListener( 'load', syncListener );
254
+ currentView?.removeEventListener(
255
+ 'message',
256
+ checkMessageForResize
257
+ );
258
+ };
259
+ }, [] );
260
+
261
+ return (
262
+ <iframe
263
+ ref={ useMergeRefs( [ ref, useFocusableIframe() ] ) }
264
+ title={ title }
265
+ tabIndex={ tabIndex }
266
+ className="components-sandbox"
267
+ sandbox="allow-scripts allow-presentation"
268
+ srcDoc={ srcDoc }
269
+ onFocus={ onFocus }
270
+ width={ Math.ceil( width ) }
271
+ height={ Math.ceil( height ) }
272
+ />
273
+ );
274
+ }
275
+
276
+ /**
277
+ * Same-origin sandbox that writes to `contentDocument` directly. This
278
+ * preserves the parent page's URL as the iframe's document URL, which
279
+ * provides a valid Referer header for nested iframes (required by
280
+ * providers like YouTube).
122
281
  *
123
- * const MySandBox = () => (
124
- * <SandBox html="<p>Content</p>" title="SandBox" type="embed" />
125
- * );
126
- * ```
282
+ * Only used when `allowSameOrigin` is true — i.e., for server-fetched
283
+ * oEmbed previews that are not directly user-controlled.
284
+ *
285
+ * This implementation is intentionally kept close to the original
286
+ * pre-refactor code to preserve past bugfixes:
287
+ * - load listener for iframe re-initialization after DOM moves (#21916)
288
+ * - forceRerender guard to avoid unnecessary full rewrites (#20176)
127
289
  */
128
- function SandBox( {
290
+ function SameOriginSandBox( {
129
291
  html = '',
130
292
  title = '',
131
293
  type,
@@ -133,7 +295,7 @@ function SandBox( {
133
295
  scripts = [],
134
296
  onFocus,
135
297
  tabIndex,
136
- }: SandBoxProps ) {
298
+ }: SandBoxContentProps ) {
137
299
  const ref = useRef< HTMLIFrameElement >( null );
138
300
  const [ width, setWidth ] = useState( 0 );
139
301
  const [ height, setHeight ] = useState( 0 );
@@ -292,4 +454,22 @@ function SandBox( {
292
454
  );
293
455
  }
294
456
 
457
+ /**
458
+ * This component provides an isolated environment for arbitrary HTML via iframes.
459
+ *
460
+ * ```jsx
461
+ * import { SandBox } from '@wordpress/components';
462
+ *
463
+ * const MySandBox = () => (
464
+ * <SandBox html="<p>Content</p>" title="SandBox" type="embed" />
465
+ * );
466
+ * ```
467
+ */
468
+ function SandBox( { allowSameOrigin = false, ...contentProps }: SandBoxProps ) {
469
+ if ( allowSameOrigin ) {
470
+ return <SameOriginSandBox { ...contentProps } />;
471
+ }
472
+ return <IsolatedSandBox { ...contentProps } />;
473
+ }
474
+
295
475
  export default SandBox;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { fireEvent, render, screen, within } from '@testing-library/react';
4
+ import { fireEvent, render, screen } from '@testing-library/react';
5
5
 
6
6
  /**
7
7
  * WordPress dependencies
@@ -16,10 +16,7 @@ import SandBox from '..';
16
16
  describe( 'SandBox', () => {
17
17
  const TestWrapper = () => {
18
18
  const [ html, setHtml ] = useState(
19
- // MutationObserver implementation from JSDom does not work as intended
20
- // with iframes so we need to ignore it for the time being.
21
- '<script type="text/javascript">window.MutationObserver = null;</script>' +
22
- '<iframe title="Mock Iframe" src="https://super.embed"></iframe>'
19
+ '<iframe title="Mock Iframe" src="https://super.embed"></iframe>'
23
20
  );
24
21
 
25
22
  const updateHtml = () => {
@@ -38,34 +35,78 @@ describe( 'SandBox', () => {
38
35
  );
39
36
  };
40
37
 
41
- it( 'should rerender with new emdeded content if html prop changes', () => {
38
+ it( 'should not include allow-same-origin by default', () => {
39
+ render( <SandBox html="<p>Hello</p>" title="Test" /> );
40
+
41
+ const iframe = screen.getByTitle< HTMLIFrameElement >( 'Test' );
42
+
43
+ expect( iframe ).toHaveAttribute(
44
+ 'sandbox',
45
+ 'allow-scripts allow-presentation'
46
+ );
47
+ expect( iframe.getAttribute( 'sandbox' ) ).not.toContain(
48
+ 'allow-same-origin'
49
+ );
50
+ } );
51
+
52
+ it( 'should set srcdoc with the provided html content', () => {
53
+ render( <SandBox html="<p>Hello</p>" title="Test Title" /> );
54
+
55
+ const iframe = screen.getByTitle< HTMLIFrameElement >( 'Test Title' );
56
+ const srcDoc = iframe.getAttribute( 'srcdoc' ) ?? '';
57
+
58
+ expect( srcDoc ).toContain( '<p>Hello</p>' );
59
+ expect( srcDoc ).toContain( '<title>Test Title</title>' );
60
+ } );
61
+
62
+ it( 'should include custom styles in srcdoc', () => {
63
+ render(
64
+ <SandBox
65
+ html="<p>Styled</p>"
66
+ title="Styled Test"
67
+ styles={ [ '.custom { color: red; }' ] }
68
+ />
69
+ );
70
+
71
+ const iframe = screen.getByTitle< HTMLIFrameElement >( 'Styled Test' );
72
+ const srcDoc = iframe.getAttribute( 'srcdoc' ) ?? '';
73
+
74
+ expect( srcDoc ).toContain( '.custom { color: red; }' );
75
+ } );
76
+
77
+ it( 'should include script tags in srcdoc', () => {
78
+ render(
79
+ <SandBox
80
+ html="<p>Script</p>"
81
+ title="Script Test"
82
+ scripts={ [ 'https://example.com/embed.js' ] }
83
+ />
84
+ );
85
+
86
+ const iframe = screen.getByTitle< HTMLIFrameElement >( 'Script Test' );
87
+ const srcDoc = iframe.getAttribute( 'srcdoc' ) ?? '';
88
+
89
+ expect( srcDoc ).toContain(
90
+ '<script src="https://example.com/embed.js">'
91
+ );
92
+ } );
93
+
94
+ it( 'should update srcdoc when html prop changes', () => {
42
95
  render( <TestWrapper /> );
43
96
 
44
97
  const iframe =
45
98
  screen.getByTitle< HTMLIFrameElement >( 'SandBox Title' );
46
99
 
47
- if ( ! iframe.contentWindow ) {
48
- throw new Error();
49
- }
50
-
51
- let sandboxedIframe = within(
52
- iframe.contentWindow.document.body
53
- ).getByTitle( 'Mock Iframe' );
54
-
55
- expect( sandboxedIframe ).toHaveAttribute(
56
- 'src',
57
- 'https://super.embed'
100
+ expect( iframe ).toHaveAttribute(
101
+ 'srcdoc',
102
+ expect.stringContaining( 'https://super.embed' )
58
103
  );
59
104
 
60
105
  fireEvent.click( screen.getByRole( 'button' ) );
61
106
 
62
- sandboxedIframe = within(
63
- iframe.contentWindow.document.body
64
- ).getByTitle( 'Mock Iframe' );
65
-
66
- expect( sandboxedIframe ).toHaveAttribute(
67
- 'src',
68
- 'https://another.super.embed'
107
+ expect( iframe ).toHaveAttribute(
108
+ 'srcdoc',
109
+ expect.stringContaining( 'https://another.super.embed' )
69
110
  );
70
111
  } );
71
112
  } );
@@ -1,4 +1,15 @@
1
1
  export type SandBoxProps = {
2
+ /**
3
+ * Whether to include `allow-same-origin` in the iframe's sandbox
4
+ * attribute. When true, nested iframes (such as third-party embeds)
5
+ * can access their own origin's cookies and storage.
6
+ *
7
+ * Only enable this for content that is NOT directly user-controlled,
8
+ * such as server-fetched oEmbed previews.
9
+ *
10
+ * @default false
11
+ */
12
+ allowSameOrigin?: boolean;
2
13
  /**
3
14
  * The HTML to render in the body of the iframe document.
4
15
  *
@@ -16,7 +16,7 @@
16
16
  width: 100%;
17
17
  max-width: var(--wpds-dimension-surface-width-lg);
18
18
  box-sizing: border-box;
19
- cursor: pointer;
19
+ cursor: var(--wpds-cursor-control);
20
20
 
21
21
  // Re-enable pointer events, which are disabled by the
22
22
  // .components-snackbar-list styles.
@@ -47,7 +47,7 @@
47
47
 
48
48
  .components-snackbar__dismiss-button {
49
49
  margin-left: $grid-unit-30;
50
- cursor: pointer;
50
+ cursor: var(--wpds-cursor-control);
51
51
  }
52
52
  }
53
53
 
@@ -18,7 +18,7 @@
18
18
  background: transparent;
19
19
  border: none;
20
20
  box-shadow: none;
21
- cursor: pointer;
21
+ cursor: var(--wpds-cursor-control);
22
22
  padding: 3px $grid-unit-20; // Use padding to offset the is-active border, this benefits Windows High Contrast mode
23
23
  margin-left: 0;
24
24
  font-weight: $font-weight-regular;
@@ -21,6 +21,9 @@ const meta: Meta< typeof TextareaControl > = {
21
21
  onChange: { action: 'onChange' },
22
22
  label: { control: { type: 'text' } },
23
23
  help: { control: { type: 'text' } },
24
+ disabled: {
25
+ control: { type: 'boolean' },
26
+ },
24
27
  value: { control: false },
25
28
  },
26
29
  parameters: {
@@ -62,6 +62,12 @@ export const StyledTextarea = styled.textarea`
62
62
  ${ inputStyleFocus }
63
63
  }
64
64
 
65
+ &:disabled {
66
+ background: ${ COLORS.ui.backgroundDisabled };
67
+ border-color: ${ COLORS.ui.borderDisabled };
68
+ color: ${ COLORS.ui.textDisabled };
69
+ }
70
+
65
71
  // Use opacity to work in various editor styles.
66
72
  &::-webkit-input-placeholder {
67
73
  color: ${ COLORS.ui.darkGrayPlaceholder };
@@ -5,7 +5,7 @@
5
5
  line-height: form-toggle.$toggle-height;
6
6
 
7
7
  &:not(.is-disabled) {
8
- cursor: pointer;
8
+ cursor: var(--wpds-cursor-control);
9
9
  }
10
10
  }
11
11
 
@@ -22,10 +22,16 @@ describe( 'ToggleControl', () => {
22
22
 
23
23
  render( <ToggleControl label="My toggle" onChange={ onChange } /> );
24
24
 
25
- screen.getByRole( 'checkbox' ).click();
25
+ screen
26
+ .getByRole( 'checkbox' )
27
+ // eslint-disable-next-line testing-library/no-node-access
28
+ .click();
26
29
  expect( onChange ).toHaveBeenLastCalledWith( true );
27
30
 
28
- screen.getByRole( 'checkbox' ).click();
31
+ screen
32
+ .getByRole( 'checkbox' )
33
+ // eslint-disable-next-line testing-library/no-node-access
34
+ .click();
29
35
  expect( onChange ).toHaveBeenLastCalledWith( false );
30
36
  } );
31
37