@wordpress/components 29.7.0 → 29.9.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.
- package/.stylelintrc.js +1 -1
- package/CHANGELOG.md +37 -0
- package/build/guide/index.js +4 -2
- package/build/guide/index.js.map +1 -1
- package/build/guide/types.js.map +1 -1
- package/build/item-group/styles.js +10 -10
- package/build/item-group/styles.js.map +1 -1
- package/build/popover/index.js +38 -18
- package/build/popover/index.js.map +1 -1
- package/build/popover/types.js.map +1 -1
- package/build/toggle-group-control/toggle-group-control/component.js +1 -1
- package/build/toggle-group-control/toggle-group-control/component.js.map +1 -1
- package/build/unit-control/utils.js +9 -8
- package/build/unit-control/utils.js.map +1 -1
- package/build-module/guide/index.js +4 -2
- package/build-module/guide/index.js.map +1 -1
- package/build-module/guide/types.js.map +1 -1
- package/build-module/item-group/styles.js +10 -10
- package/build-module/item-group/styles.js.map +1 -1
- package/build-module/popover/index.js +37 -17
- package/build-module/popover/index.js.map +1 -1
- package/build-module/popover/types.js.map +1 -1
- package/build-module/toggle-group-control/toggle-group-control/component.js +1 -1
- package/build-module/toggle-group-control/toggle-group-control/component.js.map +1 -1
- package/build-module/unit-control/utils.js +9 -8
- package/build-module/unit-control/utils.js.map +1 -1
- package/build-style/style-rtl.css +17 -12
- package/build-style/style.css +17 -12
- package/build-types/guide/index.d.ts +1 -1
- package/build-types/guide/index.d.ts.map +1 -1
- package/build-types/guide/stories/index.story.d.ts.map +1 -1
- package/build-types/guide/types.d.ts +12 -0
- package/build-types/guide/types.d.ts.map +1 -1
- package/build-types/popover/index.d.ts +18 -2
- package/build-types/popover/index.d.ts.map +1 -1
- package/build-types/popover/stories/e2e/index.story.d.ts +8 -1
- package/build-types/popover/stories/e2e/index.story.d.ts.map +1 -1
- package/build-types/popover/stories/index.story.d.ts +1 -0
- package/build-types/popover/stories/index.story.d.ts.map +1 -1
- package/build-types/popover/test/utils/index.d.ts.map +1 -1
- package/build-types/popover/types.d.ts +9 -0
- package/build-types/popover/types.d.ts.map +1 -1
- package/build-types/unit-control/utils.d.ts.map +1 -1
- package/package.json +20 -20
- package/src/button/style.scss +1 -1
- package/src/color-picker/test/index.tsx +103 -26
- package/src/combobox-control/style.scss +4 -2
- package/src/duotone-picker/style.scss +4 -0
- package/src/form-token-field/style.scss +5 -5
- package/src/guide/README.md +16 -0
- package/src/guide/index.tsx +4 -2
- package/src/guide/stories/index.story.tsx +2 -0
- package/src/guide/types.ts +12 -0
- package/src/item-group/styles.ts +1 -1
- package/src/item-group/test/__snapshots__/index.js.snap +1 -1
- package/src/popover/index.tsx +46 -21
- package/src/popover/stories/index.story.tsx +83 -2
- package/src/popover/test/utils/index.tsx +0 -1
- package/src/popover/types.ts +10 -0
- package/src/toggle-group-control/toggle-group-control/component.tsx +1 -1
- package/src/unit-control/test/utils.ts +36 -0
- package/src/unit-control/utils.ts +8 -11
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wordpress/components",
|
|
3
|
-
"version": "29.
|
|
3
|
+
"version": "29.9.0",
|
|
4
4
|
"description": "UI components for WordPress.",
|
|
5
5
|
"author": "The WordPress Contributors",
|
|
6
6
|
"license": "GPL-2.0-or-later",
|
|
@@ -44,23 +44,23 @@
|
|
|
44
44
|
"@types/gradient-parser": "0.1.3",
|
|
45
45
|
"@types/highlight-words-core": "1.2.1",
|
|
46
46
|
"@use-gesture/react": "^10.3.1",
|
|
47
|
-
"@wordpress/a11y": "^4.
|
|
48
|
-
"@wordpress/compose": "^7.
|
|
49
|
-
"@wordpress/date": "^5.
|
|
50
|
-
"@wordpress/deprecated": "^4.
|
|
51
|
-
"@wordpress/dom": "^4.
|
|
52
|
-
"@wordpress/element": "^6.
|
|
53
|
-
"@wordpress/escape-html": "^3.
|
|
54
|
-
"@wordpress/hooks": "^4.
|
|
55
|
-
"@wordpress/html-entities": "^4.
|
|
56
|
-
"@wordpress/i18n": "^5.
|
|
57
|
-
"@wordpress/icons": "^10.
|
|
58
|
-
"@wordpress/is-shallow-equal": "^5.
|
|
59
|
-
"@wordpress/keycodes": "^4.
|
|
60
|
-
"@wordpress/primitives": "^4.
|
|
61
|
-
"@wordpress/private-apis": "^1.
|
|
62
|
-
"@wordpress/rich-text": "^7.
|
|
63
|
-
"@wordpress/warning": "^3.
|
|
47
|
+
"@wordpress/a11y": "^4.23.0",
|
|
48
|
+
"@wordpress/compose": "^7.23.0",
|
|
49
|
+
"@wordpress/date": "^5.23.0",
|
|
50
|
+
"@wordpress/deprecated": "^4.23.0",
|
|
51
|
+
"@wordpress/dom": "^4.23.0",
|
|
52
|
+
"@wordpress/element": "^6.23.0",
|
|
53
|
+
"@wordpress/escape-html": "^3.23.0",
|
|
54
|
+
"@wordpress/hooks": "^4.23.0",
|
|
55
|
+
"@wordpress/html-entities": "^4.23.0",
|
|
56
|
+
"@wordpress/i18n": "^5.23.0",
|
|
57
|
+
"@wordpress/icons": "^10.23.0",
|
|
58
|
+
"@wordpress/is-shallow-equal": "^5.23.0",
|
|
59
|
+
"@wordpress/keycodes": "^4.23.0",
|
|
60
|
+
"@wordpress/primitives": "^4.23.0",
|
|
61
|
+
"@wordpress/private-apis": "^1.23.0",
|
|
62
|
+
"@wordpress/rich-text": "^7.23.0",
|
|
63
|
+
"@wordpress/warning": "^3.23.0",
|
|
64
64
|
"change-case": "^4.1.2",
|
|
65
65
|
"clsx": "^2.1.1",
|
|
66
66
|
"colord": "^2.7.0",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"deepmerge": "^4.3.0",
|
|
69
69
|
"fast-deep-equal": "^3.1.3",
|
|
70
70
|
"framer-motion": "^11.1.9",
|
|
71
|
-
"gradient-parser": "
|
|
71
|
+
"gradient-parser": "1.0.2",
|
|
72
72
|
"highlight-words-core": "^1.2.2",
|
|
73
73
|
"is-plain-object": "^5.0.0",
|
|
74
74
|
"memize": "^2.1.0",
|
|
@@ -85,5 +85,5 @@
|
|
|
85
85
|
"publishConfig": {
|
|
86
86
|
"access": "public"
|
|
87
87
|
},
|
|
88
|
-
"gitHead": "
|
|
88
|
+
"gitHead": "ab5c79cd40adbb68898536c50e035b0a734338ea"
|
|
89
89
|
}
|
package/src/button/style.scss
CHANGED
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
color: $components-color-accent;
|
|
168
168
|
background: transparent;
|
|
169
169
|
|
|
170
|
-
&:hover:not(:disabled, [aria-disabled="true"]) {
|
|
170
|
+
&:hover:not(:disabled, [aria-disabled="true"], .is-pressed) {
|
|
171
171
|
background: color-mix(in srgb, $components-color-accent 4%, transparent);
|
|
172
172
|
color: $components-color-accent-darker-20;
|
|
173
173
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { fireEvent, screen, render, waitFor } from '@testing-library/react';
|
|
5
5
|
import userEvent from '@testing-library/user-event';
|
|
6
|
+
import { click } from '@ariakit/test';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* WordPress dependencies
|
|
@@ -13,7 +14,6 @@ import { useState } from '@wordpress/element';
|
|
|
13
14
|
* Internal dependencies
|
|
14
15
|
*/
|
|
15
16
|
import { ColorPicker } from '..';
|
|
16
|
-
import { click } from '@ariakit/test';
|
|
17
17
|
|
|
18
18
|
const hslaMatcher = expect.objectContaining( {
|
|
19
19
|
h: expect.any( Number ),
|
|
@@ -41,6 +41,34 @@ const legacyColorMatcher = {
|
|
|
41
41
|
source: 'hex',
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
// Without the controlled component, slider values don't update after changes.
|
|
45
|
+
// Controlled component with state helps synchronize input box and slider during testing.
|
|
46
|
+
const ControlledColorPicker = ( {
|
|
47
|
+
onChange: onChangeProp,
|
|
48
|
+
initialColor = '#000000',
|
|
49
|
+
...restProps
|
|
50
|
+
}: React.ComponentProps< typeof ColorPicker > & { initialColor?: string } ) => {
|
|
51
|
+
const [ colorState, setColorState ] = useState( initialColor );
|
|
52
|
+
|
|
53
|
+
const internalOnChange: typeof onChangeProp = ( newColor ) => {
|
|
54
|
+
onChangeProp?.( newColor );
|
|
55
|
+
setColorState( newColor );
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
<ColorPicker
|
|
61
|
+
{ ...restProps }
|
|
62
|
+
onChange={ internalOnChange }
|
|
63
|
+
color={ colorState }
|
|
64
|
+
/>
|
|
65
|
+
<button onClick={ () => setColorState( '#4d87ba' ) }>
|
|
66
|
+
Set color to #4d87ba
|
|
67
|
+
</button>
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
44
72
|
describe( 'ColorPicker', () => {
|
|
45
73
|
describe( 'legacy props', () => {
|
|
46
74
|
it( 'should fire onChangeComplete with the legacy color format', async () => {
|
|
@@ -144,31 +172,6 @@ describe( 'ColorPicker', () => {
|
|
|
144
172
|
const user = userEvent.setup();
|
|
145
173
|
const onChange = jest.fn();
|
|
146
174
|
|
|
147
|
-
const ControlledColorPicker = ( {
|
|
148
|
-
onChange: onChangeProp,
|
|
149
|
-
...restProps
|
|
150
|
-
}: React.ComponentProps< typeof ColorPicker > ) => {
|
|
151
|
-
const [ colorState, setColorState ] = useState( '#000000' );
|
|
152
|
-
|
|
153
|
-
const internalOnChange: typeof onChangeProp = ( newColor ) => {
|
|
154
|
-
onChangeProp?.( newColor );
|
|
155
|
-
setColorState( newColor );
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
return (
|
|
159
|
-
<>
|
|
160
|
-
<ColorPicker
|
|
161
|
-
{ ...restProps }
|
|
162
|
-
onChange={ internalOnChange }
|
|
163
|
-
color={ colorState }
|
|
164
|
-
/>
|
|
165
|
-
<button onClick={ () => setColorState( '#4d87ba' ) }>
|
|
166
|
-
Set color to #4d87ba
|
|
167
|
-
</button>
|
|
168
|
-
</>
|
|
169
|
-
);
|
|
170
|
-
};
|
|
171
|
-
|
|
172
175
|
render(
|
|
173
176
|
<ControlledColorPicker
|
|
174
177
|
onChange={ onChange }
|
|
@@ -342,4 +345,78 @@ describe( 'ColorPicker', () => {
|
|
|
342
345
|
} );
|
|
343
346
|
} );
|
|
344
347
|
} );
|
|
348
|
+
|
|
349
|
+
describe.each( [
|
|
350
|
+
[ 'hsl', 'HSL' ],
|
|
351
|
+
[ 'rgb', 'RGB' ],
|
|
352
|
+
] )( 'Alpha-enabled %s format', ( format, formatLabel ) => {
|
|
353
|
+
it( `should update alpha correctly when ${ formatLabel } format is selected`, async () => {
|
|
354
|
+
const user = userEvent.setup();
|
|
355
|
+
const onChange = jest.fn();
|
|
356
|
+
|
|
357
|
+
render(
|
|
358
|
+
<ControlledColorPicker
|
|
359
|
+
onChange={ onChange }
|
|
360
|
+
enableAlpha
|
|
361
|
+
initialColor="#ffffff80"
|
|
362
|
+
/>
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const formatSelector = screen.getByRole( 'combobox' );
|
|
366
|
+
expect( formatSelector ).toBeVisible();
|
|
367
|
+
await user.selectOptions( formatSelector, format );
|
|
368
|
+
|
|
369
|
+
const alphaInput = screen.getByRole( 'spinbutton', {
|
|
370
|
+
name: 'Alpha',
|
|
371
|
+
} );
|
|
372
|
+
expect( alphaInput ).toBeVisible();
|
|
373
|
+
|
|
374
|
+
const alphaSliders = screen.getAllByRole( 'slider', {
|
|
375
|
+
name: 'Alpha',
|
|
376
|
+
} );
|
|
377
|
+
|
|
378
|
+
expect( alphaSliders ).toHaveLength( 2 );
|
|
379
|
+
|
|
380
|
+
// Choose the second slider which is the actual slider of type: input[type="range"]
|
|
381
|
+
const alphaSlider = alphaSliders.at( -1 )!;
|
|
382
|
+
|
|
383
|
+
expect( alphaSlider ).toHaveValue( '50' );
|
|
384
|
+
expect( alphaInput ).toHaveValue( 50 );
|
|
385
|
+
|
|
386
|
+
expect( onChange ).not.toHaveBeenCalled();
|
|
387
|
+
|
|
388
|
+
// Test pattern 1: Update the slider
|
|
389
|
+
fireEvent.change( alphaSlider, {
|
|
390
|
+
target: { value: 75 },
|
|
391
|
+
} );
|
|
392
|
+
|
|
393
|
+
await waitFor( () => {
|
|
394
|
+
expect( onChange ).toHaveBeenCalledTimes( 1 );
|
|
395
|
+
} );
|
|
396
|
+
|
|
397
|
+
expect( onChange ).toHaveBeenLastCalledWith( '#ffffffbf' );
|
|
398
|
+
expect( alphaInput ).toHaveValue( 75 );
|
|
399
|
+
expect( alphaSlider ).toHaveValue( '75' );
|
|
400
|
+
|
|
401
|
+
onChange.mockClear();
|
|
402
|
+
|
|
403
|
+
// Test pattern 2: Update the alphaInput
|
|
404
|
+
await user.clear( alphaInput );
|
|
405
|
+
expect( onChange ).toHaveBeenCalledTimes( 1 );
|
|
406
|
+
|
|
407
|
+
// Initially type 7 in the alpha input, we expect it to be called with #ffffff12
|
|
408
|
+
await user.keyboard( '7' );
|
|
409
|
+
|
|
410
|
+
// Now with 75% opacity we expect it to be called with #ffffffbf
|
|
411
|
+
await user.keyboard( '5' );
|
|
412
|
+
|
|
413
|
+
// Called twice, once per key stroke (`7` and `5`)
|
|
414
|
+
expect( onChange ).toHaveBeenCalledTimes( 3 );
|
|
415
|
+
expect( onChange ).toHaveBeenNthCalledWith( 2, '#ffffff12' );
|
|
416
|
+
expect( onChange ).toHaveBeenNthCalledWith( 3, '#ffffffbf' );
|
|
417
|
+
|
|
418
|
+
expect( alphaSlider ).toHaveValue( '75' );
|
|
419
|
+
expect( alphaInput ).toHaveValue( 75 );
|
|
420
|
+
} );
|
|
421
|
+
} );
|
|
345
422
|
} );
|
|
@@ -12,6 +12,8 @@ input.components-combobox-control__input[type="text"] {
|
|
|
12
12
|
margin: 0;
|
|
13
13
|
line-height: inherit;
|
|
14
14
|
min-height: auto;
|
|
15
|
+
background: $components-color-background;
|
|
16
|
+
color: $components-color-foreground;
|
|
15
17
|
|
|
16
18
|
// Resolves Zooming on iOS devices
|
|
17
19
|
// https://github.com/WordPress/gutenberg/issues/27405
|
|
@@ -26,7 +28,7 @@ input.components-combobox-control__input[type="text"] {
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
.components-combobox-control__suggestions-container {
|
|
29
|
-
@include input-control;
|
|
31
|
+
@include input-control($components-color-accent);
|
|
30
32
|
display: flex;
|
|
31
33
|
flex-wrap: wrap;
|
|
32
34
|
align-items: flex-start;
|
|
@@ -34,7 +36,7 @@ input.components-combobox-control__input[type="text"] {
|
|
|
34
36
|
padding: 0;
|
|
35
37
|
|
|
36
38
|
&:focus-within {
|
|
37
|
-
@include input-style__focus();
|
|
39
|
+
@include input-style__focus($components-color-accent);
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
.components-spinner {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
.components-form-token-field__input-container {
|
|
2
|
-
@include input-control();
|
|
2
|
+
@include input-control($components-color-accent);
|
|
3
3
|
width: 100%;
|
|
4
4
|
padding: 0;
|
|
5
5
|
cursor: text;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
&.is-active {
|
|
13
|
-
@include input-style__focus();
|
|
13
|
+
@include input-style__focus($components-color-accent);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// Token input
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
min-width: 50px;
|
|
28
28
|
background: inherit;
|
|
29
29
|
border: 0;
|
|
30
|
-
color: $
|
|
30
|
+
color: $components-color-foreground;
|
|
31
31
|
box-shadow: none;
|
|
32
32
|
|
|
33
33
|
// Resolves Zooming on iOS devices
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
.components-form-token-field__suggestion {
|
|
171
|
-
color: $
|
|
171
|
+
color: $components-color-foreground;
|
|
172
172
|
display: block;
|
|
173
173
|
font-size: $default-font-size;
|
|
174
174
|
padding: $grid-unit-10 $grid-unit-15;
|
|
@@ -178,7 +178,7 @@
|
|
|
178
178
|
|
|
179
179
|
&.is-selected {
|
|
180
180
|
background: $components-color-accent;
|
|
181
|
-
color: $
|
|
181
|
+
color: $components-color-foreground-inverted;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
&[aria-disabled="true"] {
|
package/src/guide/README.md
CHANGED
|
@@ -59,6 +59,22 @@ Use this to customize the label of the _Finish_ button shown at the end of the g
|
|
|
59
59
|
- Required: No
|
|
60
60
|
- Default: `'Finish'`
|
|
61
61
|
|
|
62
|
+
### nextButtonText
|
|
63
|
+
|
|
64
|
+
Use this to customize the label of the _Next_ button shown on each page of the guide.
|
|
65
|
+
|
|
66
|
+
- Type: `string`
|
|
67
|
+
- Required: No
|
|
68
|
+
- Default: `'Next'`
|
|
69
|
+
|
|
70
|
+
### previousButtonText
|
|
71
|
+
|
|
72
|
+
Use this to customize the label of the _Previous_ button shown on each page of the guide except the first.
|
|
73
|
+
|
|
74
|
+
- Type: `string`
|
|
75
|
+
- Required: No
|
|
76
|
+
- Default: `'Previous'`
|
|
77
|
+
|
|
62
78
|
### onFinish
|
|
63
79
|
|
|
64
80
|
A function which is called when the guide is finished. The guide is finished when the modal is closed or when the user clicks _Finish_ on the last page of the guide.
|
package/src/guide/index.tsx
CHANGED
|
@@ -55,6 +55,8 @@ function Guide( {
|
|
|
55
55
|
className,
|
|
56
56
|
contentLabel,
|
|
57
57
|
finishButtonText = __( 'Finish' ),
|
|
58
|
+
nextButtonText = __( 'Next' ),
|
|
59
|
+
previousButtonText = __( 'Previous' ),
|
|
58
60
|
onFinish,
|
|
59
61
|
pages = [],
|
|
60
62
|
}: GuideProps ) {
|
|
@@ -146,7 +148,7 @@ function Guide( {
|
|
|
146
148
|
onClick={ goBack }
|
|
147
149
|
__next40pxDefaultSize
|
|
148
150
|
>
|
|
149
|
-
{
|
|
151
|
+
{ previousButtonText }
|
|
150
152
|
</Button>
|
|
151
153
|
) }
|
|
152
154
|
{ canGoForward && (
|
|
@@ -156,7 +158,7 @@ function Guide( {
|
|
|
156
158
|
onClick={ goForward }
|
|
157
159
|
__next40pxDefaultSize
|
|
158
160
|
>
|
|
159
|
-
{
|
|
161
|
+
{ nextButtonText }
|
|
160
162
|
</Button>
|
|
161
163
|
) }
|
|
162
164
|
{ ! canGoForward && (
|
|
@@ -20,6 +20,8 @@ const meta: Meta< typeof Guide > = {
|
|
|
20
20
|
argTypes: {
|
|
21
21
|
contentLabel: { control: 'text' },
|
|
22
22
|
finishButtonText: { control: 'text' },
|
|
23
|
+
nextButtonText: { control: 'text' },
|
|
24
|
+
previousButtonText: { control: 'text' },
|
|
23
25
|
onFinish: { action: 'onFinish' },
|
|
24
26
|
},
|
|
25
27
|
};
|
package/src/guide/types.ts
CHANGED
|
@@ -40,6 +40,18 @@ export type GuideProps = {
|
|
|
40
40
|
* @default 'Finish'
|
|
41
41
|
*/
|
|
42
42
|
finishButtonText?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Use this to customize the label of the _Next_ button shown on each page of the guide.
|
|
45
|
+
*
|
|
46
|
+
* @default 'Next'
|
|
47
|
+
*/
|
|
48
|
+
nextButtonText?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Use this to customize the label of the _Previous_ button shown on each page of the guide except the first.
|
|
51
|
+
*
|
|
52
|
+
* @default 'Previous'
|
|
53
|
+
*/
|
|
54
|
+
previousButtonText?: string;
|
|
43
55
|
/**
|
|
44
56
|
* A function which is called when the guide is finished.
|
|
45
57
|
*/
|
package/src/item-group/styles.ts
CHANGED
|
@@ -115,7 +115,7 @@ Snapshot Diff:
|
|
|
115
115
|
<div>
|
|
116
116
|
<div
|
|
117
117
|
- class="components-item-group css-1iattls-PolymorphicDiv-rounded e19lxcc00"
|
|
118
|
-
+ class="components-item-group css-
|
|
118
|
+
+ class="components-item-group css-1hvp0tq-PolymorphicDiv-separated-rounded e19lxcc00"
|
|
119
119
|
data-wp-c16t="true"
|
|
120
120
|
data-wp-component="ItemGroup"
|
|
121
121
|
role="list"
|
package/src/popover/index.tsx
CHANGED
|
@@ -59,6 +59,7 @@ import type {
|
|
|
59
59
|
PopoverProps,
|
|
60
60
|
PopoverAnchorRefReference,
|
|
61
61
|
PopoverAnchorRefTopBottom,
|
|
62
|
+
PopoverSlotProps,
|
|
62
63
|
} from './types';
|
|
63
64
|
import { overlayMiddlewares } from './overlay-middlewares';
|
|
64
65
|
import { StyleProvider } from '../style-provider';
|
|
@@ -70,6 +71,13 @@ import { StyleProvider } from '../style-provider';
|
|
|
70
71
|
*/
|
|
71
72
|
export const SLOT_NAME = 'Popover';
|
|
72
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Virtual padding to account for overflow boundaries.
|
|
76
|
+
*
|
|
77
|
+
* @type {number}
|
|
78
|
+
*/
|
|
79
|
+
const OVERFLOW_PADDING = 8;
|
|
80
|
+
|
|
73
81
|
// An SVG displaying a triangle facing down, filled with a solid
|
|
74
82
|
// color and bordered in such a way to create an arrow-like effect.
|
|
75
83
|
// Keeping the SVG's viewbox squared simplify the arrow positioning
|
|
@@ -223,6 +231,7 @@ const UnforwardedPopover = (
|
|
|
223
231
|
computedFlipProp && flipMiddleware(),
|
|
224
232
|
computedResizeProp &&
|
|
225
233
|
size( {
|
|
234
|
+
padding: OVERFLOW_PADDING,
|
|
226
235
|
apply( sizeProps ) {
|
|
227
236
|
const { firstElementChild } = refs.floating.current ?? {};
|
|
228
237
|
|
|
@@ -233,7 +242,10 @@ const UnforwardedPopover = (
|
|
|
233
242
|
|
|
234
243
|
// Reduce the height of the popover to the available space.
|
|
235
244
|
Object.assign( firstElementChild.style, {
|
|
236
|
-
maxHeight: `${
|
|
245
|
+
maxHeight: `${ Math.max(
|
|
246
|
+
0,
|
|
247
|
+
sizeProps.availableHeight
|
|
248
|
+
) }px`,
|
|
237
249
|
overflow: 'auto',
|
|
238
250
|
} );
|
|
239
251
|
},
|
|
@@ -478,6 +490,20 @@ const UnforwardedPopover = (
|
|
|
478
490
|
);
|
|
479
491
|
};
|
|
480
492
|
|
|
493
|
+
// Export the PopoverSlot individually to allow typescript to pick the types up.
|
|
494
|
+
export const PopoverSlot = forwardRef< HTMLDivElement, PopoverSlotProps >(
|
|
495
|
+
( { name = SLOT_NAME }, ref ) => {
|
|
496
|
+
return (
|
|
497
|
+
<Slot
|
|
498
|
+
bubblesVirtually
|
|
499
|
+
name={ name }
|
|
500
|
+
className="popover-slot"
|
|
501
|
+
ref={ ref }
|
|
502
|
+
/>
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
);
|
|
506
|
+
|
|
481
507
|
/**
|
|
482
508
|
* `Popover` renders its content in a floating modal. If no explicit anchor is passed via props, it anchors to its parent element by default.
|
|
483
509
|
*
|
|
@@ -501,25 +527,24 @@ const UnforwardedPopover = (
|
|
|
501
527
|
* ```
|
|
502
528
|
*
|
|
503
529
|
*/
|
|
504
|
-
export const Popover =
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
Popover.__unstableSlotNameProvider = slotNameContext.Provider;
|
|
530
|
+
export const Popover = Object.assign(
|
|
531
|
+
contextConnect( UnforwardedPopover, 'Popover' ),
|
|
532
|
+
{
|
|
533
|
+
/**
|
|
534
|
+
* Renders a slot that is used internally by Popover for rendering content.
|
|
535
|
+
*/
|
|
536
|
+
Slot: Object.assign( PopoverSlot, {
|
|
537
|
+
displayName: 'Popover.Slot',
|
|
538
|
+
} ),
|
|
539
|
+
/**
|
|
540
|
+
* Provides a context to manage popover slot names.
|
|
541
|
+
*
|
|
542
|
+
* This is marked as unstable and should not be used directly.
|
|
543
|
+
*/
|
|
544
|
+
__unstableSlotNameProvider: Object.assign( slotNameContext.Provider, {
|
|
545
|
+
displayName: 'Popover.__unstableSlotNameProvider',
|
|
546
|
+
} ),
|
|
547
|
+
}
|
|
548
|
+
);
|
|
524
549
|
|
|
525
550
|
export default Popover;
|
|
@@ -36,6 +36,10 @@ const meta: Meta< typeof Popover > = {
|
|
|
36
36
|
title: 'Components/Overlays/Popover',
|
|
37
37
|
id: 'components-popover',
|
|
38
38
|
component: Popover,
|
|
39
|
+
subcomponents: {
|
|
40
|
+
// @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170
|
|
41
|
+
'Popover.Slot': Popover.Slot,
|
|
42
|
+
},
|
|
39
43
|
argTypes: {
|
|
40
44
|
anchor: { control: false },
|
|
41
45
|
anchorRef: { control: false },
|
|
@@ -86,10 +90,13 @@ export const Default: StoryObj< typeof Popover > = {
|
|
|
86
90
|
decorators: [
|
|
87
91
|
( Story ) => {
|
|
88
92
|
const [ isVisible, setIsVisible ] = useState( false );
|
|
89
|
-
const
|
|
93
|
+
const buttonRef = useRef< HTMLButtonElement | undefined >();
|
|
94
|
+
const toggleVisible = ( event: React.MouseEvent ) => {
|
|
95
|
+
if ( buttonRef.current && event.target !== buttonRef.current ) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
90
98
|
setIsVisible( ( state ) => ! state );
|
|
91
99
|
};
|
|
92
|
-
const buttonRef = useRef< HTMLButtonElement | undefined >();
|
|
93
100
|
useEffect( () => {
|
|
94
101
|
buttonRef.current?.scrollIntoView?.( {
|
|
95
102
|
block: 'center',
|
|
@@ -255,3 +262,77 @@ export const WithSlotOutsideIframe: StoryObj< typeof Popover > = {
|
|
|
255
262
|
...Default.args,
|
|
256
263
|
},
|
|
257
264
|
};
|
|
265
|
+
|
|
266
|
+
export const WithCloseHandlers: StoryObj< typeof Popover > = {
|
|
267
|
+
render: function WithCloseHandlersStory( args ) {
|
|
268
|
+
const [ isVisible, setIsVisible ] = useState( false );
|
|
269
|
+
const buttonRef = useRef< HTMLButtonElement >( null );
|
|
270
|
+
|
|
271
|
+
const toggleVisible = ( event: React.MouseEvent ) => {
|
|
272
|
+
if ( buttonRef.current && event.target !== buttonRef.current ) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
setIsVisible( ( prev ) => ! prev );
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const handleClose = () => {
|
|
279
|
+
args.onClose?.();
|
|
280
|
+
setIsVisible( false );
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const handleFocusOutside = ( e: React.SyntheticEvent ) => {
|
|
284
|
+
args.onFocusOutside?.( e );
|
|
285
|
+
setIsVisible( false );
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
useEffect( () => {
|
|
289
|
+
buttonRef.current?.scrollIntoView( {
|
|
290
|
+
block: 'center',
|
|
291
|
+
inline: 'center',
|
|
292
|
+
} );
|
|
293
|
+
}, [] );
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<div
|
|
297
|
+
style={ {
|
|
298
|
+
width: '300vw',
|
|
299
|
+
height: '300vh',
|
|
300
|
+
display: 'flex',
|
|
301
|
+
alignItems: 'center',
|
|
302
|
+
justifyContent: 'center',
|
|
303
|
+
} }
|
|
304
|
+
>
|
|
305
|
+
<Button
|
|
306
|
+
variant="secondary"
|
|
307
|
+
onClick={ toggleVisible }
|
|
308
|
+
ref={ buttonRef }
|
|
309
|
+
>
|
|
310
|
+
Toggle Popover
|
|
311
|
+
{ isVisible && (
|
|
312
|
+
<Popover
|
|
313
|
+
{ ...args }
|
|
314
|
+
onClose={ handleClose }
|
|
315
|
+
onFocusOutside={ handleFocusOutside }
|
|
316
|
+
>
|
|
317
|
+
{ args.children }
|
|
318
|
+
</Popover>
|
|
319
|
+
) }
|
|
320
|
+
</Button>
|
|
321
|
+
</div>
|
|
322
|
+
);
|
|
323
|
+
},
|
|
324
|
+
args: {
|
|
325
|
+
...Default.args,
|
|
326
|
+
focusOnMount: true,
|
|
327
|
+
children: (
|
|
328
|
+
<div style={ { width: '280px', whiteSpace: 'normal' } }>
|
|
329
|
+
<p>
|
|
330
|
+
Clicking outside triggers the onFocusOutside callback prop.
|
|
331
|
+
</p>
|
|
332
|
+
<p>
|
|
333
|
+
Pressing the Escape key triggers the onClose callback prop.
|
|
334
|
+
</p>
|
|
335
|
+
</div>
|
|
336
|
+
),
|
|
337
|
+
},
|
|
338
|
+
};
|
package/src/popover/types.ts
CHANGED
|
@@ -206,3 +206,13 @@ export type PopoverProps = {
|
|
|
206
206
|
*/
|
|
207
207
|
isAlternate?: boolean;
|
|
208
208
|
};
|
|
209
|
+
|
|
210
|
+
export type PopoverSlotProps = {
|
|
211
|
+
/**
|
|
212
|
+
* The name of the Slot in which the popover should be rendered. It should
|
|
213
|
+
* be also passed to the corresponding `PopoverSlot` component.
|
|
214
|
+
*
|
|
215
|
+
* @default 'Popover'
|
|
216
|
+
*/
|
|
217
|
+
name?: string;
|
|
218
|
+
};
|
|
@@ -54,7 +54,7 @@ function UnconnectedToggleGroupControl(
|
|
|
54
54
|
const [ controlElement, setControlElement ] = useState< HTMLElement >();
|
|
55
55
|
const refs = useMergeRefs( [ setControlElement, forwardedRef ] );
|
|
56
56
|
const selectedRect = useTrackElementOffsetRect(
|
|
57
|
-
value
|
|
57
|
+
value !== null && value !== undefined ? selectedElement : undefined
|
|
58
58
|
);
|
|
59
59
|
useAnimatedOffsetRect( controlElement, selectedRect, {
|
|
60
60
|
prefix: 'selected',
|