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

Issues/2024 12 06 #160

Merged
merged 3 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions src/components/data/attributetable/attributetable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { useId, useState } from "react";

import {
Field,
SerializedFormData,
TypedField,
string2Title,
typedFieldByFields,
Expand All @@ -19,7 +20,7 @@ export type AttributeTableProps<T extends object = object> = {
labeledObject?: Record<string, { label: React.ReactNode; value: unknown }>;
editable?: boolean;
fields?: Field<T>[] | TypedField<T>[];
formProps?: FormProps<T>;
formProps?: FormProps<Record<keyof T, SerializedFormData[string]>>;
labelCancel?: string;
labelEdit?: string;
valign?: "middle" | "start";
Expand Down Expand Up @@ -60,7 +61,7 @@ export const AttributeTable = <T extends object = object>({

const renderTable = () => {
return editable ? (
<Form
<Form<Record<keyof T, SerializedFormData[string]>>
fieldsetClassName="mykn-attributetable__body"
showActions={isFormOpenState}
secondaryActions={[
Expand Down
1 change: 1 addition & 0 deletions src/components/data/datagrid/datagrid.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export const Filterable: Story = {
argTypes: {
...DataGridComponent.argTypes,
onFilter: { action: "onFilter" },
filterTransform: { action: "filterTransform" },
},
};

Expand Down
7 changes: 5 additions & 2 deletions src/components/data/datagrid/datagrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Field,
SerializedFormData,
TypedField,
TypedSerializedFormData,
filterDataArray,
typedFieldByFields,
} from "../../../lib";
Expand Down Expand Up @@ -67,7 +68,9 @@ export type DataGridProps<T extends object = object, F extends object = T> = {
* A function transforming the filter values.
* This can be used to adjust filter input to an API spec.
*/
filterTransform?: (value: T) => F;
filterTransform?: (
value: Partial<TypedSerializedFormData<keyof T & string>>,
) => F;

/**
* Can be any valid CSS `height` property or `"fill-available-space"` to
Expand Down Expand Up @@ -272,7 +275,7 @@ type PaginatorPropsAliases = {
* filtering, row selection, and pagination.
*
* @typeParam T - The shape of a single data row.
* @typeParam F - If the shape of the filtered returned by `filterTransform`
* @typeParam F - If the shape of the data returned by `filterTransform`
*/
export const DataGrid = <T extends object = object, F extends object = T>(
props: DataGridProps<T, F>,
Expand Down
9 changes: 7 additions & 2 deletions src/components/data/datagrid/datagridfilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ export const DataGridFilter = <
e.preventDefault();
}

const data = serializeForm(input.form as HTMLFormElement) as T;
const data = serializeForm<keyof T & string>(
input.form as HTMLFormElement,
true,
);
const _data = filterTransform ? filterTransform(data) : data;

// Reset page on filter (length of dataset may change).
Expand Down Expand Up @@ -122,7 +125,9 @@ export const DataGridFilter = <
}
onClick={
field.type === "boolean"
? (e: React.MouseEvent<HTMLInputElement>) => handleFilter(e)
? (e: React.MouseEvent<HTMLInputElement>) => {
handleFilter(e);
}
: undefined
}
/>
Expand Down
55 changes: 24 additions & 31 deletions src/components/data/datagrid/datagridthead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ export const DataGridTHead = <
>() => {
const ref = useRef<HTMLTableSectionElement>(null);

const thead = ref.current;
const table = thead?.parentNode as HTMLTableElement | undefined;
const scrollPane = table?.parentNode as HTMLDivElement | undefined;
const toolbar = scrollPane?.previousSibling as HTMLDivElement | undefined;

const {
allowOverflowX,
dataGridId,
Expand All @@ -40,49 +35,53 @@ export const DataGridTHead = <
window.removeEventListener("resize", stickyFix);
window.removeEventListener("scroll", stickyFix);
};
}, [ref.current, height, renderableFields]);
}, [ref.current, height]);

/**
* Fixes sticky behaviour due to `overflow-y: auto;` and `overflow-x: auto;`
* combination not being compatible with native sticky in all cases.
*/
const stickyFix = () => {
if (!ref.current || !thead || !table || !scrollPane || !toolbar) {
if (!ref.current) {
return;
}

requestAnimationFrame(() => {
thead.style.top = "";
const thead = ref.current;
const table = thead.parentNode as HTMLTableElement;
const scrollPane = table?.parentNode as HTMLDivElement;
const toolbar = scrollPane.previousSibling as HTMLDivElement | null;

requestAnimationFrame(() => {
const classOverflowX = "mykn-datagrid__scrollpane--overflow-x";
const classOverflowY = "mykn-datagrid__scrollpane--overflow-y";

// Reset.
thead.style.top = "";
scrollPane.classList.remove(classOverflowX);
scrollPane.classList.remove(classOverflowY);

// Active conditions.
const isHeightSet = Boolean(height);
const isHeight100Percent = height === "100%"; // (100)% is edge case.
const isHeightExplicitlySet = isHeightSet && !isHeight100Percent;
const isScrollX =
allowOverflowX && scrollPane.scrollWidth - scrollPane.clientWidth > 20;
allowOverflowX && scrollPane.scrollWidth > scrollPane.clientWidth;

// Available fixes.
const shouldOverflowX = isScrollX && !isHeightExplicitlySet;
const shouldOverflowY = isHeightExplicitlySet;
const shouldStickyFix = shouldOverflowX;
const shouldOverflowX = !isHeightSet && isScrollX;
const shouldOverflowY = isHeightSet;
const shouldStickyFix = isHeight100Percent || (!height && isScrollX);

// Apply overflow CSS rules using classes.
if (scrollPane.classList.contains(classOverflowX) !== shouldOverflowX) {
scrollPane.classList.toggle(classOverflowX, shouldOverflowX);
}
if (scrollPane.classList.contains(classOverflowY) !== shouldOverflowY) {
scrollPane.classList.toggle(classOverflowY, shouldOverflowY);
}
scrollPane.classList.toggle(classOverflowX, shouldOverflowX);
scrollPane.classList.toggle(classOverflowY, shouldOverflowY);

// No need for fallback implementation as only `overflow-y: auto;` has to
// be set.
if (!shouldStickyFix) return;
if (!shouldStickyFix) {
return;
}

// Fix the sticky top behaviour.
thead.style.transform = "";
const boundingClientRect = thead.getBoundingClientRect();
const boundingTop = boundingClientRect.top;

Expand All @@ -93,16 +92,10 @@ export const DataGridTHead = <
// Invert the negative offset caused by scrolling and push the position
// in the opposite direction, then apply some compensation for the
// existing offset.
const compensation = Math.max(
const compensation =
boundingTop * -1 +
(toolbar ? toolbar.clientHeight : 0) * (isHeight100Percent ? 1 : 2),
0,
);

// No compensation.
if (!compensation) return;

thead.style.top = `${compensation}px`;
(toolbar ? toolbar.clientHeight : 0) * (isHeight100Percent ? 1 : 2);
thead.style.top = compensation + "px";
});
};

Expand Down
2 changes: 1 addition & 1 deletion src/components/data/datagrid/datagridtoolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export const DataGridToolbar = <
labelSubmit={ucFirst(_labelSaveFieldSelection)}
onSubmit={(e) => {
const form = e.target as HTMLFormElement;
const data = serializeForm(form);
const data = serializeForm(form, false);
const selectedFields = (data.fields || []) as string[];
const newTypedFieldsState = fields.map((f) => ({
...f,
Expand Down
11 changes: 9 additions & 2 deletions src/components/form/checkbox/checkboxgroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,37 @@ export type CheckboxGroupProps = ChoiceFieldProps<
>;

export const CheckboxGroup: React.FC<CheckboxGroupProps> = ({
form,
id = "",
label,
name,
options,
variant,
required,
onChange,
onClick,
}) => {
const reactId = useId();
const _id = id || reactId;

return (
<div className={clsx("mykn-checkboxgroup")}>
<div className={clsx("mykn-checkboxgroup")} aria-label={label}>
{options.map((option, index) => {
const checkboxId = `${_id}-choice-${index}`;

return (
<Checkbox
key={option.value || option.label || index}
checked={option.selected}
defaultChecked={option.defaultChecked}
form={form}
id={checkboxId}
key={option.value || option.label || index}
name={name}
required={required}
value={option.value || option.label}
variant={variant}
onChange={onChange}
onClick={onClick}
>
{option.label}
</Checkbox>
Expand Down
15 changes: 12 additions & 3 deletions src/components/form/choicefield/choicefield.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,30 @@ export type ChoiceFieldProps<
/** Can be used to generate `SelectOption` components from an array of objects. */
options: Option[];

/** The associated form's id. */
form?: string;

/** The (accessible) label .*/
label?: string;

/** Form element name. */
name?: string;

/** Whether a value is required. */
required?: boolean;

/** Form element type. */
type?: string;

/** Gets called when the input is blurred. */
onBlur?: React.FormEventHandler<FormElement>;

/** Value of the form element */
value?: Option["value"] | null;

/** The variant (style) of the form element. */
variant?: "normal" | "transparent";

/** Gets called when the input is blurred. */
onBlur?: React.FormEventHandler<FormElement>;

/**
*
* A custom "change" event created with `detail` set to the selected option.
Expand Down
2 changes: 1 addition & 1 deletion src/components/form/datepicker/datepicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export type DatePickerProps = Omit<
placeholder?: string;

/** Can be a `Date` `[Date, Date]` or a (date) `string` or a (time) `number`. */
value?: DatePickerValue | string | number;
value?: DatePickerValue | string | number | null;

labelNextYear?: string;
labelPreviousYear?: string;
Expand Down
26 changes: 15 additions & 11 deletions src/components/form/form/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,28 @@ import clsx from "clsx";
import React, { FormEvent, useContext, useEffect, useState } from "react";

import { ConfigContext } from "../../../contexts";
import { FormField } from "../../../lib/form/typeguards";
import { getValueFromFormData, serializeForm } from "../../../lib/form/utils";
import { ucFirst } from "../../../lib";
import { useIntl } from "../../../lib";
import { FormField } from "../../../lib";
import {
SerializedFormData,
getValueFromFormData,
serializeForm,
} from "../../../lib";
import {
DEFAULT_VALIDATION_ERROR_REQUIRED,
Validator,
getErrorFromErrors,
validateForm,
} from "../../../lib/form/validation";
import { forceArray } from "../../../lib/format/array";
import { ucFirst } from "../../../lib/format/string";
import { useIntl } from "../../../lib/i18n/useIntl";
} from "../../../lib";
import { forceArray } from "../../../lib";
import { ButtonProps } from "../../button";
import { Toolbar, ToolbarItem, ToolbarProps } from "../../toolbar";
import { ErrorMessage } from "../errormessage";
import { FormControl } from "../formcontrol";
import "./form.scss";

export type FormProps<T extends object = object> = Omit<
export type FormProps<T extends SerializedFormData = SerializedFormData> = Omit<
React.ComponentProps<"form">,
"onChange" | "onSubmit"
> & {
Expand Down Expand Up @@ -105,7 +109,7 @@ export type FormProps<T extends object = object> = Omit<
*
* @typeParam T - The shape of the serialized form data.
*/
export const Form = <T extends object = object>({
export const Form = <T extends SerializedFormData = SerializedFormData>({
buttonProps,
children,
debug = false,
Expand Down Expand Up @@ -134,7 +138,7 @@ export const Form = <T extends object = object>({
const _debug = debug || contextDebug;
const _nonFieldErrors = forceArray(nonFieldErrors);

const [valuesState, setValuesState] = useState(initialValues);
const [valuesState, setValuesState] = useState<T>(initialValues);
const [errorsState, setErrorsState] = useState(errors || {});

useEffect(() => {
Expand Down Expand Up @@ -178,7 +182,7 @@ export const Form = <T extends object = object>({
const form = (event.target as HTMLInputElement).form;

if (form && !onChange) {
const data = serializeForm(form, useTypedResults) as T;
const data = serializeForm<keyof T & string>(form, useTypedResults) as T;
setValuesState(data);
}
};
Expand All @@ -200,7 +204,7 @@ export const Form = <T extends object = object>({
}

const form = event.target as HTMLFormElement;
const data = serializeForm(form, useTypedResults) as T;
const data = serializeForm<keyof T & string>(form, useTypedResults) as T;

if (onSubmit) {
onSubmit(event, data);
Expand Down
9 changes: 7 additions & 2 deletions src/components/form/radio/radiogroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ export type RadioGroupProps = ChoiceFieldProps<
>;
export const RadioGroup: React.FC<RadioGroupProps> = ({
id = "",
label,
name,
options,
required,
variant,
onChange,
onClick,
}) => {
const reactId = useId();
const _id = id || reactId;
Expand All @@ -34,7 +37,7 @@ export const RadioGroup: React.FC<RadioGroupProps> = ({
};

return (
<div className={clsx("mykn-radiogroup")}>
<div className={clsx("mykn-radiogroup")} aria-label={label}>
{options.map((option, index) => {
const radioId = `${_id}-choice-${index}`;
const optionValue = option.value || option.label;
Expand All @@ -43,11 +46,13 @@ export const RadioGroup: React.FC<RadioGroupProps> = ({
<Radio
id={radioId}
key={optionValue || index}
checked={selectedValueState === optionValue}
name={name}
required={required}
value={optionValue}
variant={variant}
checked={selectedValueState === optionValue}
onChange={handleChange}
onClick={onClick}
>
{option.label}
</Radio>
Expand Down
Loading
Loading