Skip to content
Source
Sign inSign up

composerFooterLayout.ts

142 lines4.7 KB

1const COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX = 620;
2const COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX = 780;
3const RESTING_COMPOSER_IMAGE_THUMBNAIL_LIMIT = 3;
4​
5export function getRestingComposerImagePreviewCounts(imageCount: number): {
6 visibleCount: number;
7 overflowCount: number;
8} {
9 const visibleCount = Math.min(imageCount, RESTING_COMPOSER_IMAGE_THUMBNAIL_LIMIT);
10 return {
11 visibleCount,
12 overflowCount: Math.max(0, imageCount - visibleCount),
13 };
15​
16export function shouldUseCompactComposerFooter(
17 width: number | null,
18 options?: { hasWideActions?: boolean },
19): boolean {
20 const breakpoint = options?.hasWideActions
21 ? COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX
22 : COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX;
23 return width !== null && width < breakpoint;
25​
26export function shouldAnimateComposerRestingTransition(input: {
27 hasCompletedInitialLayout: boolean;
28 stateChanged: boolean;
29 hasInterruptedAnimation: boolean;
30}): boolean {
31 return input.hasCompletedInitialLayout && (input.stateChanged || input.hasInterruptedAnimation);
33​
34export function shouldUseCompactComposerPrimaryActions(
35 width: number | null,
36 options?: { hasWideActions?: boolean },
37): boolean {
38 if (!options?.hasWideActions) {
39 return false;
40 }
41 return width !== null && width < COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX;
43​
44export interface RestingComposerControlsMeasurement {
45 gap: number;
46 naturalFixedWidth: number;
47 minimumFixedWidth: number;
48 blockWidths: readonly number[];
49 overflowWidth: number;
50 iconOnlyBlockWidths?: readonly number[];
52​
53function restingComposerControlsWidth(
54 input: RestingComposerControlsMeasurement,
55 hiddenCount: number,
56 fixedWidth = input.naturalFixedWidth,
57 iconOnlyCount = 0,
58): number {
59 const { blockWidths, gap } = input;
60 const visibleCount = blockWidths.length - hiddenCount;
61 return (
62 fixedWidth +
63 blockWidths
64 .slice(0, visibleCount)
65 .reduce(
66 (sum, width, index) =>
67 sum +
68 (index >= blockWidths.length - iconOnlyCount
69 ? (input.iconOnlyBlockWidths?.[index] ?? width)
70 : width),
71 0,
72 ) +
73 (hiddenCount > 0 ? input.overflowWidth : 0) +
74 gap * (visibleCount + (hiddenCount > 0 ? 1 : 0))
75 );
77​
78/** Reserve natural composer width to prevent strip labels from repeatedly expanding into space vacated by
79 * hidden controls. */
80export function resolveRestingComposerControlsNaturalWidth(
81 input: RestingComposerControlsMeasurement,
82): number {
83 return restingComposerControlsWidth(input, 0);
85​
86const RESTING_CONTROLS_SLACK_PX = 1;
87​
88export function resolveRestingComposerControlsLayout(
89 input: RestingComposerControlsMeasurement & {
90 hostWidth: number;
91 previous?: { hiddenCount: number; iconOnlyCount?: number; visible: boolean };
92 },
93): { hiddenCount: number; iconOnlyCount?: number; visible: boolean } {
94 const { blockWidths, hostWidth, previous } = input;
95 const iconSteps = input.iconOnlyBlockWidths ? blockWidths.length : 0;
96 const previousStep = previous
97 ? previous.hiddenCount > 0
98 ? iconSteps + Math.min(previous.hiddenCount, blockWidths.length)
99 : Math.min(previous.iconOnlyCount ?? 0, iconSteps)
100 : 0;
101 let step = 0;
102 const widthAtStep = (candidate: number, fixedWidth = input.naturalFixedWidth) =>
103 restingComposerControlsWidth(
104 input,
105 Math.max(0, candidate - iconSteps),
106 fixedWidth,
107 Math.min(candidate, iconSteps),
108 );
109 // Use 1px slack for natural-width jitter on promotion; demote immediately to avoid clipping or layout
110 // flapping.
111 while (
112 step < iconSteps + blockWidths.length &&
113 widthAtStep(step) > hostWidth - (step < previousStep ? RESTING_CONTROLS_SLACK_PX : 0)
114 ) {
115 step += 1;
116 }
117 const hiddenCount = Math.max(0, step - iconSteps);
118 const iconOnlyCount = Math.min(step, iconSteps);
119 const minimumWidth = widthAtStep(step, input.minimumFixedWidth);
120 const visible =
121 previous && !previous.visible
122 ? minimumWidth <= hostWidth - RESTING_CONTROLS_SLACK_PX
123 : minimumWidth <= hostWidth;
124 return { hiddenCount, ...(input.iconOnlyBlockWidths ? { iconOnlyCount } : {}), visible };
126​
127export function resolveScrollToEndClearance(input: {
128 overlayHeight: number;
129 mainSurfaceTop: number;
130 button: { left: number; right: number };
131 attachments: ReadonlyArray<{ top: number; left: number; right: number }>;
132}): number {
133 let contentTop = input.mainSurfaceTop;
134 let top = contentTop;
135 for (const attachment of input.attachments) {
136 contentTop = Math.min(contentTop, attachment.top);
137 if (attachment.left < input.button.right && attachment.right > input.button.left) {
138 top = Math.min(top, attachment.top);
139 }
140 }
141 return Math.ceil(input.overlayHeight - (top - contentTop));