Skip to content

Commit 9d20cab

Browse files
docs(content, fab): fixed slot placement (#3563)
Co-authored-by: Liam DeBeasi <[email protected]>
1 parent 680fb37 commit 9d20cab

File tree

9 files changed

+342
-0
lines changed

9 files changed

+342
-0
lines changed

docs/api/content.md

+2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ import Fullscreen from '@site/static/usage/v8/content/fullscreen/index.md';
5454

5555
To place elements outside of the scrollable area, assign them to the `fixed` slot. Doing so will [absolutely position](https://developer.mozilla.org/en-US/docs/Web/CSS/position#absolute_positioning) the element to the top left of the content. In order to change the position of the element, it can be styled using the [top, right, bottom, and left](https://developer.mozilla.org/en-US/docs/Web/CSS/position) CSS properties.
5656

57+
The `fixedSlotPlacement` property is used to determine if content in the `fixed` slot is placed before or after the main content in the DOM. When set to `before`, fixed slot content will be placed before the main content and will therefore receive keyboard focus before the main content receives keyboard focus. This can be useful when the main content contains an infinitely-scrolling list, preventing a [FAB](./fab) or other fixed content from being reachable by pressing the tab key.
58+
5759
import Fixed from '@site/static/usage/v8/content/fixed/index.md';
5860

5961
<Fixed />

docs/api/fab.md

+10
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ import SafeArea from '@site/static/usage/v8/fab/safe-area/index.md';
6767

6868
<SafeArea />
6969

70+
### Relative to Infinite List
71+
72+
In scenarios where a view contains many interactive elements, such as an infinitely-scrolling list, it may be challenging for users to navigate to the Floating Action Button (FAB) if it is placed below all the items in the DOM.
73+
74+
By setting the `fixedSlotPlacement` property on [Content](./content) to `before`, the FAB will be placed before the main content in the DOM. This ensures that the FAB receives keyboard focus before other interactive elements receive focus, making it easier for users to access the FAB.
75+
76+
import BeforeContent from '@site/static/usage/v8/fab/before-content/index.md';
77+
78+
<BeforeContent />
79+
7080
## Button Sizing
7181

7282
Setting the `size` property of the main fab button to `"small"` will render it at a mini size. Note that this property will not have an effect when used with the inner fab buttons.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
```html
2+
<ion-content fixed-slot-placement="before">
3+
<ion-fab horizontal="end" vertical="bottom" slot="fixed">
4+
<ion-fab-button>
5+
<ion-icon name="add"></ion-icon>
6+
</ion-fab-button>
7+
</ion-fab>
8+
<ion-list>
9+
<ion-item [button]="true" *ngFor="let item of items; let index">
10+
<ion-avatar slot="start">
11+
<img [src]="'https://picsum.photos/80/80?random=' + index" alt="avatar" />
12+
</ion-avatar>
13+
<ion-label>{{ item }}</ion-label>
14+
</ion-item>
15+
</ion-list>
16+
<ion-infinite-scroll (ionInfinite)="onIonInfinite($event)">
17+
<ion-infinite-scroll-content></ion-infinite-scroll-content>
18+
</ion-infinite-scroll>
19+
</ion-content>
20+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
```tsx
2+
import { Component, OnInit } from '@angular/core';
3+
4+
import { InfiniteScrollCustomEvent } from '@ionic/angular';
5+
6+
@Component({
7+
selector: 'app-example',
8+
templateUrl: 'example.component.html',
9+
})
10+
export class ExampleComponent implements OnInit {
11+
items = [];
12+
13+
ngOnInit() {
14+
this.generateItems();
15+
}
16+
17+
private generateItems() {
18+
const count = this.items.length + 1;
19+
for (let i = 0; i < 50; i++) {
20+
this.items.push(`Item ${count + i}`);
21+
}
22+
}
23+
24+
onIonInfinite(ev) {
25+
this.generateItems();
26+
setTimeout(() => {
27+
(ev as InfiniteScrollCustomEvent).target.complete();
28+
}, 500);
29+
}
30+
}
31+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Fab</title>
7+
<link rel="stylesheet" href="../../common.css" />
8+
<script src="../../common.js"></script>
9+
<script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/core@next/dist/ionic/ionic.esm.js"></script>
10+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/core@next/css/ionic.bundle.css" />
11+
</head>
12+
13+
<body>
14+
<ion-app>
15+
<ion-content fixed-slot-placement="before">
16+
<ion-fab horizontal="end" vertical="bottom" slot="fixed">
17+
<ion-fab-button>
18+
<ion-icon name="add"></ion-icon>
19+
</ion-fab-button>
20+
</ion-fab>
21+
<ion-list></ion-list>
22+
<ion-infinite-scroll>
23+
<ion-infinite-scroll-content></ion-infinite-scroll-content>
24+
</ion-infinite-scroll>
25+
</ion-content>
26+
</ion-app>
27+
<script>
28+
const infiniteScroll = document.querySelector('ion-infinite-scroll');
29+
infiniteScroll.addEventListener('ionInfinite', (event) => {
30+
setTimeout(() => {
31+
generateItems();
32+
event.target.complete();
33+
}, 500);
34+
});
35+
36+
const list = document.querySelector('ion-list');
37+
38+
function generateItems() {
39+
const count = list.childElementCount + 1;
40+
const total = count + 50;
41+
for (let i = count; i < total; i++) {
42+
const el = document.createElement('ion-item');
43+
el.button = true;
44+
45+
const avatar = document.createElement('ion-avatar');
46+
avatar.slot = 'start';
47+
const img = document.createElement('img');
48+
img.src = `https://picsum.photos/80/80?random=${i}`;
49+
img.alt = 'Avatar';
50+
51+
avatar.appendChild(img);
52+
el.appendChild(avatar);
53+
54+
const text = document.createElement('ion-label');
55+
text.innerText = `Item ${i}`;
56+
57+
el.appendChild(text);
58+
59+
list.appendChild(el);
60+
}
61+
}
62+
63+
generateItems();
64+
</script>
65+
</body>
66+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Playground from '@site/src/components/global/Playground';
2+
3+
import javascript from './javascript.md';
4+
import react from './react.md';
5+
import vue from './vue.md';
6+
7+
import angular_example_component_html from './angular/example_component_html.md';
8+
import angular_example_component_ts from './angular/example_component_ts.md';
9+
10+
<Playground
11+
version="8"
12+
code={{
13+
javascript,
14+
react,
15+
vue,
16+
angular: {
17+
files: {
18+
'src/app/example.component.html': angular_example_component_html,
19+
'src/app/example.component.ts': angular_example_component_ts,
20+
},
21+
},
22+
}}
23+
src="usage/v8/fab/before-content/demo.html"
24+
size="medium"
25+
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
```html
2+
<ion-content fixed-slot-placement="before">
3+
<ion-fab horizontal="end" vertical="bottom" slot="fixed">
4+
<ion-fab-button>
5+
<ion-icon name="add"></ion-icon>
6+
</ion-fab-button>
7+
</ion-fab>
8+
<ion-list></ion-list>
9+
<ion-infinite-scroll>
10+
<ion-infinite-scroll-content></ion-infinite-scroll-content>
11+
</ion-infinite-scroll>
12+
</ion-content>
13+
14+
<script>
15+
const infiniteScroll = document.querySelector('ion-infinite-scroll');
16+
infiniteScroll.addEventListener('ionInfinite', (event) => {
17+
setTimeout(() => {
18+
generateItems();
19+
event.target.complete();
20+
}, 500);
21+
});
22+
23+
const list = document.querySelector('ion-list');
24+
25+
function generateItems() {
26+
const count = list.childElementCount + 1;
27+
const total = count + 50;
28+
for (let i = count; i < total; i++) {
29+
const el = document.createElement('ion-item');
30+
el.setAttribute('button', '');
31+
32+
const avatar = document.createElement('ion-avatar');
33+
avatar.slot = 'start';
34+
const img = document.createElement('img');
35+
img.src = `https://picsum.photos/80/80?random=${i}`;
36+
img.alt = 'Avatar';
37+
38+
avatar.appendChild(img);
39+
el.appendChild(avatar);
40+
41+
const text = document.createElement('ion-label');
42+
text.innerText = `Item ${i}`;
43+
44+
el.appendChild(text);
45+
46+
list.appendChild(el);
47+
}
48+
}
49+
50+
generateItems();
51+
</script>
52+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
```tsx
2+
import React, { useState, useEffect } from 'react';
3+
import {
4+
IonAvatar,
5+
IonContent,
6+
IonFab,
7+
IonFabButton,
8+
IonIcon,
9+
IonInfiniteScroll,
10+
IonInfiniteScrollContent,
11+
IonItem,
12+
IonLabel,
13+
IonList,
14+
} from '@ionic/react';
15+
16+
function Example() {
17+
const [items, setItems] = useState<string[]>([]);
18+
19+
const generateItems = () => {
20+
const newItems = [];
21+
for (let i = 0; i < 50; i++) {
22+
newItems.push(`Item ${1 + items.length + i}`);
23+
}
24+
setItems([...items, ...newItems]);
25+
};
26+
27+
useEffect(() => {
28+
generateItems();
29+
// eslint-disable-next-line react-hooks/exhaustive-deps
30+
}, []);
31+
32+
return (
33+
<IonContent fixedSlotPlacement="before">
34+
<IonFab horizontal="end" vertical="bottom" slot="fixed">
35+
<IonFabButton>
36+
<IonIcon name="add"></IonIcon>
37+
</IonFabButton>
38+
</IonFab>
39+
<IonList>
40+
{items.map((item, index) => (
41+
<IonItem key={item} button>
42+
<IonAvatar slot="start">
43+
<img src={'https://picsum.photos/80/80?random=' + index} alt="avatar" />
44+
</IonAvatar>
45+
<IonLabel>{item}</IonLabel>
46+
</IonItem>
47+
))}
48+
</IonList>
49+
<IonInfiniteScroll
50+
onIonInfinite={(ev) => {
51+
generateItems();
52+
setTimeout(() => ev.target.complete(), 500);
53+
}}
54+
>
55+
<IonInfiniteScrollContent></IonInfiniteScrollContent>
56+
</IonInfiniteScroll>
57+
</IonContent>
58+
);
59+
}
60+
export default Example;
61+
```
+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
```html
2+
<template>
3+
<ion-content fixed-slot-placement="before">
4+
<ion-fab horizontal="end" vertical="bottom" slot="fixed">
5+
<ion-fab-button>
6+
<ion-icon name="add"></ion-icon>
7+
</ion-fab-button>
8+
</ion-fab>
9+
<ion-list>
10+
<ion-item v-for="(item, index) in items" button>
11+
<ion-avatar slot="start">
12+
<img :src="'https://picsum.photos/80/80?random=' + index" alt="avatar" />
13+
</ion-avatar>
14+
<ion-label>{{ item }}</ion-label>
15+
</ion-item>
16+
</ion-list>
17+
<ion-infinite-scroll @ionInfinite="ionInfinite">
18+
<ion-infinite-scroll-content></ion-infinite-scroll-content>
19+
</ion-infinite-scroll>
20+
</ion-content>
21+
</template>
22+
23+
<script lang="ts">
24+
import {
25+
InfiniteScrollCustomEvent,
26+
IonAvatar,
27+
IonContent,
28+
IonFab,
29+
IonFabButton,
30+
IonIcon,
31+
IonImg,
32+
IonInfiniteScroll,
33+
IonInfiniteScrollContent,
34+
IonItem,
35+
IonLabel,
36+
IonList,
37+
} from '@ionic/vue';
38+
import { defineComponent, reactive } from 'vue';
39+
40+
export default defineComponent({
41+
components: {
42+
IonAvatar,
43+
IonContent,
44+
IonFab,
45+
IonFabButton,
46+
IonIcon,
47+
IonImg,
48+
IonInfiniteScroll,
49+
IonInfiniteScrollContent,
50+
IonItem,
51+
IonLabel,
52+
IonList,
53+
},
54+
setup() {
55+
const items = reactive([]);
56+
57+
const generateItems = () => {
58+
const count = items.length + 1;
59+
for (let i = 0; i < 50; i++) {
60+
items.push(`Item ${count + i}`);
61+
}
62+
};
63+
64+
const ionInfinite = (ev: InfiniteScrollCustomEvent) => {
65+
generateItems();
66+
setTimeout(() => ev.target.complete(), 500);
67+
};
68+
69+
generateItems();
70+
71+
return { ionInfinite, items };
72+
},
73+
});
74+
</script>
75+
```

0 commit comments

Comments
 (0)