|
| 1 | +// Copyright 2025 The Flutter Authors. All rights reserved. |
| 2 | +// Use of this source code is governed by a BSD-style license that can be |
| 3 | +// found in the LICENSE file. |
| 4 | + |
| 5 | +import 'package:jaspr/jaspr.dart'; |
| 6 | +import 'package:jaspr_content/jaspr_content.dart'; |
| 7 | + |
| 8 | +import '../common/button.dart'; |
| 9 | +import '../common/material_icon.dart'; |
| 10 | + |
| 11 | +class Stepper extends CustomComponent { |
| 12 | + const Stepper() : super.base(); |
| 13 | + |
| 14 | + @override |
| 15 | + Component? create(Node node, NodesBuilder builder) { |
| 16 | + if (node case ElementNode( |
| 17 | + tag: 'Stepper', |
| 18 | + :final attributes, |
| 19 | + :final children, |
| 20 | + )) { |
| 21 | + final levelStr = attributes['level'] ?? '1'; |
| 22 | + final level = int.tryParse(levelStr) ?? 1; |
| 23 | + |
| 24 | + assert( |
| 25 | + level >= 1 && level <= 6, |
| 26 | + 'Stepper level must be between 1 and 6, got $level', |
| 27 | + ); |
| 28 | + |
| 29 | + final steps = <({Node title, List<Node> content})>[]; |
| 30 | + |
| 31 | + if (children != null) { |
| 32 | + for (final child in children) { |
| 33 | + if (child case final ElementNode heading |
| 34 | + when heading.tag == 'h$level') { |
| 35 | + steps.add((title: child, content: [])); |
| 36 | + } else if (child case ElementNode( |
| 37 | + tag: 'div', |
| 38 | + attributes: {'class': 'header-wrapper'}, |
| 39 | + children: [final ElementNode heading, ..._], |
| 40 | + ) when heading.tag == 'h$level') { |
| 41 | + steps.add((title: child, content: [])); |
| 42 | + } else { |
| 43 | + if (steps.isEmpty) { |
| 44 | + throw Exception( |
| 45 | + 'Content found before first step title in Stepper. Make sure ' |
| 46 | + 'your Stepper content starts with a heading of level $level.', |
| 47 | + ); |
| 48 | + } |
| 49 | + steps.last.content.add(child); |
| 50 | + } |
| 51 | + } |
| 52 | + } |
| 53 | + |
| 54 | + assert(steps.isNotEmpty, 'Stepper must have at least one step.'); |
| 55 | + |
| 56 | + return div(classes: 'stepper', [ |
| 57 | + for (final (index, step) in steps.indexed) |
| 58 | + details(open: index == 0, [ |
| 59 | + summary([ |
| 60 | + span( |
| 61 | + classes: 'step-number', |
| 62 | + attributes: {'aria-label': 'Step ${index + 1}'}, |
| 63 | + [text('${index + 1}')], |
| 64 | + ), |
| 65 | + div(classes: 'step-title', [ |
| 66 | + builder.build([step.title]), |
| 67 | + ]), |
| 68 | + const MaterialIcon('keyboard_arrow_up'), |
| 69 | + ]), |
| 70 | + div(classes: 'step-content', [ |
| 71 | + builder.build(step.content), |
| 72 | + ]), |
| 73 | + div(classes: 'step-actions', [ |
| 74 | + Button( |
| 75 | + classes: ['next-step-button'], |
| 76 | + style: ButtonStyle.filled, |
| 77 | + content: index == steps.length - 1 ? 'Finish' : 'Continue', |
| 78 | + ), |
| 79 | + ]), |
| 80 | + ]), |
| 81 | + ]); |
| 82 | + } |
| 83 | + |
| 84 | + return null; |
| 85 | + } |
| 86 | +} |
0 commit comments