Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: add editable table text #846

Merged
merged 20 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2302519
docs: add editable table text
Sebastien-Ahkrin Dec 23, 2024
1ec8f42
feat: create switchable input
Sebastien-Ahkrin Dec 26, 2024
7540622
wip: bug with input size that take the entire place (and more) when c…
Sebastien-Ahkrin Dec 27, 2024
2615a6b
Merge remote-tracking branch 'origin/main' into feat-editable-table
Sebastien-Ahkrin Jan 7, 2025
0eb593f
feat: correctly fix the table width size
Sebastien-Ahkrin Jan 8, 2025
a7f40b6
feat: change inline editable renderer
Sebastien-Ahkrin Jan 10, 2025
7ab1fe8
refactor: make a poc
Sebastien-Ahkrin Jan 10, 2025
45a70b6
refactor: remove hardcoded values
Sebastien-Ahkrin Jan 10, 2025
076a31b
refactor: change folder names
Sebastien-Ahkrin Jan 10, 2025
f29353b
refactor: change component name
Sebastien-Ahkrin Jan 13, 2025
25f31c9
fix: run stylelint fix
Sebastien-Ahkrin Jan 13, 2025
98f1c2b
refactor: change component name
Sebastien-Ahkrin Jan 14, 2025
f61769d
refactor: add min height to empty value render
Sebastien-Ahkrin Jan 14, 2025
1210df9
refactor: remove docs for stylelint
Sebastien-Ahkrin Jan 14, 2025
6c4d4e7
refactor: make some changes according to reviewers
Sebastien-Ahkrin Jan 16, 2025
fd7ef58
refactor: change docs
Sebastien-Ahkrin Jan 17, 2025
4d3f9c7
feat: press enter will save the input
Sebastien-Ahkrin Jan 20, 2025
51510ab
fix: remove outline from input
stropitek Jan 21, 2025
6488396
refactor: change stopRendering props to be exit
Sebastien-Ahkrin Jan 21, 2025
3b8c7cc
Merge remote-tracking branch 'origin/feat-editable-table' into feat-e…
Sebastien-Ahkrin Jan 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/inline_editable_renderer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './inline_editable_renderer.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Colors } from '@blueprintjs/core';
import styled from '@emotion/styled';
import type { ReactNode } from 'react';
import { useCallback, useMemo, useState } from 'react';

export interface InlineRendererEditableProps<T extends HTMLElement> {
ref: (node: T | null) => void;
toggle: () => void;
}

export interface InlineEditableProps<T extends HTMLElement> {
renderEditable: (props: InlineRendererEditableProps<T>) => ReactNode;
children: ReactNode;
}

export const InlineEditableInput = styled.input`
width: 100%;
height: 100%;
box-shadow: 0 0 1px 1px ${Colors.GRAY1};
position: absolute;
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 an editable input field when focused or clicked
Sebastien-Ahkrin marked this conversation as resolved.
Show resolved Hide resolved
* and toggles back to the original content when the input loses focus.
*/
export function InlineEditable<T extends HTMLElement>(
Sebastien-Ahkrin marked this conversation as resolved.
Show resolved Hide resolved
props: InlineEditableProps<T>,
) {
const { children, renderEditable } = props;
const [isInputRendered, setIsInputRendered] = useState(false);

const toggle = useCallback(() => {
return setIsInputRendered((old) => !old);
}, []);

const renderEditableProps = useMemo<InlineRendererEditableProps<T>>(() => {
return {
isRendered: isInputRendered,
ref: (node) => {
if (!node) return;
node.focus();
},
toggle,
};
}, [isInputRendered, toggle]);

return (
<div style={{ position: 'relative' }}>
<div style={{ visibility: isInputRendered ? 'visible' : 'hidden' }}>
{renderEditable(renderEditableProps)}
</div>

<Container
tabIndex={isInputRendered ? -1 : 0}
onFocus={() => setIsInputRendered(true)}
onClick={toggle}
>
{children}
</Container>
</div>
);
}
199 changes: 199 additions & 0 deletions stories/components/editable.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
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<TableData>();

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 } }) => (
<InlineEditableComponent
renderEditable={({ ref, toggle }) => (
<InlineEditableInput
ref={ref}
defaultValue={getValue()}
onBlur={(event) => {
toggle();
changeValue(index, 'label', event.currentTarget.value);
}}
/>
)}
>
{getValue()}
</InlineEditableComponent>
),
}),
helper.accessor('field', {
header: 'Field',
cell: ({ getValue, row: { index } }) => (
<InlineEditableComponent
renderEditable={({ ref, toggle }) => (
<InlineEditableInput
ref={ref}
defaultValue={getValue()}
onBlur={(event) => {
toggle();
changeValue(index, 'field', event.currentTarget.value);
}}
/>
)}
>
{getValue()}
</InlineEditableComponent>
),
}),
helper.accessor('format', {
header: 'Format',
cell: ({ getValue, row: { index } }) => (
<InlineEditableComponent
renderEditable={({ ref, toggle }) => (
<InlineEditableInput
ref={ref}
defaultValue={getValue()}
onBlur={(event) => {
toggle();
changeValue(index, 'format', event.currentTarget.value);
}}
/>
)}
>
{getValue()}
</InlineEditableComponent>
),
}),
helper.accessor('visible', { header: 'Visible' }),
helper.display({
header: ' ',
cell: ({ row: { index } }) => {
return (
<Button
type="button"
icon="trash"
tabIndex={-1}
onClick={() => deleteIndex(index)}
/>
);
},
}),
];
}, [changeValue, deleteIndex]);

return (
<div
style={{
width: '100%',
display: 'flex',
flexDirection: 'row',
gap: 5,
}}
>
<div>
<Table<TableData>
data={state}
columns={columns}
estimatedRowHeight={() => 50}
/>
</div>

<pre>{JSON.stringify(state, null, 2)}</pre>
</div>
);
}

export const InlineEditable = {
render: () => {
const [state, setState] = useState('Hello, World!');

return (
<div style={{ width: 100 }}>
<span>State: {state}</span>
<InlineEditableComponent
renderEditable={({ ref, toggle }) => (
<InlineEditableInput
ref={ref}
defaultValue={state}
onBlur={(event) => {
toggle();

setState(event.currentTarget.value);
}}
/>
)}
>
{state}
</InlineEditableComponent>
</div>
);
},
} satisfies StoryObj;
Loading