harbor 0.4.3
harbor: ^0.4.3 copied to clipboard
Keeps widgets clear of whatever covers each screen edge: system bars, the keyboard, and your own headers, tab bars and sheets. No inset arithmetic.
0.4.3 #
- Fixed:
HarborSheet(clearsTopCoast: false)cast the status bar out of the sheet's clear water along with its header and body, so a flare or buoy raised in a full-height sheet could sit under the status bar. The header and body still read no top coast; the sheet's clear water, where flares, buoys andHarborChartlook, keeps the status bar out as it does with the option off. (#97) - Fixed: with no
HarborSea, aHarborCastOfforHarborMooredabove a harbor started from empty waters, so every edge read zero beneath it, not just the edges it cast off. A sheet withclearsSides: trueread a bottom inset of 0 instead of the home indicator (34 on an iPhone 17). It now starts from what a reader there sees,MediaQuery's padding. (#98)
0.4.2 #
HarborSheet(bodyClearsTide: false), and onHarborSheet.draggable, leaves the keyboard to the sheet's body, asHarbor.bodyClearsTidedoes a page's: the body runs under the keyboard and reads it inMediaQuery.viewInsets, a floating footer still rides up on it, andmaxExtentFraction(or a draggable sheet's extents) is a share of the whole height. For a toolbar or inspector that manages the keyboard itself.truestays the default, so nothing changes for existing sheets. (#95)- Fixed: a
Harborsized to its body (HarborSizing.hugBody) withmaxExtentFractionandbodyClearsTide: falseadded the keyboard's height to its cap, though its body leaves the keyboard outside its height, so with the keyboard up it grew past its fraction of the screen. The cap is now that fraction of the whole height. WithbodyClearsTide: true(every sheet until now) it is unchanged.
0.4.1 #
HarborDock.reserve(extent:)holds an edge for something drawn elsewhere (a footer in an overlay, a bar a parent paints) so the body keeps clear of it. It reachesextentin from the edge, coast included (the larger of the two) when it is the outermost dock, andextentpast the docks outside it when it is not. It paints nothing and takes no taps;kind:(a pier by default) andtide:are a dock's. The way to say this before, a pier with an empty child andminimum: extent, reserved nothing when another dock sat outside it, since only the outermost dock takes the coast and its minimum. (#92)- Docs:
HarborWaters.steadyCoastOfholds the home indicator while the keyboard is up, for a footer that keeps its height. A footer that sits on the keyboard readsMediaQuery.viewPaddingOfin a body that clears the tide: the indicator with the keyboard down, 0 with it up. (#93)
0.4.0 #
- Breaking: the side docks own the corners. A
startorenddock still runs the frame's full height, and atoporbottomdock now runs between the side docks instead of the full width under them, as a tablet'sNavigationRailsits beside itsAppBar. Measured on an iPhone 17 with an 80-wide start rail and a 16 margin: a header's mooring line starts at x 96, lined up with the body's rows, where before it was at 16, under the rail. A top or bottom dock beside a side dock is no longer handed that side's coast inMediaQuery.padding(the side dock took it), and keeps the coast on a side no side dock holds. A side dock that withdraws gives its corners back as it goes; one that widens on focus holds the header at itsrestingExtent. Migrating: a header that padded its title by the rail's width by hand (EdgeInsetsDirectional.only(start: railWidth)) drops that padding, or the title moves twice as far. A header that should paint under the rail (a full-bleed image across the top) paints that part in the body, behind the docks, withHarborOpenWater.
0.3.3 #
HarborFlares.raise(margin:)sets how far a flare keeps in from the clear water's edges: 16 at a slot or an alignment and 8 by an anchor when null, as before.EdgeInsets.zerogives a bar the width of the screen (a fixedSnackBar's look), still above the docks and off the coast; anEdgeInsetsDirectionalis resolved in the reading direction of the page that raised it. A bar that paints under the home indicator is a dock: moor it with aHarborPontoon. (#83)HarborFlareEntry.hold()stops a flare's time until the returnedHarborFlareHoldis released, for a toast under a finger or with focus in it. Holds are counted, as make-way claims are; time starts over once the last is released, as it does when a flare comes back into sight.HarborFlareEntry.heldsays whether one is on. (#84)- Fixed: a flare raised into an
Overlay(no harbor above it) and lowered before it was built stayed in the overlay and held its slot, so every later flare there waited behind it. It now leaves at once, unbuilt, andclosedcompletes with the reason it was lowered. (#85) - Fixed: with no
HarborSea, a harbor right inside the page's harbor (a header band's own) counted as a port, so a flare raised from it was shown inside that band. Without a sea, the page's outermost harbor is now its only port, as each route's first harbor is under a sea. (#86) - Docs:
HarborMakeWaysays that its claim reaches the harbor a frame after it is built, and how to make way in the frame a mode starts: claim withHarbor.of(context).makeWayin the tap that opens it, or build the dockstate: HarborDockState.withdrawn. (#82)
0.3.2+1 #
- Docs: the README links to the Harbor Field Guide in a browser (https://supposedlysam.github.io/harbor/), a pretend phone for every class with its options and a tide gauge. The example's field guide pages each say what to try, and keep their readings in view on a phone on its side. No code changes.
0.3.2 #
HarborSheet(clearsSides: true), and onHarborSheet.draggable, keeps the header, body and footer clear of the left and right coast (a phone's notch in landscape, a cutout on a side edge) and casts it off, so content beneath reads zero there and does not clear it again. The surface still runs edge to edge. Off by default: today a sheet's header and footer run its full width and are handed the side coast inMediaQuery.padding(measured on an iPhone 17 on its side, 62 each side: header and footer at x 0 to 874, padding 62 left and right), so turning it on by default would move existing sheets. PassclearsSides: trueto turn it on.showModalBottomSheet'suseSafeAreais off by default too. (#76)HarborSheet(clearsTopCoast: false), and onHarborSheet.draggable, keeps the geometryshowHarborSheet(keepsTopCoast: true)gives a sheet but casts off the top coast for its header and body, which read zero top padding there: a header that paints under the status bar and pads its own title no longer pads twice. It only matters withkeepsTopCoast: true; without it the sheet stops short of the status bar and is handed no top coast.truestays the default, so nothing changes for existing sheets. (#75)- Fixed:
HarborFairway(startsInOpenWater: true)dropped the fairway's ownpaddingandminimumat its leading end along with the clearance; it now skips only the clearance, so a first row givenpadding: EdgeInsetsDirectional.only(top: 16)sits 16 below the top (#74). - Docs: the README says how to keep a fairway off an end a header it does not own already cleared (
HarborCastOff(edges: {HarborEdge.top}), #58), and what a list that reads onlyMediaQuery.paddinggets and misses inside a harbor, withHarborFairway.paddingOfor an app-sideMediaQuery.copyWith(padding:)for the rest (#69). The note thatHarborClear.coastleaves out the keyboard, which sat among the fairway paragraphs, is withHarborMoored, which it describes. HarborWaters.dockFaceOf(context, edge): how far the docks on an edge reach, to their inner face, with or without a wake (#61). It is the wake band'sdockEdgewhere the edge has a fade wake, andHarborWatersData.docksotherwise, which is the face itself when the wake adds no clearance (no wake, a hairline, or a fade that rests at the dock's edge). It depends on the docks aspect alone, so its reader holds still while the keyboard moves. Inside a fairway it reads zero, as the fairway has cast its ends off. Additive.HarborFairway(tide:), onHarborFairway.box,HarborFairway.paddingOfandHarborFairwaySlivertoo, asHarborMoored(tide:)has it (#59).tide: falseleaves the keyboard to someone else: the bottom end and reveals leave it out, and it is not cast off, so content inside can still keep clear of it. A fairway withtide: falsedoes not rebuild as the keyboard moves. It replaces wrapping the fairway inHarborCastOff(tide: true), which also hid the keyboard from the content inside and, with the defaultedges, cast off all four edges (the keyboard-only form isHarborCastOff(edges: const <HarborEdge>{}, tide: true)). Additive:trueis the default, so nothing changes for existing callers.HarborFairway(clear:), onHarborFairway.box,HarborFairway.paddingOfandHarborFairwaySlivertoo, with the sameHarborClearandeverythingdefault asHarborMoored(#57). WithHarborClear.coastthe ends rest clear of the platform's insets alone, for a list that keeps its last row off the home indicator but runs under a frosted tab bar, or a carousel under a rail. The ends cast off the coast alone, asHarborCastOff(docks: false)does, so the rows beneath read the docks measured from the fairway's edge. Pinned sliver docks still pin at the docks' face, reveals still keep clear of everything over the viewport, and the bottom end still keeps clear of the keyboard. A fairway that clears the coast alone reads only the coast, so it holds still while the keyboard moves under a horizontal one. Additive: nothing changes for existing callers.- Fixed, and it can move content: with
HarborFairway(startsInOpenWater: true), the first sliver is handed back the leading end alone (#60). Before, it was given the fairway's ownMediaQueryand waters from above its cast-off, so it also read the trailing end (a tab bar, the home indicator), the keyboard, and on a horizontal fairway the trailing side and its mooring line, all of which the fairway had already cleared. A list whose only sliver is the first one cleared its bottom twice. Content in that sliver that readMediaQuery.padding,viewInsetsor the waters on the trailing end now reads zero there. - Fixed, and it can move content:
HarborFairwaySlivercasts off the ends it cleared, as Flutter'sSliverSafeArearemoves the padding it applied and as the README promised (#67). Before, it only padded, so aSafeArea, aHarborMooredor aMediaQuery.paddingreader in its sliver cleared the header, the status bar, the home indicator or the keyboard a second time. Content there that read those values now reads zero on the ends the sliver cleared (and no keyboard, when it cleared the bottom); an end givenclearLeading: falseorclearTrailing: falseis left as it was. HarborFleetObserverhears harbors join and leave, as aNavigatorObserverhears routes:didJoinanddidLeaveare given the harbor'sHarborController, after the frame it joined or left in. Register one withHarborSea(observers:)orHarborFleet.addObserver(andremoveObserver), on the fleet fromHarbor.of(context).fleet. (#64)HarborChart.snapshotAll()reads every harbor of every live sea with no context, for tooling outside the widget tree. A sea inside another harbor (a phone drawn in a page) is listed too, andHarborChartEntry.isolated(also in itstoJson()) says so;ext.harbor.chartleaves isolated seas out unless called withisolated=true. Fixed: the VM-service extension kept the last sea it was given after that sea was disposed. It now reads the seas that are mounted. (#64)HarborWake.fade(dockOpacity:, curve:)shapes a fade's ramp: how opaque content is at the dock's inner face (0.25 by default) and the curve out to the wake's end (Curves.linearby default). The defaults draw the ramp every fade drew before. The measured band carries them asHarborWakeBand.dockOpacityandHarborWakeBand.curve(null for a fade at its defaults, soHarborWakeMask(dockOpacity:)still applies there), and they are part of the band's and the wake's==,hashCodeand diagnostics. A wake takes no colour: it is an alpha mask, so it shows whatever is behind the content. (#70)HarborTideSource(height:, child:)feeds a keyboard the platform does not report (a plugin's, a TV's over a channel) into the tide: it rebuilds theMediaQuerybelow it withviewInsets.bottomat the larger of the platform's value andheight(logical pixels), and rebuilds nothing else whenheightchanges. Mount it aboveHarborSea, and aboveHarborScaleModel. The README's new section "A keyboard the platform does not report" has the same as aMediaQueryrecipe. (#71)- Docs: how to build a dock of a given reach, coast included, with no helper:
HarborDock.quay(minimum: reach, child: const SizedBox.shrink())reaches the larger of the coast andreach, andcoast: HarborCoastStance.nonewithSizedBox(height: reach)reaches exactlyreach. In the docks table and harbor_test's README. (#68) showHarborDialog(transitionBuilder:)brings your own entrance and exit in place of the 180 ms fade, asshowGeneralDialog's does. It is handed the route's animation curved byanimationStyle; under reduced motion it is handedkAlwaysCompleteAnimation(andkAlwaysDismissedAnimationas the secondary), so reduced motion stays in charge. Null keeps the fade. (#63)HarborDialogRoute<T>, aRawDialogRoutewithshowHarborDialog's options, is public, as Material'sDialogRouteis:showHarborDialognow pushes one, with no change in behaviour, and you can push one yourself to keep the route or choose the navigator. It reads the opening page's themes (unless giventhemes:) and, withinheritClearWater, its clear water when it is installed, as it is pushed. (#63)HarborPortalBuoy(requestFocus: true)takes keyboard focus as it opens, as a sheet with no barrier does: the buoy is a focus scope of its own that becomes the first focus of the scope around its anchor, Tab past its last control follows the navigator'srouteTraversalEdgeBehavior(a portal buoy is not modal, so it does not loop), and when it closes focus goes back to where it was if it is still in the buoy. It asserts anonDismiss, so Escape from inside it closes it, before a modal buoy it was opened from.falsestays the default, which leaves focus where it was, as aMenuAnchordoes. (#66)HarborFlares.raise(anchor:, side:, gap:, crossAlignment:)raises a flare by aHarborAnchorPoint, placed as aHarborBuoy.anchoredis (below, 8 away and centred by default): "Copied" by the button that copied. The anchor is given alone, without a slot or an alignment (asserted). An anchored flare is shown by the port of the page that raised it rather than the one on top, takes turns with the flares at the same anchor and side, is hidden with its time stopped while the anchor is out of the tree, and is lowered withHarborFlareClosedReason.removeif its page is popped, rather than moving to the port now on top. With no harbor it sits by its anchor in the nearest overlay. It keeps the live region and dismiss action.HarborFlareEntryreads backanchor,side,gapandcrossAlignment. (#65)- Docs: the README's sheets section has a recipe for a page under
PopScope(canPop: false): thereNavigator.maybePopstops before the page's local history, so back reaches only the app's handler and never a modal buoy, portal buoy or sheet with no barrier. The handler closes them first withNavigator.popwhileModalRoute.of(context)!.willHandlePopInternally, one per back press. No code changes. (#62)
0.3.1+1 #
- Docs: the README's videos play in the browser when clicked, instead of downloading. They are linked through jsDelivr, which serves them as video. No code changes.
0.3.1 #
- Behaviour change: a sheet closes on a fling down faster than 700 logical px/s, the speed at which a Material
BottomSheetcloses, and the speed is now a parameter:HarborSheet.draggable(closeFlingVelocity:)andHarborSheet(closeFlingVelocity:). A draggable sheet's header used to close it on a fling of 400 from its lowest snap, so a moderate flick (between 400 and 700) that used to close a draggable sheet now settles it back at that snap. To keep the old feel, passcloseFlingVelocity: 400. A content-sized sheet withdragToClosealready closed at 700 and is unchanged.double.infinityleaves only the floor to close a sheet. A fling on a draggable sheet's list is stillDraggableScrollableSheet's own, as in a modal bottom sheet: it closes the sheet from its lowest snap at any speed. (#48) - Signals are now flares, so a toast is not mistaken for the
signalspackage's reactive state:HarborFlares.raise,HarborFlareSlot,HarborFlareTarget,HarborFlareEntry,HarborFlareTransitionBuilder,HarborFlareClosedReasonandHarborController.flares. Not breaking: the old names (HarborSignals,HarborSignalSlot,HarborSignalTarget,HarborSignalEntry,HarborSignalTransitionBuilder,HarborSignalClosedReasonandHarborController.signals) still compile as deprecated aliases, with a warning naming the new one, and go at 1.0. The message reported when a flare has nowhere to go now namesHarborFlares.raise. HarborCastOff(docks: false)casts off the coast alone and leaves the docks to what is beneath, for a layer that keeps its child clear of the status bar and home indicator while a list inside it still rests clear of a frosted header or tab bar (#47). Beneath, the docks, the docks at rest, their wakes andMediaQuery.paddingandviewPaddingare measured from the layer's edge, and the steady coast is zero.docks: truestays the default, so nothing changes for existing callers.HarborFairwaygets no such option: it casts off only its own ends, so the docks across a horizontal fairway already reach its items.- The first line of
HarborDock.pier,HarborDock.quayand eachHarborTideStance, which an editor's hover shows, says what it does in plain words and the Flutter widget it matches: the body runs under a pier, as under an app bar withextendBodyBehindAppBar, and stops at a quay, as atScaffold.appBar; afloatdock rides up on the keyboard,pilingsstays put under it, anddryDockkeeps its space whether it is up or down.
0.3.0 #
Breaking changes #
- Every inset is an
EdgeInsetsGeometry, asPadding.paddingis:EdgeInsetsDirectionalvalues still compile; code that reads an inset back as a directional type needsHarborEdges.resolve. - Dialogs are popup routes (
RawDialogRoute, asshowGeneralDialog's): aHerono longer flies into one and aRouteObserver<PageRoute>no longer sees one as a page. - A modal
HarborBuoyholds keyboard focus as a dialog route does: it takes focus when it opens, keeps Tab inside it, closes on Escape, and gives focus back when it goes. - Defaults with no theme of their own: sheet and dialog barriers are
showGeneralDialog'sColor(0x80000000), a hairline wake is a translucent black, and every barrier harbor puts up is announced as'Dismiss'unless given a label. - Internals are behind the public handle:
Harbor.of(context)replaces constructing aHarborController, its lifecycle methods are@internal, pontoons take a typedHarborPontoonHandle, and dock records are read throughHarborChart.nearest. - Behaviour that moves things:
HarborBuoySide.before/afterare deprecated aliases ofstart/end(so.nameis'start');HarborFairway.keyboardDismissBehaviorfollows the scroll behaviour when unset; signals at one slot take turns and time only while in sight (4 s by default); a no-barrier sheet comes back after the page over it has finished popping; a fling down on a sheet's header from its lowest snap closes it.
Everything in this release #
-
Fixed: a sheet with no barrier that took focus, opened over a modal buoy, left focus on its own disposed scope when it closed, so Escape no longer reached the buoy. It now hands focus back as it closes, as a route does.
-
Breaking: a modal
HarborBuoyholds keyboard focus as a dialog route does: it is aFocusScopethat takes focus when it opens, keeps Tab inside it and stops the arrow keys at its edges, and gives focus back to the page when it closes. Escape calls itsonDismiss, from focus in the buoy or on the page behind it (not from a page of aNavigatornested in the harbor's body, whose route answers Escape first); with no modal buoy up, Escape is left to anActionsabove the harbor.HarborBuoy(requestFocus: false)leaves focus where it was, asRoute.requestFocusdoes. Opened over a focused text field, it now takes focus from the field, so the keyboard goes down as it does for a dialog; passrequestFocus: falseto keep it. Before, focus stayed on the page behind the barrier, Tab and a TV remote's D-pad walked the page, and Escape did nothing. -
A buoy's child keeps its state when the buoy turns
modalor back (a speed-dial button that is modal while it is open), when another buoy turns modal or stops being modal, and when a signal comes up in its harbor. Before, each of these built every buoy's child again from scratch. -
Breaking: a harbor's handle (
HarborController) carries only what content asks of a harbor, asScaffoldStatedoes; the harbor itself makes it and runs its lifecycle. These members are now@internal, so using them from another package is an analyzer warning (invalid_use_of_internal_member), and they may change in any release:- the
HarborController(...)constructor: get a harbor's handle withHarbor.of(context); onChanged,join(),leave(),recordLayout()andraiseSignal(): raise a signal withHarborSignals.raise(which aims at the port on top or at the sea; a nested harbor that is not a port is no longer a target of its own);- the setters of
debugLabel,dockedEdgesandroute, which stay readable (dockedEdgesnow returns an unmodifiable set); - the
HarborSignalEntry(...)constructor:HarborSignals.raisereturns the entry.
- the
-
Breaking:
HarborController.lastLayoutandrenderBoxare deprecated, read-only getters; setting either no longer compiles. ReadHarborChart.nearest(context), which has the harbor's frame, body, clear water and docks in global coordinates, orclearWater/clearWaterInGlobal(). -
Breaking:
HarborController.addPontoonreturns aHarborPontoonHandle, andupdatePontoonandremovePontoontake one, where they took an untypedObject. A handle stored asObjectneeds its type changed; the handle reads back itsedgeanddock. -
Breaking:
HarborSignalEntry.showingis aValueListenable<bool>, asNavigatorState.userGestureInProgressNotifierreads;HarborSignalEntry.owneris read-only. Lower a signal withlower()(which takes theHarborSignalClosedReasonthatclosedreports) rather than settingshowing.value. -
HarborDockSlotis deprecated and its constructor@internal: it is how a harbor builds a dock, and stops being exported in a later release. Find where docks are withHarborChart.nearest(context).docks(docksAroundinharbor_test), or find the dock's child. -
Harbor.of(context)andHarbor.maybeOf(context): the nearest harbor's handle, asScaffold.ofreturns the nearestScaffoldState. LikeHarborController.of,Harbor.ofthrows aFlutterErrornaming itself when there is no harbor above the context.HarborController.ofandmaybeOfstill work. -
HarborChart.nearest(context): the chart entry of the nearest harbor, in global coordinates, or null when there is none or it has not laid out. -
Breaking: harbor's drawn defaults carry no theme of their own, as the widgets layer's do.
showHarborSheetandshowHarborDialogshareshowGeneralDialog's barrier colour,Color(0x80000000)(the sheet's was0x66000000, the dialog's0x88000000), andHarborWake.hairline(), which everyHarborSheetfooter uses by default, is a translucent black,Color(0x1F000000), instead of a translucent white that vanished on a light bar. PassbarrierColor:orHarborWake.hairline(color:)to keep the old look. -
Breaking: a sheet or dialog barrier given no
barrierLabelis announced as'Dismiss'instead of'Close sheet'or'Close dialog'(0.2.0's), the English that Material's and Cupertino's localizations fall back to formodalBarrierDismissLabel. A test that finds the barrier by'Close sheet'or'Close dialog'looks for'Dismiss'; an app that localizes passes its own label, as before. -
Escape closes a sheet with no barrier, as back does: from focus in the sheet, or in a harbor on the page that opened it. Only while such a sheet is up, as
RawMenuAnchoranswers Escape only while its menu is open, so anActionsabove the page still gets Escape otherwise. Before, Escape did nothing while the sheet was up. -
A sheet with no barrier is a
FocusScopeof its own, with its navigator'srouteTraversalEdgeBehavior, as a route's scope is. With the default, Tab goes through the sheet and on to the page as before; under a navigator that keeps Tab inside each route, it now keeps Tab inside the sheet instead of leaving it for the page for good.FocusScope.of(context)inside the sheet now returns the sheet's scope, not the navigator's. -
showHarborSheet(requestFocus:), asshowModalBottomSheettakes it. A sheet that is a route passes it toRoute.requestFocus; a sheet with no barrier leaves focus where it was unless it is true, as a persistent bottom sheet does, and gives focus back to the page when it closes. -
HarborPortalBuoy(onDismiss:, consumeOutsideTaps:): a portal buoy closes as aMenuAnchordoes. Its buoy and itschildare oneTapRegiongroup, so a tap outside both callsonDismissand a tap on the child is left to the child; Escape with focus in either and back call it too, back before it reaches the page.consumeOutsideTaps: truekeeps that tap from also pressing what is under it. WithoutonDismissnothing changes. -
HarborBuoy(barrierLabel:): what a screen reader announces for a modal buoy's barrier,'Dismiss'when none is given, as for sheets and dialogs (it was'Close'). A Material app passesMaterialLocalizations.of(context).modalBarrierDismissLabel. -
crossAlignment:andcrossOffset:onHarborBuoy.anchoredandHarborPortalBuoy: an anchored buoy lines up with its anchor'sstartorendacross its side (in reading order above and below it, top and bottom beside it) instead of centring on it, and is moved on bycrossOffset, still held inside the clear water.HarborBuoyCrossAlignment.centeris the default. -
HarborPortalBuoy.sideOf(context)andmaybeSideOf: the side a portal buoy landed on, itssideor the opposite after a flip, so a popover can point its arrow at its anchor. It changes on the frame after the flip. -
The README's glossary gains a table that gives each harbor term its closest Flutter concept (
SafeArea,MediaQuery.paddingandviewInsets,Scaffold,showModalBottomSheet,SnackBar,OverlayPortaland more) and the difference that matters, or says there is none. The main classes' dartdoc points to the same Flutter widget, andHarborTideStateandHarborDockStatesay they are notStateobjects. Documentation only. -
Signals at the same slot or alignment of one port take turns, as a
ScaffoldMessengershows its snack bars, instead of being drawn over each other: each comes in once the one before it has run its exit, and one lowered while it waits leaves without being shown. Signals at different slots still show together. This is a behaviour change for code that raised two signals at one place and expected both on screen. -
A signal's
durationcounts only while it is in sight: from the end of its entrance, and not while another route covers its page, as a snack bar's timer starts only once it is in and its route is current. The default is 4 s, aSnackBar's, instead of 3 s. AHarborSignalEntryraised throughHarborController.raiseSignalnow times out after itsdurationtoo. -
HarborSignals.raise(persist: true)keeps a signal up until it is lowered, asSnackBar(persist:)does, for a signal with a button that a screen-reader user needs time to reach. -
HarborSignalEntry.closedcompletes with aHarborSignalClosedReason(lower,dismiss,timeout,remove) once the signal has left, asshowSnackBar's controller'scloseddoes;lower()takes the reason, ashideCurrentSnackBardoes. -
An anchored buoy or a portal buoy that is not shown, because its anchor is not in the tree, is not read out by screen readers. It was left in the semantics tree where it last sat (at first, the top-left corner), so a screen reader could reach an invisible menu or bubble. A portal buoy's semantics also follow it when its anchor moves; they stayed where it was first placed.
-
HarborBuoySide.startand.endname an anchored buoy's reading-order sides, asAlignmentDirectional.centerStartandCrossAxisAlignment.startdo.HarborBuoySide.beforeand.afterare deprecated aliases of them and place the buoy exactly as before; a side'snameis now'start'or'end'. -
Breaking: because
beforeandafterare now aliases,HarborBuoySide.before.nameis'start', andHarborBuoySide.values.byName('before')throws. Code that stores a side by name should store'start'or'end'. -
harbor's builders have named types, as Flutter's do:
HarborOpenWater.builderis aHarborWatersWidgetBuilder, andHarborSheet.draggable(builder:)is aScrollableWidgetBuilder, asDraggableScrollableSheet.builderis. The function types are unchanged, so existing builders still fit. -
HarborDock(animationStyle:)andHarborLighthouseRegion(animationStyle:)take anAnimationStyle, as Flutter's routes andMaterialApp.themeAnimationStyledo, so leaving and returning can differ. A dock withdraws and goes dark over itsreverseDurationandreverseCurveand returns over itsdurationandcurve; a region lifts over the forward pair and settles back over the reverse one.AnimationStyle.noAnimationmakes either move at once. Given, it overridesduration:andcurve:, which stay. -
Breaking:
HarborFairway.keyboardDismissBehavioris nullable and unset by default, as on aScrollView: it follows the fairway'sscrollBehavior, or else the inheritedScrollConfiguration. Before, it was alwaysmanual, which overrode an app-wideScrollBehaviorthat dismisses the keyboard on drag. PassScrollViewKeyboardDismissBehavior.manualto keep the old behaviour under such a behaviour. -
Two slivers in one
HarborFairwaywith the same key now trip Flutter's duplicate-key assertion, as they do in aCustomScrollView: a fairway keeps keyed slivers' state when they move, which a duplicate key cannot. Before, it tolerated them. -
HarborFairwaytakes the rest ofCustomScrollView's parameters, with the same defaults:scrollBehavior,center,anchor,paintOrder,dragStartBehavior,restorationIdandhitTestBehavior.HarborFairway.boxtakes those aSingleChildScrollViewhas. With acenter, both ends of the scroll rest clear of the docks, the center sliver rests clear of the leading docks, sliver docks after it pin at the docks' face, and reveals on either side of it keep clear.anchoris a fraction of the water between the docks, soanchor: 1follows the keyboard, and reveals account for it. -
Keyed slivers in a fairway keep their state when they move, as in a
CustomScrollView. -
Breaking: every inset harbor takes is an
EdgeInsetsGeometry, asPadding.paddingandListView.paddingare, resolved againstDirectionalitywhere it is used:Harbor.marginand.minimum,HarborSea.margin,HarborFairway.paddingand.minimum,HarborFairwaySliver.paddingand.minimum,HarborMoored.minimumand.extra,HarborCoast.fixed(andHarborCoast.fixedInsets) andHarborTitleSafe.fixed, and theextra:andminimum:ofHarborFairway.paddingOfandHarborMoored.clearanceOf.EdgeInsets.all(16)now compiles, and anEdgeInsetsstays on the side it names. PassingEdgeInsetsDirectionalstill compiles and behaves as before; reading one of these fields asEdgeInsetsDirectionalneedsHarborEdges.resolve(insets, textDirection), andHarborTitleSafe.resolve(size)now returns anEdgeInsetsGeometry. What harbor publishes for you to read (HarborWatersData,HarborMoored.clearanceOf's result) stays directional. -
A breakwater sheet's coverage follows the sheet as it is drawn. While the sheet slid in or out, or was dragged, the page under it was laid out against where the sheet had been painted in the frame before, so its content trailed the sheet's edge by a frame (over 80 px at the fastest point of a 300 px sheet's slide). A sheet with no barrier also stopped covering the page as soon as it began to close, so the page sprang back under the sheet still sliding out; it now keeps clear until the sheet has gone, as it already did under a sheet with a barrier.
-
harbor's widgets describe their settings to the widget inspector and
debugDumpApp(debugFillProperties), as Flutter's own do, hiding the ones at their defaults.HarborDock,HarborBuoy,HarborWake,HarborCoast,HarborTitleSafeandHarborSheetExtentareDiagnosticable, so they print asHarborDock.pier(state: dark, debugLabel: "header")rather thanInstance of 'HarborDock', and aHarborAnchorprints itsdebugLabel. TheirtoStringnow takes Flutter's{DiagnosticLevel minLevel}, so a subclass of one of them that overridestoString()needs that parameter. -
HarborSheetExtenthas value equality (==andhashCode), as its@immutablepeers already did. -
With reduced motion (
MediaQuery.disableAnimations),HarborLighthouse.reveal, and so aHarborBeacon(keepInSight: true), jumps to where it reveals instead of scrolling there over itsduration, and aHarborLighthouseRegionlifts its content at once, as docks, sheets, signals and dialogs already appear. A region also takes a newdurationgiven after it is first built. -
Misuse is reported as a
FlutterErrorthat says what to do, as Flutter's own errors are.HarborController.ofwith no harbor above throws one in release builds too (before, a bare null-check error), and points toHarborController.maybeOf. A harbor given unbounded constraints, a quay listed inside a pier andHarborSheet(dragToClose: true)outsideshowHarborSheetname the harbor'sdebugLabeland the fix in debug builds, instead of failing a plain assert. -
In debug builds, two
HarborAnchorPoints that stay attached to oneHarborAnchorpast the end of a frame are reported. Before, the buoy silently moved to whichever laid out last. -
showHarborSheet(sheetAnimationStyle:): anAnimationStyle, as onshowModalBottomSheet, sets how a sheet opens and closes, with or without a barrier:durationandcurvegoing in,reverseDurationandreverseCurvegoing out, andAnimationStyle.noAnimationfor none. A dragged sheet follows the finger with no curve and goes on along the curve when let go, so it stays under the finger with any curve, one that overshoots included. Reduced motion still wins. -
HarborSheetController, given toshowHarborSheet(controller:): closes a sheet from outside it (close(),remove()with no slide), completesclosedwhen it has left, rebuilds it (setState) and reads its slide (animation). It is aChangeNotifierthat tells its listeners when a sheet attaches and leaves. For a sheet with no barrier, whichcloseHarborSheetcould only close from inside, and for one with a barrier. -
showHarborSheet(transitionAnimationController:), as onshowModalBottomSheet: the sheet slides by the caller's controller in place of its own, and the caller disposes it. -
HarborSheet.draggable(controller:)takes aDraggableScrollableController, so a draggable sheet can be read and moved from outside it: fitted to content measured after layout, or raised to its ceiling when a field takes focus. -
HarborSheet.draggable(expand:), as onDraggableScrollableSheet:falsein a route that sizes the sheet to its content (showModalBottomSheet), so a tap above the visible sheet reaches the barrier and closes it. Before, the sheet filled the modal bottom sheet's surface and swallowed those taps. -
HarborSheetExtent(shouldCloseOnMinExtent:), as onDraggableScrollableSheet: off, a draggable sheet rests at its floor instead of closing. -
A fling down on a draggable sheet's header from its lowest height goes to its floor and closes it, as a fling on its list does. Before, the header sprang back to its rest unless flung faster than 1200. A slow release still goes to the nearest of its heights.
-
HarborSheet.close(context, [result])closes the sheetcontextis in and completes the future that opened it withresult, asNavigator.pop(context, result)does for a modal bottom sheet. A sheet with no barrier can now return a value: before, its future always completed with null. Back andNavigator.popstill close it with null, since it is not a route. -
closeHarborSheetis deprecated in favour ofHarborSheet.close, andharborAlphaWakein favour ofHarborWakeMask.alphaWake. Both old names still work. -
showHarborSheet(isDismissible:, requestFocus:, anchorPoint:), asshowModalBottomSheethas them. WithisDismissible: falsea tap on the barrier (dimmed or clear) does nothing and back still closes the sheet; before, every barrier closed its sheet on a tap.requestFocusis passed to the sheet's route, andanchorPointpicks the screen a sheet opens on beside a hinge. -
Fixed: under a body that clears the tide, on a phone with a home indicator,
HarborMoored(clear: HarborClear.coast)and every reader ofHarborWaters.of(context, aspect: HarborWatersAspect.coast)rebuilt on every frame of the keyboard, and so did a reader ofHarborWaters.steadyCoastOf. A coast reader now rebuilds only when the coast changes, andsteadyCoastOfwhen the view padding does, asMediaQuery.viewPaddingOfwould.HarborWaters.of(aspect: HarborWatersAspect.coast).coastSteadyis filled in without subscribing, as the docks aspect already did; read it withsteadyCoastOfto follow it. Values are unchanged. -
A tap on the iOS status bar scrolls only the page a
Scaffoldwould: the one whose status bar band a tap at the screen's top left reaches. A page in a nested navigator no longer scrolls once a route covers that navigator (it scrolled to the top when uncovered), nor does a page under a full-screen overlay entry or the right-hand pane of a split view. A harbor with no top inset, and one inside a sheet that stops short of the status bar, no longer scrolls either, as aScaffolddoes not. Nor does a harbor whosecoastdrops the top inset: the band is as tall as the top coast the harbor keeps. -
Breaking:
showHarborDialogpushes a popup route (aRawDialogRoute, asshowGeneralDialogdoes) instead of a page route. AHerono longer flies into a harbor dialog, aRouteObserver<PageRoute>(and analytics observers that count page routes as screens) no longer sees one as a page, aHarborSheet.draggableinside one closes it when flung below its floor, and its content is a route scope for screen readers. The API is unchanged; code that checkedroute is PageRoutefor a harbor dialog no longer matches. To migrate: checkroute is PopupRoute(orRawDialogRoute); name the dialog withrouteSettingsfor screen analytics; for aHerointo a dialog, push aPageRouteof your own. -
showHarborDialogtakesshowDialog's route options:routeSettings:,barrierLabel:('Dismiss'when none is given; a Material app passesMaterialLocalizations.of(context).modalBarrierDismissLabel),semanticLabel:(the name screen readers announce for the dialog),anchorPoint:,traversalEdgeBehavior:,requestFocus:andanimationStyle:. -
showHarborDialogmoved fromscale_model.darttodialog.dart. It is still exported frompackage:harbor/harbor.dart. -
A
HarborBeacon(keepInSight: true)also brings itself into sight when focus moves into it, asEditableTextdoes for its caret. In a form with the keyboard up, tapping the next field or pressing the keyboard's next action now reveals the whole beacon (the field and the button under it,clearanceclear) rather than only the field's caret line. Before, a beacon re-revealed only when the keyboard rose or what covers the bottom grew. This applies with the keyboard down too. Every beacon now holds aFocusnode of its own, soFocus.of(context)inside a beacon returns the beacon's node. -
HarborSignals.raise(animationStyle:, transitionBuilder:): a signal's entrance and exit, asshowSnackBar(snackBarAnimationStyle:)andshowGeneralDialog(transitionBuilder:)take them.AnimationStyle.noAnimationshows a widget that brings its own entrance as it is, instead of fading and scaling it in on top; atransitionBuilderbuilds the entrance and exit from harbor's animation. A lowered signal stays for its whole exit, and never less than the 300 ms it stayed before. -
A signal's live region has a dismiss action, as a
SnackBar's does, so a screen reader can lower it.HarborSignals.raise(liveRegion: false)leaves the semantics to a widget that is its own live region: before, harbor wrapped it in a second live region with no label. -
HarborSignalEntrygainsanimationStyle,transitionBuilder,liveRegionandlingers, the time a lowered signal stays in the tree;HarborSignalTransitionBuilderis exported. -
HarborSignals.raise(alignment:): raises a signal at an exact point in the clear water instead of a slot, placed as a buoy at that alignment would be, and in the nearest overlay's padded water when there is no harbor. AnAlignmentDirectionalis resolved in the reading direction of the page that raised it.slotis now nullable and still defaults tohigh; give one or the other. A tear-off ofraisestored in a variable typed with a non-nullableslotneeds its type updated. -
A sheet with a barrier is a semantics scope of its own, as a modal bottom sheet is: it scopes and names its route (
scopesRoute,namesRoute,explicitChildNodes), so screen readers keep to the sheet and announce it as it opens.showHarborSheet(semanticLabel:)is the name they announce. A semantics-tree snapshot of an open sheet gains this node. A sheet with no barrier is not a route and is unchanged. -
A sheet's barrier is read by a screen reader only above the sheet, as a modal bottom sheet's is (
ModalBarrier.clipDetailsNotifier): touch exploration over the sheet finds its content rather than the barrier. The clip follows the sheet's top as it slides and is dragged, and stops at a draggable sheet's top rather than its whole drag area. A semantics-tree snapshot of an open sheet shows the barrier's node ending at the sheet. -
showHarborSheet(barrierOnTapHint:): what tapping the barrier does, read as 'Double tap to …', as onModalBottomSheetRoute. A Material app passeslocalizations.scrimOnTapHint(localizations.bottomSheetLabel). -
An anchored buoy and a portal buoy follow their anchor in the frame it moves, as
OverlayPortal.overlayChildLayoutBuilderdoes. An anchor in a list row that scrolled was moved without being painted again, since each row is its own layer, so the buoy stayed where it was until something else repainted it; where the row was painted again, the buoy followed a frame late, and asked for an extra frame to do it. Each is now its own layer and is painted again at the start of every frame that is drawn, asking for no frame itself. AHarborAnchorwith no listeners of its own no longer schedules frames as it moves. -
An anchored buoy whose anchor is not in the tree takes no taps. It was already not painted, but it was still hit-tested where it last sat (at first, the top-left corner), so an invisible buoy could swallow taps meant for the page. A portal buoy already behaved this way.
-
Breaking: a sheet with no barrier comes back after a page pushed over its own has finished popping, not as the pop starts. Before, it was painted and tappable over the leaving page for the whole transition.
0.2.0 #
Breaking changes #
- Sea trials moved to
harbor_test. harbor no longer depends onflutter_test. Runflutter pub add dev:harbor_testand importpackage:harbor_test/harbor_test.dart;package:harbor/testing.dartis now empty and deprecated. HarborBuoy(modal: true)is modal and needsonDismiss: a barrier keeps taps off the page, and a tap beside the buoy or back callsonDismiss.HarborBuoy.alignmentis anAlignmentGeometryandHarborBuoy.marginanEdgeInsetsGeometry. PassingAlignmentandEdgeInsetsstill compiles; reading the fields as the physical types needsresolve(textDirection).- Behaviour that moves things: an anchored buoy's
beforeandafterfollow reading order under right-to-left; a sheet with no barrier closes on back before its page; on a dual-screen device, sheets, dialogs, signals and buoys keep off the hinge; a signal raised with no harbor above it shows in the nearest overlay instead of asserting.
Everything in this release #
-
Breaking:
HarborBuoy(modal: true)is modal. It puts a barrier over the page and its docks (barrierColor, clear by default), a tap beside it or back calls the newonDismiss, and screen readers leave the page alone while it is up; it still hides the buoys listed before it.onDismissis required withmodal: true. Before, a modal buoy only hid the buoys before it, and taps and back reached the page. -
Sheets, dialogs, signals and buoys keep off a foldable's hinge, as Material's dialogs and bottom sheets do: each is kept to one screen. A flat fold, which has no width, may still be spanned.
HarborTrialDevicegainsdisplayFeatures;foldableOpendeclares its fold, and the newdualScreenOpena hinge. -
A dialog's builder sees the opener's themes, and a dialog appears without fading under reduced motion, as sheets and signals already do.
-
A sheet with no barrier is tied to the page that opened it: back closes it before the page, the iOS back swipe stands aside while it is up, it hides while another page is on top, and it leaves when its page is replaced. Before, back popped the page from under it, and opened from above the page's harbor it could outlive the page.
-
After a turn or a foldable opening, a page under a breakwater sheet is no longer laid out for one frame against the cover the sheet had in the old shape (it could overflow), and a signal follows the clear water of the harbor that raised it instead of staying boxed into the old screen.
-
HarborSignals.raisewith no harbor above the context shows the signal in the nearestOverlay, clear ofMediaQuery.paddingandviewInsets, instead of asserting in debug and showing nothing in release. With no overlay either, it reports aFlutterError. -
A signal's timers are cancelled when it is lowered or its harbor leaves with nowhere to move it, so a widget test that removes the tree with a signal up no longer fails with a pending timer.
-
Readers rebuild only for what they read.
HarborWaters.ofdepends onMediaQuery's padding (and, outside a harbor, its size) rather than all of it, and only where itsaspectneeds them.HarborTide.isInOfrebuilds when the keyboard comes or goes, not on every frame it moves.HarborMoored,HarborMooringLine,HarborOpenWater,HarborDryDockand a horizontalHarborFairwayno longer rebuild while the keyboard animates. Layout is unchanged. -
HarborWaters.steadyCoastOf(context, edge)andHarborWatersData.coastSteady: the coast as it is with the keyboard down, so a footer keeps the home indicator's height while the keyboard is up. -
HarborFairway(minimum:),HarborFairwaySliver(minimum:)andHarborFairway.paddingOf(minimum:): a floor on each end's clearance, as onSafeArea. -
HarborFairway.boxtakes its child's size across the scroll when that axis is unbounded, so a horizontal row in aColumnis as tall as its content instead of throwing. -
Docs: mooring the bottom edge alone clears the coast and the keyboard but not a header;
HarborCoastFeature.hingeandHarborController.isPortare reserved and not yet read. -
showHarborSheet(routeSettings:): the sheet's route carries its settings, so navigator observers and route-name analytics see it. -
showHarborSheet(barrierLabel:): what a screen reader announces for the barrier,'Close sheet'when none is given. A Material app passesMaterialLocalizations.of(context).modalBarrierDismissLabel. -
HarborSheet(contentBuilder:): wraps the header, body and footer above the surface. A Material app wraps them in a transparentMaterialso text fields and ink work (the README has the recipe, with the bottom sheet theme's width). -
harbor's core imports no design library, and a test keeps it that way: Flutter 3.47 moved Material and Cupertino into
material_uiandcupertino_ui. -
HarborSheet(clip:): clips the sheet to a shape, so an edge-to-edge body follows the surface's rounded top. -
HarborSheet(dragToClose: true): a content-sized sheet can be dragged down to close. Off by default. -
HarborSheetExtent(snapSizes:): the heights a draggable sheet snaps to. A fling on its header goes to the next one its way, as a fling on its list does. -
Fixed: a draggable sheet in a route
showHarborSheetdid not open (showModalBottomSheet,showGeneralDialog) did nothing when dragged below its floor; it now closes that route. -
HarborPortalBuoy: an anchored buoy opened from anywhere in the tree (a menu from a list row, a popover from a button), built onOverlayPortal. It is placed in the nearest harbor's clear water by its child or aHarborAnchor, flips to the other side of its anchor when its side has no room, and is held inside the clear water otherwise. -
HarborBuoySide.beforeand.afterare in reading order: under right-to-left, an anchored buoybeforeits anchor sits on its right. They were placed as if left-to-right. Accessibility and Flutter's conventions for custom widgets. -
A dock that is dark or withdrawn is skipped by keyboard focus and by screen readers. Before, Tab still reached a dark dock's controls, and a withdrawn dock was still read out and focusable.
-
A signal is a live region, so screen readers announce it as it appears.
-
A signal keeps the themes of the place that raised it, as a sheet does.
-
A sheet's builder sees the opener's themes in its own
context. Before, they reached only the widgets below what the builder returned. -
Docks, signals and sheets honour
MediaQuery.disableAnimations: with reduced motion they appear and leave without moving. -
On iOS, a tap on the status bar scrolls a harbor page's primary scroll view to the top, as it does under a
Scaffold. -
Breaking:
HarborBuoy.alignmentis anAlignmentGeometry, andHarborBuoy.marginandHarborPortalBuoy.marginareEdgeInsetsGeometry, soAlignmentDirectional.bottomEndplaces a buoy by reading direction. Code passingAlignmentandEdgeInsetsstill compiles; code reading.alignmentor.marginas the physical types needs aresolve(textDirection). -
Breaking: sea trials moved to their own package,
harbor_test, so harbor no longer depends onflutter_testand an app that depends on harbor no longer gets the test framework in its own dependencies. Addharbor_testas adev_dependencyand importpackage:harbor_test/harbor_test.dartin place ofpackage:harbor/testing.dart.pumpSeaTrial,HarborTrialDeviceandisInClearWaterare unchanged. -
package:harbor/testing.dartis now empty and deprecated: the analyzer reports where it is imported, with a pointer toharbor_test. It is removed in the next release.
0.1.0 #
The first harbor.
Harbor,HarborSea: frames whose edges docks are built against, laid out in one pass (docks, then the body, then the buoys).HarborDock.pier/.quay: measured docks that content sails under or starts beyond; wakes (HarborWake.fade,.hairline), tide stances (float, pilings, dry dock), states (open, dark, withdrawn) with extent policies, resting extents, minimums.HarborCoast: the platform's insets, fixed coasts,HarborCoast.none, and TV title-safe.HarborTide,HarborDryDock: the keyboard's height after it is consumed, its phase and its high-water mark.- Content:
HarborMoored,HarborMooringLine,HarborFairway(vertical and horizontal, reveals widened by what covers the edge),HarborFairwaySliver,HarborSliverDock,HarborOpenWater,HarborCastOff, and theHarborWatersbreakdown. HarborMakeWay,HarborPontoon,HarborController: claims on an edge and docks moored from deep in the tree.HarborBuoy(aligned, anchored, modal),HarborAnchor,HarborSignals.showHarborSheet,HarborSheet(content-sized and draggable), breakwaters,showHarborDialog.HarborLighthouse,HarborBeacon,HarborLighthouseRegion: reveal, keep in sight, lift, and coverage.HarborScaleModel: a reference screen scaled to fit, with insets re-based.HarborChart,HarborChartOverlay, and theext.harbor.chartVM-service extension.package:harbor/testing.dart:pumpSeaTrial,HarborTrialDevicepresets andisInClearWater.