Skip to content

Commit

Permalink
✨ - feat: make AttributeTable editable
Browse files Browse the repository at this point in the history
  • Loading branch information
svenvandescheur committed Jun 14, 2024
1 parent 459de79 commit f0b312d
Show file tree
Hide file tree
Showing 12 changed files with 303 additions and 50 deletions.
17 changes: 8 additions & 9 deletions src/components/data/attributetable/attributetable.scss
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
.mykn-attributetable {
display: table;

&__row {
display: table-row;
&__body {
display: grid;
grid-template-columns: auto auto;
column-gap: var(--spacing-h);
}

&__cell {
display: table-cell;
font-family: var(--typography-font-family-body);
display: flex;
align-items: center;
font-family: var(--typography-font-family-body), sans-serif;
font-size: var(--typography-font-size-body-s);
font-weight: var(--typography-font-weight-normal);
line-height: var(--typography-line-height-body-s);
margin: 0;

&:not(:last-child) {
padding-right: var(--spacing-h);
}
}

&__key {
&__cell--key {
font-weight: var(--typography-font-weight-bold);
}
}
74 changes: 65 additions & 9 deletions src/components/data/attributetable/attributetable.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { userEvent, within } from "@storybook/test";
import * as React from "react";

import { ButtonLink } from "../../button";
Expand All @@ -13,6 +14,21 @@ const meta: Meta<typeof AttributeTable> = {
export default meta;
type Story = StoryObj<typeof meta>;

export const AttributeTableComponent: Story = {
args: {
object: {
url: "https://www.example.com",
omschrijving: "Afvalpas vervangen",
zaaktype: "https://www.example.com",
versie: 2,
opmerkingen: null,
actief: false,
toekomstig: false,
concept: true,
},
},
};

export const LabeledAttributeTableComponent: Story = {
args: {
labeledObject: {
Expand Down Expand Up @@ -56,17 +72,57 @@ export const LabeledAttributeTableComponentWithNodes: Story = {
},
};

export const AttributeTableComponent: Story = {
export const Editable: Story = {
args: {
editable: true,
fields: [
{
name: "first_name",
type: "string",
},
{
name: "last_name",
type: "string",
},
{
name: "school_year",
type: "string",
options: [
{ label: "Freshman" },
{ label: "Sophomore" },
{ label: "Junior" },
{ label: "Senior" },
{ label: "Graduate" },
],
},
],
object: {
url: "https://www.example.com",
omschrijving: "Afvalpas vervangen",
zaaktype: "https://www.example.com",
versie: 2,
opmerkingen: null,
actief: false,
toekomstig: false,
concept: true,
first_name: "",
last_name: "",
school_year: "",
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

const editFirstName = canvas.getByLabelText('Edit "First name"');
await userEvent.click(editFirstName, { delay: 10 });
const firstName = canvas.getByLabelText("First name");
await userEvent.type(firstName, "John", { delay: 10 });

const editLastName = canvas.getByLabelText('Edit "Last name"');
await userEvent.click(editLastName, { delay: 10 });
const lastName = canvas.getByLabelText("Last name");
await userEvent.type(lastName, "John", { delay: 10 });

const editSchoolYear = canvas.getByLabelText('Edit "School year"');
await userEvent.click(editSchoolYear, { delay: 10 });
const schoolYear = canvas.getByLabelText("School year");
await userEvent.click(schoolYear);
const junior = canvas.getByText("Junior");
await userEvent.click(junior);

const submit = canvas.getByText("Submit");
await userEvent.click(submit);
},
};
181 changes: 154 additions & 27 deletions src/components/data/attributetable/attributetable.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,192 @@
import React from "react";
import React, { useId, useState } from "react";

import {
Attribute,
AttributeData,
Field,
LabeledAttributeData,
TypedField,
field2Title,
isPrimitive,
typedFieldByFields,
useIntl,
} from "../../../lib";
import { Button } from "../../button";
import { Form, FormControl, FormProps } from "../../form";
import { Value } from "../value";
import "./attributetable.scss";

export type AttributeTableProps = {
editable?: boolean;
formProps?: FormProps;
object?: AttributeData;
labeledObject?: LabeledAttributeData;
fields?: Array<keyof LabeledAttributeData | keyof AttributeData>;
};

export type AttributeTableRowProps = {
object?: AttributeData;
labeledObject?: LabeledAttributeData;
field: keyof LabeledAttributeData | keyof AttributeData;
labelEdit?: string;
labelCancel?: string;
fields?: Field[] | TypedField[];
};

export const AttributeTable: React.FC<AttributeTableProps> = ({
editable = false,
object = {},
labeledObject = {},
fields = Object.keys(object).concat(Object.keys(labeledObject)),
formProps,
labelEdit,
labelCancel,
...props
}) => (
<div className="mykn-attributetable" {...props}>
{fields.map((field) => (
}) => {
const intl = useIntl();
const [isFormOpenState, setIsFormOpenState] = useState(false);
const typedFields = typedFieldByFields(fields, [object]);

const _labelCancel = labelCancel
? labelCancel
: intl.formatMessage({
id: "mykn.components.AttributeTable.labelCancel",
description: "mykn.components.AttributeTable: The cancel label",
defaultMessage: "Annuleren",
});

const renderTable = () => {
return editable ? (
<Form
fieldsetClassName="mykn-attributetable__body"
showActions={isFormOpenState}
secondaryActions={[
{
children: _labelCancel,
variant: "secondary",
onClick: () => setIsFormOpenState(false),
},
]}
{...formProps}
>
{renderRows()}
</Form>
) : (
<div className="mykn-attributetable__body">{renderRows()}</div>
);
};

const renderRows = () =>
typedFields.map((field) => (
<AttributeTableRow
key={field}
key={field.name}
editable={editable}
field={field}
isFormOpen={isFormOpenState}
object={object}
labeledObject={labeledObject}
labelEdit={labelEdit}
onClick={() => setIsFormOpenState(true)}
/>
))}
</div>
);
));

return (
<div className="mykn-attributetable" {...props}>
{renderTable()}
</div>
);
};

export type AttributeTableRowProps = {
editable?: boolean;
object?: AttributeData;
labeledObject?: LabeledAttributeData;
field: TypedField;
isFormOpen: boolean;
labelEdit?: string;
onClick: React.MouseEventHandler;
};

export const AttributeTableRow: React.FC<AttributeTableRowProps> = ({
editable = false,
field,
isFormOpen,
object = {},
labeledObject = {},
field,
labelEdit,
onClick,
}) => {
const fieldInObject = Object.keys(object).includes(field);
let value = fieldInObject ? object[field] : labeledObject[field].value;
const id = useId();
const intl = useIntl();
const [isEditingState, setIsEditingState] = useState(false);
const name = field.name;
const fieldInObject = Object.keys(object).includes(name);
const label = fieldInObject ? field2Title(name) : labeledObject[name].label;
const rawValue = fieldInObject ? object[name] : labeledObject[name].value;
const isEditing = isFormOpen && isEditingState;

if (isPrimitive(value) || value === null) {
value = <Value value={value as Attribute} />;
}
const handleCLick: React.MouseEventHandler = (e) => {
e.preventDefault();
setIsEditingState(true);
onClick(e);
};

const label = fieldInObject ? field2Title(field) : labeledObject[field].label;
const _labelEdit = labelEdit
? labelEdit
: intl.formatMessage(
{
id: "mykn.components.AttributeTable.labelEdit",
description:
"mykn.components.AttributeTable: The edit value (accessible) label",
defaultMessage: 'Bewerk "{label}"',
},
{ ...field, label: label || field.name },
);

/**
* Renders the value (if not already a React.ReactNode).
*/
const renderValue = () => {
return editable ? (
<Button
variant="transparent"
pad={false}
onClick={handleCLick}
aria-label={_labelEdit}
>
{renderDisplayValue()}
</Button>
) : (
renderDisplayValue()
);
};

const renderDisplayValue = () => {
if (isPrimitive(rawValue) || rawValue === null) {
return <Value value={rawValue as Attribute} />;
}

return rawValue;
};

const renderInput = () => {
return (
<FormControl
id={`${id}_input`}
defaultChecked={
field.type === "boolean" ? Boolean(rawValue) : undefined
}
name={name}
options={field.options}
type={field.type === "number" ? "number" : undefined}
value={rawValue?.toString()}
hidden={!isEditing}
/>
);
};

return (
<div className="mykn-attributetable__row">
<div className="mykn-attributetable__cell mykn-attributetable__key">
{label}
<>
<div className="mykn-attributetable__cell mykn-attributetable__cell--key">
{isEditing ? <label htmlFor={`${id}_input`}>{label}</label> : label}
</div>
<div className="mykn-attributetable__cell">{value}</div>
</div>
<div className="mykn-attributetable__cell mykn-attributetable__cell--value">
{(!editable || !isEditing) && renderValue()}
{editable && renderInput()}
</div>
</>
);
};
8 changes: 6 additions & 2 deletions src/components/form/form/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export type FormProps = Omit<
/** The direction in which to render the form. */
direction?: "vertical" | "horizontal";

/** The classname to use for the fieldset. */
fieldsetClassName?: string;

/** The initial form values, only applies on initial render. */
initialValues?: AttributeData<Attribute | Attribute[]>;

Expand Down Expand Up @@ -91,6 +94,7 @@ export const Form: React.FC<FormProps> = ({
direction = "vertical",
errors,
fields = [],
fieldsetClassName = "mykn-form__fieldset",
initialValues = {},
secondaryActions = [],
labelSubmit = "",
Expand Down Expand Up @@ -194,7 +198,7 @@ export const Form: React.FC<FormProps> = ({
)}

{Boolean(fields?.length) && (
<div className="mykn-form__fieldset">
<div className={fieldsetClassName}>
{fields.map((field, index) => {
const value =
(field.value as string) ||
Expand All @@ -216,7 +220,7 @@ export const Form: React.FC<FormProps> = ({
</div>
)}

{children && <div className="mykn-form__fieldset">{children}</div>}
{children && <div className={fieldsetClassName}>{children}</div>}

{_debug && <pre role="log">{JSON.stringify(valuesState)}</pre>}

Expand Down
4 changes: 4 additions & 0 deletions src/components/form/select/select.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
position: relative;
text-align: start;

&[aria-hidden="true"] {
display: none;
}

&--size-fit-content {
width: fit-content;
}
Expand Down
Loading

0 comments on commit f0b312d

Please sign in to comment.