From 8ec81aa86356af764ac881177c207abd4b71b925 Mon Sep 17 00:00:00 2001 From: Alex Inkin Date: Sat, 14 Sep 2024 14:54:34 +0400 Subject: [PATCH] fix(core): `Calendar` fix visual gaps in range (#8961) Co-authored-by: taiga-family-bot --- .../mobile-calendar-sheet.style.less | 137 ++++++---------- .../mobile-calendar-sheet.template.html | 9 +- .../mobile-calendar.style.less | 6 - .../calendar/calendar-sheet.component.ts | 54 +++---- .../calendar/calendar-sheet.style.less | 17 +- .../calendar/calendar-sheet.template.html | 45 +++--- .../calendar/calendar-year.component.ts | 85 +++------- .../calendar/calendar-year.style.less | 21 +-- .../calendar/calendar-year.template.html | 3 +- .../test/calendar-sheet.component.spec.ts | 2 +- .../test/calendar-year.component.spec.ts | 34 +--- .../core/components/root/root.component.ts | 11 ++ projects/core/styles/mixins/date-picker.less | 152 ++++++++++++++++++ projects/core/styles/mixins/picker.less | 1 + projects/core/styles/mixins/picker.scss | 1 + projects/core/styles/taiga-ui-local.less | 1 + projects/core/styles/theme/wrapper.less | 18 ++- .../mobile-calendar/input-date.spec.ts | 2 +- .../calendar-month.component.ts | 75 +++------ .../calendar-month/calendar-month.style.less | 25 +-- .../calendar-month.template.html | 13 +- .../test/calendar-month.component.spec.ts | 76 +-------- .../components/segmented/segmented.style.less | 7 +- .../button-group/button-group.style.less | 1 + .../directives/password/password.directive.ts | 5 - projects/kit/styles/components/chip.less | 2 + .../testing/core/calendar-sheet.harness.ts | 2 +- .../testing/core/calendar-year.harness.ts | 2 +- 28 files changed, 359 insertions(+), 448 deletions(-) create mode 100644 projects/core/styles/mixins/date-picker.less diff --git a/projects/addon-mobile/components/mobile-calendar-sheet/mobile-calendar-sheet.style.less b/projects/addon-mobile/components/mobile-calendar-sheet/mobile-calendar-sheet.style.less index 792db333c350..8e9aaf1289fb 100644 --- a/projects/addon-mobile/components/mobile-calendar-sheet/mobile-calendar-sheet.style.less +++ b/projects/addon-mobile/components/mobile-calendar-sheet/mobile-calendar-sheet.style.less @@ -1,12 +1,9 @@ @import '@taiga-ui/core/styles/taiga-ui-local'; -@itemSize: 2.75rem; -@accent: var(--tui-text-action); -@faded: var(--tui-background-base-alt); - -.picker(@itemSize); +.date-picker(); :host { + display: block; inline-size: 20.75rem; // TODO: investigate problem in mobile calendar /* stylelint-disable-next-line */ @@ -14,118 +11,74 @@ &._ios { inline-size: 22.625rem; + + .t-row { + block-size: 3.125rem; + font-size: 1.0625rem; + } + + .t-cell[data-range='start'], + .t-cell[data-range='end'], + .t-cell[data-range='active'] { + font-weight: 600; + } } } .t-row { block-size: 3rem; - justify-content: flex-start; + justify-content: space-between !important; + font-family: inherit; + font-size: 1.125rem; + padding: 0.125rem; + box-sizing: border-box; - &:first-child { - justify-content: flex-end; + &:first-child .t-cell:not(.t-cell_empty) ~ .t-cell_empty, + &:last-child .t-cell_empty { + display: none; } - :host._ios & { - block-size: 3.125rem; + &:last-child .t-cell:not(.t-cell_empty) ~ .t-cell_empty { + display: flex; } } .t-cell { - block-size: @itemSize; + inline-size: 2.5rem; + block-size: 2.5rem; border-radius: 100%; overflow: hidden; - margin-right: 0.25rem; - - &:last-child, - :host._ios &:last-child { - margin-right: 0; - } + mask: none; + border: none; + text-decoration: none; - &::before { - // TODO: investigate problem in mobile calendar - /* stylelint-disable-next-line */ - right: -100vw; + &_empty { + visibility: hidden; } - &:first-child::before { - // TODO: investigate problem in mobile calendar - /* stylelint-disable-next-line */ - left: -100vw; - } - - &_today::after { - block-size: 0.375rem; - inline-size: 0.375rem; - background-color: @accent; - } - - &_interval { - &::before { - background-color: @faded; + &_today { + &::after { + content: '•'; + text-align: center; + line-height: 4rem; + font-size: 1.5rem; + color: var(--tui-text-action); } - &:last-child:first-child::before { - // TODO: investigate problem in mobile calendar - /* stylelint-disable-next-line */ - right: -100vw; + &[data-range='start']::after, + &[data-range='end']::after, + &[data-range='active']::after { + color: inherit; } } - &_disabled { - opacity: var(--tui-disabled-opacity); - } - - &[data-range='single'], &[data-range='start'], - &[data-range='end'] { - border-radius: 100%; - background-color: @accent; - color: var(--tui-text-primary-on-accent-2); + &[data-range='end'], + &[data-range='active'] { font-weight: 500; - - &::after { - background-color: var(--tui-background-base); - } - - :host._ios & { - font-weight: 600; - } - } - - &[data-range='start']::before { - left: 50%; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - - &[data-range='end']:not(:first-child)::before { - left: 50%; - background: var(--tui-background-base); - border-radius: 0; - } - - &[data-range='end']:first-child::before { - right: 50%; - background-color: @faded; - } - - :host._ios & { - margin-right: 0.5625rem; - - &::before { - border-radius: 0.5rem; - } - } -} - -@media screen and (max-width: 22.4375rem) { - .t-cell, - :host._ios .t-cell { - margin-right: 0.125rem; } - .t-cell:last-child, - :host._ios .t-cell:last-child { - margin-right: 0; + &::after { + mask: none; } } diff --git a/projects/addon-mobile/components/mobile-calendar-sheet/mobile-calendar-sheet.template.html b/projects/addon-mobile/components/mobile-calendar-sheet/mobile-calendar-sheet.template.html index 63ae4a2c7e03..6592ca6457d2 100644 --- a/projects/addon-mobile/components/mobile-calendar-sheet/mobile-calendar-sheet.template.html +++ b/projects/addon-mobile/components/mobile-calendar-sheet/mobile-calendar-sheet.template.html @@ -4,6 +4,10 @@ automation-id="tui-primitive-calendar-mobile__row" class="t-row" > +
{{ item.day }}
+
diff --git a/projects/addon-mobile/components/mobile-calendar/mobile-calendar.style.less b/projects/addon-mobile/components/mobile-calendar/mobile-calendar.style.less index 77640cd21787..a574f5e48ef0 100644 --- a/projects/addon-mobile/components/mobile-calendar/mobile-calendar.style.less +++ b/projects/addon-mobile/components/mobile-calendar/mobile-calendar.style.less @@ -207,13 +207,7 @@ .t-calendar { margin: 0 auto; - font-family: inherit; - font-size: 1.125rem; transition: opacity 0.2s; - - :host._ios & { - font-size: 1.0625rem; - } } .t-week, diff --git a/projects/core/components/calendar/calendar-sheet.component.ts b/projects/core/components/calendar/calendar-sheet.component.ts index 4a71b43ba25d..8c886bae8a05 100644 --- a/projects/core/components/calendar/calendar-sheet.component.ts +++ b/projects/core/components/calendar/calendar-sheet.component.ts @@ -14,10 +14,9 @@ import {TuiLet} from '@taiga-ui/cdk/directives/let'; import {TuiRepeatTimes} from '@taiga-ui/cdk/directives/repeat-times'; import {TuiMapperPipe} from '@taiga-ui/cdk/pipes/mapper'; import type {TuiBooleanHandler, TuiHandler} from '@taiga-ui/cdk/types'; -import {tuiNullableSame} from '@taiga-ui/cdk/utils/miscellaneous'; +import {tuiNullableSame, tuiPure} from '@taiga-ui/cdk/utils/miscellaneous'; import {TuiCalendarSheetPipe, TuiOrderWeekDaysPipe} from '@taiga-ui/core/pipes'; import {TUI_DAY_TYPE_HANDLER, TUI_SHORT_WEEK_DAYS} from '@taiga-ui/core/tokens'; -import type {TuiRangeState} from '@taiga-ui/core/types'; export type TuiMarkerHandler = TuiHandler; @@ -37,7 +36,7 @@ export type TuiMarkerHandler = TuiHandler day.daySame(item)) ? 'active' : null; } - if (!(value instanceof TuiDayRange)) { - return value.find((day) => day.daySame(item)) ? 'single' : null; + const range = this.getRange(value, hoveredItem); + + if (value.isSingleDay && range.isSingleDay && value.from.daySame(item)) { + return 'active'; } - if ( - (value.from.daySame(item) && !value.isSingleDay) || - (hoveredItem?.dayAfter(value.from) && - value.from.daySame(item) && - value.isSingleDay) || - (hoveredItem?.daySame(item) && - hoveredItem.dayBefore(value.from) && - value.isSingleDay) - ) { + if (range.from.daySame(item)) { return 'start'; } - if ( - (value.to.daySame(item) && !value.isSingleDay) || - (hoveredItem?.dayBefore(value.from) && - value.from.daySame(item) && - value.isSingleDay) || - (hoveredItem?.daySame(item) && - hoveredItem.dayAfter(value.from) && - value.isSingleDay) - ) { + if (range.to.daySame(item)) { return 'end'; } - return value.isSingleDay && value.from.daySame(item) ? 'single' : null; + return range.from.dayBefore(item) && range.to.dayAfter(item) ? 'middle' : null; } protected get isSingleDayRange(): boolean { @@ -143,10 +128,10 @@ export class TuiCalendarSheet { protected readonly toMarkers = ( day: TuiDay, today: boolean, - inRange: boolean, + range: string | null, markerHandler: TuiMarkerHandler | null, ): [string, string] | [string] | null => { - if (today || inRange) { + if (today || ['active', 'end', 'start'].includes(range || '')) { return null; } @@ -167,6 +152,13 @@ export class TuiCalendarSheet { this.dayClick.emit(item); } + @tuiPure + private getRange(value: TuiDayRange, hoveredItem: TuiDay | null): TuiDayRange { + return value.isSingleDay + ? TuiDayRange.sort(value.from, hoveredItem ?? value.to) + : value; + } + private updateHoveredItem(day: TuiDay | null): void { if (tuiNullableSame(this.hoveredItem, day, (a, b) => a.daySame(b))) { return; diff --git a/projects/core/components/calendar/calendar-sheet.style.less b/projects/core/components/calendar/calendar-sheet.style.less index 7a3579796b32..602e928d76b4 100644 --- a/projects/core/components/calendar/calendar-sheet.style.less +++ b/projects/core/components/calendar/calendar-sheet.style.less @@ -1,11 +1,9 @@ @import '@taiga-ui/core/styles/taiga-ui-local'; -@itemSize: 2.25rem; +.date-picker(); -.picker(@itemSize); - -:host { - inline-size: @itemSize * 7; +.t-cell { + inline-size: 2.25rem; } [data-type='weekday'] { @@ -30,16 +28,13 @@ } } -.t-item { - display: flex; - flex-direction: column; -} - -.t-item_unavailable { +.t-cell_unavailable { opacity: var(--tui-disabled-opacity); } .t-dots { + position: absolute; + bottom: 0; display: flex; justify-content: center; margin-top: -0.5rem; diff --git a/projects/core/components/calendar/calendar-sheet.template.html b/projects/core/components/calendar/calendar-sheet.template.html index 5bbbe0c1f610..ae3b066658e9 100644 --- a/projects/core/components/calendar/calendar-sheet.template.html +++ b/projects/core/components/calendar/calendar-sheet.template.html @@ -18,41 +18,34 @@ automation-id="tui-calendar-sheet__cell" class="t-cell" [attr.data-range]="getItemRange(item)" + [attr.data-type]="item | tuiMapper: dayTypeHandler" [class.t-cell_disabled]="disabledItemHandler(item)" - [class.t-cell_interval]="itemIsInterval(item)" [class.t-cell_today]="itemIsToday(item)" [class.t-cell_unavailable]="itemIsUnavailable(item)" (click)="onItemClick(item)" (tuiHoveredChange)="onItemHovered($event && item)" > + {{ item.day }}
- {{ item.day }}
-
-
-
+ class="t-dot" + [style.background]="markers?.[0]" + >
+
diff --git a/projects/core/components/calendar/calendar-year.component.ts b/projects/core/components/calendar/calendar-year.component.ts index c41395c408ec..0d50988edc5d 100644 --- a/projects/core/components/calendar/calendar-year.component.ts +++ b/projects/core/components/calendar/calendar-year.component.ts @@ -6,10 +6,11 @@ import { Output, } from '@angular/core'; import {TUI_FALSE_HANDLER} from '@taiga-ui/cdk/constants'; -import type {TuiDay, TuiDayRange} from '@taiga-ui/cdk/date-time'; +import type {TuiDay} from '@taiga-ui/cdk/date-time'; import { MAX_YEAR, MIN_YEAR, + TuiDayRange, TuiMonth, TuiMonthRange, TuiYear, @@ -18,10 +19,8 @@ import {TuiHovered} from '@taiga-ui/cdk/directives/hovered'; import {TuiLet} from '@taiga-ui/cdk/directives/let'; import {TuiRepeatTimes} from '@taiga-ui/cdk/directives/repeat-times'; import type {TuiBooleanHandler} from '@taiga-ui/cdk/types'; -import {tuiInRange} from '@taiga-ui/cdk/utils/math'; import {tuiIsNumber} from '@taiga-ui/cdk/utils/miscellaneous'; import {TuiScrollIntoView} from '@taiga-ui/core/components/scrollbar'; -import type {TuiRangeState} from '@taiga-ui/core/types'; const LIMIT = 100; const ITEMS_IN_ROW = 4; @@ -34,7 +33,7 @@ const ITEMS_IN_ROW = 4; styleUrls: ['./calendar-year.style.less'], changeDetection: ChangeDetectionStrategy.OnPush, host: { - '[class._single]': 'isSingle', + '[class._picking]': 'isSingle', }, }) export class TuiCalendarYear { @@ -73,78 +72,38 @@ export class TuiCalendarYear { ); } - public getItemRange(item: number): TuiRangeState | null { + public getItemRange(item: number): 'active' | 'end' | 'middle' | 'start' | null { const {value, hoveredItem} = this; - if (value === null) { - return null; - } - if (value instanceof TuiYear) { - return value.year === item ? 'single' : null; + return value.year === item ? 'active' : null; } if (tuiIsNumber(value)) { - return value === item ? 'single' : null; + return value === item ? 'active' : null; } if (!(value instanceof TuiMonthRange)) { - return value.find((day) => day.year === item) ? 'single' : null; - } - - if ( - (value.from.year === item && !value.from.yearSame(value.to)) || - (hoveredItem !== null && - hoveredItem > value.from.year && - value.from.year === item && - value.from.yearSame(value.to)) || - (hoveredItem !== null && - hoveredItem === item && - hoveredItem < value.from.year && - value.from.yearSame(value.to)) - ) { - return 'start'; - } - - if ( - (value.to.year === item && !value.from.yearSame(value.to)) || - (hoveredItem !== null && - hoveredItem < value.from.year && - value.from.year === item && - value.from.yearSame(value.to)) || - (hoveredItem !== null && - hoveredItem === item && - hoveredItem > value.from.year && - value.from.yearSame(value.to)) - ) { - return 'end'; + return value?.find((day) => day.year === item) ? 'active' : null; } - return value.from.yearSame(value.to) && value.from.year === item - ? 'single' - : null; - } - - public itemIsInterval(item: number): boolean { - const {value, hoveredItem} = this; + const hovered = this.isSingle ? hoveredItem : null; + const from = Math.min(value.from.year, hovered ?? value.to.year); + const to = Math.max(value.from.year, hovered ?? value.to.year); - if (!this.isRange(value)) { - return false; + if (from === to && value.from.year === value.to.year && from === item) { + return 'active'; } - if (!value.from.yearSame(value.to)) { - return value.from.year <= item && value.to.year > item; + if (from === item) { + return 'start'; } - if (hoveredItem === null || value.from.year === hoveredItem) { - return false; + if (to === item) { + return 'end'; } - return tuiInRange( - item, - Math.min(value.from.year, hoveredItem), - Math.max(value.from.year, hoveredItem), - ); + return from < item && item < to ? 'middle' : null; } public onItemHovered(hovered: boolean, item: number): void { @@ -152,19 +111,15 @@ export class TuiCalendarYear { } protected get isSingle(): boolean { - return this.isRange(this.value) && this.value.from.yearSame(this.value.to); + return this.value instanceof TuiMonthRange + ? this.value.from.monthSame(this.value.to) + : this.value instanceof TuiDayRange && this.value.isSingleDay; } protected get rows(): number { return Math.ceil((this.calculatedMax - this.calculatedMin) / ITEMS_IN_ROW); } - protected isRange( - item: TuiMonthRange | TuiYear | number | readonly TuiDay[] | null, - ): item is TuiMonthRange { - return item instanceof TuiMonthRange; - } - protected scrollItemIntoView(item: number): boolean { return this.initialItem === item; } diff --git a/projects/core/components/calendar/calendar-year.style.less b/projects/core/components/calendar/calendar-year.style.less index ce37ab6fd216..8b2f52fa06ba 100644 --- a/projects/core/components/calendar/calendar-year.style.less +++ b/projects/core/components/calendar/calendar-year.style.less @@ -1,21 +1,14 @@ @import '@taiga-ui/core/styles/taiga-ui-local'; -@itemSize: 3.9375rem; - -.picker(@itemSize); +.date-picker(); :host { - inline-size: @itemSize * 4; + display: block; + inline-size: 16rem; } -.t-row { - margin: 0.875rem 0; - - &:first-child { - margin-top: 0; - } - - &:last-child { - margin-bottom: 0; - } +.t-cell { + inline-size: 4rem; + border-block-start-width: 0.5rem; + border-block-end-width: 0.5rem; } diff --git a/projects/core/components/calendar/calendar-year.template.html b/projects/core/components/calendar/calendar-year.template.html index fba70c2f93d1..9b2604fe6bcf 100644 --- a/projects/core/components/calendar/calendar-year.template.html +++ b/projects/core/components/calendar/calendar-year.template.html @@ -10,13 +10,12 @@ class="t-cell" [attr.data-range]="getItemRange(item)" [class.t-cell_disabled]="isDisabled(item)" - [class.t-cell_interval]="itemIsInterval(item)" [class.t-cell_today]="itemIsToday(item)" [tuiScrollIntoView]="scrollItemIntoView(item)" (click)="yearClick.emit(item)" (tuiHoveredChange)="onItemHovered($event, item)" > -
{{ item }}
+ {{ item }} diff --git a/projects/core/components/calendar/test/calendar-sheet.component.spec.ts b/projects/core/components/calendar/test/calendar-sheet.component.spec.ts index 3761de939c6e..3424a3ebde4a 100644 --- a/projects/core/components/calendar/test/calendar-sheet.component.spec.ts +++ b/projects/core/components/calendar/test/calendar-sheet.component.spec.ts @@ -142,7 +142,7 @@ describe('CalendarSheet', () => { component.value = new TuiDayRange(day1, day1); - expect(component.getItemRange(day1)).toBe('single'); + expect(component.getItemRange(day1)).toBe('active'); }); }); diff --git a/projects/core/components/calendar/test/calendar-year.component.spec.ts b/projects/core/components/calendar/test/calendar-year.component.spec.ts index 5e33bb7d9974..b7134bf3c1b1 100644 --- a/projects/core/components/calendar/test/calendar-year.component.spec.ts +++ b/projects/core/components/calendar/test/calendar-year.component.spec.ts @@ -88,7 +88,7 @@ describe('TuiCalendarYearComponent', () => { expect(component.getItemRange(item)).toBe('end'); }); - it('returns single correctly', () => { + it('returns active correctly', () => { const item = 2018; component.value = new TuiDayRange( @@ -96,37 +96,7 @@ describe('TuiCalendarYearComponent', () => { new TuiDay(item, 2, 2), ); - expect(component.getItemRange(item)).toBe('single'); - }); - }); - - describe('itemIsInterval', () => { - it('works correctly if item is in value range', () => { - component.value = new TuiDayRange( - new TuiDay(2018, 4, 20), - new TuiDay(2020, 4, 22), - ); - - expect(component.itemIsInterval(2019)).toBe(true); - }); - - it('returns false if item is in value range of same year and no item is hovered', () => { - component.value = new TuiDayRange( - new TuiDay(2019, 4, 20), - new TuiDay(2019, 4, 22), - ); - - expect(component.itemIsInterval(2019)).toBe(false); - }); - - it('works correctly if item is in value range of same year and there is hovered item', () => { - component.value = new TuiDayRange( - new TuiDay(2019, 4, 20), - new TuiDay(2019, 4, 22), - ); - component.onItemHovered(true, 2017); - - expect(component.itemIsInterval(2018)).toBe(true); + expect(component.getItemRange(item)).toBe('active'); }); }); }); diff --git a/projects/core/components/root/root.component.ts b/projects/core/components/root/root.component.ts index cc07d0d99368..809a06645705 100644 --- a/projects/core/components/root/root.component.ts +++ b/projects/core/components/root/root.component.ts @@ -1,3 +1,4 @@ +/// import {AsyncPipe, DOCUMENT, NgIf} from '@angular/common'; import { ChangeDetectionStrategy, @@ -7,6 +8,7 @@ import { ViewEncapsulation, } from '@angular/core'; import {toSignal} from '@angular/core/rxjs-interop'; +import {EVENT_MANAGER_PLUGINS} from '@angular/platform-browser'; import {TUI_VERSION} from '@taiga-ui/cdk/constants'; import {TuiPlatform} from '@taiga-ui/cdk/directives/platform'; import {tuiWatch, tuiZonefreeScheduler} from '@taiga-ui/cdk/observables'; @@ -19,6 +21,7 @@ import {TuiHints} from '@taiga-ui/core/directives/hint'; import {TuiBreakpointService} from '@taiga-ui/core/services'; import {TUI_ANIMATIONS_SPEED, TUI_REDUCED_MOTION, TUI_THEME} from '@taiga-ui/core/tokens'; import {tuiGetDuration} from '@taiga-ui/core/utils'; +import {PreventEventPlugin} from '@taiga-ui/event-plugins'; import type {Observable} from 'rxjs'; import {debounceTime, map, of} from 'rxjs'; @@ -72,5 +75,13 @@ export class TuiRoot { 'data-tui-theme', inject(TUI_THEME).toLowerCase(), ); + + ngDevMode && + console.assert( + inject(EVENT_MANAGER_PLUGINS).find( + (plugin) => plugin instanceof PreventEventPlugin, + ), + 'NG_EVENT_PLUGINS is missing from global providers', + ); } } diff --git a/projects/core/styles/mixins/date-picker.less b/projects/core/styles/mixins/date-picker.less new file mode 100644 index 000000000000..edc157d39cce --- /dev/null +++ b/projects/core/styles/mixins/date-picker.less @@ -0,0 +1,152 @@ +@import 'mixins.less'; + +.date-picker() { + .t-row { + display: flex; + justify-content: flex-start; + font: var(--tui-font-text-m); + + &:first-child { + justify-content: flex-end; + } + + &:last-child { + justify-content: flex-start; + } + } + + .t-cell { + position: relative; + display: flex; + align-items: center; + justify-content: center; + line-height: 2rem; + isolation: isolate; + cursor: pointer; + overflow: hidden; + border: 0.125rem solid transparent; + box-sizing: border-box; + mask: linear-gradient( + transparent calc(50% - 1rem), + #000 calc(50% - 1rem), + #000 calc(50% + 1rem), + transparent calc(50% + 1rem) + ); + + &:first-child { + border-inline-start-color: transparent !important; + } + + &:last-child { + border-inline-end-color: transparent !important; + } + + &::before, + &::after { + .fullsize(absolute, inset); + + content: ''; + z-index: -1; + border-radius: var(--tui-radius-m); + } + + &::after { + mask: + url('data:image/svg+xml,') + right/0.75rem 100% no-repeat, + linear-gradient(#000, #000) left/calc(100% - 0.7rem) 100% no-repeat; + } + + &[data-range]::before { + background: var(--tui-background-base-alt); + } + + :host._picking &[data-range]::before { + background: var(--tui-background-neutral-1-hover); + } + + &[data-range='middle'] { + border-color: var(--tui-background-base-alt); + + :host._picking & { + border-color: var(--tui-background-neutral-1-hover); + } + + &:not(:first-child)::before { + border-start-start-radius: 0; + border-end-start-radius: 0; + } + + &:not(:last-child)::before { + border-start-end-radius: 0; + border-end-end-radius: 0; + } + } + + &[data-range='start'] { + border-inline-end-color: var(--tui-background-base-alt); + color: var(--tui-text-primary-on-accent-1); + + :host._picking & { + border-inline-end-color: var(--tui-background-neutral-1-hover); + } + + &:not(:last-child)::before { + right: -1rem; + } + + &::after { + background: var(--tui-background-accent-1); + } + } + + &[data-range='end'] { + border-inline-start-color: var(--tui-background-base-alt); + color: var(--tui-text-primary-on-accent-1); + + :host._picking & { + border-inline-start-color: var(--tui-background-neutral-1-hover); + } + + &:not(:first-child)::before { + left: -1rem; + } + + &::after { + background: var(--tui-background-accent-1); + transform: scale(-1, 1); + } + } + + &[data-range='active'] { + color: var(--tui-text-primary-on-accent-1); + + &::after { + background: var(--tui-background-accent-1); + mask: none; + } + } + + &_disabled { + opacity: var(--tui-disabled-opacity); + pointer-events: none; + } + + &_today { + text-decoration: underline; + text-underline-offset: 0.25rem; + } + + @media (hover: hover) { + &:hover:not([data-range='start']):not([data-range='end'])::before { + background: var(--tui-background-neutral-1-hover); + } + + &[data-range='start']:hover::after, + &[data-range='end']:hover::after, + &[data-range='active']:hover::after { + background: var(--tui-background-accent-1-hover); + } + } + } +} diff --git a/projects/core/styles/mixins/picker.less b/projects/core/styles/mixins/picker.less index e25dc5bc6b73..bf1d4ed91d56 100644 --- a/projects/core/styles/mixins/picker.less +++ b/projects/core/styles/mixins/picker.less @@ -1,5 +1,6 @@ @import 'mixins.less'; +// @deprecated .picker(@itemSize) { :host { display: block; diff --git a/projects/core/styles/mixins/picker.scss b/projects/core/styles/mixins/picker.scss index 123ac5e4365b..19071bc81460 100644 --- a/projects/core/styles/mixins/picker.scss +++ b/projects/core/styles/mixins/picker.scss @@ -1,5 +1,6 @@ @import 'mixins.scss'; +// @deprecated @mixin picker($itemSize) { :host { display: block; diff --git a/projects/core/styles/taiga-ui-local.less b/projects/core/styles/taiga-ui-local.less index ee7319b85eba..e754c531e54d 100644 --- a/projects/core/styles/taiga-ui-local.less +++ b/projects/core/styles/taiga-ui-local.less @@ -1,4 +1,5 @@ @import 'mixins/browsers.less'; +@import 'mixins/date-picker.less'; @import 'mixins/mixins.less'; @import 'mixins/picker.less'; @import 'mixins/slider.less'; diff --git a/projects/core/styles/theme/wrapper.less b/projects/core/styles/theme/wrapper.less index 6c6118b2f645..15bac3463047 100644 --- a/projects/core/styles/theme/wrapper.less +++ b/projects/core/styles/theme/wrapper.less @@ -132,14 +132,6 @@ background-size: 0.5rem 0.5rem; } - table &[data-appearance='table']:not(._focused)::after { - border-width: 0; - } - - table &._focused { - z-index: 1; - } - &[data-appearance='table']::after { border-width: 1px; color: var(--tui-border-normal); @@ -191,6 +183,16 @@ }); } +table [tuiWrapper][data-appearance='table'] { + &[data-appearance='table']:not(._focused)::after { + border-width: 0; + } + + &._focused { + z-index: 1; + } +} + [tuiWrapper][data-appearance='icon'] { .transition(opacity); diff --git a/projects/demo-playwright/tests/addon-mobile/mobile-calendar/input-date.spec.ts b/projects/demo-playwright/tests/addon-mobile/mobile-calendar/input-date.spec.ts index e90d94b5928d..69a4e68e202f 100644 --- a/projects/demo-playwright/tests/addon-mobile/mobile-calendar/input-date.spec.ts +++ b/projects/demo-playwright/tests/addon-mobile/mobile-calendar/input-date.spec.ts @@ -6,7 +6,7 @@ import {TUI_PLAYWRIGHT_MOBILE_USER_AGENT} from '../../../playwright.options'; test.describe('InputDate and mobile user agent', () => { const date = new Date(2023, 10, 1); - const november = '.t-month-wrapper:nth-child(2) .t-cell'; + const november = '.t-month-wrapper:nth-child(2) .t-cell:not(.t-cell_empty)'; test.use({ viewport: {width: 430, height: 932}, diff --git a/projects/kit/components/calendar-month/calendar-month.component.ts b/projects/kit/components/calendar-month/calendar-month.component.ts index 2f2ce6ed5e81..a2130786aac0 100644 --- a/projects/kit/components/calendar-month/calendar-month.component.ts +++ b/projects/kit/components/calendar-month/calendar-month.component.ts @@ -7,6 +7,7 @@ import { Input, Output, } from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {TUI_FALSE_HANDLER} from '@taiga-ui/cdk/constants'; import { TUI_FIRST_DAY, @@ -18,13 +19,13 @@ import { } from '@taiga-ui/cdk/date-time'; import {TuiHovered} from '@taiga-ui/cdk/directives/hovered'; import {TuiLet} from '@taiga-ui/cdk/directives/let'; +import {TuiRepeatTimes} from '@taiga-ui/cdk/directives/repeat-times'; import type {TuiBooleanHandler} from '@taiga-ui/cdk/types'; import {tuiNullableSame, tuiPure} from '@taiga-ui/cdk/utils/miscellaneous'; import {TuiCalendarYear} from '@taiga-ui/core/components/calendar'; import {TuiLink} from '@taiga-ui/core/components/link'; import {TuiScrollbar} from '@taiga-ui/core/components/scrollbar'; import {TuiSpinButton} from '@taiga-ui/core/components/spin-button'; -import type {TuiRangeState} from '@taiga-ui/core/types'; import {TUI_CALENDAR_MONTHS} from '@taiga-ui/kit/tokens'; const TODAY = TuiDay.currentLocal(); @@ -40,6 +41,7 @@ const TODAY = TuiDay.currentLocal(); TuiHovered, TuiLet, TuiLink, + TuiRepeatTimes, TuiScrollbar, TuiSpinButton, ], @@ -47,12 +49,12 @@ const TODAY = TuiDay.currentLocal(); styleUrls: ['./calendar-month.style.less'], changeDetection: ChangeDetectionStrategy.OnPush, host: { - '[class._single]': 'isSingle', + '[class._picking]': 'isSingle', }, }) export class TuiCalendarMonth { protected isYearPickerShown = false; - protected readonly months$ = inject(TUI_CALENDAR_MONTHS); + protected readonly months = toSignal(inject(TUI_CALENDAR_MONTHS)); @Input() public value: TuiMonth | TuiMonthRange | null = null; @@ -87,10 +89,7 @@ export class TuiCalendarMonth { public hoveredItem: TuiMonth | null = null; public get isSingle(): boolean { - return ( - this.value !== null && - (this.value instanceof TuiMonth || this.value.isSingleMonth) - ); + return this.value instanceof TuiMonthRange && this.value.isSingleMonth; } public onNextYear(): void { @@ -101,66 +100,34 @@ export class TuiCalendarMonth { this.updateActiveYear(this.year.append({year: -1})); } - public isItemInsideRange(month: TuiMonth): boolean { + public getItemRange(item: TuiMonth): 'active' | 'end' | 'middle' | 'start' | null { const {value, hoveredItem} = this; - if (value === null || value instanceof TuiMonth) { - return false; - } - - if (!value.isSingleMonth) { - return value.from.monthSameOrBefore(month) && value.to.monthAfter(month); + if (!(value instanceof TuiMonthRange)) { + return value?.monthSame(item) ? 'active' : null; } - if (hoveredItem === null) { - return false; - } - - const range = TuiMonthRange.sort(value.from, hoveredItem); - - return range.from.monthSameOrBefore(month) && range.to.monthAfter(month); - } - - public getItemRange(item: TuiMonth): TuiRangeState | null { - const {value, hoveredItem} = this; - - if (value === null) { - return null; - } + const months = item.month + item.year * 12; + const hovered = hoveredItem ? hoveredItem.month + hoveredItem.year * 12 : null; + const from = value.from.month + value.from.year * 12; + const to = value.to.month + value.to.year * 12; + const picking = this.isSingle ? hovered : null; + const min = Math.min(from, to, picking ?? from); + const max = Math.max(from, to, picking ?? from); - if (value instanceof TuiMonth) { - return value.monthSame(item) ? 'single' : null; + if (min === max && min === months) { + return 'active'; } - const theFirstOfRange = value.from.monthSame(item) && !value.isSingleMonth; - const hoveredItemAfterFrom = - hoveredItem?.monthAfter(value.from) && - value.from.monthSame(item) && - value.isSingleMonth; - const hoveredItemIsCandidateToBeFrom = - hoveredItem?.monthSame(item) && - hoveredItem?.monthBefore(value.from) && - value.isSingleMonth; - - if (theFirstOfRange || hoveredItemAfterFrom || hoveredItemIsCandidateToBeFrom) { + if (min === months) { return 'start'; } - const theLastOfRange = value.to.monthSame(item) && !value.isSingleMonth; - const hoveredItemBeforeTo = - value.to.monthSame(item) && - hoveredItem?.monthBefore(value.to) && - value.isSingleMonth; - const hoveredItemIsCandidateToBeTo = - hoveredItem?.monthSame(item) && - hoveredItem?.monthAfter(value.from) && - value.isSingleMonth; - - if (theLastOfRange || hoveredItemBeforeTo || hoveredItemIsCandidateToBeTo) { + if (max === months) { return 'end'; } - return value.isSingleMonth && value.from.monthSame(item) ? 'single' : null; + return min < months && months < max ? 'middle' : null; } protected get computedMin(): TuiMonth { diff --git a/projects/kit/components/calendar-month/calendar-month.style.less b/projects/kit/components/calendar-month/calendar-month.style.less index cbdf9ffd0fe0..e828886b5690 100644 --- a/projects/kit/components/calendar-month/calendar-month.style.less +++ b/projects/kit/components/calendar-month/calendar-month.style.less @@ -1,32 +1,23 @@ @import '@taiga-ui/core/styles/taiga-ui-local'; -@itemSize: 3.9375rem; - -.picker(@itemSize); +.date-picker(); :host { display: block; - block-size: 13.625rem; - inline-size: @itemSize * 4; + block-size: 12rem; + inline-size: 16rem; padding: 1.125rem; box-sizing: content-box; } -.t-row { - flex-wrap: wrap; - margin-top: 1.4375rem; +.t-spin { + margin-block-end: 1rem; } .t-cell { - &:nth-child(n + 5) { - margin-top: 1.75rem; - } -} - -.t-cell_interval:nth-child(4n) { - &::before { - right: 0; - } + inline-size: 4rem; + border-block-start-width: 0.75rem; + border-block-end-width: 0.75rem; } .t-scrollbar { diff --git a/projects/kit/components/calendar-month/calendar-month.template.html b/projects/kit/components/calendar-month/calendar-month.template.html index a9bd9d8a21bf..07f33d551653 100644 --- a/projects/kit/components/calendar-month/calendar-month.template.html +++ b/projects/kit/components/calendar-month/calendar-month.template.html @@ -12,6 +12,7 @@ -
- +
+
-
{{ month }}
+ {{ months()?.[row * 4 + column] }}
diff --git a/projects/kit/components/calendar-month/test/calendar-month.component.spec.ts b/projects/kit/components/calendar-month/test/calendar-month.component.spec.ts index 30f4e50bf333..d92739d0edd2 100644 --- a/projects/kit/components/calendar-month/test/calendar-month.component.spec.ts +++ b/projects/kit/components/calendar-month/test/calendar-month.component.spec.ts @@ -51,78 +51,6 @@ describe('CalendarMonth', () => { fixture.detectChanges(); }); - describe('isSingle', () => { - it('returns true if there is a value and it is a month', () => { - component.value = TODAY; - - expect(component.isSingle).toBe(true); - }); - - it('returns true if there is a value and it is a single month range', () => { - component.value = new TuiMonthRange(TODAY, TODAY); - - expect(component.isSingle).toBe(true); - }); - - it('returns false if there is no value', () => { - component.value = null; - - expect(component.isSingle).toBe(false); - }); - }); - - describe('isItemInsideRange', () => { - it('returns false if no value', () => { - component.value = null; - - const candidate = new TuiMonth(TODAY.year, 5); - - expect(component.isItemInsideRange(candidate)).toBe(false); - }); - - it('returns false if value is month', () => { - const candidate = new TuiMonth(TODAY.year, 5); - - component.value = candidate; - - expect(component.isItemInsideRange(candidate)).toBe(false); - }); - - it('returns false if it is not hovered item inside singe month range', () => { - const candidate = new TuiMonth(TODAY.year, 5); - - component.value = new TuiMonthRange( - new TuiMonth(TODAY.year, 5), - new TuiMonth(TODAY.year, 5), - ); - - expect(component.isItemInsideRange(candidate)).toBe(false); - }); - - it('returns true if it is hovered item inside singe month range', () => { - const candidate = new TuiMonth(TODAY.year, 5); - - component.hoveredItem = candidate; - component.value = new TuiMonthRange( - new TuiMonth(TODAY.year, 5), - new TuiMonth(TODAY.year, 5), - ); - - expect(component.isItemInsideRange(candidate)).toBe(false); - }); - - it('returns true if value inside a month range', () => { - const candidate = new TuiMonth(TODAY.year, 5); - - component.value = new TuiMonthRange( - new TuiMonth(TODAY.year, 2), - new TuiMonth(TODAY.year, 7), - ); - - expect(component.isItemInsideRange(candidate)).toBe(true); - }); - }); - describe('getItemRange', () => { it('returns null if no value', () => { const month = new TuiMonth(TODAY.year, 7); @@ -132,12 +60,12 @@ describe('CalendarMonth', () => { expect(component.getItemRange(month)).toBeNull(); }); - it('returns single if value is single month choice', () => { + it('returns active if value is single month choice', () => { const month = new TuiMonth(TODAY.year, 7); component.value = month; - expect(component.getItemRange(month)).toBe('single'); + expect(component.getItemRange(month)).toBe('active'); }); it('returns start if item is start of range', () => { diff --git a/projects/kit/components/segmented/segmented.style.less b/projects/kit/components/segmented/segmented.style.less index 42071eaadf33..379478848630 100644 --- a/projects/kit/components/segmented/segmented.style.less +++ b/projects/kit/components/segmented/segmented.style.less @@ -114,7 +114,12 @@ tui-segmented { } & > label > input:not([tuiRadio]) { - .fullsize(); + position: absolute; + top: -0.125rem; + left: -0.125rem; + right: -0.125rem; + bottom: -0.125rem; + background: transparent !important; } &::before { diff --git a/projects/kit/directives/button-group/button-group.style.less b/projects/kit/directives/button-group/button-group.style.less index 0f65001f6349..2e8c4723d931 100644 --- a/projects/kit/directives/button-group/button-group.style.less +++ b/projects/kit/directives/button-group/button-group.style.less @@ -25,6 +25,7 @@ cursor: pointer; color: var(--tui-text-action); text-align: center; + text-decoration: none; &:active { background: var(--tui-background-neutral-1); diff --git a/projects/kit/directives/password/password.directive.ts b/projects/kit/directives/password/password.directive.ts index b82229d92664..c5b548d0e379 100644 --- a/projects/kit/directives/password/password.directive.ts +++ b/projects/kit/directives/password/password.directive.ts @@ -31,7 +31,6 @@ import {TUI_PASSWORD_OPTIONS} from './password.options'; host: { style: 'cursor: pointer', '(click)': 'toggle()', - '[style.pointer-events]': 'disabled ? "none" : null', '[style.border]': 'textfield.options.size() === "s" ? "0.25rem solid transparent" : null', }, @@ -62,10 +61,6 @@ export class TuiPassword { computed(() => (this.hidden() ? this.texts()[0] : this.texts()[1])), ); - protected get disabled(): boolean { - return !this.textfield.el?.nativeElement.value; - } - protected toggle(): void { this.hidden.set(!this.hidden()); this.textfield.el?.nativeElement.setAttribute( diff --git a/projects/kit/styles/components/chip.less b/projects/kit/styles/components/chip.less index 8ceb55f14b1e..5c3dbf5d94e4 100644 --- a/projects/kit/styles/components/chip.less +++ b/projects/kit/styles/components/chip.less @@ -31,6 +31,7 @@ tui-chip, padding: var(--t-padding); block-size: var(--t-size); inline-size: fit-content; + isolation: isolate; .interactive({ cursor: pointer; @@ -94,6 +95,7 @@ tui-chip, & > input[tuiChip] { .fullsize(); + z-index: -1; margin: 0; } diff --git a/projects/testing/core/calendar-sheet.harness.ts b/projects/testing/core/calendar-sheet.harness.ts index d6d3ba58db03..8bba5b46f8c1 100644 --- a/projects/testing/core/calendar-sheet.harness.ts +++ b/projects/testing/core/calendar-sheet.harness.ts @@ -21,7 +21,7 @@ class TuiDayCellHarness extends ComponentHarness { } public async getText(): Promise { - return (await this.locatorFor('.t-item')()).text(); + return (await this.host()).text(); } public async click(): Promise { diff --git a/projects/testing/core/calendar-year.harness.ts b/projects/testing/core/calendar-year.harness.ts index c4bc349752b4..d6fd97a4e34b 100644 --- a/projects/testing/core/calendar-year.harness.ts +++ b/projects/testing/core/calendar-year.harness.ts @@ -21,7 +21,7 @@ class TuiYearCellHarness extends ComponentHarness { } public async getText(): Promise { - return (await this.locatorFor('.t-item')()).text(); + return (await this.host()).text(); } public async click(): Promise {