forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request elastic#18 from eokoneyo/skeleton-share
Skeleton Tabbed Modal
- Loading branch information
Showing
16 changed files
with
587 additions
and
88 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
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
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
File renamed without changes.
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,79 @@ | ||
--- | ||
id: sharedUX/Components/KibanaTabbedModal | ||
slug: /shared-ux/components/tabbed-modal | ||
title: Tabbed Modal | ||
description: A wrapper around `EuiAvatar` tailored for use in Kibana solutions. | ||
tags: ['shared-ux', 'component'] | ||
date: 2024-03-07 | ||
--- | ||
|
||
## Description | ||
|
||
A wrapper around `EuiModal` for displaying and managing tabs tailored for use in Kibana solutions. | ||
|
||
## Usage | ||
|
||
A fairly trival use case might look like this | ||
|
||
```tsx | ||
const hello: IModalTabDeclaration<{ message: string }> = { | ||
id: 'hello', | ||
title: 'Say Hello', | ||
initialState: { | ||
message: 'Hello World!', | ||
}, | ||
content: ({ state }) => { | ||
// we can access state here to impact renders | ||
return <h1>Click the button to trigger a message</h1>; | ||
}, | ||
modalActionBtn: { | ||
label: 'Click me!', | ||
handler: ({ state }) => { | ||
// state can also be accessed here if neccessary | ||
alert(state.message); | ||
}, | ||
}, | ||
}; | ||
|
||
<TabbedModal tabs={[hello]} /> | ||
``` | ||
|
||
a slightly more complex use case, where we'd like changes from interactions within the rendered tab | ||
content to propagate to the state might look similar to this. | ||
|
||
```tsx | ||
const hello: IModalTabDeclaration<{ message: string }> = { | ||
id: 'hello', | ||
title: 'Say Hello', | ||
initialState: { | ||
message: 'Hello World!', | ||
}, | ||
reducer(state, action) { | ||
switch(action.type) { | ||
case 'CHANGE_MESSAGE': | ||
return { | ||
...state, | ||
message: action.payload | ||
}; | ||
default: | ||
return State; | ||
} | ||
}, | ||
content: ({ dispatch }) => { | ||
useEffect(() => { | ||
dispatch({ type: 'CHANGE_MESSAGE', payload: 'Hello from the other side!' }); | ||
}, []); | ||
// we can access state here to impact renders | ||
return <h1>Click the button to trigger a message</h1>; | ||
}, | ||
modalActionBtn: { | ||
label: 'Click me!', | ||
handler: ({ state }) => { | ||
// message displayed in alert here would be different from the value set in our initial state | ||
alert(state.message); | ||
}, | ||
}, | ||
}; | ||
|
||
<TabbedModal tabs={[hello]} /> | ||
``` |
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
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,6 @@ | ||
{ | ||
"type": "shared-common", | ||
"id": "@kbn/shared-ux-tabbed-modal", | ||
"owner": "@elastic/appex-sharedux" | ||
} | ||
|
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,6 @@ | ||
{ | ||
"name": "@kbn/shared-ux-tabbed-modal", | ||
"private": true, | ||
"version": "1.0.0", | ||
"license": "SSPL-1.0 OR Elastic License 2.0" | ||
} |
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,152 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import React, { | ||
createContext, | ||
useContext, | ||
useReducer, | ||
useMemo, | ||
useRef, | ||
useCallback, | ||
type PropsWithChildren, | ||
type ReactElement, | ||
type Dispatch, | ||
} from 'react'; | ||
import { type EuiTabProps, type CommonProps } from '@elastic/eui'; | ||
|
||
interface IDispatchAction { | ||
type: string; | ||
payload: any; | ||
} | ||
|
||
export type IModalTabState = Record<string, unknown>; | ||
|
||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions | ||
type IModalMetaState = { | ||
selectedTabId: string | null; | ||
}; | ||
|
||
type IReducer<S extends IModalTabState> = (state: S, action: IDispatchAction) => S; | ||
|
||
export type IModalTabContent<S extends IModalTabState> = (props: { | ||
state: S; | ||
dispatch: Dispatch<IDispatchAction>; | ||
}) => ReactElement; | ||
|
||
interface IModalTabActionBtn<S> extends CommonProps { | ||
label: string; | ||
handler: (args: { state: S }) => void; | ||
} | ||
|
||
export interface IModalTabDeclaration<S extends IModalTabState> extends EuiTabProps { | ||
id: string; | ||
title: string; | ||
initialState?: Partial<S>; | ||
reducer?: IReducer<S>; | ||
content: IModalTabContent<S>; | ||
modalActionBtn: IModalTabActionBtn<S>; | ||
} | ||
|
||
interface IModalContext<S extends IModalTabState = IModalTabState> { | ||
tabs: Array<Exclude<IModalTabDeclaration<S>, 'reducer' | 'initialState'>>; | ||
state: { meta: IModalMetaState } & Record<string, S>; | ||
dispatch: Dispatch<IDispatchAction>; | ||
} | ||
|
||
const ModalContext = createContext<IModalContext>({ | ||
tabs: [], | ||
state: { | ||
meta: { | ||
selectedTabId: null, | ||
}, | ||
}, | ||
dispatch: () => {}, | ||
}); | ||
|
||
/** | ||
* @description defines state transition for meta information to manage the modal, meta action types | ||
* must be prefixed with the string 'META_' | ||
*/ | ||
const modalMetaReducer: IReducer<IModalMetaState> = (state, action) => { | ||
switch (action.type) { | ||
case 'META_selectedTabId': | ||
return { | ||
...state, | ||
selectedTabId: action.payload as string, | ||
}; | ||
default: | ||
return state; | ||
} | ||
}; | ||
|
||
export type IModalContextProviderProps<Tabs extends Array<IModalTabDeclaration<IModalTabState>>> = | ||
PropsWithChildren<{ | ||
tabs: Tabs; | ||
selectedTabId: Tabs[number]['id']; | ||
}>; | ||
|
||
export function ModalContextProvider<T extends Array<IModalTabDeclaration<IModalTabState>>>({ | ||
tabs, | ||
selectedTabId, | ||
children, | ||
}: IModalContextProviderProps<T>) { | ||
const modalTabDefinitions = useRef<IModalContext['tabs']>([]); | ||
|
||
const initialModalState = useRef<IModalContext['state']>({ | ||
// instantiate state with default meta information | ||
meta: { | ||
selectedTabId, | ||
}, | ||
}); | ||
|
||
const reducersMap = useMemo( | ||
() => | ||
tabs.reduce((result, { id, reducer, initialState, ...rest }) => { | ||
initialModalState.current[id] = initialState ?? {}; | ||
modalTabDefinitions.current.push({ id, ...rest }); | ||
result[id] = reducer; | ||
return result; | ||
}, {}), | ||
[tabs] | ||
); | ||
|
||
const combineReducers = useCallback( | ||
function (reducers: Record<string, IReducer<IModalTabState>>) { | ||
return (state: IModalContext['state'], action: IDispatchAction) => { | ||
const newState = { ...state }; | ||
|
||
if (/^meta_/i.test(action.type)) { | ||
newState.meta = modalMetaReducer(newState.meta, action); | ||
} else { | ||
newState[selectedTabId] = reducers[selectedTabId](newState[selectedTabId], action); | ||
} | ||
|
||
return newState; | ||
}; | ||
}, | ||
[selectedTabId] | ||
); | ||
|
||
const createInitialState = useCallback((state: IModalContext['state']) => { | ||
return state; | ||
}, []); | ||
|
||
const [state, dispatch] = useReducer( | ||
combineReducers(reducersMap), | ||
initialModalState.current, | ||
createInitialState | ||
); | ||
|
||
return ( | ||
<ModalContext.Provider value={{ tabs: modalTabDefinitions.current, state, dispatch }}> | ||
{children} | ||
</ModalContext.Provider> | ||
); | ||
} | ||
|
||
export const useModalContext = () => useContext(ModalContext); |
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,52 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import type { ComponentProps } from 'react'; | ||
import { AbstractStorybookMock } from '@kbn/shared-ux-storybook-mock'; | ||
|
||
import TabbedModal from '../..'; | ||
|
||
type TabbedModalProps = ComponentProps<typeof TabbedModal>; | ||
type TabbedModalServiceArguments = Record<string, unknown>; | ||
|
||
type Arguments = TabbedModalProps & TabbedModalServiceArguments; | ||
|
||
/** | ||
* Storybook parameters provided from the controls addon. | ||
*/ | ||
export type Params = Record<keyof Arguments, any>; | ||
|
||
export class StorybookMock extends AbstractStorybookMock< | ||
TabbedModalProps, | ||
TabbedModalServiceArguments, | ||
TabbedModalProps, | ||
TabbedModalServiceArguments | ||
> { | ||
propArguments = { | ||
tabs: { | ||
control: { | ||
type: 'array', | ||
}, | ||
defaultValue: [], | ||
}, | ||
}; | ||
|
||
serviceArguments = {}; | ||
|
||
dependencies = []; | ||
|
||
getProps(params?: Params): TabbedModalProps { | ||
return { | ||
tabs: this.getArgumentValue('tabs', params), | ||
}; | ||
} | ||
|
||
getServices(params: Params): TabbedModalServiceArguments { | ||
return {}; | ||
} | ||
} |
Oops, something went wrong.