-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>
- Loading branch information
1 parent
98ba5c2
commit 52ec524
Showing
14 changed files
with
475 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
packages/interface/src/components/preferences-modal-base-option/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
#__unstablePreferencesModalBaseOption | ||
|
||
`__unstablePreferencesModalBaseOption` renders a toggle meant to be used with `PreferencesModal`. | ||
|
||
This component implements a `ToggleControl` component from the `@wordpress/components` package. | ||
|
||
**It is an unstable component so is subject to breaking changes at any moment. Use at own risk.** | ||
|
||
## Example | ||
|
||
```jsx | ||
function MyEditorPreferencesOption() { | ||
return ( | ||
<__unstablePreferencesModalBaseOption | ||
label={ label } | ||
isChecked={ isChecked } | ||
onChange={ setIsChecked } | ||
> | ||
{ isChecked !== areCustomFieldsEnabled && ( | ||
<CustomFieldsConfirmation willEnable={ isChecked } /> | ||
) } | ||
</__unstablePreferencesModalBaseOption> | ||
) | ||
} | ||
``` | ||
|
||
## Props | ||
|
||
### help | ||
### label | ||
### isChecked | ||
### onChange | ||
|
||
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. | ||
|
||
### children | ||
|
||
Components to be rendered as content. | ||
|
||
- Type: `Element` | ||
- Required: No. | ||
|
20 changes: 20 additions & 0 deletions
20
packages/interface/src/components/preferences-modal-base-option/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { ToggleControl } from '@wordpress/components'; | ||
|
||
function BaseOption( { help, label, isChecked, onChange, children } ) { | ||
return ( | ||
<div className="interface-preferences-modal__option"> | ||
<ToggleControl | ||
help={ help } | ||
label={ label } | ||
checked={ isChecked } | ||
onChange={ onChange } | ||
/> | ||
{ children } | ||
</div> | ||
); | ||
} | ||
|
||
export default BaseOption; |
21 changes: 21 additions & 0 deletions
21
packages/interface/src/components/preferences-modal-base-option/style.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
.interface-preferences-modal__option { | ||
.components-base-control { | ||
.components-base-control__field { | ||
align-items: center; | ||
display: flex; | ||
margin-bottom: 0; | ||
|
||
& > label { | ||
flex-grow: 1; | ||
padding: 0.6rem 0 0.6rem 10px; | ||
} | ||
} | ||
} | ||
|
||
.components-base-control__help { | ||
margin: -$grid-unit-10 0 $grid-unit-10 58px; | ||
font-size: $helptext-font-size; | ||
font-style: normal; | ||
color: $gray-700; | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
packages/interface/src/components/preferences-modal-section/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
|
||
`PreferencesModalSection` renders a section (as a fieldset) meant to be used with `PreferencesModal`. | ||
|
||
## Example | ||
|
||
See the `PreferencesModal` readme for usage info. | ||
|
||
|
||
## Props | ||
|
||
### title | ||
|
||
The title of the section | ||
|
||
- Type: `String` | ||
- Required: Yes. | ||
|
||
### description | ||
|
||
The description for the section. | ||
|
||
- Type: `String` | ||
- Required: No. | ||
|
||
### children | ||
|
||
Components to be rendered as content. | ||
|
||
- Type: `Element` | ||
- Required: Yes. |
17 changes: 17 additions & 0 deletions
17
packages/interface/src/components/preferences-modal-section/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
const Section = ( { description, title, children } ) => ( | ||
<fieldset className="interface-preferences-modal__section"> | ||
<legend> | ||
<h2 className="interface-preferences-modal__section-title"> | ||
{ title } | ||
</h2> | ||
{ description && ( | ||
<p className="interface-preferences-modal__section-description"> | ||
{ description } | ||
</p> | ||
) } | ||
</legend> | ||
{ children } | ||
</fieldset> | ||
); | ||
|
||
export default Section; |
20 changes: 20 additions & 0 deletions
20
packages/interface/src/components/preferences-modal-section/style.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
.interface-preferences-modal__section { | ||
margin: 0 0 2.5rem 0; | ||
|
||
&:last-child { | ||
margin: 0; | ||
} | ||
} | ||
|
||
.interface-preferences-modal__section-title { | ||
font-size: 0.9rem; | ||
font-weight: 600; | ||
margin-top: 0; | ||
} | ||
|
||
.interface-preferences-modal__section-description { | ||
margin: -$grid-unit-10 0 $grid-unit-10 0; | ||
font-size: $helptext-font-size; | ||
font-style: normal; | ||
color: $gray-700; | ||
} |
14 changes: 14 additions & 0 deletions
14
packages/interface/src/components/preferences-modal-tabs/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# PreferencesModalTabs | ||
|
||
`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. | ||
|
||
## Example | ||
|
||
See the `PreferencesModal` readme for usage info. | ||
## Props | ||
### sections | ||
|
||
Sections to populate the modal with. Takes an array of objects, where each should include `name`, `tablabel` and `content`. | ||
|
||
- Type: `Array` | ||
- Required: Yes. |
156 changes: 156 additions & 0 deletions
156
packages/interface/src/components/preferences-modal-tabs/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useViewportMatch } from '@wordpress/compose'; | ||
import { | ||
__experimentalNavigatorProvider as NavigatorProvider, | ||
__experimentalNavigatorScreen as NavigatorScreen, | ||
__experimentalNavigatorButton as NavigatorButton, | ||
__experimentalNavigatorBackButton as NavigatorBackButton, | ||
__experimentalItemGroup as ItemGroup, | ||
__experimentalItem as Item, | ||
__experimentalHStack as HStack, | ||
__experimentalText as Text, | ||
__experimentalTruncate as Truncate, | ||
FlexItem, | ||
TabPanel, | ||
Card, | ||
CardHeader, | ||
CardBody, | ||
} from '@wordpress/components'; | ||
import { useMemo, useCallback, useState } from '@wordpress/element'; | ||
import { chevronLeft, chevronRight, Icon } from '@wordpress/icons'; | ||
import { isRTL, __ } from '@wordpress/i18n'; | ||
|
||
const PREFERENCES_MENU = 'preferences-menu'; | ||
|
||
export default function PreferencesModalTabs( { sections } ) { | ||
const isLargeViewport = useViewportMatch( 'medium' ); | ||
|
||
// This is also used to sync the two different rendered components | ||
// between small and large viewports. | ||
const [ activeMenu, setActiveMenu ] = useState( PREFERENCES_MENU ); | ||
/** | ||
* Create helper objects from `sections` for easier data handling. | ||
* `tabs` is used for creating the `TabPanel` and `sectionsContentMap` | ||
* is used for easier access to active tab's content. | ||
*/ | ||
const { tabs, sectionsContentMap } = useMemo( () => { | ||
let mappedTabs = { | ||
tabs: [], | ||
sectionsContentMap: {}, | ||
}; | ||
if ( sections.length ) { | ||
mappedTabs = sections.reduce( | ||
( accumulator, { name, tabLabel: title, content } ) => { | ||
accumulator.tabs.push( { name, title } ); | ||
accumulator.sectionsContentMap[ name ] = content; | ||
return accumulator; | ||
}, | ||
{ tabs: [], sectionsContentMap: {} } | ||
); | ||
} | ||
return mappedTabs; | ||
}, [ sections ] ); | ||
|
||
const getCurrentTab = useCallback( | ||
( tab ) => sectionsContentMap[ tab.name ] || null, | ||
[ sectionsContentMap ] | ||
); | ||
|
||
let modalContent; | ||
// We render different components based on the viewport size. | ||
if ( isLargeViewport ) { | ||
modalContent = ( | ||
<TabPanel | ||
className="interface-preferences__tabs" | ||
tabs={ tabs } | ||
initialTabName={ | ||
activeMenu !== PREFERENCES_MENU ? activeMenu : undefined | ||
} | ||
onSelect={ setActiveMenu } | ||
orientation="vertical" | ||
> | ||
{ getCurrentTab } | ||
</TabPanel> | ||
); | ||
} else { | ||
modalContent = ( | ||
<NavigatorProvider | ||
initialPath="/" | ||
className="interface-preferences__provider" | ||
> | ||
<NavigatorScreen path="/"> | ||
<Card isBorderless size="small"> | ||
<CardBody> | ||
<ItemGroup> | ||
{ tabs.map( ( tab ) => { | ||
return ( | ||
<NavigatorButton | ||
key={ tab.name } | ||
path={ tab.name } | ||
as={ Item } | ||
isAction | ||
> | ||
<HStack justify="space-between"> | ||
<FlexItem> | ||
<Truncate> | ||
{ tab.title } | ||
</Truncate> | ||
</FlexItem> | ||
<FlexItem> | ||
<Icon | ||
icon={ | ||
isRTL() | ||
? chevronLeft | ||
: chevronRight | ||
} | ||
/> | ||
</FlexItem> | ||
</HStack> | ||
</NavigatorButton> | ||
); | ||
} ) } | ||
</ItemGroup> | ||
</CardBody> | ||
</Card> | ||
</NavigatorScreen> | ||
{ sections.length && | ||
sections.map( ( section ) => { | ||
return ( | ||
<NavigatorScreen | ||
key={ `${ section.name }-menu` } | ||
path={ section.name } | ||
> | ||
<Card isBorderless size="large"> | ||
<CardHeader | ||
isBorderless={ false } | ||
justify="left" | ||
size="small" | ||
gap="6" | ||
> | ||
<NavigatorBackButton | ||
icon={ | ||
isRTL() | ||
? chevronRight | ||
: chevronLeft | ||
} | ||
aria-label={ __( | ||
'Navigate to the previous view' | ||
) } | ||
/> | ||
<Text size="16"> | ||
{ section.tabLabel } | ||
</Text> | ||
</CardHeader> | ||
<CardBody>{ section.content }</CardBody> | ||
</Card> | ||
</NavigatorScreen> | ||
); | ||
} ) } | ||
</NavigatorProvider> | ||
); | ||
} | ||
|
||
return modalContent; | ||
} |
35 changes: 35 additions & 0 deletions
35
packages/interface/src/components/preferences-modal-tabs/style.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
$vertical-tabs-width: 160px; | ||
|
||
.interface-preferences__tabs { | ||
.components-tab-panel__tabs { | ||
position: absolute; | ||
top: $header-height + $grid-unit-30; | ||
// Aligns button text instead of button box. | ||
left: $grid-unit-20; | ||
width: $vertical-tabs-width; | ||
.components-tab-panel__tabs-item { | ||
border-radius: $radius-block-ui; | ||
font-weight: 400; | ||
&.is-active { | ||
background: $gray-100; | ||
box-shadow: none; | ||
font-weight: 500; | ||
} | ||
&:focus:not(:disabled) { | ||
box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); | ||
} | ||
} | ||
} | ||
.components-tab-panel__tab-content { | ||
padding-left: $grid-unit-30; | ||
margin-left: $vertical-tabs-width; | ||
} | ||
} | ||
|
||
@media (max-width: #{ ($break-medium - 1) }) { | ||
// Keep the navigator component from overflowing the modal content area | ||
// to ensure that sticky position elements stick where intended. | ||
.interface-preferences__provider { | ||
height: 100%; | ||
} | ||
} |
Oops, something went wrong.