Skip to content

Commit

Permalink
UseControllableState-API (#2585)
Browse files Browse the repository at this point in the history
* 🎨 Added useControllableState

* 🎨 Forbedret Accordion-api for controlled

* 🎨 Forbedret readmore-api for controlled

* 🎨 Forbedret internt expandablerow-api for controlled open

* 🎨 Forbedret tooltip-api for controlled open

* 🎨 Forbedret dropdown-api for controlled open

* 📝 La til dokumentasjon

* 📝 changeset

* 📝 Bedre kommentarer

* 🎨 Forenklet hooks

* 🎨 DefaultValue er required i useControllabeState
  • Loading branch information
KenAJoh authored Jan 11, 2024
1 parent 3ee714e commit 76eefdf
Show file tree
Hide file tree
Showing 13 changed files with 197 additions and 100 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-months-relate-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@navikt/ds-react": minor
---

Accordion.Item, Dropdown, ReadMore og Tooltip: Har en ny prop `onOpenChange?: (open: boolean) => void;` som forteller nå-state når `open`-state endrer seg. Dette vil være nyttig hvis man ikke bruker controlled-state, men fortsatt ønsker å vite om komponenten er `open` eller ikke (f.eks logging).
5 changes: 5 additions & 0 deletions .changeset/friendly-months-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@navikt/ds-react": minor
---

Controlled-state: Accordion.Item, Dropdown, ReadMore, Table.ExpandableRow og Tooltip har oppdatert intern håndtering av controlled state. Endringen skal ikke påvirke dagens API.
44 changes: 22 additions & 22 deletions @navikt/core/react/src/accordion/AccordionItem.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import cl from "clsx";
import React, {
createContext,
forwardRef,
useContext,
useRef,
useState,
} from "react";
import { AccordionContext } from "./AccordionContext";
import React, { createContext, forwardRef, useContext, useRef } from "react";
import { omit } from "../util";
import { useControllableState } from "../util/hooks/useControllableState";
import { AccordionContext } from "./AccordionContext";

export interface AccordionItemProps
extends React.HTMLAttributes<HTMLDivElement> {
Expand All @@ -26,6 +21,10 @@ export interface AccordionItemProps
* @default false
*/
defaultOpen?: boolean;
/**
* Callback for current open-state
*/
onOpenChange?: (open: boolean) => void;
}

export interface AccordionItemContextProps {
Expand All @@ -37,20 +36,21 @@ export const AccordionItemContext =
createContext<AccordionItemContextProps | null>(null);

const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
({ children, className, open, defaultOpen = false, ...rest }, ref) => {
const [internalOpen, setInternalOpen] = useState<boolean>(defaultOpen);
const context = useContext(AccordionContext);
(
{ children, className, open, defaultOpen = false, onOpenChange, ...rest },
ref,
) => {
const [_open, _setOpen] = useControllableState({
defaultValue: defaultOpen,
value: open,
onChange: onOpenChange,
});

const [_open, _setOpen] = useState(defaultOpen);
const context = useContext(AccordionContext);
const shouldAnimate = useRef<boolean>(!(Boolean(open) || defaultOpen));

const handleOpen = () => {
if (open === undefined) {
const newOpen = !_open;
_setOpen(newOpen);
setInternalOpen(newOpen);
} else {
setInternalOpen(!open);
}
_setOpen((x) => !x);
shouldAnimate.current = true;
};

Expand All @@ -61,7 +61,7 @@ const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
return (
<div
className={cl("navds-accordion__item", className, {
"navds-accordion__item--open": open ?? internalOpen,
"navds-accordion__item--open": _open,
"navds-accordion__item--neutral": context?.variant === "neutral",
"navds-accordion__item--no-animation": !shouldAnimate.current,
})}
Expand All @@ -70,15 +70,15 @@ const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
>
<AccordionItemContext.Provider
value={{
open: open ?? internalOpen,
open: _open,
toggleOpen: handleOpen,
}}
>
{children}
</AccordionItemContext.Provider>
</div>
);
}
},
);

export default AccordionItem;
13 changes: 8 additions & 5 deletions @navikt/core/react/src/accordion/accordion.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { useState } from "react";
import { Accordion, AccordionProps } from ".";
import { Table } from "..";
import AccordionContent from "./AccordionContent";
import AccordionHeader from "./AccordionHeader";
import AccordionItem from "./AccordionItem";
import { Accordion, AccordionProps } from ".";
import { Table } from "..";

export default {
title: "ds-react/Accordion",
Expand Down Expand Up @@ -78,22 +78,25 @@ const Item = (props) => {

if (props.defaultOpen) {
return (
<Accordion.Item defaultOpen={props.defaultOpen}>
<Accordion.Item
defaultOpen={props.defaultOpen}
onOpenChange={console.log}
>
<Accordion.Header>Accordion header text</Accordion.Header>
<SmallContent />
</Accordion.Item>
);
}

return props.controlled ? (
<Accordion.Item open={open}>
<Accordion.Item open={open} onOpenChange={console.log}>
<Accordion.Header onClick={() => setOpen(!open)}>
Accordion header text
</Accordion.Header>
<Content />
</Accordion.Item>
) : (
<Accordion.Item>
<Accordion.Item onOpenChange={console.log}>
<Accordion.Header>Accordion header text</Accordion.Header>
<Content />
</Accordion.Item>
Expand Down
25 changes: 14 additions & 11 deletions @navikt/core/react/src/dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState } from "react";
import { useControllableState } from "../util/hooks/useControllableState";
import Menu, { MenuType } from "./Menu";
import Toggle, { ToggleProps } from "./Toggle";
import { DropdownContext } from "./context";
Expand All @@ -22,6 +23,10 @@ export interface DropdownProps {
* Controlled state of the dropdown. When set, you will need to handle onClose and onSelect manually.
*/
open?: boolean;
/**
* Change handler for open
*/
onOpenChange?: (open: boolean) => void;
}

export interface DropdownType extends React.FC<DropdownProps> {
Expand Down Expand Up @@ -74,28 +79,26 @@ export const Dropdown = (({
closeOnSelect = true,
defaultOpen = false,
open,
onOpenChange,
}) => {
const [isOpen, setIsOpen] = useState<boolean>(defaultOpen);
const [anchorEl, setAnchorEl] = useState<Element | null>(null);

const handleToggle = (v: boolean) => {
if (open === undefined) {
setIsOpen(v);
}
};
const [_open, _setOpen] = useControllableState({
defaultValue: defaultOpen,
value: open,
onChange: onOpenChange,
});

return (
<DropdownContext.Provider
value={{
isOpen: open ?? isOpen,
handleToggle,
isOpen: _open,
handleToggle: _setOpen,
anchorEl,
setAnchorEl,
onSelect: (event) => {
onSelect?.(event);
if (closeOnSelect) {
open === undefined && setIsOpen(false);
}
closeOnSelect && _setOpen(false);
},
}}
>
Expand Down
2 changes: 1 addition & 1 deletion @navikt/core/react/src/dropdown/Toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const Toggle = forwardRef<HTMLButtonElement, ToggleProps>(
className={cl("navds-dropdown__toggle", className)}
/>
);
}
},
);

export default Toggle;
28 changes: 17 additions & 11 deletions @navikt/core/react/src/read-more/ReadMore.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import cl from "clsx";
import React, { forwardRef, useState } from "react";
import React, { forwardRef } from "react";
import { ChevronDownIcon } from "@navikt/aksel-icons";
import { BodyLong } from "../typography";
import { useControllableState } from "../util/hooks/useControllableState";

export interface ReadMoreProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
Expand All @@ -23,6 +24,10 @@ export interface ReadMoreProps
* @default false
*/
defaultOpen?: boolean;
/**
* Callback for current open-state
*/
onOpenChange?: (open: boolean) => void;
/**
* Changes fontsize for content
* @default medium
Expand Down Expand Up @@ -57,21 +62,24 @@ export const ReadMore = forwardRef<HTMLButtonElement, ReadMoreProps>(
defaultOpen = false,
onClick,
size = "medium",
onOpenChange,
...rest
},
ref
) => {
const [internalOpen, setInternalOpen] = useState<boolean>(defaultOpen);

const isOpened = open ?? internalOpen;
const [_open, _setOpen] = useControllableState({
defaultValue: defaultOpen,
value: open,
onChange: onOpenChange,
});

return (
<div
className={cl(
"navds-read-more",
`navds-read-more--${size}`,
className,
{ "navds-read-more--open": isOpened }
{ "navds-read-more--open": _open }
)}
>
<button
Expand All @@ -82,12 +90,10 @@ export const ReadMore = forwardRef<HTMLButtonElement, ReadMoreProps>(
"navds-body-short--small": size === "small",
})}
onClick={(e) => {
if (open === undefined) {
setInternalOpen((isOpen) => !isOpen);
}
_setOpen((x) => !x);
onClick?.(e);
}}
aria-expanded={isOpened}
aria-expanded={_open}
>
<ChevronDownIcon
className="navds-read-more__expand-icon"
Expand All @@ -98,9 +104,9 @@ export const ReadMore = forwardRef<HTMLButtonElement, ReadMoreProps>(

<BodyLong
as="div"
aria-hidden={!isOpened}
aria-hidden={!_open}
className={cl("navds-read-more__content", {
"navds-read-more__content--closed": !isOpened,
"navds-read-more__content--closed": !_open,
})}
size={size}
>
Expand Down
4 changes: 4 additions & 0 deletions @navikt/core/react/src/read-more/readmore.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default meta;
export const Default: StoryFn<{
controlled: boolean;
size: "medium" | "small";
defaultOpen: boolean;
}> = (props) => {
const [state, setState] = useState(false);

Expand All @@ -20,6 +21,8 @@ export const Default: StoryFn<{
onClick={() => setState((x) => !x)}
header="Grunnen til at vi spør om dette og i tillegg ber om vedlegg"
size={props.size}
onOpenChange={console.log}
defaultOpen={props.defaultOpen}
>
<div style={{ maxWidth: 300 }}>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Mollitia,
Expand All @@ -32,6 +35,7 @@ export const Default: StoryFn<{
Default.args = {
controlled: false,
size: "medium",
defaultOpen: false,
};
Default.argTypes = {
size: {
Expand Down
30 changes: 16 additions & 14 deletions @navikt/core/react/src/table/ExpandableRow.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ChevronDownIcon } from "@navikt/aksel-icons";
import cl from "clsx";
import React, { forwardRef, useState } from "react";
import React, { forwardRef } from "react";
import { ChevronDownIcon } from "@navikt/aksel-icons";
import { useId } from "../util";
import AnimateHeight from "../util/AnimateHeight";
import { useControllableState } from "../util/hooks/useControllableState";
import DataCell from "./DataCell";
import Row, { RowProps } from "./Row";

Expand Down Expand Up @@ -69,15 +70,16 @@ export const ExpandableRow: ExpandableRowType = forwardRef(
},
ref
) => {
const [internalOpen, setInternalOpen] = useState<boolean>(defaultOpen);
const [_open, _setOpen] = useControllableState({
defaultValue: defaultOpen,
value: open,
onChange: onOpenChange,
});

const id = useId();
const isOpen = open ?? internalOpen;

const expansionHandler = (e) => {
onOpenChange?.(!isOpen);
if (open === undefined) {
setInternalOpen((oldOpen) => !oldOpen);
}
_setOpen((x) => !x);
e.stopPropagation();
};

Expand All @@ -90,7 +92,7 @@ export const ExpandableRow: ExpandableRowType = forwardRef(
{...rest}
ref={ref}
className={cl("navds-table__expandable-row", className, {
"navds-table__expandable-row--open": isOpen,
"navds-table__expandable-row--open": _open,
"navds-table__expandable-row--expansion-disabled":
expansionDisabled,
"navds-table__expandable-row--clickable": expandOnRowClick,
Expand All @@ -103,32 +105,32 @@ export const ExpandableRow: ExpandableRowType = forwardRef(
{togglePlacement === "right" && children}
<DataCell
className={cl("navds-table__toggle-expand-cell", {
"navds-table__toggle-expand-cell--open": isOpen,
"navds-table__toggle-expand-cell--open": _open,
})}
>
{!expansionDisabled && (
<button
className="navds-table__toggle-expand-button"
type="button"
aria-controls={id}
aria-expanded={isOpen}
aria-expanded={_open}
onClick={expansionHandler}
>
<ChevronDownIcon
className="navds-table__expandable-icon"
title={isOpen ? "Vis mindre" : "Vis mer"}
title={_open ? "Vis mindre" : "Vis mer"}
/>
</button>
)}
</DataCell>
{togglePlacement === "left" && children}
</Row>
<tr className="navds-table__expanded-row" aria-hidden={!isOpen} id={id}>
<tr className="navds-table__expanded-row" aria-hidden={!_open} id={id}>
<td colSpan={colSpan} className="navds-table__expanded-row-cell">
<AnimateHeight
className="navds-table__expanded-row-collapse"
innerClassName="navds-table__expanded-row-content"
height={isOpen ? "auto" : 0}
height={_open ? "auto" : 0}
duration={250}
>
{content}
Expand Down
Loading

0 comments on commit 76eefdf

Please sign in to comment.