Skip to content

Commit

Permalink
Merge pull request elastic#18 from eokoneyo/skeleton-share
Browse files Browse the repository at this point in the history
Skeleton Tabbed Modal
  • Loading branch information
rshen91 authored Mar 7, 2024
2 parents 1568ff4 + d7fa09f commit d079781
Show file tree
Hide file tree
Showing 16 changed files with 587 additions and 88 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,7 @@ test/plugin_functional/plugins/session_notifications @elastic/kibana-core
x-pack/plugins/session_view @elastic/kibana-cloud-security-posture
packages/kbn-set-map @elastic/kibana-operations
examples/share_examples @elastic/appex-sharedux
packages/kbn-share-modal @elastic/appex-sharedux
src/plugins/share @elastic/appex-sharedux
packages/kbn-shared-svg @elastic/obs-ux-infra_services-team
packages/shared-ux/avatar/solution @elastic/appex-sharedux
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,7 @@
"@kbn/shared-ux-router-types": "link:packages/shared-ux/router/types",
"@kbn/shared-ux-storybook-config": "link:packages/shared-ux/storybook/config",
"@kbn/shared-ux-storybook-mock": "link:packages/shared-ux/storybook/mock",
"@kbn/shared-ux-tabbed-modal": "link:packages/shared-ux/modal/tabbed",
"@kbn/shared-ux-utility": "link:packages/kbn-shared-ux-utility",
"@kbn/slo-schema": "link:x-pack/packages/kbn-slo-schema",
"@kbn/snapshot-restore-plugin": "link:x-pack/plugins/snapshot_restore",
Expand Down
File renamed without changes.
79 changes: 79 additions & 0 deletions packages/shared-ux/modal/tabbed/README.mdx
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]} />
```
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export { TabbedModal } from './src/tabbed_modal';
export type { IModalTabDeclaration } from './src/context';
6 changes: 6 additions & 0 deletions packages/shared-ux/modal/tabbed/kibana.jsonc
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"
}

6 changes: 6 additions & 0 deletions packages/shared-ux/modal/tabbed/package.json
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"
}
152 changes: 152 additions & 0 deletions packages/shared-ux/modal/tabbed/src/context/index.tsx
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);
52 changes: 52 additions & 0 deletions packages/shared-ux/modal/tabbed/src/storybook/setup.ts
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 {};
}
}
Loading

0 comments on commit d079781

Please sign in to comment.