Skip to content

Commit

Permalink
feat(core): BreakpointService add new service (#3806)
Browse files Browse the repository at this point in the history
Co-authored-by: Alex Inkin <alexander@inkin.ru>
Co-authored-by: Nikita Barsukov <nikita.s.barsukov@gmail.com>
Co-authored-by: Maksim Ivanov <omaxphp@yandex.ru>
  • Loading branch information
4 people committed Mar 31, 2023
1 parent 78d546b commit 6451a66
Show file tree
Hide file tree
Showing 15 changed files with 354 additions and 0 deletions.
53 changes: 53 additions & 0 deletions projects/core/services/breakpoint.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {Inject, Injectable} from '@angular/core';
import {WINDOW} from '@ng-web-apis/common';
import {TuiMedia} from '@taiga-ui/core/interfaces';
import {TUI_MEDIA} from '@taiga-ui/core/tokens';
import {fromEvent, merge, Observable} from 'rxjs';
import {map, share, startWith} from 'rxjs/operators';

/**
* Service to provide the current breakpoint based on Taiga UI's media queries
*/
@Injectable({
providedIn: `root`,
})
export class TuiBreakpointService extends Observable<MediaKey | null> {
constructor(@Inject(TUI_MEDIA) media: TuiMedia, @Inject(WINDOW) windowRef: Window) {
const breakpoints = getBreakpoints(media);
const events$ = breakpoints.map(({query}) =>
fromEvent<MediaQueryListEvent>(windowRef.matchMedia(query), `change`),
);
const media$ = merge(...events$).pipe(
map(() => currentBreakpoint(breakpoints, windowRef.innerWidth).name),
startWith(currentBreakpoint(breakpoints, windowRef.innerWidth).name),
share(),
);

super(subscriber => media$.subscribe(subscriber));
}
}

type MediaKey = Omit<keyof TuiMedia, 'tablet'>;

interface Breakpoint {
name: MediaKey;
query: string;
width: number;
}

function getBreakpoints(media: TuiMedia): Breakpoint[] {
return Object.entries(media).map(([name, width]) => ({
name: name as MediaKey,
/**
* @note:
* min-width query in css is inclusive, but in window.matchMedia it is exclusive
* so we need to subtract 1px to get the same result
*/
query: `(max-width: ${width - 1}px)`,
width,
}));
}

function currentBreakpoint(breakpoints: Breakpoint[], innerWidth: number): Breakpoint {
return breakpoints.find(({width}) => innerWidth < width) ?? breakpoints.slice(-1)[0];
}
1 change: 1 addition & 0 deletions projects/core/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './breakpoint.service';
export * from './format-date.service';
export * from './hint.service';
export * from './night-theme.service';
Expand Down
61 changes: 61 additions & 0 deletions projects/core/services/test/breakpoint.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {TestBed} from '@angular/core/testing';
import {WINDOW} from '@ng-web-apis/common';
import {configureTestSuite} from '@taiga-ui/testing';

import {TuiMedia} from '../../interfaces';
import {TUI_MEDIA} from '../../tokens';
import {TuiBreakpointService} from '../breakpoint.service';

describe(`TuiBreakpointService`, () => {
const mock: HTMLDivElement = document.createElement(`div`);
let service: TuiBreakpointService;
const mediaMock: TuiMedia = {
mobile: 768,
desktopSmall: 1024,
desktopLarge: 1280,
};

const windowMock: any = {
matchMedia: jest
.fn()
.mockReturnValue({...mock, matches: true, media: `(max-width: 767px)`}),
innerWidth: 700,
};

configureTestSuite(() => {
TestBed.configureTestingModule({
providers: [
TuiBreakpointService,
{
provide: TUI_MEDIA,
useValue: mediaMock,
},
{
provide: WINDOW,
useValue: windowMock,
},
],
});
});

beforeEach(() => {
service = TestBed.inject(TuiBreakpointService);
});

afterEach(() => {
jest.clearAllMocks();
});

it(`should create`, () => {
expect(service).toBeTruthy();
});

it(`should emit the current breakpoint name when subscribed to`, () => {
const observerMock = jest.fn();

const subscription = service.subscribe(observerMock);

expect(observerMock).toHaveBeenCalledWith(`mobile`);
subscription.unsubscribe();
});
});
2 changes: 2 additions & 0 deletions projects/core/tokens/is-mobile-resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {distinctUntilChanged, map, share, startWith} from 'rxjs/operators';
import {TUI_MEDIA} from './media';

/**
* @deprecated use {@link https://taiga-ui.dev/services/breakpoint-service TuiBreakpointService}
* TODO: drop in v4.0
* Mobile resolution stream for private providers
*/
export const TUI_IS_MOBILE_RES = new InjectionToken<Observable<boolean>>(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
describe(`Breakpoint service`, () => {
for (const {width, height} of [
{width: 768, height: 900},
{width: 1024, height: 900},
{width: 1280, height: 900},
]) {
it(`${width}x${height}`, () => {
cy.viewport(width, height);
cy.tuiVisit(`/services/breakpoint-service`);

cy.get(`tui-doc-example[heading="Basic"]`)
.findByAutomationId(`tui-doc-example`)
.tuiScrollIntoView()
.tuiWaitBeforeScreenshot()
.matchImageSnapshot(`breakpoint_${width}x${height}`);
});
}
});
9 changes: 9 additions & 0 deletions projects/demo/src/modules/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1710,6 +1710,15 @@ export const ROUTES: Routes = [
title: `AlertService`,
},
},
{
path: `services/breakpoint-service`,
loadChildren: async () =>
(await import(`../services/breakpoint/breakpoint.module`))
.ExampleTuiBreakpointModule,
data: {
title: `BreakpointService`,
},
},
{
path: `services/destroy-service`,
loadChildren: async () =>
Expand Down
6 changes: 6 additions & 0 deletions projects/demo/src/modules/app/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,12 @@ export const pages: TuiDocPages = [
keywords: `уведомление, нотификация, бабл, облачко, alert, notification`,
route: `/services/alert-service`,
},
{
section: $localize`Tools`,
title: `BreakpointService`,
keywords: `breakpoint`,
route: `/services/breakpoint-service`,
},
{
section: $localize`Tools`,
title: `DestroyService`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDocExample} from '@taiga-ui/addon-doc';

@Component({
selector: 'example-tui-breakpoint',
templateUrl: './breakpoint.template.html',
changeDetection,
})
export class ExampleTuiBreakpointComponent {
provideService = import('./examples/provide-service.md?raw');
injectService = import('./examples/inject-service.md?raw');

readonly example: TuiDocExample = {
TypeScript: import('./examples/1/component.ts?raw'),
HTML: import('./examples/1/template.html?raw'),
};
}
18 changes: 18 additions & 0 deletions projects/demo/src/modules/services/breakpoint/breakpoint.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
import {TuiAddonDocModule, tuiGenerateRoutes} from '@taiga-ui/addon-doc';

import {ExampleTuiBreakpointComponent} from './breakpoint.component';
import {TuiBreakpointExample} from './examples/1/component';

@NgModule({
imports: [
CommonModule,
TuiAddonDocModule,
RouterModule.forChild(tuiGenerateRoutes(ExampleTuiBreakpointComponent)),
],
declarations: [ExampleTuiBreakpointComponent, TuiBreakpointExample],
exports: [ExampleTuiBreakpointComponent],
})
export class ExampleTuiBreakpointModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<tui-doc-page
header="BreakpointService"
package="CORE"
path="core/services/breakpoint.service.ts"
>
<ng-template pageTab>
<p i18n>Service to observe changes in the current breakpoint.</p>
<tui-doc-example
id="basic"
i18n-heading
heading="Basic"
[content]="example"
>
<tui-breakpoint-example></tui-breakpoint-example>
</tui-doc-example>
</ng-template>
<ng-template pageTab="Setup">
<ol class="b-demo-steps">
<li>
<p i18n>
Add
<code>TuiBreakpointService</code>
to the providers of your component:
</p>

<tui-doc-code
filename="myComponent.component.ts"
[code]="provideService"
></tui-doc-code>
</li>
<li>
<p i18n>
Inject
<code>TuiBreakpointService</code>
into your component:
</p>

<tui-doc-code
filename="myComponent.component.ts"
[code]="injectService"
></tui-doc-code>
</li>
</ol>
</ng-template>
</tui-doc-page>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {Component, Inject} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiBreakpointService} from '@taiga-ui/core';

@Component({
selector: `tui-breakpoint-example`,
templateUrl: `./template.html`,
styleUrls: [`./style.less`],
changeDetection,
encapsulation,
})
export class TuiBreakpointExample {
constructor(
@Inject(TuiBreakpointService)
readonly breakpoint$: TuiBreakpointService,
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
@import 'taiga-ui-local';

:host {
display: block;
}

table {
width: 100%;
border-spacing: 0;
}

th,
td {
text-align: left;
border: 1px solid var(--tui-base-03);
height: 3.375rem;
padding: 0 1rem;
vertical-align: middle;
}

.mobile {
display: block;
}

.desktop-small {
display: none;
}

.desktop-large {
display: none;
}

@media @tui-tablet-min {
.mobile {
display: none;
}

.desktop-small {
display: block;
}

.desktop-large {
display: none;
}
}

@media @tui-desktop-min {
.mobile {
display: none;
}

.desktop-small {
display: none;
}

.desktop-large {
display: block;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<p>Change the viewport of this window to see changes in breakpoint</p>

<table class="tui-space_top-4">
<thead>
<tr>
<th>CSS</th>
<th>Breakpoint service</th>
</tr>
</thead>
<tbody>
<td>
<div class="mobile">Mobile breakpoint</div>
<div class="desktop-small">Desktop small breakpoint</div>
<div class="desktop-large">Desktop large breakpoint</div>
</td>
<td>
<ng-container *ngIf="breakpoint$ | async as breakpoint">
<div *ngIf="breakpoint === 'mobile'">Mobile breakpoint</div>
<div *ngIf="breakpoint === 'desktopSmall'">Desktop small breakpoint</div>
<div *ngIf="breakpoint === 'desktopLarge'">Desktop large breakpoint</div>
</ng-container>
</td>
</tbody>
</table>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
```ts
import {Inject} from '@angular/core';
import {TuiBreakpointService} from '@taiga-ui/core';

// ...
export class MyComponent {
constructor(
@Inject(TuiBreakpointService)
private readonly breakpoint$: TuiBreakpointService,
) {}
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
```ts
import {Component} from '@angular/core';
import {TuiBreakpointService} from '@taiga-ui/core';

@Component({
// ...
providers: [TuiBreakpointService],
})
export class MyComponent {}
```

0 comments on commit 6451a66

Please sign in to comment.