Skip to content

Commit 52ec524

Browse files
Add reusable preferences modal to interface package. (#39153)
* Add reusable preferences modal to interface package. * Rearrange and export files * Make sure sections exist. * Add readmes for all components. * Update readme code example. Co-authored-by: Daniel Richards <daniel.richards@automattic.com> * Split out tabs from modal. * Mark base option component unstable. * Add classname and move some styles around. Co-authored-by: Daniel Richards <daniel.richards@automattic.com>
1 parent 98ba5c2 commit 52ec524

File tree

14 files changed

+475
-0
lines changed

14 files changed

+475
-0
lines changed

packages/interface/src/components/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ export { default as PinnedItems } from './pinned-items';
66
export { default as MoreMenuDropdown } from './more-menu-dropdown';
77
export { default as MoreMenuFeatureToggle } from './more-menu-feature-toggle';
88
export { default as ActionItem } from './action-item';
9+
export { default as PreferencesModal } from './preferences-modal';
10+
export { default as PreferencesModalTabs } from './preferences-modal-tabs';
11+
export { default as PreferencesModalSection } from './preferences-modal-section';
12+
export { default as ___unstablePreferencesModalBaseOption } from './preferences-modal-base-option';
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#__unstablePreferencesModalBaseOption
2+
3+
`__unstablePreferencesModalBaseOption` renders a toggle meant to be used with `PreferencesModal`.
4+
5+
This component implements a `ToggleControl` component from the `@wordpress/components` package.
6+
7+
**It is an unstable component so is subject to breaking changes at any moment. Use at own risk.**
8+
9+
## Example
10+
11+
```jsx
12+
function MyEditorPreferencesOption() {
13+
return (
14+
<__unstablePreferencesModalBaseOption
15+
label={ label }
16+
isChecked={ isChecked }
17+
onChange={ setIsChecked }
18+
>
19+
{ isChecked !== areCustomFieldsEnabled && (
20+
<CustomFieldsConfirmation willEnable={ isChecked } />
21+
) }
22+
</__unstablePreferencesModalBaseOption>
23+
)
24+
}
25+
```
26+
27+
## Props
28+
29+
### help
30+
### label
31+
### isChecked
32+
### onChange
33+
34+
These props are passed directly to ToggleControl, so see [ToggleControl readme](https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/toggle-control/README.md) for more info.
35+
36+
### children
37+
38+
Components to be rendered as content.
39+
40+
- Type: `Element`
41+
- Required: No.
42+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
import { ToggleControl } from '@wordpress/components';
5+
6+
function BaseOption( { help, label, isChecked, onChange, children } ) {
7+
return (
8+
<div className="interface-preferences-modal__option">
9+
<ToggleControl
10+
help={ help }
11+
label={ label }
12+
checked={ isChecked }
13+
onChange={ onChange }
14+
/>
15+
{ children }
16+
</div>
17+
);
18+
}
19+
20+
export default BaseOption;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.interface-preferences-modal__option {
2+
.components-base-control {
3+
.components-base-control__field {
4+
align-items: center;
5+
display: flex;
6+
margin-bottom: 0;
7+
8+
& > label {
9+
flex-grow: 1;
10+
padding: 0.6rem 0 0.6rem 10px;
11+
}
12+
}
13+
}
14+
15+
.components-base-control__help {
16+
margin: -$grid-unit-10 0 $grid-unit-10 58px;
17+
font-size: $helptext-font-size;
18+
font-style: normal;
19+
color: $gray-700;
20+
}
21+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
2+
`PreferencesModalSection` renders a section (as a fieldset) meant to be used with `PreferencesModal`.
3+
4+
## Example
5+
6+
See the `PreferencesModal` readme for usage info.
7+
8+
9+
## Props
10+
11+
### title
12+
13+
The title of the section
14+
15+
- Type: `String`
16+
- Required: Yes.
17+
18+
### description
19+
20+
The description for the section.
21+
22+
- Type: `String`
23+
- Required: No.
24+
25+
### children
26+
27+
Components to be rendered as content.
28+
29+
- Type: `Element`
30+
- Required: Yes.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const Section = ( { description, title, children } ) => (
2+
<fieldset className="interface-preferences-modal__section">
3+
<legend>
4+
<h2 className="interface-preferences-modal__section-title">
5+
{ title }
6+
</h2>
7+
{ description && (
8+
<p className="interface-preferences-modal__section-description">
9+
{ description }
10+
</p>
11+
) }
12+
</legend>
13+
{ children }
14+
</fieldset>
15+
);
16+
17+
export default Section;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.interface-preferences-modal__section {
2+
margin: 0 0 2.5rem 0;
3+
4+
&:last-child {
5+
margin: 0;
6+
}
7+
}
8+
9+
.interface-preferences-modal__section-title {
10+
font-size: 0.9rem;
11+
font-weight: 600;
12+
margin-top: 0;
13+
}
14+
15+
.interface-preferences-modal__section-description {
16+
margin: -$grid-unit-10 0 $grid-unit-10 0;
17+
font-size: $helptext-font-size;
18+
font-style: normal;
19+
color: $gray-700;
20+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# PreferencesModalTabs
2+
3+
`PreferencesModalTabs` creates a tabbed interface meant to be used inside a `PreferencesModal`. Markup differs between small and large viewports; on small the tabs are closed by default.
4+
5+
## Example
6+
7+
See the `PreferencesModal` readme for usage info.
8+
## Props
9+
### sections
10+
11+
Sections to populate the modal with. Takes an array of objects, where each should include `name`, `tablabel` and `content`.
12+
13+
- Type: `Array`
14+
- Required: Yes.
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
import { useViewportMatch } from '@wordpress/compose';
5+
import {
6+
__experimentalNavigatorProvider as NavigatorProvider,
7+
__experimentalNavigatorScreen as NavigatorScreen,
8+
__experimentalNavigatorButton as NavigatorButton,
9+
__experimentalNavigatorBackButton as NavigatorBackButton,
10+
__experimentalItemGroup as ItemGroup,
11+
__experimentalItem as Item,
12+
__experimentalHStack as HStack,
13+
__experimentalText as Text,
14+
__experimentalTruncate as Truncate,
15+
FlexItem,
16+
TabPanel,
17+
Card,
18+
CardHeader,
19+
CardBody,
20+
} from '@wordpress/components';
21+
import { useMemo, useCallback, useState } from '@wordpress/element';
22+
import { chevronLeft, chevronRight, Icon } from '@wordpress/icons';
23+
import { isRTL, __ } from '@wordpress/i18n';
24+
25+
const PREFERENCES_MENU = 'preferences-menu';
26+
27+
export default function PreferencesModalTabs( { sections } ) {
28+
const isLargeViewport = useViewportMatch( 'medium' );
29+
30+
// This is also used to sync the two different rendered components
31+
// between small and large viewports.
32+
const [ activeMenu, setActiveMenu ] = useState( PREFERENCES_MENU );
33+
/**
34+
* Create helper objects from `sections` for easier data handling.
35+
* `tabs` is used for creating the `TabPanel` and `sectionsContentMap`
36+
* is used for easier access to active tab's content.
37+
*/
38+
const { tabs, sectionsContentMap } = useMemo( () => {
39+
let mappedTabs = {
40+
tabs: [],
41+
sectionsContentMap: {},
42+
};
43+
if ( sections.length ) {
44+
mappedTabs = sections.reduce(
45+
( accumulator, { name, tabLabel: title, content } ) => {
46+
accumulator.tabs.push( { name, title } );
47+
accumulator.sectionsContentMap[ name ] = content;
48+
return accumulator;
49+
},
50+
{ tabs: [], sectionsContentMap: {} }
51+
);
52+
}
53+
return mappedTabs;
54+
}, [ sections ] );
55+
56+
const getCurrentTab = useCallback(
57+
( tab ) => sectionsContentMap[ tab.name ] || null,
58+
[ sectionsContentMap ]
59+
);
60+
61+
let modalContent;
62+
// We render different components based on the viewport size.
63+
if ( isLargeViewport ) {
64+
modalContent = (
65+
<TabPanel
66+
className="interface-preferences__tabs"
67+
tabs={ tabs }
68+
initialTabName={
69+
activeMenu !== PREFERENCES_MENU ? activeMenu : undefined
70+
}
71+
onSelect={ setActiveMenu }
72+
orientation="vertical"
73+
>
74+
{ getCurrentTab }
75+
</TabPanel>
76+
);
77+
} else {
78+
modalContent = (
79+
<NavigatorProvider
80+
initialPath="/"
81+
className="interface-preferences__provider"
82+
>
83+
<NavigatorScreen path="/">
84+
<Card isBorderless size="small">
85+
<CardBody>
86+
<ItemGroup>
87+
{ tabs.map( ( tab ) => {
88+
return (
89+
<NavigatorButton
90+
key={ tab.name }
91+
path={ tab.name }
92+
as={ Item }
93+
isAction
94+
>
95+
<HStack justify="space-between">
96+
<FlexItem>
97+
<Truncate>
98+
{ tab.title }
99+
</Truncate>
100+
</FlexItem>
101+
<FlexItem>
102+
<Icon
103+
icon={
104+
isRTL()
105+
? chevronLeft
106+
: chevronRight
107+
}
108+
/>
109+
</FlexItem>
110+
</HStack>
111+
</NavigatorButton>
112+
);
113+
} ) }
114+
</ItemGroup>
115+
</CardBody>
116+
</Card>
117+
</NavigatorScreen>
118+
{ sections.length &&
119+
sections.map( ( section ) => {
120+
return (
121+
<NavigatorScreen
122+
key={ `${ section.name }-menu` }
123+
path={ section.name }
124+
>
125+
<Card isBorderless size="large">
126+
<CardHeader
127+
isBorderless={ false }
128+
justify="left"
129+
size="small"
130+
gap="6"
131+
>
132+
<NavigatorBackButton
133+
icon={
134+
isRTL()
135+
? chevronRight
136+
: chevronLeft
137+
}
138+
aria-label={ __(
139+
'Navigate to the previous view'
140+
) }
141+
/>
142+
<Text size="16">
143+
{ section.tabLabel }
144+
</Text>
145+
</CardHeader>
146+
<CardBody>{ section.content }</CardBody>
147+
</Card>
148+
</NavigatorScreen>
149+
);
150+
} ) }
151+
</NavigatorProvider>
152+
);
153+
}
154+
155+
return modalContent;
156+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
$vertical-tabs-width: 160px;
2+
3+
.interface-preferences__tabs {
4+
.components-tab-panel__tabs {
5+
position: absolute;
6+
top: $header-height + $grid-unit-30;
7+
// Aligns button text instead of button box.
8+
left: $grid-unit-20;
9+
width: $vertical-tabs-width;
10+
.components-tab-panel__tabs-item {
11+
border-radius: $radius-block-ui;
12+
font-weight: 400;
13+
&.is-active {
14+
background: $gray-100;
15+
box-shadow: none;
16+
font-weight: 500;
17+
}
18+
&:focus:not(:disabled) {
19+
box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
20+
}
21+
}
22+
}
23+
.components-tab-panel__tab-content {
24+
padding-left: $grid-unit-30;
25+
margin-left: $vertical-tabs-width;
26+
}
27+
}
28+
29+
@media (max-width: #{ ($break-medium - 1) }) {
30+
// Keep the navigator component from overflowing the modal content area
31+
// to ensure that sticky position elements stick where intended.
32+
.interface-preferences__provider {
33+
height: 100%;
34+
}
35+
}

0 commit comments

Comments
 (0)