Mixins / foldkit-mixins-ui

MessageScroller

namespace · packages/mixins-ui/src/messageScroller.ts:1

export * as MessageScroller from './messageScroller.js'

In the guides

Members

MessageScroller.FollowEnd

const · packages/mixins-ui/src/messageScroller.ts:136

const FollowEnd: Mount.MountDefinitionNoArgs<
  'MessageScroller.FollowEnd',
  { readonly _tag: 'LeftEnd' } | { readonly _tag: 'ReachedEnd' }
>

Follows the log's end, as a Mount on the log, whose parent is what scrolls. It tells the reader leaving the end (a scroll up) from the log growing: a scroll that moves up and leaves the end stops following, and one that reaches within NEAR of the end starts again. While following, any growth (a message, a streamed word, a picture loading) scrolls to the end; otherwise the first message in view is held where it was, so a page of older messages lands above it unseen. It turns the browser's own scroll anchoring off on the scroller, which would correct the same growth a second time, and which Safari lacks. Without a ResizeObserver (a DOM with no layout) it only reports the end reached. A Scene test of a page that draws the scroller resolves it: Scene.Mount.resolve(FollowEnd, Message.ReachedEnd()).

MessageScroller.init

function · packages/mixins-ui/src/messageScroller.ts:72

function init(options: { readonly id: string }): Model

MessageScroller.logId

function · packages/mixins-ui/src/messageScroller.ts:79

function logId(id: string): string

The id of the log of the scroller id, which ScrollToEnd looks up.

MessageScroller.Message

const · packages/mixins-ui/src/messageScroller.ts:59

const Message: MessageUnion<{
  readonly LeftEnd: {}
  readonly ReachedEnd: {}
  readonly Appended: { readonly count: Schema.Number }
  readonly Jumped: {}
  readonly CompletedScrollToEnd: {}
}>

MessageScroller.MessageScrollerSlots

const · packages/mixins-ui/src/messageScroller.ts:37

const MessageScrollerSlots: Slots.Contract<{
  readonly root: Slot.UnnamedSlot<
    Capability.Capability<
      'Container',
      readonly [
        Capability.Capability<
          'Interactive',
          readonly [Capability.Capability<'Base', readonly []>]
        >,
      ]
    >,
    readonly Slot.EventName[],
    readonly Slot.AttributeName[],
    readonly Slot.RequirementName[],
    boolean
  >
  readonly log: Slot.UnnamedSlot<
    Capability.Capability<
      'Container',
      readonly [
        Capability.Capability<
          'Interactive',
          readonly [Capability.Capability<'Base', readonly []>]
        >,
      ]
    >,
    readonly Slot.EventName[],
    readonly [Attr.AttrToken<'role'>, Attr.AttrToken<'aria-label'>],
    readonly Slot.RequirementName[],
    boolean
  >
  readonly jump: Slot.UnnamedSlot<
    Capability.Capability<
      'Interactive',
      readonly [Capability.Capability<'Base', readonly []>]
    >,
    readonly [Event.EventToken<'click'>],
    readonly [Attr.AttrToken<'aria-label'>],
    readonly Slot.RequirementName[],
    boolean
  >
  readonly count: Slot.UnnamedSlot<
    Capability.Capability<
      'Container',
      readonly [
        Capability.Capability<
          'Interactive',
          readonly [Capability.Capability<'Base', readonly []>]
        >,
      ]
    >,
    readonly Slot.EventName[],
    readonly Slot.AttributeName[],
    readonly Slot.RequirementName[],
    boolean
  >
}>

A message scroller: a conversation's messages in a ScrollArea that keeps the newest in view while the reader is at the bottom, stops following once they scroll up, and offers a button back to the latest that counts what arrived meanwhile. Older messages loaded above leave what the reader looks at where it was.

scroll position -> FollowEnd -> LeftEnd / ReachedEnd -> update -> following
new message -> Appended -> unseen (while not following)
jump button -> Jumped -> ScrollToEnd -> ReachedEnd

The scroll position is the browser's, and the Mount on the log reads it: while the log's end is in view it scrolls to the end whenever the log grows, a streamed answer included, and otherwise keeps the first message in view at the same place. The Model only mirrors what the Mount saw, for the button; the application tells it what arrived (Appended), since only the application knows a new message from an older one loaded above.

The log is the APG's role="log", named by label: a reader hears what is added to it, and a ChatMessage still streaming is aria-busy, so an answer is read once whole. The scroller fills the height of what holds it.

MessageScroller.MessageScrollerView

interface · packages/mixins-ui/src/messageScroller.ts:253

interface MessageScrollerView<ParentMessage> {
  readonly model: Model
  readonly label: string
  readonly content: ReadonlyArray<Html>
  readonly onMessage: (message: Message) => NoInfer<ParentMessage>
  readonly jumpLabel?: string | undefined
  readonly unseenLabel?: ((count: number) => string) | undefined
  readonly style?: NamedStyle<typeof MessageScrollerSlots> | undefined
  readonly mixins?: MixinList<ParentMessage> | undefined
  readonly areaStyle?: NamedStyle<typeof ScrollAreaSlots> | undefined
  readonly areaMixins?: MixinList<ParentMessage> | undefined
}

MessageScroller.Model

const · packages/mixins-ui/src/messageScroller.ts:49

const Model: Schema.Struct<{
  readonly id: Schema.String
  readonly following: Schema.Boolean
  readonly unseen: Schema.Number
}>

MessageScroller.ScrollToEnd

const · packages/mixins-ui/src/messageScroller.ts:102

const ScrollToEnd: Command.CommandDefinitionWithArgs<
  'MessageScroller.ScrollToEnd',
  { id: Schema.String },
  Effect.Effect<{ readonly _tag: 'CompletedScrollToEnd' }, never, never>
>

Scrolls the scroller id to its end once the render has committed, smoothly unless motion is stilled. A jump button that held the focus hands it to the scrolling area, since the button goes once the end is in view.

MessageScroller.update

function · packages/mixins-ui/src/messageScroller.ts:237

function update(model: Model, message: Message): Return

The scroller's update; the same Model when nothing changes.

MessageScroller.view

function · packages/mixins-ui/src/messageScroller.ts:276

function view<ParentMessage>(
  options: MessageScrollerView<ParentMessage>,
  h: HtmlBuilder<ParentMessage>,
): Html