Skip to content

Commit 9d68409

Browse files
feat(drawer): Add inline version
1 parent 9dc8662 commit 9d68409

File tree

15 files changed

+462
-24
lines changed

15 files changed

+462
-24
lines changed

angular/bootstrap/src/components/drawer/drawer.component.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ export class DrawerComponent extends BaseWidgetDirective<DrawerWidget> {
219219
/**
220220
* CSS classes to be applied on the widget main container
221221
*
222-
* @defaultValue `'w-full'`
222+
* @defaultValue `''`
223223
*/
224224
readonly className = input<string>(undefined, {alias: 'auClassName'});
225225

@@ -230,6 +230,14 @@ export class DrawerComponent extends BaseWidgetDirective<DrawerWidget> {
230230
*/
231231
readonly resizable = input(undefined, {alias: 'auResizable', transform: auBooleanAttribute});
232232

233+
/**
234+
* If `true`, the drawer is inline.
235+
* When inline mode is enabled, the drawer stays in the document flow and moves content as it expands/resizes.
236+
*
237+
* @defaultValue `false`
238+
*/
239+
readonly inline = input(undefined, {alias: 'auInline', transform: auBooleanAttribute});
240+
233241
/**
234242
* An event emitted when the drawer size changes (width or height depending on the orientation).
235243
*
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type {DrawerPositions} from '@agnos-ui/angular-bootstrap';
2+
import {DrawerComponent, DrawerHeaderDirective} from '@agnos-ui/angular-bootstrap';
3+
import {Component, signal} from '@angular/core';
4+
import {FormsModule} from '@angular/forms';
5+
6+
@Component({
7+
template: `
8+
<button class="btn btn-primary mb-3" (click)="toggleDrawer(drawer)">Toggle Inline Drawer</button>
9+
<div class="d-flex align-items-center mb-3">
10+
<label for="drawerPlacement" class="me-3">Placement:</label>
11+
<select id="drawerPlacement" [(ngModel)]="drawerPlacement" class="w-auto form-select">
12+
<option value="inline-start">Left</option>
13+
<option value="inline-end">Right</option>
14+
<option value="block-start">Top</option>
15+
<option value="block-end">Bottom</option>
16+
</select>
17+
</div>
18+
19+
<div class="d-flex demo-inline-drawer" [class.flex-column]="drawerPlacement().includes('block')">
20+
<div [style.order]="drawerPlacement().endsWith('end') ? 2 : 1">
21+
<au-component #drawer auDrawer [auClassName]="drawerPlacement()" auResizable auInline auVisible>
22+
<ng-template auDrawerHeader>
23+
<h2>Inline Drawer</h2>
24+
<button class="btn-close ms-auto" (click)="drawer.api.close()" aria-label="Close"></button>
25+
</ng-template>
26+
<div class="p-3">
27+
<h6>Drawer Content</h6>
28+
<ul>
29+
<li>No backdrop overlay</li>
30+
<li>Stays in document flow</li>
31+
<li>Pushes page content</li>
32+
<li>Page remains scrollable</li>
33+
<li>Fully interactable</li>
34+
</ul>
35+
</div>
36+
</au-component>
37+
</div>
38+
39+
<div class="flex-grow-1 p-3" [style.order]="drawerPlacement().endsWith('end') ? 1 : 2">
40+
<h6>Main Page Content</h6>
41+
<p>This content is pushed aside by the inline drawer. You can interact with everything on this page even when the drawer is open.</p>
42+
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
43+
<button class="btn btn-secondary">Clickable Button</button>
44+
<p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
45+
<input type="text" class="form-control mt-2" placeholder="Type here..." />
46+
<p class="mt-2">
47+
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
48+
proident.
49+
</p>
50+
</div>
51+
</div>
52+
`,
53+
styles: `
54+
.demo-inline-drawer {
55+
border: 1px solid #dee2e6;
56+
padding: 1rem;
57+
border-radius: 0.375rem;
58+
}
59+
`,
60+
imports: [DrawerComponent, DrawerHeaderDirective, FormsModule],
61+
})
62+
export default class InlineDrawerComponent {
63+
readonly drawerPlacement = signal<DrawerPositions>('inline-start');
64+
65+
async toggleDrawer(drawer: DrawerComponent) {
66+
if (drawer.state.visible()) {
67+
await drawer.api.close();
68+
} else {
69+
await drawer.api.open();
70+
}
71+
}
72+
}

core-bootstrap/src/scss/drawer.scss

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@
77

88
display: inline-flex;
99
flex-direction: column;
10-
background: #fff;
11-
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.2);
10+
flex-shrink: 0;
11+
background-color: var(--#{$prefix}body-bg, #fff);
1212
z-index: var(--#{$prefix}drawer-z-index);
13-
1413
--#{$prefix}drawer-size: max-content;
1514
--#{$prefix}drawer-min-size: 0;
1615
--#{$prefix}drawer-max-size: none;
@@ -49,6 +48,7 @@
4948
&.inline-start {
5049
inset-inline-start: 0;
5150
inset-block: 0;
51+
border-inline-end: 1px solid #e2e2e2;
5252
.au-splitter {
5353
inset-block: 0;
5454
inset-inline: auto calc(var(--#{$prefix}drawer-splitter-size) / -2);
@@ -58,6 +58,7 @@
5858
&.inline-end {
5959
inset-inline-end: 0;
6060
inset-block: 0;
61+
border-inline-start: 1px solid #e2e2e2;
6162
.au-splitter {
6263
inset-block: 0;
6364
inset-inline: calc(var(--#{$prefix}drawer-splitter-size) auto / -2);
@@ -68,6 +69,7 @@
6869
&.block-start {
6970
inset-inline: 0;
7071
inset-block: 0;
72+
border-block-end: 1px solid #e2e2e2;
7173
.au-splitter {
7274
inset-block: auto calc(var(--#{$prefix}drawer-splitter-size) / -2);
7375
inset-inline: 0;
@@ -77,13 +79,17 @@
7779
&.block-end {
7880
inset-inline: 0;
7981
inset-block-end: 0;
82+
border-block-start: 1px solid #e2e2e2;
8083
.au-splitter {
8184
inset-block: calc(var(--#{$prefix}drawer-splitter-size) auto / -2);
8285
inset-inline: 0;
8386
}
8487
}
8588

8689
.au-drawer-header {
90+
display: inline-flex;
91+
align-items: center;
92+
width: 100%;
8793
padding: 0.75rem 1rem;
8894
font-weight: 600;
8995
border-bottom: 1px solid #e2e2e2;

core/src/components/drawer/drawer.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,30 @@ describe(`Drawer`, () => {
304304
testArea.removeChild(drawerElement);
305305
});
306306

307+
test(`should properly initialize inline drawer and hide backdrop`, () => {
308+
testArea.innerHTML = `
309+
<div id="drawerElement"></div>
310+
`;
311+
const drawer = createDrawer({
312+
props: {
313+
bodyScroll: false,
314+
backdrop: true,
315+
inline: true,
316+
},
317+
});
318+
319+
const drawerElement = document.getElementById('drawerElement')!;
320+
const directive = drawer.directives.drawerDirective(drawerElement);
321+
322+
expect(drawer.stores.inline$()).toBe(true);
323+
expect(drawer.stores.backdropHidden$()).toBe(true);
324+
expect(drawer.stores.container$()).toBeNull();
325+
326+
expect(drawerElement.getAttribute('style')).toContain('relative');
327+
328+
directive?.destroy?.();
329+
});
330+
307331
describe('checks events', () => {
308332
test('onMinimizedChange when mouse is in the viewport', () => {
309333
const {destroy, mouseDown, mouseMove, mouseUp, state} = prepareTest('50px');

core/src/components/drawer/drawer.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ interface DrawerCommonPropsAndState extends WidgetsCommonPropsAndState {
3333
/**
3434
* CSS classes to be applied on the widget main container
3535
*
36-
* @defaultValue `'w-full'`
36+
* @defaultValue `''`
3737
*/
3838
className: string;
3939
/**
@@ -72,6 +72,13 @@ interface DrawerCommonPropsAndState extends WidgetsCommonPropsAndState {
7272
* @defaultValue `null`
7373
*/
7474
size: number | null;
75+
/**
76+
* If `true`, the drawer is inline.
77+
* When inline mode is enabled, the drawer stays in the document flow and moves content as it expands/resizes.
78+
*
79+
* @defaultValue `false`
80+
*/
81+
inline: boolean;
7582
}
7683

7784
/**
@@ -290,7 +297,7 @@ const defaultDrawerConfig: DrawerProps = {
290297
ariaLabelledBy: '',
291298
backdropClass: '',
292299
backdropTransition: noop,
293-
className: 'w-full',
300+
className: '',
294301
visible: false,
295302
container: typeof window !== 'undefined' ? document.body : null,
296303
transition: noop,
@@ -307,6 +314,7 @@ const defaultDrawerConfig: DrawerProps = {
307314
bodyScroll: false,
308315
size: null,
309316
focusOnInit: true,
317+
inline: false,
310318
};
311319

312320
const configValidator: ConfigValidator<DrawerProps> = {
@@ -332,6 +340,7 @@ const configValidator: ConfigValidator<DrawerProps> = {
332340
bodyScroll: typeBoolean,
333341
size: typeNumberOrNull,
334342
focusOnInit: typeBoolean,
343+
inline: typeBoolean,
335344
};
336345

337346
/**
@@ -342,14 +351,14 @@ const configValidator: ConfigValidator<DrawerProps> = {
342351
export const createDrawer: WidgetFactory<DrawerWidget> = createWidgetFactory('drawer', (config?: PropsConfig<DrawerProps>) => {
343352
const [
344353
{
345-
backdrop$,
354+
backdrop$: _backdrop$,
346355
backdropTransition$,
347356
backdropClass$,
348-
bodyScroll$,
357+
bodyScroll$: _bodyScroll$,
349358
transition$,
350359
verticalTransition$,
351360
visible$: requestedVisible$,
352-
container$,
361+
container$: _container$,
353362
className$,
354363
size$: _dirtySize$,
355364
animated$,
@@ -363,11 +372,17 @@ export const createDrawer: WidgetFactory<DrawerWidget> = createWidgetFactory('dr
363372
onMaximizedChange$,
364373
onResizingChange$,
365374
focusOnInit$,
375+
inline$,
366376
...stateProps
367377
},
368378
patch,
369379
] = writablesForProps(defaultDrawerConfig, config, configValidator);
370380

381+
// Override props when inline mode is enabled
382+
const backdrop$ = computed(() => (inline$() ? false : _backdrop$()));
383+
const bodyScroll$ = computed(() => (inline$() ? true : _bodyScroll$()));
384+
const container$ = computed(() => (inline$() ? null : _container$()));
385+
371386
const size$ = bindableProp(_dirtySize$, onSizeChange$, (value) => (value ? Math.round(value) : value));
372387

373388
const isVertical$ = computed(() => {
@@ -397,14 +412,17 @@ export const createDrawer: WidgetFactory<DrawerWidget> = createWidgetFactory('dr
397412
createAttributesDirective(() => ({
398413
attributes: {
399414
class: className$,
400-
role: readable('dialog'),
401-
'aria-describedby': ariaDescribedBy$,
402-
'aria-labelledby': ariaLabelledBy$,
403-
'aria-modal': readable('true'),
415+
role: computed(() => (inline$() ? 'complementary' : 'dialog')),
416+
'aria-describedby': computed(() => ariaDescribedBy$() || undefined),
417+
'aria-labelledby': computed(() => ariaLabelledBy$() || undefined),
418+
'aria-modal': computed(() => (inline$() ? undefined : 'true')),
404419
tabIndex: readable('-1'),
405420
},
406421
styles: {
407422
position: computed(() => {
423+
if (inline$()) {
424+
return 'relative';
425+
}
408426
const container = container$();
409427
return container && isBrowserHTMLElement(container) && container !== document.body ? 'relative' : 'fixed';
410428
}),
@@ -578,6 +596,7 @@ export const createDrawer: WidgetFactory<DrawerWidget> = createWidgetFactory('dr
578596
backdropHidden$,
579597
hidden$,
580598
isVertical$,
599+
inline$,
581600
}),
582601
patch,
583602
api: {

demo/src/routes/docs/[framework]/components/drawer/examples/+page.svelte

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,25 @@
33
import Section from '$lib/layout/Section.svelte';
44
import sampleBasic from '@agnos-ui/samples/bootstrap/drawer/basic';
55
import sampleBody from '@agnos-ui/samples/bootstrap/drawer/body';
6+
import sampleInline from '@agnos-ui/samples/bootstrap/drawer/inline';
67
import samplePosition from '@agnos-ui/samples/bootstrap/drawer/position';
78
import sampleSizes from '@agnos-ui/samples/bootstrap/drawer/sizes';
89
</script>
910

1011
<Section label="Basic drawer" id="basic" level={2}>
1112
<Sample title="Basic example" sample={sampleBasic} height={190} noresize />
1213
</Section>
14+
<Section label="Inline drawer" id="inline" level={2}>
15+
<p>
16+
An inline drawer stays in the document flow and pushes the page content instead of overlaying it. The page remains fully scrollable and
17+
interactable.
18+
</p>
19+
<p>
20+
It is your responsibility to properly position the drawer in the context. In case of flex layout, make sure to set the correct order for the
21+
drawer and page content based on the drawer's placement.
22+
</p>
23+
<Sample title="Inline drawer example" sample={sampleInline} height={600} noresize />
24+
</Section>
1325
<Section label="Drawer size" id="positions" level={2}>
1426
<p>You can customize the drawer's width or height by adjusting the following CSS variables:</p>
1527
<p class="ps-5">

e2e/drawer/drawer.e2e-spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,10 @@ test.describe(`Drawer tests`, () => {
136136
.toStrictEqual(
137137
assign(expectedBoundingBox, {
138138
width: 1280,
139-
height: 500,
139+
height: 501,
140140
}),
141141
);
142-
expect(await drawerPO.statePosition()).toStrictEqual(assign(expectedVariables, {'--bs-drawer-size': '500px'}));
142+
expect(await drawerPO.statePosition()).toStrictEqual(assign(expectedVariables, {'--bs-drawer-size': '501px'}));
143143

144144
mousePos = await drawerPO.hoverOnSplitter();
145145
await page.mouse.down();
@@ -149,7 +149,7 @@ test.describe(`Drawer tests`, () => {
149149
await expect
150150
.poll(async () => await drawerPO.locatorRoot.boundingBox())
151151
.toStrictEqual(assign(expectedBoundingBox, {height: expectedBoundingBox.height + 300 - mousePos.y}));
152-
expect(await drawerPO.statePosition()).toStrictEqual(assign(expectedVariables, {'--bs-drawer-size': '300px'}));
152+
expect(await drawerPO.statePosition()).toStrictEqual(assign(expectedVariables, {'--bs-drawer-size': '301px'}));
153153

154154
await drawerPO.locatorBackdrop.click();
155155
await drawerDemoPO.locatorToggleDrawerButton.click();

e2e/samplesMarkup.singlebrowser-e2e-spec.ts-snapshots/bootstrap-drawer-basic.html

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
data-agnos-ignore-inert="data-agnos-ignore-inert"
1818
/>
1919
<div
20-
aria-describedby=""
21-
aria-labelledby=""
2220
aria-modal="true"
2321
class="au-drawer collapse collapse-horizontal inline-start show"
2422
role="dialog"

e2e/samplesMarkup.singlebrowser-e2e-spec.ts-snapshots/bootstrap-drawer-body.html

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@
6464
data-agnos-ignore-inert="data-agnos-ignore-inert"
6565
/>
6666
<div
67-
aria-describedby=""
68-
aria-labelledby=""
6967
aria-modal="true"
7068
class="au-drawer collapse collapse-horizontal inline-start show"
7169
role="dialog"

0 commit comments

Comments
 (0)