diff --git a/src/components/inline_editable_renderer/index.ts b/src/components/inline_editable_renderer/index.ts new file mode 100644 index 00000000..d2dd3714 --- /dev/null +++ b/src/components/inline_editable_renderer/index.ts @@ -0,0 +1 @@ +export * from './inline_editable_renderer.js'; diff --git a/src/components/inline_editable_renderer/inline_editable_renderer.tsx b/src/components/inline_editable_renderer/inline_editable_renderer.tsx new file mode 100644 index 00000000..75850be7 --- /dev/null +++ b/src/components/inline_editable_renderer/inline_editable_renderer.tsx @@ -0,0 +1,86 @@ +import { Colors } from '@blueprintjs/core'; +import styled from '@emotion/styled'; +import type { KeyboardEvent, ReactNode } from 'react'; +import { useCallback, useMemo, useState } from 'react'; + +export interface InlineRendererEditableProps { + ref: (node: T | null) => void; + /** + * Function to exit the editable state and display the children content. + */ + exit: () => void; + onKeyDown: (event: KeyboardEvent) => void; +} + +export interface InlineEditableProps { + renderEditable: (props: InlineRendererEditableProps) => ReactNode; + children: ReactNode; +} + +export const InlineEditableInput = styled.input` + width: 100%; + height: 100%; + box-shadow: 0 0 1px 1px ${Colors.GRAY1}; + position: absolute; + outline: none; + inset: 0; +`; + +const Container = styled.div` + min-width: 100%; + width: 100%; + min-height: 21px; + + :focus, + :hover { + box-shadow: 0 0 1px 1px ${Colors.GRAY1}; + } +`; + +/** + * The `InlineEditable` component allows for inline editing of its content. + * It renders a component with `renderEditable` when focused or clicked + * and toggles back to the original content when the input loses focus. + */ +export function InlineEditable( + props: InlineEditableProps, +) { + const { children, renderEditable } = props; + const [isInputRendered, setIsInputRendered] = useState(false); + + const toggle = useCallback(() => { + return setIsInputRendered((old) => !old); + }, []); + + const renderEditableProps = useMemo>(() => { + return { + isRendered: isInputRendered, + onKeyDown: (event) => { + if (event.key === 'Enter') { + setIsInputRendered(false); + } + }, + ref: (node) => { + if (!node) return; + node.focus(); + }, + exit: () => setIsInputRendered(false), + }; + }, [isInputRendered]); + + return ( +
+
+ {renderEditable(renderEditableProps)} +
+ + setIsInputRendered(true)} + onClick={toggle} + > + {children} + +
+ ); +} diff --git a/stories/components/editable.stories.tsx b/stories/components/editable.stories.tsx new file mode 100644 index 00000000..6dbf6c67 --- /dev/null +++ b/stories/components/editable.stories.tsx @@ -0,0 +1,203 @@ +import { Button } from '@blueprintjs/core'; +import type { StoryObj } from '@storybook/react'; +import { useCallback, useMemo, useState } from 'react'; + +import { createTableColumnHelper, Table } from '../../src/components/index.js'; +import { + InlineEditable as InlineEditableComponent, + InlineEditableInput, +} from '../../src/components/inline_editable_renderer/index.js'; + +export default { + title: 'Components / InlineEditableComponent', +}; + +interface TableData { + id: number; + label: string; + field: string; + format: string; + visible: boolean; +} + +const helper = createTableColumnHelper(); + +const data: TableData[] = [ + { label: 'Name', field: 'info.name', format: '', visible: true }, + { + label: 'Number of scans', + field: 'info.numberOfScans', + format: '0', + visible: true, + }, + { + label: 'Pulse sequence', + field: 'info.pulseSequence', + format: '', + visible: true, + }, + { + label: 'Frequency', + field: 'meta..DIGITSERRE', + format: '0', + visible: false, + }, +].map((item, index) => ({ id: index, ...item })); + +export function InsideTable() { + const [state, setState] = useState(data); + + const deleteIndex = useCallback((index: number) => { + return setState((prev) => prev.filter((_, i) => i !== index)); + }, []); + + const changeValue = useCallback( + (rowIndex: number, key: string, value: string) => { + setState((prev) => { + const element = prev.at(rowIndex); + + if (!element) { + return prev; + } + + return prev.map((item, index) => { + if (index === rowIndex) { + return { ...element, [key]: value }; + } + + return item; + }); + }); + }, + [], + ); + + const columns = useMemo(() => { + return [ + helper.accessor('id', { header: '#' }), + helper.accessor('label', { + header: 'Label', + cell: ({ getValue, row: { index } }) => ( + ( + { + exit(); + changeValue(index, 'label', event.currentTarget.value); + }} + /> + )} + > + {getValue()} + + ), + }), + helper.accessor('field', { + header: 'Field', + cell: ({ getValue, row: { index } }) => ( + ( + { + exit(); + changeValue(index, 'field', event.currentTarget.value); + }} + /> + )} + > + {getValue()} + + ), + }), + helper.accessor('format', { + header: 'Format', + cell: ({ getValue, row: { index } }) => ( + ( + { + exit(); + changeValue(index, 'format', event.currentTarget.value); + }} + /> + )} + > + {getValue()} + + ), + }), + helper.accessor('visible', { header: 'Visible' }), + helper.display({ + header: ' ', + cell: ({ row: { index } }) => { + return ( +