From bef56488ad0ba492fc0039f1255e7cfdad49bd0d Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 16 May 2022 15:20:18 +0200 Subject: [PATCH] Introduce SearchModalProvider to allow modal to be controlled externally Signed-off-by: Eric Peterson --- .changeset/search-gonna-get-hopons.md | 5 + plugins/search/api-report.md | 31 +++++-- plugins/search/package.json | 1 + .../src/components/SearchModal/index.tsx | 6 +- .../components/SearchModal/useSearchModal.tsx | 92 ++++++++++++++++++- .../SidebarSearchModal/SidebarSearchModal.tsx | 11 ++- plugins/search/src/index.ts | 8 +- 7 files changed, 141 insertions(+), 13 deletions(-) create mode 100644 .changeset/search-gonna-get-hopons.md diff --git a/.changeset/search-gonna-get-hopons.md b/.changeset/search-gonna-get-hopons.md new file mode 100644 index 0000000000..5b9947b804 --- /dev/null +++ b/.changeset/search-gonna-get-hopons.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': patch +--- + +Introduced a ``, which can optionally be placed higher up in the react tree in order to allow control of search modal visibility from outside the modal itself. diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 05dcdd89b3..21b9df5dda 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -195,6 +195,28 @@ export interface SearchModalProps { toggleModal: () => void; } +// @public +export const SearchModalProvider: ({ + children, + showInitially, +}: SearchModalProviderProps) => JSX.Element; + +// @public +export type SearchModalProviderProps = { + children: ReactNode; + showInitially?: boolean; +}; + +// @public +export type SearchModalValue = { + state: { + hidden: boolean; + open: boolean; + }; + toggleModal: () => void; + setOpen: (open: boolean) => void; +}; + // Warning: (ae-missing-release-tag) "SearchPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -297,12 +319,5 @@ export type SidebarSearchProps = { }; // @public -export function useSearchModal(initialState?: boolean): { - state: { - hidden: boolean; - open: boolean; - }; - toggleModal: () => void; - setOpen: (open: boolean) => void; -}; +export function useSearchModal(initialState?: boolean): SearchModalValue; ``` diff --git a/plugins/search/package.json b/plugins/search/package.json index 109ed2a455..476b67504c 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -43,6 +43,7 @@ "@backstage/plugin-search-react": "^0.2.0-next.2", "@backstage/theme": "^0.2.15", "@backstage/types": "^1.0.0", + "@backstage/version-bridge": "^1.0.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", diff --git a/plugins/search/src/components/SearchModal/index.tsx b/plugins/search/src/components/SearchModal/index.tsx index 51e5d94c07..0bb1df4a7b 100644 --- a/plugins/search/src/components/SearchModal/index.tsx +++ b/plugins/search/src/components/SearchModal/index.tsx @@ -15,4 +15,8 @@ */ export { SearchModal } from './SearchModal'; export type { SearchModalChildrenProps, SearchModalProps } from './SearchModal'; -export { useSearchModal } from './useSearchModal'; +export { SearchModalProvider, useSearchModal } from './useSearchModal'; +export type { + SearchModalProviderProps, + SearchModalValue, +} from './useSearchModal'; diff --git a/plugins/search/src/components/SearchModal/useSearchModal.tsx b/plugins/search/src/components/SearchModal/useSearchModal.tsx index 3871687d1a..09afb34008 100644 --- a/plugins/search/src/components/SearchModal/useSearchModal.tsx +++ b/plugins/search/src/components/SearchModal/useSearchModal.tsx @@ -14,7 +14,88 @@ * limitations under the License. */ -import { useCallback, useState } from 'react'; +import React, { ReactNode, useCallback, useContext, useState } from 'react'; +import { + createVersionedContext, + createVersionedValueMap, +} from '@backstage/version-bridge'; + +/** + * The state of the search modal, as well as functions for changing the modal's + * visibility. + * + * @public + */ +export type SearchModalValue = { + state: { + hidden: boolean; + open: boolean; + }; + toggleModal: () => void; + setOpen: (open: boolean) => void; +}; + +const SearchModalContext = createVersionedContext<{ + 1: SearchModalValue | undefined; +}>('analytics-context'); + +/** + * Props for the SearchModalProvider. + * @public + */ +export type SearchModalProviderProps = { + /** + * Children which should have access to the SearchModal context and the + * associated useSearchModal() hook. + */ + children: ReactNode; + + /** + * Pass true if the modal should be rendered initially. + */ + showInitially?: boolean; +}; + +/** + * A context provider responsible for storing and managing state related to the + * search modal. + * + * @remarks + * If you need to control visibility of the search toggle outside of the modal + * itself, you can optionally place this higher up in the react tree where your + * custom code and the search modal share the same context. + * + * @example + * ```tsx + * import { + * SearchModalProvider, + * SidebarSearchModal, + * } from '@backstage/plugin-search'; + * + * // ... + * + * + * + * + * {({ toggleModal }) => } + * + * + * ``` + * + * @public + */ +export const SearchModalProvider = ({ + children, + showInitially, +}: SearchModalProviderProps) => { + const value = useSearchModal(showInitially); + const versionedValue = createVersionedValueMap({ 1: value }); + return ( + + {children} + + ); +}; /** * Use this hook to manage the state of {@link SearchModal} @@ -27,6 +108,10 @@ import { useCallback, useState } from 'react'; * functions for changing the visibility of the modal. */ export function useSearchModal(initialState = false) { + // Check for any existing parent context. + const parentContext = useContext(SearchModalContext); + const parentContextValue = parentContext?.atVersion(1); + const [state, setState] = useState({ hidden: !initialState, open: initialState, @@ -50,5 +135,8 @@ export function useSearchModal(initialState = false) { [], ); - return { state, toggleModal, setOpen }; + // Inherit from parent context, if set. + return parentContextValue + ? parentContextValue + : { state, toggleModal, setOpen }; } diff --git a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx index 4310c03b15..191a09395c 100644 --- a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx +++ b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx @@ -20,6 +20,7 @@ import { IconComponent } from '@backstage/core-plugin-api'; import { SearchModal, SearchModalChildrenProps, + SearchModalProvider, useSearchModal, } from '../SearchModal'; @@ -28,7 +29,7 @@ export type SidebarSearchModalProps = { children?: (props: SearchModalChildrenProps) => JSX.Element; }; -export const SidebarSearchModal = (props: SidebarSearchModalProps) => { +const SidebarSearchModalContent = (props: SidebarSearchModalProps) => { const { state, toggleModal } = useSearchModal(); const Icon = props.icon ? props.icon : SearchIcon; @@ -48,3 +49,11 @@ export const SidebarSearchModal = (props: SidebarSearchModalProps) => { ); }; + +export const SidebarSearchModal = (props: SidebarSearchModalProps) => { + return ( + + + + ); +}; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index dccb2ce835..1457ac781d 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -34,10 +34,16 @@ export type { SearchFilterComponentProps, SearchFilterWrapperProps, } from './components/SearchFilter'; -export { SearchModal, useSearchModal } from './components/SearchModal'; +export { + SearchModal, + SearchModalProvider, + useSearchModal, +} from './components/SearchModal'; export type { SearchModalChildrenProps, SearchModalProps, + SearchModalProviderProps, + SearchModalValue, } from './components/SearchModal'; export { SearchPage as Router } from './components/SearchPage'; export { SearchResultPager } from './components/SearchResultPager';