Skip to content

Commit fcda638

Browse files
authored
Merge pull request #449 from acelaya-forks/feature/tailwind-improvements
Add Details tailwind-based component
2 parents ff66919 + c5deba1 commit fcda638

File tree

10 files changed

+101
-6
lines changed

10 files changed

+101
-6
lines changed

CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
66

7-
## [Unreleased]
7+
## [0.8.11] - 2025-04-20
88
### Added
9-
* *Nothing*
9+
* Add default tailwind styles for `code` elements.
10+
* Add tailwind-based `Details` component.
1011

1112
### Changed
1213
* `useToggle` can now return its result as a tuple or as an object, with the former being deprecated.
1314
* Change focus ring for secondary tailwind-based buttons, to match the button color.
15+
* Add `cursor-pointer` to `Label` component.
16+
* Add `cursor-[inherit]` to tailwind-based `Checkbox` and `ToggleSwitch` components.
1417

1518
### Deprecated
1619
* *Nothing*

dev/Menu.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const Menu: FC = () => {
1616
<li><Link to="/tailwind/form/buttons">Buttons</Link></li>
1717
<li><Link to="/tailwind/surfaces/cards">Cards</Link></li>
1818
<li><Link to="/tailwind/content/tables">Tables</Link></li>
19+
<li><Link to="/tailwind/content/details">Details</Link></li>
1920
<li><Link to="/tailwind/navigation/paginator">Paginator</Link></li>
2021
<li><Link to="/tailwind/navigation/nav-pills">NavPills</Link></li>
2122
<li><Link to="/tailwind/feedback/dialogs">Dialogs</Link></li>

dev/tailwind/TailwindComponents.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { FC } from 'react';
22
import { Route, Routes } from 'react-router';
3+
import { DetailsPage } from './content/DetailsPage';
34
import { TablePage } from './content/TablePage';
45
import { MessagePage } from './feedback/MessagePage';
56
import { ModalDialogPage } from './feedback/ModalDialogPage';
@@ -22,6 +23,7 @@ export const TailwindComponents: FC = () => {
2223
<Route path="/form/buttons" element={<ButtonsPage />} />
2324
<Route path="/surfaces/cards" element={<CardsPage />} />
2425
<Route path="/content/tables" element={<TablePage />} />
26+
<Route path="/content/details" element={<DetailsPage />} />
2527
<Route path="/navigation/paginator" element={<PaginatorPage />} />
2628
<Route path="/navigation/nav-pills">
2729
<Route path="" element={<NavPillsPage />} />
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { FC } from 'react';
2+
import { Checkbox, Label } from '../../../src/tailwind';
3+
import { Details } from '../../../src/tailwind/content/Details';
4+
5+
export const DetailsPage: FC = () => {
6+
return (
7+
<div className="tw:flex tw:flex-col tw:gap-y-4">
8+
<div className="tw:flex tw:flex-col tw:gap-y-2">
9+
<h2>Details</h2>
10+
<Details summary="Click here to toggle">
11+
<div>This is the content</div>
12+
<div>And this is more content</div>
13+
<Label className="tw:flex tw:items-center tw:gap-1">
14+
<Checkbox /> The content is mounted only while the details are open
15+
</Label>
16+
</Details>
17+
</div>
18+
</div>
19+
);
20+
};

src/hooks/use-timeout.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function useTimeout(
2121
/** Test seam. Defaults to global clearTimeout */
2222
clearTimeout_ = globalThis.clearTimeout,
2323
): UseTimeoutResult {
24-
const timeoutRef = useRef<ReturnType<typeof setTimeout_> | null>(null);
24+
const timeoutRef = useRef<ReturnType<typeof setTimeout_>>(null);
2525

2626
const clearCurrentTimeout = useCallback(() => {
2727
if (timeoutRef.current) {

src/tailwind/content/Details.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { clsx } from 'clsx';
2+
import type { FC, HTMLProps, ReactNode } from 'react';
3+
import { useEffect , useRef,useState } from 'react';
4+
5+
export type DetailsProps = Omit<HTMLProps<HTMLDetailsElement>, 'ref'> & {
6+
summary: ReactNode;
7+
summaryClasses?: string;
8+
};
9+
10+
export const Details: FC<DetailsProps> = ({ children, summary, summaryClasses, ...rest }) => {
11+
const detailsRef = useRef<HTMLDetailsElement>(null);
12+
const [isOpen, setIsOpen] = useState(false);
13+
14+
useEffect(() => {
15+
const detailsEl = detailsRef.current;
16+
const toggleHandler = () => setIsOpen(!!detailsEl?.open);
17+
18+
detailsEl?.addEventListener('toggle', toggleHandler);
19+
return () => detailsEl?.removeEventListener('toggle', toggleHandler);
20+
}, []);
21+
22+
return (
23+
<details ref={detailsRef} {...rest}>
24+
<summary className={clsx('tw:focus-ring tw:px-1 tw:-mx-1 tw:rounded-sm', summaryClasses)}>{summary}</summary>
25+
{isOpen && (
26+
<div className="tw:mt-3 tw:flex tw:flex-col tw:gap-y-3">
27+
{children}
28+
</div>
29+
)}
30+
</details>
31+
);
32+
};

src/tailwind/form/BooleanControl.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const BooleanControl = forwardRef<HTMLInputElement, BooleanControlProps>(
1717
ref={ref}
1818
type="checkbox"
1919
className={clsx(
20-
'tw:appearance-none tw:focus-ring',
20+
'tw:appearance-none tw:focus-ring tw:cursor-[inherit]',
2121
'tw:border-1 tw:border-lm-input-border tw:dark:border-dm-input-border',
2222
'tw:bg-lm-primary tw:dark:bg-dm-primary tw:checked:bg-brand tw:bg-no-repeat',
2323
// Use different background color when rendered inside a card

src/tailwind/form/Label.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import { clsx } from 'clsx';
12
import type { FC, HTMLProps } from 'react';
23

34
export type LabelProps = HTMLProps<HTMLLabelElement> & {
45
required?: boolean;
56
};
67

7-
export const Label: FC<LabelProps> = ({ required, children, ...rest }) => (
8-
<label {...rest}>
8+
export const Label: FC<LabelProps> = ({ required, children, className, ...rest }) => (
9+
<label className={clsx('tw:cursor-pointer', className)} {...rest}>
910
{children}
1011
{required && <span className="tw:text-danger tw:ml-1" data-testid="required-indicator">*</span>}
1112
</label>

src/tailwind/tailwind.preset.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@
143143
p {
144144
@apply tw:m-0;
145145
}
146+
147+
code {
148+
@apply tw:text-sm tw:text-pink-600 tw:dark:text-pink-500 tw:font-mono;
149+
}
146150
}
147151

148152
@utility focus-ring-base {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { screen } from '@testing-library/react';
2+
import type { UserEvent } from '@testing-library/user-event';
3+
import type { DetailsProps } from '../../../src/tailwind/content/Details';
4+
import { Details } from '../../../src/tailwind/content/Details';
5+
import { checkAccessibility } from '../../__helpers__/accessibility';
6+
import { renderWithEvents } from '../../__helpers__/setUpTest';
7+
8+
describe('<Details />', () => {
9+
const setUp = (props: Omit<DetailsProps, 'summary' | 'children'> = {}) => renderWithEvents(
10+
<Details summary="Click me" {...props}>
11+
<div>These are the children</div>
12+
</Details>,
13+
);
14+
const openDetails = (user: UserEvent) => user.click(screen.getByText('Click me'));
15+
16+
it.each([
17+
setUp,
18+
async () => {
19+
const { user, container } = setUp();
20+
await openDetails(user);
21+
return { container };
22+
},
23+
])('passes a11y checks', (doSetUp) => checkAccessibility(doSetUp()));
24+
25+
it('renders children only while it is open', async () => {
26+
const { user } = setUp();
27+
28+
expect(screen.queryByText('These are the children')).not.toBeInTheDocument();
29+
await openDetails(user);
30+
expect(screen.getByText('These are the children')).toBeInTheDocument();
31+
});
32+
});

0 commit comments

Comments
 (0)