diff --git a/packages/interface/src/components/index.js b/packages/interface/src/components/index.js index 27b41f4ecb999e..aa03fa34ae8c6b 100644 --- a/packages/interface/src/components/index.js +++ b/packages/interface/src/components/index.js @@ -6,3 +6,7 @@ export { default as PinnedItems } from './pinned-items'; export { default as MoreMenuDropdown } from './more-menu-dropdown'; export { default as MoreMenuFeatureToggle } from './more-menu-feature-toggle'; export { default as ActionItem } from './action-item'; +export { default as PreferencesModal } from './preferences-modal'; +export { default as PreferencesModalTabs } from './preferences-modal-tabs'; +export { default as PreferencesModalSection } from './preferences-modal-section'; +export { default as ___unstablePreferencesModalBaseOption } from './preferences-modal-base-option'; diff --git a/packages/interface/src/components/preferences-modal-base-option/README.md b/packages/interface/src/components/preferences-modal-base-option/README.md new file mode 100644 index 00000000000000..03c89960b6850c --- /dev/null +++ b/packages/interface/src/components/preferences-modal-base-option/README.md @@ -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 && ( + + ) } + + ) +} +``` + +## 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. + diff --git a/packages/interface/src/components/preferences-modal-base-option/index.js b/packages/interface/src/components/preferences-modal-base-option/index.js new file mode 100644 index 00000000000000..bf027b17354a26 --- /dev/null +++ b/packages/interface/src/components/preferences-modal-base-option/index.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { ToggleControl } from '@wordpress/components'; + +function BaseOption( { help, label, isChecked, onChange, children } ) { + return ( +
+ + { children } +
+ ); +} + +export default BaseOption; diff --git a/packages/interface/src/components/preferences-modal-base-option/style.scss b/packages/interface/src/components/preferences-modal-base-option/style.scss new file mode 100644 index 00000000000000..88143354a497be --- /dev/null +++ b/packages/interface/src/components/preferences-modal-base-option/style.scss @@ -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; + } +} diff --git a/packages/interface/src/components/preferences-modal-section/README.md b/packages/interface/src/components/preferences-modal-section/README.md new file mode 100644 index 00000000000000..6e78ca05c0aa81 --- /dev/null +++ b/packages/interface/src/components/preferences-modal-section/README.md @@ -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. diff --git a/packages/interface/src/components/preferences-modal-section/index.js b/packages/interface/src/components/preferences-modal-section/index.js new file mode 100644 index 00000000000000..3ad08b557ff85f --- /dev/null +++ b/packages/interface/src/components/preferences-modal-section/index.js @@ -0,0 +1,17 @@ +const Section = ( { description, title, children } ) => ( +
+ +

+ { title } +

+ { description && ( +

+ { description } +

+ ) } +
+ { children } +
+); + +export default Section; diff --git a/packages/interface/src/components/preferences-modal-section/style.scss b/packages/interface/src/components/preferences-modal-section/style.scss new file mode 100644 index 00000000000000..135545986836ad --- /dev/null +++ b/packages/interface/src/components/preferences-modal-section/style.scss @@ -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; +} diff --git a/packages/interface/src/components/preferences-modal-tabs/README.md b/packages/interface/src/components/preferences-modal-tabs/README.md new file mode 100644 index 00000000000000..e1ffbd86a41b62 --- /dev/null +++ b/packages/interface/src/components/preferences-modal-tabs/README.md @@ -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. \ No newline at end of file diff --git a/packages/interface/src/components/preferences-modal-tabs/index.js b/packages/interface/src/components/preferences-modal-tabs/index.js new file mode 100644 index 00000000000000..bc8f7358b834d4 --- /dev/null +++ b/packages/interface/src/components/preferences-modal-tabs/index.js @@ -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 = ( + + { getCurrentTab } + + ); + } else { + modalContent = ( + + + + + + { tabs.map( ( tab ) => { + return ( + + + + + { tab.title } + + + + + + + + ); + } ) } + + + + + { sections.length && + sections.map( ( section ) => { + return ( + + + + + + { section.tabLabel } + + + { section.content } + + + ); + } ) } + + ); + } + + return modalContent; +} diff --git a/packages/interface/src/components/preferences-modal-tabs/style.scss b/packages/interface/src/components/preferences-modal-tabs/style.scss new file mode 100644 index 00000000000000..d09c8b3572bd23 --- /dev/null +++ b/packages/interface/src/components/preferences-modal-tabs/style.scss @@ -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%; + } +} diff --git a/packages/interface/src/components/preferences-modal/README.md b/packages/interface/src/components/preferences-modal/README.md new file mode 100644 index 00000000000000..96ecdf03dcc136 --- /dev/null +++ b/packages/interface/src/components/preferences-modal/README.md @@ -0,0 +1,69 @@ +# PreferencesModal + +`PreferencesModal` renders a modal with editor preferences. It can take a `PreferencesModalTabs` component, which accepts multiple tabs, and/or other child components. On small viewports, the modal is fullscreen. + +This component implements a `Modal` component from the `@wordpress/components` package. + +Sections passed to this component should use `PreferencesModalSection` component from the `@wordpress/interface` package. + + +## Example + +```jsx +function MyEditorPreferencesModal() { + const { closeModal } = useDispatch( editPostStore ); + const sections = [ + { + name: 'section 1', + tabLabel: __( 'Section 1' ), + content: ( + + + + ) + + } + { + name: 'section 2', + tabLabel: __( 'Section 2' ), + content: ( + + // Section content here + + ) + + } + ] + return ( + + + + ); +} +``` + +## Props + +### closeModal + +A function to call when closing the modal. + +- Type: `Function` +- Required: Yes. diff --git a/packages/interface/src/components/preferences-modal/index.js b/packages/interface/src/components/preferences-modal/index.js new file mode 100644 index 00000000000000..b91be8b21204a3 --- /dev/null +++ b/packages/interface/src/components/preferences-modal/index.js @@ -0,0 +1,18 @@ +/** + * WordPress dependencies + */ +import { Modal } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +export default function PreferencesModal( { closeModal, children } ) { + return ( + + { children } + + ); +} diff --git a/packages/interface/src/components/preferences-modal/style.scss b/packages/interface/src/components/preferences-modal/style.scss new file mode 100644 index 00000000000000..007b4b7c4e5e51 --- /dev/null +++ b/packages/interface/src/components/preferences-modal/style.scss @@ -0,0 +1,25 @@ +.interface-preferences-modal { + // To keep modal dimensions consistent as subsections are navigated, width + // and height are used instead of max-(width/height). + @include break-small() { + width: calc(100% - #{ $grid-unit-20 * 2 }); + height: calc(100% - #{ $header-height * 2 }); + } + @include break-medium() { + width: $break-medium - $grid-unit-20 * 2; + } + @include break-large() { + height: 70%; + } + + // Clears spacing to flush fit the navigator component to the modal edges. + @media (max-width: #{ ($break-medium - 1) }) { + .components-modal__content { + padding: 0; + + &::before { + content: none; + } + } + } +} diff --git a/packages/interface/src/style.scss b/packages/interface/src/style.scss index e6950de411156a..be111640a0a796 100644 --- a/packages/interface/src/style.scss +++ b/packages/interface/src/style.scss @@ -4,3 +4,7 @@ @import "./components/interface-skeleton/style.scss"; @import "./components/more-menu-dropdown/style.scss"; @import "./components/pinned-items/style.scss"; +@import "./components/preferences-modal/style.scss"; +@import "./components/preferences-modal-tabs/style.scss"; +@import "./components/preferences-modal-section/style.scss"; +@import "./components/preferences-modal-base-option/style.scss";